Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ Use `pnpm` only. Dependencies are exact-pinned; do not widen or update them unle

`src/components/ui` contains locally owned shadcn/ui primitives. Compose those before adding a component, and edit a primitive only when its change should become the repository standard. Keep the semantic theme variables in `src/styles/globals.css` intact.

`dist/`, `public/`, and `convos.site.json` are generated deployment artifacts. Run `pnpm build` after source changes, then commit all three together. Do not edit the manifest by hand.
`dist/`, `public/`, and `convos.site.json` are generated deployment artifacts. Use `pnpm build` only when a local production build is specifically needed. `pnpm run deploy` performs its own clean build and commits all three outputs together, so never run a separate build immediately before deploying. Do not edit generated artifacts or the manifest by hand.

Capability calls stay in `src/lib/artifacts.server.ts`; never import that module from client components.

`pnpm dev` uses the explicitly configured local fixture KV. Production builds never fall back to fixtures. `pnpm preview:platform` builds first, then serves the committed artifact contract through a local Worker Loader at both the pinned and historical mount paths printed by the preview root.
`pnpm dev` uses the explicitly configured local fixture KV. Production builds never fall back to fixtures. `pnpm preview:platform` builds first, then serves the committed artifact contract through a local Worker Loader at both the pinned and historical mount paths printed by the preview root. Stop the exact dev or preview process you started before running checks or deploying; never use broad process-killing commands.

`pnpm site:url` prints this assistant's stable pinned site URL from the runtime-provided public base URL and instance ID without making a network request.

