diff --git a/AGENTS.md b/AGENTS.md index 276416e..2229961 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. diff --git a/TEMPLATE.json b/TEMPLATE.json index 911101c..d1565f3 100644 --- a/TEMPLATE.json +++ b/TEMPLATE.json @@ -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?", diff --git a/scripts/deploy.mjs b/scripts/deploy.mjs index b5df694..fd56e3c 100644 --- a/scripts/deploy.mjs +++ b/scripts/deploy.mjs @@ -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, @@ -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 ( @@ -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, @@ -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], @@ -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"]); @@ -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); diff --git a/skills/manage-agent-site/SKILL.md b/skills/manage-agent-site/SKILL.md index e22e73c..b9ddef1 100644 --- a/skills/manage-agent-site/SKILL.md +++ b/skills/manage-agent-site/SKILL.md @@ -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. @@ -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: @@ -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.