From 002dd39e150177c92e1de4f96a614113018bb6e3 Mon Sep 17 00:00:00 2001 From: Raghu Betina Date: Fri, 18 Sep 2026 19:11:21 -0500 Subject: [PATCH] Keep archived planning context out of app work Place continuation and recovery in .firstdraft/design while keeping private authoring state separate from generated context. Require the matching CLI 0.3.0 candidate so installed agents use the same layout. Exercise both root commands through the real packaged executable, preserving Git history and ignored files. Keep the published catalog and release identities unchanged until coordinated qualification. --- .github/workflows/ci.yml | 4 +- .github/workflows/publish.yml | 8 +- README.md | 7 +- RELEASING.md | 12 +-- evals/create-full-stack-app/cases.json | 11 +-- .../claude-plugin/.claude-plugin/plugin.json | 2 +- packages/claude-plugin/package.template.json | 2 +- release/compatibility.json | 6 +- script/cli-contract/config.mjs | 6 +- script/cli-contract/fixtures.mjs | 6 +- script/cli-contract/packed-executable.mjs | 70 ++++++++++----- script/cli-contract/plan-journey.mjs | 22 ++++- skills/create-full-stack-app/SKILL.md | 11 +-- .../references/diagnostics-and-recovery.md | 85 +++++++++++-------- .../references/foundation-plan-019.md | 17 ++-- test/interview-evaluation-foundation.test.mjs | 4 +- test/plugin-release-order.test.mjs | 19 +---- test/release-compatibility.test.mjs | 4 +- test/repository.test.mjs | 28 +++--- 19 files changed, 189 insertions(+), 135 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1763f2b..b10ad7a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,8 +46,8 @@ jobs: fetch-depth: 0 path: tmp/firstdraft-cli persist-credentials: false - - run: git -C tmp/firstdraft-cli merge-base --is-ancestor 799a184cb2453ceadf5575f7b46ba975e084f192 HEAD - - run: git -C tmp/firstdraft-cli checkout --detach 799a184cb2453ceadf5575f7b46ba975e084f192 + - run: git -C tmp/firstdraft-cli merge-base --is-ancestor 137ef9ceff7469e43f072009e3bba941abc6cd4c HEAD + - run: git -C tmp/firstdraft-cli checkout --detach 137ef9ceff7469e43f072009e3bba941abc6cd4c - run: node script/check-cli-contract.mjs tmp/firstdraft-cli - run: node script/check-claude-plugin-package.mjs --cli-root tmp/firstdraft-cli - name: Check Codex installation and Skill discovery diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 2de6919..0b47ad4 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -60,8 +60,8 @@ jobs: fetch-depth: 0 path: tmp/firstdraft-cli persist-credentials: false - - run: git -C tmp/firstdraft-cli merge-base --is-ancestor 799a184cb2453ceadf5575f7b46ba975e084f192 HEAD - - run: git -C tmp/firstdraft-cli checkout --detach 799a184cb2453ceadf5575f7b46ba975e084f192 + - run: git -C tmp/firstdraft-cli merge-base --is-ancestor 137ef9ceff7469e43f072009e3bba941abc6cd4c HEAD + - run: git -C tmp/firstdraft-cli checkout --detach 137ef9ceff7469e43f072009e3bba941abc6cd4c - run: node script/check-cli-registry-package.mjs --cli-root tmp/firstdraft-cli - run: node script/check-claude-plugin-package.mjs --cli-root tmp/firstdraft-cli @@ -118,8 +118,8 @@ jobs: fetch-depth: 0 path: tmp/firstdraft-cli persist-credentials: false - - run: git -C tmp/firstdraft-cli merge-base --is-ancestor 799a184cb2453ceadf5575f7b46ba975e084f192 HEAD - - run: git -C tmp/firstdraft-cli checkout --detach 799a184cb2453ceadf5575f7b46ba975e084f192 + - run: git -C tmp/firstdraft-cli merge-base --is-ancestor 137ef9ceff7469e43f072009e3bba941abc6cd4c HEAD + - run: git -C tmp/firstdraft-cli checkout --detach 137ef9ceff7469e43f072009e3bba941abc6cd4c - run: node script/check-cli-registry-package.mjs --cli-root tmp/firstdraft-cli - run: node script/claude-plugin-package.mjs pack "$RUNNER_TEMP/plugin" --cli-root tmp/firstdraft-cli - name: Verify publication bytes diff --git a/README.md b/README.md index d388728..e02b6a8 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,10 @@ canonical Skills and bundled CLI are packaged once for Claude Code and Codex. UI app's own design and components. The public catalog and Drawing Board pins determine what an installed workspace actually receives. -Plugin `0.2.5` documents default Account settings and observed service release identities, with its digest in -[release compatibility](release/compatibility.json). It packages only `create-full-stack-app`. The UI Skill -auditions remain deferred source. +Source candidate `0.3.0` requires CLI `0.3.0` and preserves the planning workspace under `.firstdraft/design/` after +root adoption. Its identities belong in [release compatibility](release/compatibility.json); it is unpublished. +The public catalog remains on plugin `0.2.5`. Only `create-full-stack-app` is packaged; the UI Skill auditions remain +deferred source. Trying First Draft as a tester? Start with the [Drawing Board guide](https://github.com/firstdraft/drawing-board#build-an-app-with-first-draft). diff --git a/RELEASING.md b/RELEASING.md index 1e45c68..00e7c32 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -8,7 +8,6 @@ Coordinate the service, CLI, and Skills. [History](evidence/release-history.md) |---|---| | Released plugin source | `@firstdraft.com/claude-code@0.2.5`, tag `claude-v0.2.5` | | Qualified packed SHA-256 | `d9f466076a05e867019bc3e20c35b89076cc83195e781d9203c039d166becde6` | -| Public plugin package | `@firstdraft.com/claude-code@0.2.5` | | Catalog manifest | Plugin `0.2.5` in `.claude-plugin/marketplace.json` | | Plugin npm `next` / `latest` | `0.2.5` / `0.2.5` | | Compatible CLI | `@firstdraft.com/cli@0.2.2` | @@ -18,7 +17,12 @@ Coordinate the service, CLI, and Skills. [History](evidence/release-history.md) [`release/compatibility.json`](release/compatibility.json) owns candidate compatibility; the shared marketplace manifest owns public catalog selection. Drawing Board's source-Skill pin is separate. -API 0.3.1 permits omitted return overrides; CLI accepts 0.3. + +Unpublished candidate `@firstdraft.com/claude-code@0.3.0` requires `@firstdraft.com/cli@0.3.0` for `.firstdraft/design/`. +The breaking root-adoption contract needs both minor bumps; API/Plan ranges and public selections stay unchanged. +Packed SHA-256 `9648469aea9a3f0959e1e4eabde6626edc381d2f3b24ac3cb99be26176edf41b` does not prove registry parity +or hosted root adoption. + [Release evidence](evidence/2026-09-15-account-authoring-0.2.5.md) records each completed qualification and distribution step. Recheck before mutations. @@ -75,10 +79,6 @@ syntax; do not add compatibility aliases. and service identities. Candidate compatibility and local validation never prove authentication, service compatibility, a fresh public install, a successful Compile, or GitHub Publication. -Source `@firstdraft.com/claude-code@0.2.5` has SHA-256 -`d9f466076a05e867019bc3e20c35b89076cc83195e781d9203c039d166becde6`. -[Qualification](evidence/2026-09-15-account-authoring-0.2.5.md) covers Account guidance; UI Skills stay deferred. - Plugin 0.2.4's [Qualification](evidence/2026-09-14-ui-authoring-skill-0.2.4-qualification.md) records the [defined checks](evals/create-full-stack-app/ui-foundation-release.md) and limits. The procedure below records 0.2.2 smokes. diff --git a/evals/create-full-stack-app/cases.json b/evals/create-full-stack-app/cases.json index 52b7d84..5304d89 100644 --- a/evals/create-full-stack-app/cases.json +++ b/evals/create-full-stack-app/cases.json @@ -1024,16 +1024,17 @@ { "id": "compile-prepared-current-root", "should_trigger": true, - "prompt": "I approve the unchanged staged Movie Catalog with SHA-256 52cdb2900607023ad9a10456af35231369bd27c3bf32786297fe3d3eea017a3f and its reviewed support result. Put the generated application at this current workspace root, moving my existing design materials under design. Do not create a GitHub repository. Tell me how to boot it and add features here afterward.", + "prompt": "I approve the unchanged staged Movie Catalog with SHA-256 52cdb2900607023ad9a10456af35231369bd27c3bf32786297fe3d3eea017a3f and its reviewed support result. Use the coordinated archive CLI source candidate to put the generated application at this current workspace root, preserving my existing planning materials directly under .firstdraft/design. Do not create a GitHub repository. Tell me how to boot it and add features here afterward.", "expectations": [ "Treats the explicit current-root request as selection of firstdraft plan compile --output . and never substitutes an absent application directory or zero-flag Publication without another user decision.", "Rereads the staged Plan, verifies the approved SHA-256 and semantic model remain unchanged, and runs the selected Compile exactly once without another command-level confirmation or preparatory push or status command.", - "Before mutation, applies the CLI-owned POSIX current-root preconditions: the physical current directory is writable, has no reserved design or .firstdraft-root-output entry, contains only eligible top-level entries, and is not nested inside another worktree; an existing Git root must be clean, nonsparse, operation-free, and have no submodule metadata.", + "Before mutation, verifies the identified CLI candidate supports the requested archive layout instead of assuming published CLI 0.2.2 implements it. Applies the CLI-owned POSIX current-root preconditions: the physical current directory is writable, has no reserved .firstdraft/design or top-level .firstdraft-root-output entry, contains only eligible top-level entries, and is not nested inside another worktree; an existing Git root must be clean, nonsparse, operation-free, and have no submodule metadata.", "Lets the CLI own the root transaction, artifact verification, same-filesystem moves, rollback journal, and installation instead of moving, deleting, archiving, or overwriting workspace material itself.", - "On success, reports the validated output path, file count, manifest digest, Compilation status, and root_adoption result; prior non-Git top-level material is under design and an existing root .git and history are preserved with the transformation staged.", - "Recognizes that the approved Plan and private CLI state moved to design/.firstdraft, runs any later First Draft plan or compilation command from design, and never initializes a replacement Project in the generated application root.", + "On success, reports the validated output path, file count, manifest digest, Compilation status, and root_adoption result; prior non-Git top-level material is directly under .firstdraft/design and an existing root .git and history are preserved with the transformation staged.", + "Recognizes that the approved Plan and private CLI state moved to .firstdraft/design/.firstdraft, runs any later First Draft plan or compilation command from .firstdraft/design, and never initializes a replacement Project in the generated application root. Keeps that private state separate from the artifact-verified generated .firstdraft/submitted-foundation-plan.json and .firstdraft/gaps.json.", "Hands off setup, boot, tests, and feature development at the generated Rails workspace root using its README, preserving the same existing .git, history, and remotes rather than initializing a nested repository or changing remotes.", - "Does not run the moved design/script/initialize-application or design/script/application-smoke helpers after root adoption; they belong to the optional nested application mode, which this prompt did not select.", + "Does not run the moved .firstdraft/design/script/initialize-application or .firstdraft/design/script/application-smoke helpers after root adoption; they belong to the optional nested application mode, which this prompt did not select.", + "Treats the archive as optional planning context: ordinary setup, preview, browser tests, and source development use the generated root and platform commands without requiring archived helpers or copying those helpers into the application. Does not remove the archive without a user request or migrate already materialized applications automatically.", "Explains that requested features can be implemented directly in ordinary Rails source without First Draft Capabilities, Plan edits, or another Compile, and does not restart the First Draft workflow just to continue application development.", "Recognizes that a non-Git root stays non-Git and makes no Publication, GitHub repository, repository URL, deployment, or newly created Git repository claim.", "On invalid_output_path or incomplete rollback, preserves owner material and follows the structured recovery reason rather than deleting a conflicting path, running Git restoration blindly, or retrying an outcome-unknown mutation." diff --git a/packages/claude-plugin/.claude-plugin/plugin.json b/packages/claude-plugin/.claude-plugin/plugin.json index 568d4b7..d1a062d 100644 --- a/packages/claude-plugin/.claude-plugin/plugin.json +++ b/packages/claude-plugin/.claude-plugin/plugin.json @@ -2,7 +2,7 @@ "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "firstdraft", "displayName": "First Draft", - "version": "0.2.5", + "version": "0.3.0", "description": "Author a Foundation Plan and compile a bounded Rails, iPhone, and Android application", "author": { "name": "First Draft", diff --git a/packages/claude-plugin/package.template.json b/packages/claude-plugin/package.template.json index 0bee72f..0267d74 100644 --- a/packages/claude-plugin/package.template.json +++ b/packages/claude-plugin/package.template.json @@ -1,6 +1,6 @@ { "name": "@firstdraft.com/claude-code", - "version": "0.2.5", + "version": "0.3.0", "description": "First Draft Foundation Plan authoring and UI development for Claude Code and Codex", "license": "MIT", "type": "module", diff --git a/release/compatibility.json b/release/compatibility.json index 9dfe5bc..1e82efd 100644 --- a/release/compatibility.json +++ b/release/compatibility.json @@ -1,10 +1,10 @@ { "format": "firstdraft.release-compatibility/1", "component": "skills", - "version": "0.2.5", + "version": "0.3.0", "plugin_source": { "package": "@firstdraft.com/claude-code", - "tarball_sha256": "d9f466076a05e867019bc3e20c35b89076cc83195e781d9203c039d166becde6" + "tarball_sha256": "9648469aea9a3f0959e1e4eabde6626edc381d2f3b24ac3cb99be26176edf41b" }, "requires": { "api_contract": [ @@ -12,7 +12,7 @@ "< 0.4.0" ], "cli": [ - "= 0.2.2" + "= 0.3.0" ], "foundation_plan_formats": [ "firstdraft.foundation-plan.sketch/0.19" diff --git a/script/cli-contract/config.mjs b/script/cli-contract/config.mjs index 2ecfee6..fff8c4a 100644 --- a/script/cli-contract/config.mjs +++ b/script/cli-contract/config.mjs @@ -1,8 +1,8 @@ -export const cliRevision = "799a184cb2453ceadf5575f7b46ba975e084f192"; +export const cliRevision = "137ef9ceff7469e43f072009e3bba941abc6cd4c"; export const cliRuntimeSha256 = - "e48e4b583e6f06a1d7a50aa19a87da2b24b225eaa5806f3130b9ad4ba6c43a72"; + "7e9fdcf42dd887a6e8f6d9f17755600aa3b841a282f7fcdfaff638fe4467cb28"; export const cliPackageName = "@firstdraft.com/cli"; -export const cliPackageVersion = "0.2.2"; +export const cliPackageVersion = "0.3.0"; export const safeGithubReasonCodes = Object.freeze([ "github.configuration_missing", diff --git a/script/cli-contract/fixtures.mjs b/script/cli-contract/fixtures.mjs index 934ae43..cc2ba43 100644 --- a/script/cli-contract/fixtures.mjs +++ b/script/cli-contract/fixtures.mjs @@ -324,6 +324,7 @@ export function compilationArtifact( provenanceAnalysisId = compilationAnalysisId, provenanceCompilerRelease = compilerRelease, provenanceTarget = compilationTarget, + additionalFiles = [], } = {}, ) { const modelFile = artifactFile( @@ -336,7 +337,8 @@ export function compilationArtifact( const files = [ modelFile, artifactFile("ios/bin/ios", "#!/bin/sh\nexit 0\n", "core:ios", 0o755), - ]; + ...additionalFiles, + ].sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0); const metadata = { files: files.map( ({ path, sha256, mode, owner, source_subject_uuids }) => ({ @@ -422,7 +424,7 @@ export function problemResponse( ); } -function artifactFile(path, contents, owner, mode = 0o644) { +export function artifactFile(path, contents, owner, mode = 0o644) { const source = Buffer.from(contents); return { path, diff --git a/script/cli-contract/packed-executable.mjs b/script/cli-contract/packed-executable.mjs index 07c4dbb..c5d3510 100644 --- a/script/cli-contract/packed-executable.mjs +++ b/script/cli-contract/packed-executable.mjs @@ -22,9 +22,11 @@ import { storedApiUrl, } from "./config.mjs"; import { + artifactFile, artifactResponse, compilationArtifact, compilationProjection, + gapSetDocument, jsonResponse, } from "./fixtures.mjs"; import { @@ -192,7 +194,14 @@ export async function verifyPackedExecutable(context) { async function verifyPackedDownload(context, project) { const retainedHead = "1".repeat(64); - const artifact = compilationArtifact(retainedHead); + const submittedPlan = readFileSync(path.join(project, ".firstdraft", "foundation-plan.json")); + const gapsSource = `${JSON.stringify(gapSetDocument({ headSourceSha256: retainedHead, graphVersion: 7 }), null, 2)}\n`; + const artifact = compilationArtifact(retainedHead, { + additionalFiles: [ + artifactFile(".firstdraft/submitted-foundation-plan.json", submittedPlan, "compiler:context"), + artifactFile(".firstdraft/gaps.json", gapsSource, "compiler:context"), + ], + }); const envelope = JSON.parse(artifact.source.toString("utf8")); assert.equal(envelope.provenance.head_source_sha256, retainedHead); assert.notEqual(envelope.provenance.foundation_plan.sha256, retainedHead); @@ -270,7 +279,7 @@ async function verifyPackedDownload(context, project) { ); assert.equal( readFileSync( - path.join(rootProject, "design", "product-notes.md"), + path.join(rootProject, ".firstdraft", "design", "product-notes.md"), "utf8", ), "Design notes\n", @@ -278,11 +287,17 @@ async function verifyPackedDownload(context, project) { const rootBody = JSON.parse(rootResult.stdout); assert.equal(rootBody.output.path, realpathSync(rootProject)); assert.deepEqual(rootBody.output.root_adoption, { - design_path: path.join(realpathSync(rootProject), "design"), + design_path: path.join(realpathSync(rootProject), ".firstdraft", "design"), moved_entry_count: 2, git_repository_preserved: false, git_index_replaced: false, }); + assert.deepEqual( + readFileSync(path.join(rootProject, ".firstdraft", "submitted-foundation-plan.json")), + submittedPlan, + ); + assert.equal(readFileSync(path.join(rootProject, ".firstdraft", "gaps.json"), "utf8"), gapsSource); + assert.equal(existsSync(path.join(rootProject, "design")), false); assert.deepEqual(requests.slice(2), [ ["GET", `/v1/projects/${projectId}/compilations/${compilationId}`], [ @@ -331,6 +346,8 @@ async function verifyPackedGitRootDownload({ git(root, ["init", "--quiet"]); git(root, ["config", "user.name", "First Draft Contract"]); git(root, ["config", "user.email", "contract@firstdraft.test"]); + const originalRemote = "https://github.com/firstdraft-contract/planning.git"; + git(root, ["remote", "add", "origin", originalRemote]); const initialized = invokeExecutable( context.executable, ["plan", "init", "--name", "Movie Catalog"], @@ -338,18 +355,22 @@ async function verifyPackedGitRootDownload({ ); assert.equal(initialized.status, 0); pinRemoteState(root, { apiUrl, projectIdentifier: projectId }); + const originalState = readFileSync(path.join(root, ".firstdraft", "state.json")); writeFileSync(path.join(root, ".gitignore"), ".env\n"); writeFileSync(path.join(root, "README.md"), "Design README\n"); writeFileSync(path.join(root, ".env"), "SECRET=value\n"); writeFileSync(path.join(root, "notes.md"), "Untracked notes\n"); mkdirSync(path.join(root, "wireframes")); writeFileSync(path.join(root, "wireframes", "home.md"), "Home sketch\n"); - git(root, ["add", ".gitignore", "README.md", ".firstdraft", "wireframes"]); + mkdirSync(path.join(root, "design")); + writeFileSync(path.join(root, "design", "sketch.md"), "Earlier design folder\n"); + git(root, ["add", ".gitignore", "README.md", ".firstdraft", "wireframes", "design"]); git(root, ["commit", "--quiet", "-m", "Design application"]); const originalHead = git(root, ["rev-parse", "HEAD"]).trim(); assert.deepEqual(git(root, ["ls-files"]).trim().split("\n").sort(), [ ".gitignore", "README.md", + "design/sketch.md", "wireframes/home.md", ]); const requestCount = requests.length; @@ -362,34 +383,38 @@ async function verifyPackedGitRootDownload({ assert.equal(result.status, 0, result.stderr); const body = JSON.parse(result.stdout); assert.deepEqual(body.output.root_adoption, { - design_path: path.join(realpathSync(root), "design"), - moved_entry_count: 6, + design_path: path.join(realpathSync(root), ".firstdraft", "design"), + moved_entry_count: 7, git_repository_preserved: true, git_index_replaced: true, }); assert.equal(git(root, ["rev-parse", "HEAD"]).trim(), originalHead); + assert.equal(git(root, ["remote", "get-url", "origin"]).trim(), originalRemote); assert.deepEqual(git(root, ["ls-files"]).trim().split("\n").sort(), [ + ".firstdraft/design/.gitignore", + ".firstdraft/design/README.md", + ".firstdraft/design/design/sketch.md", + ".firstdraft/design/wireframes/home.md", + ".firstdraft/gaps.json", + ".firstdraft/submitted-foundation-plan.json", "app/models/movie.rb", - "design/.gitignore", - "design/README.md", - "design/wireframes/home.md", "ios/bin/ios", ]); - assert.equal(gitStatus(root, ["check-ignore", "design/.env"]), 0); + assert.equal(gitStatus(root, ["check-ignore", ".firstdraft/design/.env"]), 0); assert.equal( - gitStatus(root, ["check-ignore", "design/.firstdraft/state.json"]), + gitStatus(root, ["check-ignore", ".firstdraft/design/.firstdraft/state.json"]), 0, ); assert.equal( - gitStatus(root, ["ls-files", "--error-unmatch", "design/.firstdraft"]), + gitStatus(root, ["ls-files", "--error-unmatch", ".firstdraft/design/.firstdraft"]), 1, ); assert.equal( - gitStatus(root, ["ls-files", "--error-unmatch", "design/.env"]), + gitStatus(root, ["ls-files", "--error-unmatch", ".firstdraft/design/.env"]), 1, ); assert.equal( - gitStatus(root, ["ls-files", "--error-unmatch", "design/notes.md"]), + gitStatus(root, ["ls-files", "--error-unmatch", ".firstdraft/design/notes.md"]), 1, ); assert.equal( @@ -397,18 +422,19 @@ async function verifyPackedGitRootDownload({ false, ); assert.equal(requests.length, requestCount + 2); - assert.equal( - readFileSync( - path.join(root, "design", ".firstdraft", "state.json"), - "utf8", - ).includes(projectId), - true, + assert.deepEqual( + readFileSync(path.join(root, ".firstdraft", "design", ".firstdraft", "state.json")), + originalState, ); assert.equal( readFileSync(path.join(root, "app", "models", "movie.rb"), "utf8"), "class Movie < ApplicationRecord\nend\n", ); - assert.equal(JSON.parse(artifact.source).files.length, 2); + for (const file of JSON.parse(artifact.source).files) { + assert.deepEqual(readFileSync(path.join(root, file.path)), Buffer.from(file.contents_base64, "base64")); + } + assert.equal(existsSync(path.join(root, "design")), false); + assert.equal(JSON.parse(artifact.source).files.length, 4); } function verifyPackedRootRefusals({ apiUrl, context, requests }) { @@ -426,7 +452,7 @@ function verifyPackedRootRefusals({ apiUrl, context, requests }) { 0, ); pinRemoteState(reserved, { apiUrl, projectIdentifier: projectId }); - mkdirSync(path.join(reserved, "design")); + mkdirSync(path.join(reserved, ".firstdraft", "design")); const reservedRequests = requests.length; const reservedResult = invokeExecutable( context.executable, diff --git a/script/cli-contract/plan-journey.mjs b/script/cli-contract/plan-journey.mjs index 788cb5a..6b38be9 100644 --- a/script/cli-contract/plan-journey.mjs +++ b/script/cli-contract/plan-journey.mjs @@ -19,9 +19,11 @@ import { import { acceptedPlanResponse, analysisProjection, + artifactFile, artifactResponse, compilationArtifact, compilationProjection, + gapSetDocument, jsonResponse, problemResponse, publicationProjection, @@ -56,10 +58,15 @@ async function verifyHappyRootCompile(context, planSource) { }); writeFileSync(path.join(cwd, "product-notes.md"), "Design notes\n"); const digest = sha256(planSource); + const gapsSource = `${JSON.stringify(gapSetDocument({ headSourceSha256: digest }), null, 2)}\n`; const artifact = compilationArtifact(digest, { foundationPlanSha256: digest, provenanceGraphVersion: 1, provenanceAnalysisId: analysisId, + additionalFiles: [ + artifactFile(".firstdraft/submitted-foundation-plan.json", planSource, "compiler:context"), + artifactFile(".firstdraft/gaps.json", gapsSource, "compiler:context"), + ], }); const queued = compilationProjection("queued", { headSourceSha256: digest, @@ -100,10 +107,10 @@ async function verifyHappyRootCompile(context, planSource) { compilation: succeeded.compilation, output: { path: realpathSync(cwd), - file_count: 2, + file_count: 4, manifest_sha256: artifact.manifestSha256, root_adoption: { - design_path: path.join(realpathSync(cwd), "design"), + design_path: path.join(realpathSync(cwd), ".firstdraft", "design"), moved_entry_count: 2, git_repository_preserved: false, git_index_replaced: false, @@ -122,15 +129,22 @@ async function verifyHappyRootCompile(context, planSource) { "class Movie < ApplicationRecord\nend\n", ); assert.equal( - readFileSync(path.join(cwd, "design", "product-notes.md"), "utf8"), + readFileSync(path.join(cwd, ".firstdraft", "design", "product-notes.md"), "utf8"), "Design notes\n", ); assert.equal( readFileSync( - path.join(cwd, "design", ".firstdraft", "foundation-plan.json"), + path.join(cwd, ".firstdraft", "design", ".firstdraft", "foundation-plan.json"), ).equals(planSource), true, ); + assert.deepEqual( + readFileSync(path.join(cwd, ".firstdraft", "submitted-foundation-plan.json")), + planSource, + ); + assert.equal(readFileSync(path.join(cwd, ".firstdraft", "gaps.json"), "utf8"), gapsSource); + assert.equal(existsSync(path.join(cwd, ".firstdraft", "design", ".firstdraft", "state.json")), true); + assert.equal(existsSync(path.join(cwd, "design")), false); assert.equal(existsSync(path.join(cwd, ".firstdraft-root-output")), false); assert.equal(existsSync(path.join(cwd, ".git")), false); assert.deepEqual( diff --git a/skills/create-full-stack-app/SKILL.md b/skills/create-full-stack-app/SKILL.md index 726dae3..8e2c702 100644 --- a/skills/create-full-stack-app/SKILL.md +++ b/skills/create-full-stack-app/SKILL.md @@ -11,7 +11,8 @@ workspace or **Compile and publish through First Draft**. Neither mode deploys. ## Current boundary -Targets plugin 0.2.5, published CLI 0.2.2, and service contract 0.3; compatibility does not establish catalog selection. +Targets plugin 0.3.0, CLI 0.3.0, and service contract 0.3; compatibility does not establish catalog selection. +This source candidate is unreleased. - Bounded generation includes Web Accounts, Policies, Scaffolds, development data, and selected iPhone/Android clients. Web uses stock Zinc tokens; Appearance controls theme, native colors, and Web icons. Omitted theme means @@ -87,7 +88,7 @@ firstdraft_cli --version firstdraft_cli --help ``` -Require the version probe to succeed with one exact `0.2.2` output line and no other output, and top-level help that +Require the version probe to succeed with one exact `0.3.0` output line and no other output, and top-level help that lists `generate`, `plan`, and `compilation`. Existing cross-repository contract tests own the exhaustive leaf-command matrix, including separate stdout and stderr assertions; startup should not rediscover it through a synthesized shell loop. The compatible CLI supplies these public commands: @@ -108,8 +109,7 @@ the already requested CLI operation without asking them to authorize it again. ## Initialize or resume the local Plan -After root adoption, run later First Draft commands from `design/`, where `.firstdraft/` moved; never initialize the -generated root. +After root adoption, run later First Draft commands from `.firstdraft/design/`; never initialize the generated root. If `.firstdraft/` does not exist, establish or propose the application name, then initialize: @@ -235,7 +235,8 @@ After the exact candidate's semantic read-back is approved, read ``` For POSIX root adoption, read the recovery preconditions and use `.`. It preserves a root `.git`, stages - the move to `design`, and creates no repository; absent output creates no `.git`. Neither mode starts Publication. + the move directly to `.firstdraft/design/`, and creates no repository; absent output creates no `.git`. Neither + mode starts Publication. - For selected Publication, run zero-flag mode: ```sh diff --git a/skills/create-full-stack-app/references/diagnostics-and-recovery.md b/skills/create-full-stack-app/references/diagnostics-and-recovery.md index 85d8a54..6733245 100644 --- a/skills/create-full-stack-app/references/diagnostics-and-recovery.md +++ b/skills/create-full-stack-app/references/diagnostics-and-recovery.md @@ -8,12 +8,12 @@ JSON object. An unrecognized prefixed line, a progress line after the envelope, interleaved output fail closed. Branch on the object's stable `error` and structured fields rather than the human-readable `detail` or broad process exit status. -The reviewed CLI contract in this stack is revision -`799a184cb2453ceadf5575f7b46ba975e084f192`, with JavaScript-source runtime digest -`e48e4b583e6f06a1d7a50aa19a87da2b24b225eaa5806f3130b9ad4ba6c43a72`. Its source package is -`@firstdraft.com/cli@0.2.2`, published under npm `next` with exact source-package parity. Check the command surface -rather than assuming the version alone establishes compatibility. CLI availability does not prove plugin/catalog -publication, service authentication, staging compatibility, or a complete user journey. +The reviewed source-candidate CLI is revision +`137ef9ceff7469e43f072009e3bba941abc6cd4c`, with JavaScript-source runtime digest +`7e9fdcf42dd887a6e8f6d9f17755600aa3b841a282f7fcdfaff638fe4467cb28`. Its source package is +`@firstdraft.com/cli@0.3.0` and is unpublished. Check the command surface rather than assuming the version alone +establishes compatibility. These source checks do not prove plugin/catalog publication, service authentication, +staging compatibility, or a complete user journey. ## Contents @@ -131,6 +131,11 @@ is unclear, ask before the mutation rather than treating a generic Compile reque ### Direct local output +CLI 0.3.0 archives the original workspace under `.firstdraft/design/`. This coordinated candidate is unpublished; +published CLI 0.2.2 still uses top-level `design/`. Use an explicitly identified compatible source candidate for this +flow; otherwise stop before root adoption and report the mismatch. Preserve the actual layout of an already +materialized application; this change does not migrate it. + `plan compile --output` accepts an absent destination or a spelling that resolves to the physical current directory. It validates the destination before Plan mutation, then starts one direct conditional Compilation. An absent path is checked again after analysis. Current-root adoption instead holds one owned transaction lock, rechecks the original @@ -141,33 +146,41 @@ a retained download shortcut, or archive tooling. The ordinary `./application` path must remain absent. Current-root adoption is selected only by `.`, `./`, an absolute current-directory spelling, or another spelling resolving to the same physical directory. It is POSIX-only, applies to any eligible real directory rather than a Drawing Board-specific shape, and requires a writable directory other -than the filesystem root with no top-level `design` or `.firstdraft-root-output`. Entries other than `.git` must be regular files -or real same-filesystem directories. A Git root must have a clean tracked worktree and index, no unmerged or sparse -state or in-progress Git operation, and no submodule metadata; untracked and ignored design material may remain. -A directory nested inside another worktree is ineligible. - -On success, root adoption creates `design`, moves every preexisting non-Git top-level entry beneath it, installs the -artifact at the root, and reports `root_adoption`. It preserves an existing root `.git` and history, prepares an index -that stages tracked paths beneath `design` plus exact generated paths, and does not stage previously untracked or -ignored files. A non-Git root stays non-Git. Inspect and commit the staged transformation before destructive Git -restoration. The CLI never creates a repository in either direct mode. The existing `.firstdraft` Plan and private -state move to `design/.firstdraft`; after success, run later First Draft `plan` and `compilation` commands from -`design/`, never initialize a replacement Project in the generated application root. Use the generated root for -ordinary application development. +than the filesystem root with no `.firstdraft/design` or top-level `.firstdraft-root-output`. Entries other than +`.git` must be regular files or real same-filesystem directories. A Git root must have a clean tracked worktree and +index, no unmerged or sparse state or in-progress Git operation, and no submodule metadata; untracked and ignored +design material may remain. A directory nested inside another worktree is ineligible. + +On success, root adoption preserves every preexisting non-Git top-level entry directly beneath `.firstdraft/design/`, +installs the artifact at the root, and reports `root_adoption`. It preserves an existing root `.git` and history, +prepares an index that stages tracked paths beneath `.firstdraft/design/` plus exact generated paths, and does not +stage previously untracked or ignored files. A non-Git root stays non-Git. Inspect and commit the staged +transformation before destructive Git restoration. The CLI never creates a repository in either direct mode. +The existing `.firstdraft` Plan and private state move to `.firstdraft/design/.firstdraft/`; they remain separate from +the generated root's +`.firstdraft/submitted-foundation-plan.json` and `.firstdraft/gaps.json`, whose exact artifact bytes the CLI still +verifies. After success, run later First Draft `plan` and `compilation` commands from `.firstdraft/design/`, never +initialize a replacement Project in the generated application root. Use the generated root for ordinary application +development; setup, preview, and tests do not require the archive. Retain it for later planning when useful. An `invalid_output_path` preflight makes no request. For an absent destination that appears during analysis, the second check may follow an accepted Plan push and analysis reads but still prevents Compilation. For root adoption, correct only the reported precondition or select an absent output; do not delete or overwrite owner material. A `materialization_failed` result with `reason: "root_rollback_incomplete"` retains the private -`.firstdraft-root-output` journal. Do not delete it or run Git restoration commands: inspect the journal and restore -its named index and paths to the recorded original identities before removing the transaction. Do not repeat an +`.firstdraft-root-output` journal. Do not delete it or run Git restoration commands. Reconcile the journal with the +current path identities; some moves may already have been reversed. Restore the exact original index through Git's +index lock when applicable. Reverse the remaining `artifact_moves` in reverse recorded order, moving each +destination back to its source, before reversing the remaining `design_moves` in reverse recorded order. Design +destinations are inside transaction staging: reversing the generated `.firstdraft` artifact move returns the +archive there before the original planning `.firstdraft` can return to the root. Never merge or overwrite either +`.firstdraft` directory. Verify the recorded original snapshot before removing the transaction. Do not repeat an outcome-unknown push. Direct mode never starts GitHub Publication. Success writes one validated JSON object containing the Project, Compilation, absolute output path, file count, and manifest digest. Absent output contains only artifact files and -creates no repository or `.git`. Root adoption additionally contains preserved `design` and existing `.git` paths -and reports the move/index facts above. The CLI runs no formatter and repairs nothing. When absent output is nested -in Drawing Board or another Git worktree, leave nested-Git initialization to that workspace's own workflow. +creates no repository or `.git`. Root adoption additionally contains preserved `.firstdraft/design/` and existing +`.git` paths and reports the move/index facts above. The CLI runs no formatter and repairs nothing. When absent +output is nested in Drawing Board or another Git worktree, leave nested-Git initialization to that workspace's own workflow. Progress contains only the analysis and Compilation messages from the stable table below. Direct Compile itself creates no Publication or repository; report later GitHub publication or pushes separately. A direct wait timeout @@ -182,9 +195,10 @@ output, then stop until First Draft or an operator reconciles the Project. Do no #### Root-adoption handoff After current-root adoption, the generated Rails application is the workspace root and the original design material -is under `design/`. Keep the existing root `.git`, history, and remotes when present. With an existing Git root, -inspect and commit the staged baseline before setup or feature work, keeping credentials such as `design/.env` and -private CLI state ignored. Make **Create GitHub repository**, or **Push** when a remote exists, an early checkpoint. +is under `.firstdraft/design/`. Keep the existing root `.git`, history, and remotes when present. With an existing Git +root, inspect and commit the staged baseline before setup or feature work, keeping credentials such as +`.firstdraft/design/.env` and private CLI state ignored. Make **Create GitHub repository**, or **Push** when a remote +exists, an early checkpoint. Confirm that the destination and applicable remote writes are authorized; approval of direct Compile alone does not authorize them. If the user declines, report that the baseline remains local and continue their requested local work without asking again. @@ -213,17 +227,18 @@ returned repository URL from the generated application's Git root: git remote add origin https://github.com/OWNER/REPO.git && git push -u origin HEAD ``` -Verify private visibility, the remote baseline commit, and the retained `design/` files before reporting the source -published to GitHub. Report its URL and commit; send later commits with `git push`. After an ambiguous creation -result, inspect the Codespace's repository association and the approved repository read-only before any retry. +Verify private visibility, the remote baseline commit, and the retained `.firstdraft/design/` files before reporting +the source published to GitHub. Report its URL and commit; send later commits with `git push`. After an ambiguous +creation result, inspect the Codespace's repository association and the approved repository read-only before any retry. If creation succeeded but pushing failed, retain the repository and resolve the push failure without creating another one. This publishes the existing Git history; do not invoke zero-flag Compile or First Draft Publication to publish an already-compiled workspace. Follow the generated root's README for setup and boot, then continue requested feature work in ordinary Rails source. -Do not run Drawing Board's moved `design/script/initialize-application` or `design/script/application-smoke`: those -helpers belong to the optional nested `./application` mode. Adding application features does not require First Draft -Capabilities, Plan edits, or another Compile. +Do not run Drawing Board's moved `.firstdraft/design/script/initialize-application` or +`.firstdraft/design/script/application-smoke`: those helpers belong to the optional nested `./application` mode. +Use ordinary generated application and platform commands for preview and tests rather than archived helpers. +Adding application features does not require First Draft Capabilities, Plan edits, or another Compile. #### UI continuation @@ -362,8 +377,8 @@ or provenance-changing response requires reconciling the CLI and service contrac `compilation.head_source_sha256`; 5. verifies transport metadata and exact bytes against the retained artifact digest, then validates the canonical Foundation Plan digest, envelope, manifest, paths, modes, Base64 contents, and file digests; and -6. installs an absent private sibling tree with one atomic rename, or applies the same current-root transaction used - by direct Compile. +6. installs an absent private sibling tree with one atomic rename, or applies direct Compile's + [current-root transaction and CLI compatibility boundary](#direct-local-output). `invalid_output_path` is safe to correct because no request was made. Preserve an existing destination; choose another absent path or correct only a named current-root precondition. Artifact or materialization errors are not a diff --git a/skills/create-full-stack-app/references/foundation-plan-019.md b/skills/create-full-stack-app/references/foundation-plan-019.md index 3d6615c..faf2893 100644 --- a/skills/create-full-stack-app/references/foundation-plan-019.md +++ b/skills/create-full-stack-app/references/foundation-plan-019.md @@ -116,17 +116,18 @@ The bundled schema was copied byte-for-byte from `c13aff4894073ea88fcf48c1f9900039e8b83966dee90591a7caa71421a5a666`. This is exact contract provenance, not release or execution evidence. -The current fixtures and check use reviewed root-output CLI revision -`799a184cb2453ceadf5575f7b46ba975e084f192`, with JavaScript-source runtime digest -`e48e4b583e6f06a1d7a50aa19a87da2b24b225eaa5806f3130b9ad4ba6c43a72`, as contract provenance rather than release +The source candidate and pinned contract check use reviewed root-output CLI revision +`137ef9ceff7469e43f072009e3bba941abc6cd4c`, with JavaScript-source runtime digest +`7e9fdcf42dd887a6e8f6d9f17755600aa3b841a282f7fcdfaff638fe4467cb28`, as contract provenance rather than release or execution evidence. It exposes `generate uuid`, `generate application-key`, `plan init`, `plan push`, `plan status`, zero-flag `plan compile`, direct `plan compile --output`, `compilation status`, and `compilation download`. It has no public `plan subject-id` or `plan publish`. The coordinated checkout declares the -experimental `@firstdraft.com/cli@0.2.2` package. Direct output accepts the ordinary absent destination and, on -POSIX, explicit current-root adoption with `--output .`; the recovery reference owns its preconditions. CLI 0.2.2 is -published under npm `next` with exact source-package parity. Check commands rather than inferring compatibility from -a version number. CLI availability does not prove plugin/catalog publication, authentication, staging compatibility, -or a complete user journey. +unpublished `@firstdraft.com/cli@0.3.0` package. Direct output accepts the ordinary absent destination and, on POSIX, +explicit current-root adoption with `--output .`; the recovery reference owns its preconditions. Check commands +rather than inferring compatibility from a version number. These source checks do not prove plugin/catalog +publication, authentication, staging compatibility, or a complete user journey. +CLI 0.3.0 uses `.firstdraft/design/`; published CLI 0.2.2 still uses top-level `design/`. See the +[direct-output compatibility boundary](diagnostics-and-recovery.md#direct-local-output). Selected native projects compose separate pinned Cores under `ios/` and `android/`. Each emitted `FOUNDATION_PROVENANCE.json` records the exact revision, archive digest, and replaced application seams. Use the diff --git a/test/interview-evaluation-foundation.test.mjs b/test/interview-evaluation-foundation.test.mjs index 5f5a8d9..ce2f1b4 100644 --- a/test/interview-evaluation-foundation.test.mjs +++ b/test/interview-evaluation-foundation.test.mjs @@ -655,8 +655,8 @@ test("pre-Compile evals separate approval, diagnostics, and execution", async () assert( expectationIncludes( root, - "Plan and private CLI state moved to design/.firstdraft", - "later First Draft plan or compilation command from design", + "Plan and private CLI state moved to .firstdraft/design/.firstdraft", + "later First Draft plan or compilation command from .firstdraft/design", "never initializes a replacement Project", ), ); diff --git a/test/plugin-release-order.test.mjs b/test/plugin-release-order.test.mjs index 982ae6a..db8ff1d 100644 --- a/test/plugin-release-order.test.mjs +++ b/test/plugin-release-order.test.mjs @@ -149,21 +149,14 @@ test("prospective release order rejects incoherent current identities", () => { ); }); -test("candidate reconciliation reads npm, fetched tags, and the catalog", async () => { +test("unpublished candidate reconciliation reads npm, fetched tags, and the catalog", async () => { const invocations = []; - const compatibilitySource = await readFile( - new URL("../release/compatibility.json", import.meta.url), - "utf8", - ); const result = await checkPluginReleaseOrder({ requireCurrentTag: false, root: fileURLToPath(new URL("../", import.meta.url)), spawn(command, arguments_, options) { invocations.push([command, arguments_, options]); if (command === "git") { - if (arguments_[0] === "show") { - return { status: 0, stderr: "", stdout: compatibilitySource }; - } return { status: 0, stderr: "", @@ -188,20 +181,16 @@ test("candidate reconciliation reads npm, fetched tags, and the catalog", async }, }); - assert.equal(result.candidateVersion, "0.2.5"); + assert.equal(result.candidateVersion, "0.3.0"); assert.deepEqual(result.catalogVersions, ["0.2.5"]); assert.deepEqual(result.taggedVersions, ["0.1.0-alpha.3", "0.1.0", "0.1.1", "0.2.0", "0.2.1", "0.2.2", "0.2.3", "0.2.4", "0.2.5"]); - assert.equal(result.releaseState, "catalog"); - assert.equal(invocations.length, 3); + assert.equal(result.releaseState, "prospective"); + assert.equal(invocations.length, 2); assert.deepEqual(invocations[1][1], [ "for-each-ref", "--format=%(refname:strip=3)", "refs/release-check/tags/claude-v*", ]); - assert.deepEqual(invocations[2][1], [ - "show", - "refs/release-check/tags/claude-v0.2.5:release/compatibility.json", - ]); }); test("default publish reconciliation rejects an already-promoted catalog", async (t) => { diff --git a/test/release-compatibility.test.mjs b/test/release-compatibility.test.mjs index bbddd14..c1cf52f 100644 --- a/test/release-compatibility.test.mjs +++ b/test/release-compatibility.test.mjs @@ -28,11 +28,11 @@ test("release compatibility matches the installable plugin manifest", async () = assert.deepEqual(compatibility, { format: "firstdraft.release-compatibility/1", component: "skills", - version: "0.2.5", + version: "0.3.0", plugin_source: { package: "@firstdraft.com/claude-code", tarball_sha256: - "d9f466076a05e867019bc3e20c35b89076cc83195e781d9203c039d166becde6", + "9648469aea9a3f0959e1e4eabde6626edc381d2f3b24ac3cb99be26176edf41b", }, requires: { api_contract: [">= 0.3.1", "< 0.4.0"], diff --git a/test/repository.test.mjs b/test/repository.test.mjs index 938ea0f..7c08ea1 100644 --- a/test/repository.test.mjs +++ b/test/repository.test.mjs @@ -106,9 +106,9 @@ const compilationEvidenceCliBaseline = const compilationEvidenceCliRuntimeDigest = "205e664df0ed9c7e63651a1c2c01e749a04d8879fe7f62cc4c1e13b66dce738d"; const cliContractBaseline = - "799a184cb2453ceadf5575f7b46ba975e084f192"; + "137ef9ceff7469e43f072009e3bba941abc6cd4c"; const cliContractRuntimeDigest = - "e48e4b583e6f06a1d7a50aa19a87da2b24b225eaa5806f3130b9ad4ba6c43a72"; + "7e9fdcf42dd887a6e8f6d9f17755600aa3b841a282f7fcdfaff638fe4467cb28"; const previousPublicCliContractBaseline = "d38ef3e54a6476b3a91f22a17fe7bd47aa6d6d68"; const previousPublicCliContractRuntimeDigest = @@ -143,7 +143,7 @@ const freshModelPublicationTree = "5815d094e204f8b3928ff5b5467ef85e2551d109"; const freshModelPublicationCommit = "37cc23d7cf7a1448fb7dfd4be8aee27c6e389ead"; -const preparedCliPackage = "@firstdraft.com/cli@0.2.2"; +const preparedCliPackage = "@firstdraft.com/cli@0.3.0"; const previousPreparedCliPackage = "@firstdraft.com/cli@0.2.1"; const prettyJsonSha256 = (value) => createHash("sha256") @@ -408,9 +408,12 @@ test("revision pins remain exhaustive across coordination surfaces", async () => /CLI contract check requires a checkout at the exact reviewed revision/, ); for (const source of [foundationPlanReference, diagnosticsReference]) { - assert(source.includes(`\`${preparedCliPackage}\``)); - assert.match(source, /published under npm `next` with exact source-package parity/); - assert.match(source, /does not prove plugin(?:\/| or )catalog\s+publication/); + const packageParagraph = source.split(/\n\s*\n/).find((paragraph) => + paragraph.includes(`\`${preparedCliPackage}\``), + ); + assert(packageParagraph, "the reference must identify the prepared CLI package"); + assert.match(packageParagraph, /\bunpublished\b/); + assert.match(source, /do not prove plugin(?:\/| or )catalog\s+publication/); } const workflow = ( @@ -538,7 +541,8 @@ test("historical plugin receipts stay separate from current availability", async for (const source of [candidateSkill, candidateModelingGuide]) { assert.doesNotMatch(source, /live [Pp]ublication remains unproved/); } - assert.match(candidateSkill, /published CLI 0\.2\.2/); + assert.match(candidateSkill, /CLI 0\.3\.0/); + assert.match(candidateSkill, /source candidate is unreleased/); assert.match( candidateSkill, /compatibility does not establish catalog selection/, @@ -917,8 +921,8 @@ test("Claude Code packaging selects canonical authoring source exactly once", as version: "0.2.5", registry: "https://registry.npmjs.org/", }); - assert.equal(packageTemplate.version, "0.2.5"); - assert.equal(installableManifest.version, "0.2.5"); + assert.equal(packageTemplate.version, "0.3.0"); + assert.equal(installableManifest.version, "0.3.0"); assert.equal(packageTemplate.dependencies, undefined); assert.deepEqual(installableManifest.skills, checkoutManifest.skills); assert.equal(installableManifest.userConfig, undefined); @@ -3024,7 +3028,7 @@ test("local capability check uses the shared helper for version and help probes" ); assert.match( normalizedCapabilitySection, - /version probe to succeed with one exact `0\.2\.2` output line and no other output.*?top-level help that lists `generate`, `plan`, and `compilation`.*?separate stdout and stderr assertions/, + /version probe to succeed with one exact `0\.3\.0` output line and no other output.*?top-level help that lists `generate`, `plan`, and `compilation`.*?separate stdout and stderr assertions/, ); assert.match( normalizedCapabilitySection, @@ -4065,8 +4069,8 @@ test("product Compile and retained Compilation evals match the CLI contract", as ); hasExpectation( root, - "Plan and private CLI state moved to design/.firstdraft", - "later First Draft plan or compilation command from design", + "Plan and private CLI state moved to .firstdraft/design/.firstdraft", + "later First Draft plan or compilation command from .firstdraft/design", "never initializes a replacement Project", ); hasExpectation(