diff --git a/.github/workflows/drawing-board.yml b/.github/workflows/drawing-board.yml index 5bc69f1..5508f8c 100644 --- a/.github/workflows/drawing-board.yml +++ b/.github/workflows/drawing-board.yml @@ -11,7 +11,7 @@ on: candidate: description: Exact firstdraft/drawing-board commit to qualify required: true - default: 058fb28aff15782b9ff764628f7ff77651a7e77e + default: 5c7c84c1c6fe5568829c1a50b030b57be7eb25e1 permissions: contents: read @@ -27,7 +27,7 @@ jobs: - name: Select the exact template candidate id: candidate env: - CANDIDATE: ${{ inputs.candidate || '058fb28aff15782b9ff764628f7ff77651a7e77e' }} + CANDIDATE: ${{ inputs.candidate || '5c7c84c1c6fe5568829c1a50b030b57be7eb25e1' }} run: | [[ "$CANDIDATE" =~ ^[0-9a-f]{40}$ ]] echo "sha=$CANDIDATE" >> "$GITHUB_OUTPUT" diff --git a/drawing-board/README.md b/drawing-board/README.md index c9c01aa..bff4f08 100644 --- a/drawing-board/README.md +++ b/drawing-board/README.md @@ -14,7 +14,7 @@ A repository created from this template must provide one ready-to-use workspace - the Dev Container installs the latest public Claude Code and Codex releases and the exact reviewed First Draft CLI; - every Skill declared by one exact source revision is linked into both agents; -- `.env` supplies the shared staging origin and token without entering Git; +- `.env` supplies the production origin and token without entering Git; - bare `firstdraft` on the Codespace PATH resolves to `bin/firstdraft`, and AGENTS.md routes Skill-issued commands through that wrapper; and - the same container carries the current generated Foundation's Ruby and Node toolchain plus healthy PostgreSQL; @@ -352,13 +352,11 @@ Never commit a First Draft API token, GitHub token, agent credential, or generat Board candidate for common credential shapes and verifies that `.env` remains ignored. The shared ignored `.env` is the credential path for both agents; do not add agent-specific token configuration. -The template wrapper intentionally selects staging. Its existing `.env` format keeps the staging token under -`FIRSTDRAFT_API_TOKEN`; the wrapper maps it to the CLI's `FIRSTDRAFT_STAGING_API_TOKEN`, removes the production token -and legacy plugin settings from the child environment, and overrides any inherited staging token. This applies to -the version probe as well as the requested command. A blank `.env` token never falls back to shell credentials. -The standalone CLI defaults to production and selects staging with `--staging`; Drawing Board's wrapper continues -to select staging through its required URL. Production defaults, GitHub Publication, and Service deployment -are owned by [firstdraft/firstdraft](https://github.com/firstdraft/firstdraft); Skill and plugin delivery are owned by +The template wrapper requires the production origin, `https://firstdraft.com`, which matches the pinned CLI's +default. It passes the `.env` token to the CLI as `FIRSTDRAFT_API_TOKEN`, replacing any inherited value, and removes +legacy plugin settings from the child environment. This applies to the version probe as well as the requested +command. A blank `.env` token never falls back to shell credentials. GitHub Publication and Service deployment are +owned by [firstdraft/firstdraft](https://github.com/firstdraft/firstdraft); Skill and plugin delivery are owned by [firstdraft/skills](https://github.com/firstdraft/skills). ## Documentation diff --git a/drawing-board/script/check b/drawing-board/script/check index e63cdd5..5b8d087 100755 --- a/drawing-board/script/check +++ b/drawing-board/script/check @@ -95,8 +95,8 @@ if [[ "$actual_bin_entries" != "$expected_bin_entries" ]]; then exit 1 fi -if [[ "$(cat .env.example)" != $'FIRSTDRAFT_API_URL=https://staging.firstdraft.com\nFIRSTDRAFT_API_TOKEN=' ]]; then - echo ".env.example must select staging and leave its token blank." >&2 +if [[ "$(cat .env.example)" != $'FIRSTDRAFT_API_URL=https://firstdraft.com\nFIRSTDRAFT_API_TOKEN=' ]]; then + echo ".env.example must select production and leave its token blank." >&2 exit 1 fi if ! git check-ignore --quiet .env || git ls-files --error-unmatch .env >/dev/null 2>&1; then @@ -170,9 +170,8 @@ fi "$checks/check-depth-one" -if [[ "$FIRSTDRAFT_CLI_DEFAULT_API_URL" != "https://firstdraft.com" || \ - "$FIRSTDRAFT_STAGING_API_URL" != "https://staging.firstdraft.com" ]]; then - echo "The pinned releases no longer match the documented staging-wrapper contract." >&2 +if [[ "$FIRSTDRAFT_CLI_DEFAULT_API_URL" != "https://firstdraft.com" ]]; then + echo "The pinned releases no longer match the documented production-wrapper contract." >&2 exit 1 fi diff --git a/drawing-board/script/check-agent-setup.mjs b/drawing-board/script/check-agent-setup.mjs index ea23843..6876bbf 100644 --- a/drawing-board/script/check-agent-setup.mjs +++ b/drawing-board/script/check-agent-setup.mjs @@ -148,7 +148,7 @@ writeFileSync(path.join(prefix, "bin/firstdraft"), "#!/usr/bin/env node\\nconsol [path.join(environment.CODEX_HOME, "sessions/session.jsonl"), '{"fixture":"existing Codex conversation"}\n'], [path.join(environment.CLAUDE_CONFIG_DIR, "skills/user-skill/SKILL.md"), "Claude user Skill\n"], [path.join(home, ".agents/skills/user-skill/SKILL.md"), "Codex user Skill\n"], - [path.join(workspace, ".env"), `FIRSTDRAFT_API_URL=https://staging.firstdraft.com\n${"FIRSTDRAFT_API_TOKEN"}=fixture-placeholder\n`], + [path.join(workspace, ".env"), `FIRSTDRAFT_API_URL=https://firstdraft.com\n${"FIRSTDRAFT_API_TOKEN"}=fixture-placeholder\n`], ]); for (const [file, content] of preserved) write(file, content); const verifyPreserved = () => { diff --git a/drawing-board/script/check-firstdraft-wrapper.mjs b/drawing-board/script/check-firstdraft-wrapper.mjs index 6be7383..9380b81 100644 --- a/drawing-board/script/check-firstdraft-wrapper.mjs +++ b/drawing-board/script/check-firstdraft-wrapper.mjs @@ -32,9 +32,8 @@ const arguments_ = process.argv.slice(2); const probe = { apiUrl: process.env.FIRSTDRAFT_API_URL, arguments_, - stagingTokenIsExpected: process.env.FIRSTDRAFT_STAGING_API_TOKEN === "test-token", - stagingTokenPresent: Boolean(process.env.FIRSTDRAFT_STAGING_API_TOKEN), - productionTokenPresent: Object.prototype.hasOwnProperty.call(process.env, "FIRSTDRAFT_API_TOKEN"), + tokenIsExpected: process.env.FIRSTDRAFT_API_TOKEN === "test-token", + tokenPresent: Boolean(process.env.FIRSTDRAFT_API_TOKEN), legacyUrlPresent: Object.prototype.hasOwnProperty.call(process.env, "FIRSTDRAFT_BASE_URL"), pluginOptionsPresent: [ "CLAUDE_PLUGIN_OPTION_API_TOKEN", @@ -58,7 +57,7 @@ fs.writeFileSync(process.env.FIRSTDRAFT_TEST_OUTPUT, JSON.stringify(probe)); const writeEnvironment = ({ apiToken = "", - apiUrl = "https://staging.firstdraft.com", + apiUrl = "https://firstdraft.com", extra = "", mode = 0o600, } = {}) => { @@ -72,7 +71,6 @@ fs.writeFileSync(process.env.FIRSTDRAFT_TEST_OUTPUT, JSON.stringify(probe)); const testEnvironment = { ...process.env, FIRSTDRAFT_API_TOKEN: "ambient-production-token", - FIRSTDRAFT_STAGING_API_TOKEN: "ambient-staging-token", FIRSTDRAFT_API_URL: "https://wrong.example.com", FIRSTDRAFT_BASE_URL: "https://legacy.example.com", CLAUDE_PLUGIN_OPTION_API_TOKEN: "uppercase-token", @@ -91,7 +89,7 @@ fs.writeFileSync(process.env.FIRSTDRAFT_TEST_OUTPUT, JSON.stringify(probe)); const symlinkTarget = path.join(testRepository, "environment-target"); fs.writeFileSync( symlinkTarget, - `FIRSTDRAFT_API_URL=https://staging.firstdraft.com\n${"FIRSTDRAFT_API_TOKEN"}=\n`, + `FIRSTDRAFT_API_URL=https://firstdraft.com\n${"FIRSTDRAFT_API_TOKEN"}=\n`, ); fs.chmodSync(symlinkTarget, 0o600); fs.symlinkSync(symlinkTarget, path.join(testRepository, ".env")); @@ -105,7 +103,7 @@ fs.writeFileSync(process.env.FIRSTDRAFT_TEST_OUTPUT, JSON.stringify(probe)); writeEnvironment(); assert.deepEqual(readConfiguration(testRepository), { apiToken: "", - apiUrl: "https://staging.firstdraft.com", + apiUrl: "https://firstdraft.com", }); assert.equal(requiresApiToken(["plan", "push"]), true); assert.equal(requiresApiToken(["plan", "push", "--help"]), false); @@ -113,10 +111,6 @@ fs.writeFileSync(process.env.FIRSTDRAFT_TEST_OUTPUT, JSON.stringify(probe)); assert.equal(requiresApiToken(["generate", "uuid"]), false); assert.equal(requiresApiToken(["future", "network-command"]), true); assert.equal(requiresApiToken(["--version"]), false); - assert.equal(requiresApiToken(["--staging", "--version"]), false); - assert.equal(requiresApiToken(["--staging", "plan", "init", "--name", "Test"]), false); - assert.equal(requiresApiToken(["--staging", "generate", "uuid"]), false); - assert.equal(requiresApiToken(["--staging", "plan", "push"]), true); await assert.rejects( run({ arguments_: ["plan", "push"], @@ -139,27 +133,25 @@ fs.writeFileSync(process.env.FIRSTDRAFT_TEST_OUTPUT, JSON.stringify(probe)); }); assert.deepEqual(result, {signal: null, status: 0}); assert.deepEqual(JSON.parse(fs.readFileSync(probeOutput, "utf8")), { - apiUrl: "https://staging.firstdraft.com", + apiUrl: "https://firstdraft.com", arguments_: ["plan", "push"], legacyUrlPresent: false, pluginOptionsPresent: false, - productionTokenPresent: false, - stagingTokenIsExpected: true, - stagingTokenPresent: true, + tokenIsExpected: true, + tokenPresent: true, }); assert.deepEqual(JSON.parse(fs.readFileSync(versionProbeOutput, "utf8")), { - apiUrl: "https://staging.firstdraft.com", + apiUrl: "https://firstdraft.com", arguments_: ["--version"], legacyUrlPresent: false, pluginOptionsPresent: false, - productionTokenPresent: false, - stagingTokenIsExpected: true, - stagingTokenPresent: true, + tokenIsExpected: true, + tokenPresent: true, }); writeEnvironment(); const localResult = await run({ - arguments_: ["--staging", "plan", "init", "--name", "Test"], + arguments_: ["plan", "init", "--name", "Test"], downstreamCli: fakeCli, environment: testEnvironment, root: testRepository, @@ -167,22 +159,20 @@ fs.writeFileSync(process.env.FIRSTDRAFT_TEST_OUTPUT, JSON.stringify(probe)); }); assert.deepEqual(localResult, {signal: null, status: 0}); assert.deepEqual(JSON.parse(fs.readFileSync(probeOutput, "utf8")), { - apiUrl: "https://staging.firstdraft.com", - arguments_: ["--staging", "plan", "init", "--name", "Test"], + apiUrl: "https://firstdraft.com", + arguments_: ["plan", "init", "--name", "Test"], legacyUrlPresent: false, pluginOptionsPresent: false, - productionTokenPresent: false, - stagingTokenIsExpected: false, - stagingTokenPresent: false, + tokenIsExpected: false, + tokenPresent: false, }); assert.deepEqual(JSON.parse(fs.readFileSync(versionProbeOutput, "utf8")), { - apiUrl: "https://staging.firstdraft.com", + apiUrl: "https://firstdraft.com", arguments_: ["--version"], legacyUrlPresent: false, pluginOptionsPresent: false, - productionTokenPresent: false, - stagingTokenIsExpected: false, - stagingTokenPresent: false, + tokenIsExpected: false, + tokenPresent: false, }); const injectionMarker = path.join(temporaryRoot, "injected"); @@ -196,7 +186,7 @@ fs.writeFileSync(process.env.FIRSTDRAFT_TEST_OUTPUT, JSON.stringify(probe)); writeEnvironment({apiToken: "test-token", mode: 0o644}); assert.throws(() => readConfiguration(testRepository), /mode 0600/); - writeEnvironment({apiToken: "test-token", apiUrl: "https://firstdraft.com"}); + writeEnvironment({apiToken: "test-token", apiUrl: "https://staging.firstdraft.com"}); await assert.rejects( run({ arguments_: ["plan", "compile"], @@ -205,7 +195,7 @@ fs.writeFileSync(process.env.FIRSTDRAFT_TEST_OUTPUT, JSON.stringify(probe)); root: testRepository, stdio: "ignore", }), - /FIRSTDRAFT_API_URL in .env must be https:\/\/staging\.firstdraft\.com/, + /FIRSTDRAFT_API_URL in .env must be https:\/\/firstdraft\.com\./, ); writeEnvironment({apiToken: "test-token"}); diff --git a/drawing-board/script/devcontainer-smoke b/drawing-board/script/devcontainer-smoke index 5a48f57..44ea828 100755 --- a/drawing-board/script/devcontainer-smoke +++ b/drawing-board/script/devcontainer-smoke @@ -66,12 +66,12 @@ test -f .env test -O .env test "$(stat -c '%a' .env)" = "600" -FIRSTDRAFT_STAGING_API_URL="$FIRSTDRAFT_STAGING_API_URL" node -e ' +FIRSTDRAFT_CLI_DEFAULT_API_URL="$FIRSTDRAFT_CLI_DEFAULT_API_URL" node -e ' const {parseEnv} = require("node:util"); const parsed = parseEnv(require("node:fs").readFileSync(".env", "utf8")); const valid = Object.keys(parsed).sort().join(",") === "FIRSTDRAFT_API_TOKEN,FIRSTDRAFT_API_URL" && - parsed.FIRSTDRAFT_API_URL === process.env.FIRSTDRAFT_STAGING_API_URL && + parsed.FIRSTDRAFT_API_URL === process.env.FIRSTDRAFT_CLI_DEFAULT_API_URL && parsed.FIRSTDRAFT_API_TOKEN === ""; process.exit(valid ? 0 : 1); '