`pnpm deploy` requires `CODE_STORAGE_GIT_URL`, `CODE_STORAGE_GIT_TOKEN`, `POOL_URL`, `PUBLIC_BASE_URL`, and `INSTANCE_ID` from the assistant runtime. It commits generated output when needed, pushes with a process-only credential helper, and prints the pinned URL only after synchronous activation confirms the pushed SHA.
`pnpm run deploy` requires `CODE_STORAGE_GIT_URL`, `CODE_STORAGE_GIT_TOKEN`, `POOL_URL`, `PUBLIC_BASE_URL`, and `INSTANCE_ID` from the assistant runtime. Never use `pnpm deploy`, which invokes pnpm's unrelated workspace-deploy command. The script times its build, staging, commit, push, and activation phases; commits generated output when needed; pushes with a process-only credential helper; and prints the pinned URL only after synchronous activation confirms the pushed SHA.
2 changes: 1 addition & 1 deletion TEMPLATE.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"description": "A friendly, general-purpose assistant that helps groups stay organized, answers questions, and gets things done in chat.",
"emoji": "🤖",
"personality": "Warm, concise, and practical. You keep replies short and skimmable, ask a clarifying question when a request is ambiguous, and never pad answers with filler. You're upbeat without being saccharine, and you're candid when you don't know something.",
"additionalInstructions": "Match the group's tone and energy. Prefer plain language over jargon. Summarize long or complex answers into the key points first, then offer more detail if asked. You have a website for this group. When the group asks you to create, change, preview, test, deploy, or publish the website, load the manage-agent-site skill from your workspace and follow it. You can deploy validated changes with pnpm deploy and print the stable live URL at any time with pnpm site:url.",
"additionalInstructions": "Match the group's tone and energy. Prefer plain language over jargon. Summarize long or complex answers into the key points first, then offer more detail if asked. You have a website for this group. When the group asks you to create, change, preview, test, deploy, or publish the website, load the manage-agent-site skill from your workspace and follow it. You can deploy validated changes with pnpm run deploy and print the stable live URL at any time with pnpm site:url.",
"exampleResponses": [
{
"user": "can you keep track of what we decide in here?",
Expand Down
153 changes: 104 additions & 49 deletions scripts/deploy.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { spawn } from "node:child_process";
import { dirname, resolve } from "node:path";
import { performance } from "node:perf_hooks";
import { fileURLToPath } from "node:url";
import {
buildPinnedSiteUrl,
Expand All @@ -13,6 +14,7 @@ const gitToken = requiredEnvironment("CODE_STORAGE_GIT_TOKEN");
const poolUrl = requiredUrl("POOL_URL");
const publicBaseUrl = requiredUrl("PUBLIC_BASE_URL");
const instanceId = requiredEnvironment("INSTANCE_ID");
const deploymentStartedAt = performance.now();

const parsedGitUrl = new URL(gitUrl);
if (
Expand All @@ -29,6 +31,24 @@ function redact(value) {
return value.replaceAll(gitToken, "[redacted]");
}

function elapsedSeconds(startedAt) {
return ((performance.now() - startedAt) / 1000).toFixed(2);
}

async function timed(label, operation) {
const startedAt = performance.now();
try {
const value = await operation();
console.error(`[deploy] ${label}: ${elapsedSeconds(startedAt)}s`);
return value;
} catch (error) {
console.error(
`[deploy] ${label}: failed after ${elapsedSeconds(startedAt)}s`,
);
throw error;
}
}

function run(
command,
args,
Expand Down Expand Up @@ -83,9 +103,10 @@ async function output(command, args) {
return (await run(command, args)).stdout;
}

await run("pnpm", ["build"], { inherit: true });
await run("node", ["scripts/validate-artifacts.mjs"], { inherit: true });
await run("git", ["add", "-A"]);
await timed("build and validate artifacts", () =>
run("pnpm", ["build"], { inherit: true }),
);
await timed("stage workspace", () => run("git", ["add", "-A"]));

const staged = await run("git", ["diff", "--cached", "--quiet"], {
allowExitCodes: [0, 1],
Expand All @@ -104,12 +125,16 @@ if (staged.code === 1) {
if (email.code === 1 || email.stdout === "") {
identityArgs.push("-c", "user.email=site-bot@convos.org");
}
await run("git", [
...identityArgs,
"commit",
"-m",
"Publish agent site artifacts",
]);
await timed("create deployment commit", () =>
run("git", [
...identityArgs,
"commit",
"-m",
"Publish agent site artifacts",
]),
);
} else {
console.error("[deploy] create deployment commit: skipped (no changes)");
}

const branch = await output("git", ["branch", "--show-current"]);
Expand All @@ -120,54 +145,84 @@ await run("git", ["check-ref-format", "--branch", branch]);
// it. The token is never placed in argv, a remote URL, or repository config.
const credentialHelper =
'!f() { if [ "$1" = get ]; then printf "username=t\\npassword=%s\\n" "$CODE_STORAGE_GIT_TOKEN"; fi; }; f';
await run("git", [
"-c",
"credential.helper=",
"-c",
`credential.helper=${credentialHelper}`,
"push",
gitUrl,
`HEAD:refs/heads/${branch}`,
], { includeGitCredential: true });
await timed("push deployment commit", () =>
run(
"git",
[
"-c",
"credential.helper=",
"-c",
`credential.helper=${credentialHelper}`,
"push",
gitUrl,
`HEAD:refs/heads/${branch}`,
],
{ includeGitCredential: true },
),
);

const commitSha = await output("git", ["rev-parse", "HEAD"]);
if (!/^[a-f0-9]{40}$/.test(commitSha)) {
throw new Error("could not resolve a full SHA-1 commit after push");
}

const response = await fetch(
new URL("/api/internal/site/deployment", poolUrl),
{
method: "PUT",
headers: { "content-type": "application/json" },
body: JSON.stringify({ commit_sha: commitSha }),
redirect: "manual",
const result = await timed(
"materialize, validate, and activate",
async () => {
const response = await fetch(
new URL("/api/internal/site/deployment", poolUrl),
{
method: "PUT",
headers: { "content-type": "application/json" },
body: JSON.stringify({ commit_sha: commitSha }),
redirect: "manual",
},
);
const responseText = await response.text();
let result;
try {
result = JSON.parse(responseText);
} catch {
throw new Error(
`runtime deployment returned non-JSON (${response.status}): ${responseText.slice(0, 1000)}`,
);
}
if (!response.ok) {
throw new Error(
`runtime deployment failed (${response.status}): ${JSON.stringify(result).slice(0, 1000)}`,
);
}
if (
result?.ok !== true ||
result.commit_sha !== commitSha ||
result.active_commit_sha !== commitSha ||
result.validation?.status !== 200
) {
throw new Error(
"runtime deployment response did not confirm activation of the pushed commit",
);
}
return result;
},
);
const responseText = await response.text();
let result;
try {
result = JSON.parse(responseText);
} catch {
throw new Error(
`runtime deployment returned non-JSON (${response.status}): ${responseText.slice(0, 1000)}`,
);
}
if (!response.ok) {
throw new Error(
`runtime deployment failed (${response.status}): ${JSON.stringify(result).slice(0, 1000)}`,
);
}
if (
result?.ok !== true ||
result.commit_sha !== commitSha ||
result.active_commit_sha !== commitSha ||
result.validation?.status !== 200
) {
throw new Error(
"runtime deployment response did not confirm activation of the pushed commit",
);
}

const reusedArtifact =
typeof result.reused_artifact === "boolean"
? String(result.reused_artifact)
: "unknown";
const downloadedFiles = Number.isSafeInteger(result.downloaded_files)
? result.downloaded_files
: "unknown";
const uploadedBlobs = Number.isSafeInteger(result.uploaded_blobs)
? result.uploaded_blobs
: "unknown";
const validationMs = Number.isSafeInteger(result.validation.duration_ms)
? result.validation.duration_ms
: "unknown";
console.error(
`[deploy] activation details: reused_artifact=${reusedArtifact} downloaded_files=${downloadedFiles} uploaded_blobs=${uploadedBlobs} validation_ms=${validationMs}`,
);
console.error(`[deploy] total: ${elapsedSeconds(deploymentStartedAt)}s`);

const siteUrl = buildPinnedSiteUrl(publicBaseUrl, instanceId);
console.log(siteUrl.href);
29 changes: 22 additions & 7 deletions skills/manage-agent-site/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,20 +240,27 @@ Use `pnpm dev` for the short feedback loop and `pnpm preview:platform` for the
final platform-compatibility check. Do not use `wrangler deploy`; this
application is deployed as a bundle inside the assistant Worker.

Before validation or deployment, stop the exact dev and platform-preview
processes you started through their terminal session or recorded PID. Do not
leave Vite, Wrangler, or workerd previews competing with the production build.
Never use broad cleanup commands such as `pkill node` or `killall`.

## Validate

Before publishing, run:
After source changes have stabilized, run:

```bash
pnpm check
pnpm build
git diff --check
git status --short
```

`pnpm check` runs React Router type generation, TypeScript, and unit tests.
`pnpm build` bundles client and server code and rejects missing, stale,
oversized, unsafe, or hash-mismatched artifacts. The platform limits the
Run it once after the last relevant source, test, TypeScript, package, or
configuration change. If it already passed and none of those inputs changed,
do not repeat it. The deployment command performs the clean production build
and rejects missing, stale, oversized, unsafe, or hash-mismatched artifacts, so
do not run `pnpm build` immediately before deploying. The platform limits the
manifest to 1,000 files total, so keep the application bundled instead of
generating many small static files.

Expand All @@ -279,7 +286,7 @@ workspace change, so remove temporary files and do not proceed when unrelated
or suspicious changes are present.

```bash
pnpm deploy
pnpm run deploy
```

This single command:
Expand All @@ -295,8 +302,16 @@ This single command:
It prints the live pinned URL only after the new commit returns HTTP 200 from
`GET /` and activation succeeds. A failed build, push, materialization, or
validation leaves the previous pinned commit live. Fix the reported error and
run `pnpm deploy` again; do not bypass activation with a manual Git push or raw
runtime request.
run `pnpm run deploy` again; do not bypass activation with a manual Git push or
raw runtime request. Never use `pnpm deploy`: pnpm reserves that shorthand for
its unrelated workspace-deploy command.

Run the deployment directly with the terminal tool and a 300-second timeout.
Do not pipe it through `tail`, because a successful `tail` can hide the deploy
command's nonzero exit status. The script reports timings for its build,
staging, commit, push, and activation phases, followed by artifact reuse,
download, upload, and validation metrics. Use those timings to diagnose a slow
or failed deployment instead of rerunning it speculatively.

After success, request the printed URL and report the live outcome concisely.
Use the printed URL rather than constructing one from environment values.
Expand Down