diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b278158..e73b638 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -17,6 +17,7 @@ jobs: timeout-minutes: 10 permissions: contents: read + actions: read steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -50,10 +51,18 @@ jobs: git rev-list --first-parent refs/remotes/origin/main > "$RUNNER_TEMP/main-first-parent" grep -Fqx "$release_sha" "$RUNNER_TEMP/main-first-parent" # release-source-checks:end - - run: npm ci --ignore-scripts - - run: npm audit - - run: npm run check - - run: npm pack --dry-run --json --ignore-scripts + - name: Reuse successful CI for this source + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + release_sha="$(git rev-parse 'HEAD^{commit}')" + ci_url="$(gh run list --repo "$GITHUB_REPOSITORY" --workflow ci.yml \ + --branch main --event push --commit "$release_sha" --status success \ + --limit 1 --json url --jq '.[0].url // empty')" + test -n "$ci_url" || { echo "No successful main CI run for $release_sha." >&2; exit 1; } + echo "Reusing $ci_url" + - run: npm run pack:check publish: name: Publish to npm @@ -77,7 +86,7 @@ jobs: - name: Verify approved release env: NPM_RELEASE_ENABLED: ${{ vars.NPM_RELEASE_ENABLED }} - # Inline by design: the approved job reruns the reviewed checks before OIDC publication. + # Recheck mutable refs after environment approval, without rerunning CI. # test/package.test.js enforces synchronization with verify. run: | # release-source-checks:begin @@ -104,4 +113,4 @@ jobs: grep -Fqx "$release_sha" "$RUNNER_TEMP/main-first-parent" # release-source-checks:end - name: Publish verified source with OIDC - run: npm publish --access public --tag next --provenance --ignore-scripts + run: npm publish --access public --tag latest --provenance --ignore-scripts diff --git a/AGENTS.md b/AGENTS.md index 3620f20..c94ee0f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,10 +4,10 @@ Start with `docs/README.md` and follow its task routes. Detailed command semanti handled-error recovery in `docs/errors.md`, living release policy in `RELEASING.md`, and dated release observations in `docs/release-history.md`. When behavior changes, update its owning document in the same change. -- After merging to `main`, report the exact merged SHA and ask whether to coordinate and promote the three-repository - candidate. If the user defers, call the SHA unpromoted. Never publish npm, deploy First Draft, or release the - plugin without explicit approval. -- Treat npm publication under `next` as candidate availability, not a completed stable release. A stable CLI release - is complete only after that exact candidate passes its explicitly named release-specific qualification, is - separately approved, and is selected by npm's `latest` dist-tag. Preserve dated alpha observations as history - rather than describing them as current channel state. +- `firstdraft plan compile` defaults to local output in the current directory. GitHub publication requires + `--github`; Codespaces is a fallback. Keep Skill callers and recovery instructions aligned with this boundary. +- A coordinated release needs explicit approval once. Reuse an existing approval for its named scope; do not ask + again between repository publication steps. A merge alone does not authorize a release. +- Publish approved versions directly to `latest`. Reuse successful CI for the exact source and relevant smoke + evidence. When changed behavior needs a smoke, use local compilation; Codespaces and Revyl are not release gates. + Preserve dated release observations as history. diff --git a/README.md b/README.md index 531547d..4000f8c 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,13 @@ firstdraft is the command-line client shared by First Draft agents and automatio files, calls the versioned Service API, exposes reviewed analysis and GapSets, materializes verified Compilations, and coordinates private GitHub publication. -Trying First Draft as a tester? Start with the -[Drawing Board guide](https://github.com/firstdraft/drawing-board#build-an-app-with-first-draft), which installs a -compatible CLI and Skill together. +Start with the [local development guide](https://gist.github.com/raghubetina/3d424a97a1eaa6de8c406e67f32a237e): +install the CLI and Skill, then compile into your current folder with `firstdraft plan compile --output .`. No Drawing Board +clone or GitHub push is required. The [Drawing Board guide](https://github.com/firstdraft/drawing-board#build-an-app-with-first-draft) +is the Codespaces fallback. + +CLI 0.4 makes `--output .` the default. Keep the explicit flag with CLI 0.3, whose zero-flag command selects GitHub +publication. ## What this repository owns @@ -17,7 +21,7 @@ compatible CLI and Skill together. - retained Compilation inspection and artifact download; - terminal output, exit status, and recovery contracts; - the dependency-free npm package; and -- package provenance and release promotion. +- package provenance and publication. The Service owns Foundation Plan meaning and server-side lifecycle. Skills own the agent conversation. This repository owns the exact command and transport behavior between them. @@ -29,7 +33,7 @@ repository owns the exact command and transport behavior between them. | Change the CLI | [Agent instructions](https://github.com/firstdraft/cli/blob/main/AGENTS.md), then [documentation map](docs/README.md) | | Find a command or output contract | [Command reference](docs/commands.md) | | Interpret an error or recover safely | [Errors and recovery](docs/errors.md) | -| Prepare or promote a package | [Release runbook](RELEASING.md) | +| Prepare or publish a package | [Release runbook](RELEASING.md) | | Inspect dated package observations | [Release history](docs/release-history.md) | | Report a vulnerability | [Security policy](SECURITY.md) | @@ -84,7 +88,7 @@ npm install --global @firstdraft.com/cli ``` Pin an exact compatible version when a repeatable installation matters; [RELEASING.md](RELEASING.md) owns channel -and promotion meaning. +and release meaning. The published package: @@ -101,6 +105,5 @@ including the release runbook and dated release history, ships with the package. ## Release boundary -Merging source is not package publication. Publishing a candidate, moving npm dist-tags, coordinating the Skills -package, and promoting a stable release are distinct steps in [RELEASING.md](RELEASING.md). Verify the exact packed -digest and Service compatibility before any promotion. +Merging source is not package publication. An approved coordinated release publishes directly to `latest`, reusing +successful CI for the exact source. [RELEASING.md](RELEASING.md) owns the short release and recovery procedure. diff --git a/RELEASING.md b/RELEASING.md index f090f8e..30bad02 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -1,222 +1,104 @@ # Releasing First Draft CLI -This document owns living release policy and the operator runbook. Dated tag, package, qualification, and dist-tag -observations belong in [release history](docs/release-history.md) and must be rechecked before an operation. - -Publishing is a separate, explicit action after a release-preparation pull request has merged. npm registry bytes -and package versions cannot be replaced, so do not create or push a release tag as a dry run. - -Only a version that npm reports as published or a protected release tag that exists is immutable. Before either -exists, an unpublished candidate may be revised without changing its proposed version; its exact source SHA and -reviewed digests identify it. Reconcile the registry and remote tags before deciding that a version was consumed. - -## Pre-1.0 version and channel policy - -Before `1.0.0`, First Draft CLI uses ordinary `0.MINOR.PATCH` versions. Increase `MINOR` for a breaking -compatibility-line change. Increase `PATCH` for a change that is otherwise backward-compatible within the current -minor line. Never reuse a published version to preserve compatibility; publish the next version required by this -policy. Do not add aliases or shims solely to make a breaking compatibility line look patch-compatible. The policy -applies to ordinary versions; historical prereleases do not establish an ordinary compatibility line. - -Version semantics and npm distribution channels are independent. An approved candidate is published first under -the approval-gated `next` tag even when it has an ordinary version such as `0.1.0`. The release workflow does not -move `latest`. Moving `latest` requires a later, separate approval after the exact `next` candidate has completed -its explicitly named release-specific qualification. Candidate publication is not stable release completion. A -stable CLI release is complete only when that separately approved candidate is selected by npm's `latest` dist-tag. -Release-specific qualification means the exact gate named for that candidate; it does not imply unrelated or full -service qualification. Until promotion, `latest` remains the supported stable release; a distinct `next` candidate -is supported only for its named qualification. When both tags identify one version, that version fills both roles. - -## Coordinated candidate eligibility - -`release/compatibility.json` declares this package's SemVer version, the First Draft API-contract range it accepts, -and the exact Foundation Plan formats it accepts. It is source-only release metadata and is intentionally absent from -the npm tarball. The normal test suite validates the manifest's shape, keeps its version equal to `package.json`, and -binds its Foundation Plan format to the implemented CLI constant. - -The `script/release_compatibility_check` evaluator in `firstdraft/firstdraft` reads this declaration with the matching -declarations from exact, clean checkouts of `firstdraft/firstdraft` and `firstdraft/skills`. It implements SemVer 2.0 -precedence. CLI `0.2.x` requires the service's `0.3.x` API contract because Analysis now returns the complete reviewed -GapSet and digest. The released CLI `0.1.0` accepts only API `0.2.x`, uses a generic 2 MiB response bound for Analysis, -and cannot safely consume every schema-valid API `0.3.x` result. CLI `0.2.x` retains that generic bound but gives -Analysis and Compilation artifacts dedicated 128 MiB bounds. CLI `0.3.x` retains those bounds and requires API -`0.4.x`, Plan `firstdraft.foundation-plan.sketch/0.20`, and target profile `rails-sketch/2026-09`. Its new minor line -combines that contract transition with the incompatible root archive-path change to `.firstdraft/design`; Skills -and other callers require the matching CLI line. Existing applications are not migrated. - -The API `0.4.x` transition keeps `/v1` paths, authentication, statuses, and ETag behavior. Its breaking boundary is -the new Plan and target identity, including removal of record-wide Validation targets, normalization order, -optional Home selection, and target realization changes. The Service owns those semantics. The CLI initializes -the current identity, preserves authored bytes when pushing, and validates the current artifact identity; it does -not translate earlier Plans or artifacts. The proposed CLI `0.3.0` may retain that version while unpublished and -untagged under the policy above. This source declaration does not establish package availability or deployment. - -Comparator arrays form one conjunction, while -`foundation_plan_formats` lists alternatives. A prerelease satisfies a comparator set only when a comparator -explicitly names a prerelease with the same major, minor, and patch numbers. Skills names the candidate CLI version -explicitly, so a stale comparator makes the three-repository candidate ineligible. -`firstdraft.release-compatibility/1` is intentionally closed. The evaluator in `firstdraft/firstdraft` rejects an -unrecognized format and unknown keys, so adding a key requires a coordinated compatibility-format bump rather than -silently changing version 1. - -A compatible result establishes candidate eligibility, not authorization or runtime proof. Exact Git SHAs identify -the three-repository candidate. A merge to `main` is integration only: report the merged SHA and ask the user whether -to coordinate the three repositories and promote that candidate. If promotion is declined, record the SHA as -unpromoted. - -Promotion is manual and approval-gated. One operator serializes mutations: qualify the exact candidate on staging, -obtain human approval, and only then promote the approved service revision to production or authorize the -corresponding npm and plugin releases. Do not publish npm, deploy either environment, or release the plugin merely -because the compatibility check passes. - -## Repository and registry controls - -Before a release, a repository administrator must confirm: - -1. Confirm `firstdraft/cli` is public. The release workflow deliberately removes checkout credentials and re-fetches - the public release refs anonymously. -2. Confirm `main` has pull-request and CI requirements and a `v*` tag ruleset restricts tag creation, update, and - deletion. -3. Confirm the GitHub environment named `npm` is restricted to release tags, requires an explicit reviewer, disables - administrator bypass, and defines `NPM_RELEASE_ENABLED=true`. The workflow fails before publishing when this - variable is absent. -4. Confirm that the `firstdraft.com` npm organization and `@firstdraft.com/cli` package still identify the intended - publisher and repository. The publisher account must have write-protecting 2FA enabled. Verify authenticated - identity, organization membership, package identity, and current tags: - - ```sh - npm whoami - npm org ls firstdraft.com --json - npm view '@firstdraft.com/cli' name repository.url versions dist-tags --json - ``` - -5. Before creating a `v` tag, verify [npm trusted publishing](https://docs.npmjs.com/trusted-publishers/) - for the exact package, repository, workflow, protected environment, and allowed publish operation. No persistent - npm credential is permitted for a release workflow. - -Use the repository-pinned Node.js 24.18.0 toolchain with npm 11.16.0 to verify the organization's durable read/write -access: - -```sh -npm --version -npm access list packages firstdraft.com:developers '@firstdraft.com/cli' --json -``` - -Require the package to report `read-write` access for the `developers` team. If access is missing or changed, stop: -restoring it is a separate administrator and security mutation, not a routine release step. - -For every release, inspect the existing trusted-publisher relationship read-only: - -```sh -npm trust list '@firstdraft.com/cli' -``` - -Confirm `npm trust list` reports type `github`, repository `firstdraft/cli`, file `publish.yml`, environment `npm`, -and permission `createPackage`, which is npm's trust-list vocabulary for the allowed publish operation. npm does not -validate the saved relationship by attempting an exchange, so each case-sensitive value must be inspected. If the -relationship is missing or changed, stop: restoring it is a separate administrator and security mutation, not a -routine release step. The publish job must remain on a GitHub-hosted runner with `id-token: write` and must not read -`NODE_AUTH_TOKEN`, an npm token, or any GitHub Actions secret. Confirm the repository and `npm` environment secret -lists contain no npm automation secret. Trusted publishing's short-lived OIDC exchange is the sole workflow -publication credential; an authentication failure stops the release and must never fall back to a persistent token. - -As optional defense-in-depth after trusted publication is operationally proven, an npm administrator may complete -the separate security-key ceremony and set package **Publishing access** to **Require two-factor authentication and -disallow tokens**. Do not report that setting as enabled until it is directly observed. - -## Prepare a release - -1. Update `package.json`, `package-lock.json`, and `release/compatibility.json` to the exact release version. -2. When that version changes, coordinate the matching explicit CLI comparator in `firstdraft/skills` before - qualification; a stale comparator intentionally makes the three-repository candidate ineligible. -3. Apply the pre-1.0 policy: use a minor increment for a breaking compatibility line and a patch increment for a - change that is otherwise backward-compatible. Keep the initial distribution under `next` independently of that - version choice. Do not move `latest` during release publication. -4. Confirm neither the exact package version nor its `v` tag already exists. If both remain absent, - the unpublished candidate may retain its proposed version while its exact SHA and digests are revised. -5. Re-run `npm trust list '@firstdraft.com/cli'`, verify the exact `github`/repository/file/environment/`createPackage` - relationship described above, and confirm the workflow contains no persistent npm credential or GitHub Actions - secret. -6. Update user-facing documentation and release notes for behavior changes. -7. Run: - - ```sh - npm ci --ignore-scripts - npm audit - npm run check - ``` - -8. Merge the reviewed pull request only after local and hosted checks pass. - -## Publish - -The manual boundary is creation of the version tag. From an up-to-date, clean `main`, verify the intended commit and -confirm that both the intended package version and `v` tag are absent. If either identity is already -consumed, prepare the next version required by the pre-1.0 policy rather than moving or reusing it. Otherwise create -and push the tag. Push one release tag at a time; the workflow serializes publication, but GitHub retains at most one -pending run in a concurrency group. - -The workflow rejects accidental or stale inputs unless they use a protected `v*` tag in `firstdraft/cli`, the tag -equals `v` plus the version in `package.json`, the remote tag still identifies the triggering commit, and that commit -appears in the first-parent history of `origin/main`. First-parent membership allows an older reviewed `main` state -after another change lands while rejecting intermediate commits from a merged side branch. The workflow reruns the -complete check, waits for approval in the `npm` environment, reverifies the remote refs, and publishes to the public -registry with provenance under `next`. It authenticates only through the exact npm trusted-publisher relationship and -the job's short-lived GitHub OIDC token; it reads no persistent npm credential or GitHub Actions secret. - -The tag ruleset and `npm` environment approval are the external trust boundary because a tag-push run loads its -workflow from the tagged commit. Before approving the `npm` deployment, the reviewer must confirm: - -- The tag, package version, and commit SHA are the intended release. -- The commit is a known reviewed state in protected `main` history and its required checks passed. -- `.github/workflows/publish.yml` at that commit is the reviewed workflow, still selects the `npm` environment, and - publishes the public `@firstdraft.com/cli` package only under `next` with provenance. -- npm lists the exact `github`/`firstdraft/cli`/`publish.yml`/`npm`/`createPackage` trusted-publisher relationship, - and the GitHub repository and `npm` environment contain no npm automation secret. -- The unprivileged verification job passed for that exact commit. - -Do not move or reuse a release tag. If the tagged commit is not a first-parent state of `main`, merge the intended -change and prepare a new version rather than moving an already shared tag. +An approved coordinated release publishes directly to npm's `latest` channel. Tests and review belong before merge; +publication reuses successful CI for the exact source. It does not repeat the suite or require a second +`next`-to-`latest` promotion. Dated observations remain in [release history](docs/release-history.md). + +A merge alone does not authorize publication. Obtain one approval for the intended coordinated release, or use the +approval already given for that scope. The existing GitHub `npm` environment protection still applies; its approval +executes the same release decision. Do not ask for another conversational approval between already-approved steps. + +## Version and compatibility policy + +Before `1.0.0`, use ordinary `0.MINOR.PATCH` versions: increase `MINOR` for a breaking compatibility-line change and +`PATCH` for a backward-compatible change within that line. Never reuse a published version or move a protected +release tag. An unpublished, untagged candidate can retain its proposed version while its source changes. + +CLI `0.4.x` makes `firstdraft plan compile` equivalent to `firstdraft plan compile --output .`; the former GitHub +default becomes explicit `--github`. This is a breaking CLI change from `0.3.x`, without a Service API change. +Both lines use API `0.4.x`, Plan `firstdraft.foundation-plan.sketch/0.20`, target `rails-sketch/2026-09`, and the +`.firstdraft/design` root archive. Existing applications and old Plans are not migrated. + +`release/compatibility.json` declares the package version, accepted API-contract range, and accepted Plan formats. +It is source-only metadata, validated by the normal test suite and absent from the npm tarball. Coordinate the +explicit CLI comparator and bundled CLI pin in `firstdraft/skills` when this version changes. The service's +`script/release_compatibility_check` compares the three exact revisions; compatibility establishes eligibility, +not authorization or runtime proof. Its closed `firstdraft.release-compatibility/1` format rejects unknown keys. + +## Prepare before merge + +1. Update `package.json`, `package-lock.json`, and `release/compatibility.json`, and align the Skills CLI requirement. +2. Update the command, error, and Skill guidance affected by the change. Preserve dated release evidence. +3. Run focused checks while developing and the repository's required CI for the merge candidate. For a fresh + checkout, the complete local check is `npm ci --ignore-scripts`, `npm audit`, then `npm run check`. +4. Review and merge the change. Wait for the existing `CI` workflow to pass for the selected `main` SHA; publication + uses that run instead of starting another one. + +Use existing smoke evidence when it covers the changed behavior. If changed CLI/Service/Skill behavior warrants a +live smoke, use a simple Plan, compile locally with `firstdraft plan compile --output .`, and boot the generated app locally +when runtime behavior changed. A CLI dispatch-only change can be covered by local command and packed-package tests. +Do not require Codespaces, GitHub Publication, native builds, or Revyl for a routine release. Codespaces is a fallback +development environment. Additional integration checks belong only to changes affecting those integrations. + +## Publish the approved source + +From a clean checkout of the selected `main` revision: + +1. Confirm the exact package version and `v` tag are both unused. If either identity is already + consumed, prepare the next version required by the pre-1.0 policy rather than moving or reusing it. +2. Confirm the intended three revisions are compatible and the coordinated release approval covers them. +3. Create and push `v` at that source revision. Push one release tag at a time; the workflow + serializes publication and GitHub retains at most one pending run in a concurrency group. +4. Approve the existing `npm` environment deployment for that tag. The workflow publishes with provenance under + `latest`; no separate dist-tag mutation is needed. + +The workflow requires a protected `v*` tag in `firstdraft/cli`, the matching `package.json` version, an unchanged +remote tag, and a commit in the first-parent history of protected `main`. It finds a successful `CI` push run for +that exact SHA using `gh run list`, checks the package file allowlist, then rechecks mutable refs after environment +approval. It does not install development dependencies, rerun tests or audit, or request interactive npm login. + +If CI is still running, let that run finish and rerun the failed publication verification job. Resolve failing +checks in CI itself; publication does not start a duplicate suite. A source fix after tagging requires a new version. +Do not retest unrelated surfaces merely because time has passed since merge. ## Verify and recover -After publication, inspect the registry before retrying any reported failure; the package may already exist. From -the tagged checkout, verify the exact version, `next` dist-tag, unchanged `latest` dist-tag, integrity metadata, and -provenance metadata: +After publication, inspect the registry before retrying a failed workflow; the immutable version may already exist: ```sh FD_CLI_RELEASE_VERSION="$(node -p "require('./package.json').version")" npm view "@firstdraft.com/cli@$FD_CLI_RELEASE_VERSION" \ - version dist.integrity dist.shasum repository.url engines bin --json + version dist.integrity dist.shasum dist.attestations repository.url engines bin --json npm dist-tag ls '@firstdraft.com/cli' ``` -Install that exact version into a fresh temporary prefix, confirm `firstdraft --version`, compare the -packed file list with the release workflow, and run `npm audit signatures` after an exact installation. +Confirm the intended version is `latest` and has integrity/provenance metadata. Install that exact version in a +temporary prefix, confirm `firstdraft --version`, and run `npm audit signatures` there to verify the published +artifact. This checks distribution; it does not repeat application qualification. Record the version, source, +package integrity, and any relevant smoke evidence in the dated release record. -If OIDC authentication fails, reconcile both the registry version and protected remote tag first. If only npm's -listed relationship is wrong, correct it on npm and rerun the existing workflow's failed jobs without changing the -tagged source. If the workflow filename, environment, permissions, or other identity at the tagged commit is wrong, -the protected tag is immutable: prepare the next version rather than moving the tag. Never add a token fallback. +If OIDC authentication fails, reconcile the registry version and protected tag before retrying. Correct a broken +trusted-publisher relationship when necessary, then rerun failed jobs at the existing tag. If the tagged workflow +identity itself is wrong, prepare a new version; never move the tag or add a persistent-token fallback. -A published version cannot be overwritten or reused. For a bad release, move `next` only to a known-good compatible -version if one exists; otherwise deprecate the bad version and publish a corrected higher version. Treat -unpublishing as an exceptional incident response, not a routine rollback. +For a bad release, move `latest` to a known-good compatible version as an incident rollback, or deprecate the bad +version and publish a corrected higher version. Unpublishing is exceptional incident response, not routine rollback. -## Promote the release-specific qualified candidate +## Publisher configuration -Publishing under `next` is not promotion to the default install channel. After the exact `next` version completes -its named release-specific qualification and a human separately approves promotion, one operator may move `latest` -to that exact version from the tagged checkout: +These are durable repository and npm controls, not a per-release account audit. Verify them when provisioning, +changing publisher configuration, or diagnosing an actual failure: -```sh -FD_CLI_RELEASE_VERSION="$(node -p "require('./package.json').version")" -npm dist-tag add "@firstdraft.com/cli@$FD_CLI_RELEASE_VERSION" latest -npm dist-tag ls '@firstdraft.com/cli' -``` +- `firstdraft/cli` is public; `main` requires pull requests and CI, and a `v*` ruleset restricts tag mutation. +- The `npm` GitHub environment is limited to release tags, requires its existing reviewer, disables administrator + bypass, and defines `NPM_RELEASE_ENABLED=true`. +- npm trusted publishing identifies package `@firstdraft.com/cli`, repository `firstdraft/cli`, workflow + `publish.yml`, environment `npm`, and permission `createPackage`. The publishing account retains the intended + organization access and write-protecting 2FA. Configure these with an administrator only when needed. +- Publication runs on a GitHub-hosted runner with `id-token: write`, pinned Node.js 24.18.0 and npm 11.16.0. npm's + short-lived OIDC exchange is the only publication credential; no persistent npm token or Actions secret is used. + The CI lookup uses GitHub's read-only workflow token. -Verify both tags after the mutation, then append the exact dated observation and qualification boundary to -[release history](docs/release-history.md). The stable CLI release is complete only after `next` and `latest` both -name the exact release-specific qualified version. Do not call a candidate fully promoted before that equality is -observed. Do not move `latest` merely because a release merged, published successfully, or passed candidate -compatibility checks, and do not use a dist-tag change to repair or disguise a bad immutable version. +Ordinary installation and use require no npm login. Ordinary trusted publication requires no local maintainer +login or per-release security-key ceremony. Request npm interaction only when npm requires it for a governance +change or an actual authentication failure. See [npm trusted publishing](https://docs.npmjs.com/trusted-publishers/). diff --git a/SECURITY.md b/SECURITY.md index d00c328..80b5449 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -6,10 +6,8 @@ sensitive details in a public Issue. ## Supported versions -During coordinated trials, the stable release currently identified by npm's `latest` tag receives security fixes. A -different version under the approval-gated `next` tag is supported only for its explicitly named release-specific -qualification; it does not displace the stable release before separate promotion approval. When `next` and `latest` -identify the same version, that release fills both roles. +The release currently identified by npm's `latest` tag receives security fixes. Approved releases publish directly +to `latest`; historical `next` tags do not establish a separate supported channel. Distribution channels are independent of version syntax. Before `1.0.0`, increasing the minor version starts a breaking compatibility line; increasing the patch version is otherwise backward-compatible within that line. All diff --git a/docs/README.md b/docs/README.md index 14f1eeb..435784d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -3,15 +3,16 @@ Use this page to find the narrowest authoritative document for a task. Runtime source and tests remain the final evidence for implemented behavior; if they contradict a document, surface the contradiction instead of guessing. -| If the task concerns... | Read first | -| -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | -| Beginner tester journey | [Drawing Board guide](https://github.com/firstdraft/drawing-board#build-an-app-with-first-draft) | -| Direct CLI installation, repository orientation, or package contract | [Root README](../README.md) | -| Commands, options, environment variables, API behavior, output, or materialization | [Command reference](commands.md) | -| Stable errors, exit codes, retry safety, ambiguous outcomes, or local recovery | [Errors and recovery](errors.md) | -| Version policy, release preparation, publication, verification, rollback, or promotion | [Release policy and runbook](../RELEASING.md) | -| What was observed for an earlier tag, package, or dist-tag | [Release history](release-history.md) | -| Vulnerability reporting | [Security policy](../SECURITY.md) | +| Task | Read first | +| -------------------------------- | ------------------------------------------------------------------------------------------------ | +| Local app development | [Local guide](https://gist.github.com/raghubetina/3d424a97a1eaa6de8c406e67f32a237e) | +| Codespaces fallback | [Drawing Board guide](https://github.com/firstdraft/drawing-board#build-an-app-with-first-draft) | +| Installation or package contract | [Root README](../README.md) | +| Commands, API, or output | [Command reference](commands.md) | +| Errors and recovery | [Errors and recovery](errors.md) | +| Versioning and publication | [Release policy and runbook](../RELEASING.md) | +| Dated release observations | [Release history](release-history.md) | +| Vulnerability reporting | [Security policy](../SECURITY.md) | ## Authority boundaries @@ -21,7 +22,7 @@ evidence for implemented behavior; if they contradict a document, surface the co - [errors.md](errors.md) owns handled-error interpretation and recovery guidance. - [RELEASING.md](../RELEASING.md) owns living release policy and the operator runbook. - [release-history.md](release-history.md) preserves dated release observations. Recheck live tags, package versions, - dist-tags, access, and trusted-publisher state before relying on them operationally. + and dist-tags before relying on them operationally; publisher configuration is checked when it changes or fails. - The source repository's `AGENTS.md` routes agent work; it should stay compact rather than duplicate these documents. ## Retrieval quality diff --git a/docs/commands.md b/docs/commands.md index a8c1969..66a8ca9 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -3,9 +3,9 @@ This page owns the detailed public semantics of the current command surface. Run `firstdraft --help` or a command group's `--help` for concise executable syntax. See [Errors and recovery](errors.md) before retrying a failed mutation. -The current `0.3.x` source line contains the auditable command shell, local Foundation Plan initialization, local +The current `0.4.x` source line contains the auditable command shell, local Foundation Plan initialization, local application-key and UUID generation, conditional whole-document push, whole-graph analysis status polling, direct -Compile-and-materialize and private publish orchestration, and retained-Compilation inspection. CLI `0.3.x` +Compile-and-materialize and private publish orchestration, and retained-Compilation inspection. CLI `0.4.x` requires the service's `0.4.x` API contract. See the [release policy](../RELEASING.md) for versioning and channel semantics and [release history](release-history.md) for the transition from prereleases. @@ -18,7 +18,8 @@ semantics and [release history](release-history.md) for the transition from prer | `firstdraft generate uuid` | No | Generate one or more Foundation Plan subject identities | | `firstdraft plan push` | Yes | Conditionally submit the exact whole Plan | | `firstdraft plan status` | Yes | Read or wait for the current whole-graph analysis | -| `firstdraft plan compile` | Yes | Push and analyze, then materialize or publish | +| `firstdraft plan compile` | Yes | Push and analyze, then materialize in the current folder | +| `firstdraft plan compile --github` | Yes | Push and analyze, then publish to private GitHub | | `firstdraft compilation status` | Yes | Inspect a retained Compilation by ID | | `firstdraft compilation download` | Yes | Verify and materialize a successful retained Compilation | @@ -134,14 +135,20 @@ a bounded number of times because the command sends only `GET` requests. See ## Compile the current Plan -To compile into a local application directory, run: +Compile into the current local folder: ```sh -firstdraft plan compile --output ./application +firstdraft plan compile ``` +This is equivalent to `firstdraft plan compile --output .`. Use `--output ./application` for another absent +directory, or `--github` to publish to a private GitHub repository. `--github` and `--output` are mutually exclusive. +No GitHub connection, repository clone, or push is required for local compilation. Compilation runs on the First +Draft service; output and the application runtime are local. CLI `0.3.x` used GitHub Publication as its default; +scripts that require that behavior must add `--github` when upgrading to `0.4.x`. + Both `plan compile` modes first push the exact current bytes in -`.firstdraft/foundation-plan.json`, even when those bytes are unchanged, and saves the accepted ETag using the same +`.firstdraft/foundation-plan.json`, even when those bytes are unchanged, and save the accepted ETag using the same contract as `plan push`. It then waits up to two minutes for an analysis whose graph version and `head_source_sha256` exactly match that accepted push, polling past a terminal result retained for an older Head. Invalid JSON, schema diagnostics, semantic diagnostics, a failed analysis, a superseded analysis, or a recurring @@ -154,21 +161,21 @@ the accepted source SHA-256 from the saved ETag, hashes the current local bytes, ### Materialize a direct Compilation -With `--output`, the CLI accepts either an explicit absent destination beneath an existing real directory or a path +Without `--github`, the CLI accepts either an explicit absent destination beneath an existing real directory or a path that resolves to the physical current directory. It validates either destination before pushing the Plan. An absent destination is checked again after analysis; root adoption instead holds its owned lock and performs the exact pre-move identity recheck described below. Other existing destinations remain invalid, so `--output ./application` retains its absent-directory contract. -The nested archive layout below describes the unreleased `0.3.0` candidate. +The nested archive layout below is shared by CLI `0.3.x` and `0.4.x`. [Published CLI `0.2.2`](release-history.md#022-publication-and-registry-observation) archives at top-level `design/`; existing applications are not migrated automatically. -`--output .` is the noninteractive root-adoption mode. `./`, an absolute spelling of the current directory, and +The default `--output .` is the noninteractive root-adoption mode. `./`, an absolute spelling of the current directory, and another spelling that resolves to that same physical directory select the same mode. It works at any real current directory that meets the preconditions below and does not recognize Drawing Board or another repository layout -specially. This first root-adoption contract supports POSIX filesystems; Windows retains absent-directory output -and refuses root adoption as `root_platform_unsupported`. Before starting Compilation, the CLI requires: +specially. Root adoption supports POSIX filesystems; on Windows, use `--output ./application` because current-folder +output returns `root_platform_unsupported`. Before starting Compilation, the CLI requires: - the current directory to be a real, writable, non-filesystem-root directory; - no existing `.firstdraft/design` archive or top-level `.firstdraft-root-output` transaction path, including @@ -274,10 +281,10 @@ only. ### Publish through GitHub -Without `--output`, the existing GitHub Publication journey remains unchanged: +Use the explicit GitHub option for the Publication journey: ```sh -firstdraft plan compile +firstdraft plan compile --github ``` Invoking this form authorizes the internal GitHub Publication lifecycle. The command writes stable human-readable @@ -312,8 +319,8 @@ exact Plan push and valid Analysis. ## Inspect a retained Compilation These lower-level commands are for callers that already hold a retained Compilation ID from authenticated API -metadata or operational tooling. The no-output `plan compile` form prints only the final repository URL, while -`plan compile --output` waits for and downloads its own direct Compilation: +metadata or operational tooling. `plan compile --github` prints only the final repository URL, while the default +`plan compile` form waits for and downloads its own direct Compilation: ```sh firstdraft compilation status 01900000-0000-7000-8000-000000000001 diff --git a/docs/errors.md b/docs/errors.md index 32938c2..481042a 100644 --- a/docs/errors.md +++ b/docs/errors.md @@ -22,11 +22,11 @@ phase-specific recovery: - `phase: "push"` means the Plan mutation may have been accepted. Stop and reconcile local Head state. Until First Draft has a Foundation Plan Head reconciliation endpoint, an accepted request whose response cannot be verified may require manual recovery. Do not construct an ETag from the Plan digest or trust an unverified response. -- `phase: "compilation"` means a direct `--output` Compilation may have started, but its retained identity is unknown. +- `phase: "compilation"` means a local Compilation (the default mode, with optional `--output`) may have started, but its retained identity is unknown. The CLI never repeats that `POST`. Do not start another Compilation until the Project is reconciled through First Draft or an operator can identify the retained work. - `phase: "publication"` means the singleton Publication mutation was not resolved. Do not run concurrent Compile - commands. After the prior invocation exits, wait and rerun `plan compile` with unchanged Plan bytes to safely + commands. After the prior invocation exits, wait and rerun `plan compile --github` with unchanged Plan bytes to safely reconcile or resume the retained singleton. `plan push` also reports `request_outcome_unknown` if a failure happens after sending its request. Local state remains @@ -45,12 +45,12 @@ will not repair it. Only the lower-level `compilation status ` command is read-only. Its `compilation_status_unavailable` result is safe to retry a bounded number of times; `invalid_compilation_status` requires contract reconciliation. A wait stops rather than following a changed -analysis or Compilation identity. This read-only retry guidance does not apply to `plan compile --output`, which +analysis or Compilation identity. This read-only retry guidance does not apply to `plan compile`, which starts a new Compilation after analysis. ## Direct Compilation recovery -Do not blindly rerun `plan compile --output` after its Compilation start was accepted or may have been accepted. +Do not blindly rerun `plan compile` after its Compilation start was accepted or may have been accepted. That command creates new work; it is not a retained-Compilation reconciliation command. If `request_outcome_unknown` reports `phase: "compilation"`, the start request did not yield a validated retained @@ -85,7 +85,7 @@ operation is safe to remove. After successful adoption, run retained status and After `compilation_wait_timed_out`, retained work may still continue. Use `firstdraft compilation status ` for one read-only status check; do not rerun -`plan compile --output`. `compilation_failed`, `compilation_cancelled`, and `compilation_changed` already carry the +`plan compile`. `compilation_failed`, `compilation_cancelled`, and `compilation_changed` already carry the validated `current` projection appropriate to their stopping boundary. Authentication recovery may refresh the credential, but it must continue from the retained ID rather than starting another Compilation. @@ -95,7 +95,7 @@ The Publication is a Project singleton. If its initial conditional `PUT` is ambi read-only singleton `GET` and never automatically repeats the mutation in that invocation. After `publication_status_unavailable` or `publication_wait_timed_out`, retained work may still continue. Do not run -concurrent Compile commands. Wait, then rerun `plan compile` with unchanged Plan bytes; the conditional request +concurrent Compile commands. Wait, then rerun `plan compile --github` with unchanged Plan bytes; the conditional request safely reconciles or resumes the same retained singleton without creating another Compilation, repository, or push. The same recovery applies when an invocation exits after an unresolved Publication start. @@ -123,10 +123,10 @@ stopped without following the replacement. | Analysis waits | `analysis_changed`, `wait_timed_out`, `analysis_wait_timed_out` | 1 | The pinned analysis changed or remained processing at the deadline. | | `plan compile` | `plan_not_valid` | 1 | Analysis completed without `valid`; `current` contains diagnostics and status. | | `plan compile` | `local_plan_changed` | 1 | Local bytes or saved state changed after acceptance, before the selected mutation. | -| `plan compile --output` | `compilation_start_rejected`, `compilation_status_unavailable`, `invalid_compilation_status` | 1 | Direct start was rejected or retained status failed; post-start errors include `current`. | -| `plan compile --output` | `compilation_changed`, `compilation_wait_timed_out`, `compilation_failed`, `compilation_cancelled` | 1 | The pinned direct Compilation changed, timed out, failed, or was cancelled. | -| `plan compile` | `publication_start_rejected`, `publication_status_unavailable`, `invalid_publication_status` | 1 | Publication start or status failed its validated transport contract. | -| `plan compile` | `publication_changed`, `publication_wait_timed_out`, `publication_failed`, `publication_cancelled` | 1 | The pinned Publication changed, timed out, or reached a non-success terminal state. | +| `plan compile` | `compilation_start_rejected`, `compilation_status_unavailable`, `invalid_compilation_status` | 1 | Direct start was rejected or retained status failed; post-start errors include `current`. | +| `plan compile` | `compilation_changed`, `compilation_wait_timed_out`, `compilation_failed`, `compilation_cancelled` | 1 | The pinned direct Compilation changed, timed out, failed, or was cancelled. | +| `plan compile --github` | `publication_start_rejected`, `publication_status_unavailable`, `invalid_publication_status` | 1 | Publication start or status failed its validated transport contract. | +| `plan compile --github` | `publication_changed`, `publication_wait_timed_out`, `publication_failed`, `publication_cancelled` | 1 | The pinned Publication changed, timed out, or reached a non-success terminal state. | | `compilation status`, `compilation download` | `compilation_status_unavailable`, `invalid_compilation_status` | 1 | The retained status could not be read or violated its exact contract. | | `compilation status --wait` | `compilation_changed`, `compilation_wait_timed_out` | 1 | Retained identity/provenance changed or the wait ended. | | `compilation download` | `compilation_not_succeeded` | 1 | Status was not `succeeded`; no artifact request was made. | diff --git a/package-lock.json b/package-lock.json index b735ff9..16b6f91 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@firstdraft.com/cli", - "version": "0.3.0", + "version": "0.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@firstdraft.com/cli", - "version": "0.3.0", + "version": "0.4.0", "license": "MIT", "bin": { "firstdraft": "bin/firstdraft.js" diff --git a/package.json b/package.json index 8732af2..cc461dd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@firstdraft.com/cli", - "version": "0.3.0", + "version": "0.4.0", "description": "Command-line interface for First Draft", "license": "MIT", "type": "module", @@ -29,7 +29,7 @@ "access": "public", "provenance": true, "registry": "https://registry.npmjs.org/", - "tag": "next" + "tag": "latest" }, "scripts": { "check": "npm run typecheck && npm run lint && npm run format:check && npm test && npm run pack:check && npm run pack:smoke", diff --git a/release/compatibility.json b/release/compatibility.json index 93a6e59..fc6e5ae 100644 --- a/release/compatibility.json +++ b/release/compatibility.json @@ -1,7 +1,7 @@ { "format": "firstdraft.release-compatibility/1", "component": "cli", - "version": "0.3.0", + "version": "0.4.0", "requires": { "api_contract": [">= 0.4.0", "< 0.5.0"], "foundation_plan_formats": ["firstdraft.foundation-plan.sketch/0.20"] diff --git a/scripts/smoke-package.js b/scripts/smoke-package.js index 04368eb..d26eb21 100644 --- a/scripts/smoke-package.js +++ b/scripts/smoke-package.js @@ -613,7 +613,7 @@ async function exercisePackedCompilation(projectDirectory) { ); const compiled = await spawnPackedCliAsync( - ["plan", "compile"], + ["plan", "compile", "--github"], projectDirectory, ); assert.deepEqual(compiled, { @@ -681,7 +681,7 @@ First Draft: Application compiled. } const rootOutput = await spawnPackedCliAsync( - ["plan", "compile", "--output", "."], + ["plan", "compile"], projectDirectory, ); const expectedCompilationStarts = process.platform === "win32" ? 1 : 2; diff --git a/src/cli.js b/src/cli.js index 9d4eb26..04f46fc 100644 --- a/src/cli.js +++ b/src/cli.js @@ -38,7 +38,7 @@ import { PlanCompileAnalysisRejectedError, PlanCompileAnalysisUnavailableError, PlanCompilePushRejectedError, - compilePlan, + compilePlanToGitHub, compilePlanToDirectory, } from "./commands/plan-compile.js"; import { initializePlan } from "./commands/plan-init.js"; @@ -182,9 +182,11 @@ const PLAN_COMPILE_HELP = `First Draft CLI Usage: firstdraft plan compile firstdraft plan compile --output + firstdraft plan compile --github Options: - --output Materialize the generated application here + --output Materialize here (default: .) + --github Publish to a private GitHub repository -h, --help Show help Environment: @@ -192,12 +194,11 @@ Environment: FIRSTDRAFT_API_URL Override the initial API origin The command submits the exact current whole-file Plan, waits for its analysis, -and proceeds only when that analysis is valid. Without --output, it conditionally -creates or replays the internal GitHub Publication lifecycle and prints the -private repository URL. With --output, it starts one direct Compilation and -atomically materializes the verified artifact into an absent directory, or -preserves existing root material under .firstdraft/design when the output is ., without -starting GitHub Publication. Progress is written to stderr. +and proceeds only when that analysis is valid. By default it materializes the +verified application in the current directory, preserving existing root material +under .firstdraft/design. --output can select another absent directory. +--github selects the GitHub Publication lifecycle and prints the private repository +URL; it cannot be combined with --output. Progress is written to stderr. `; const COMPILATION_HELP = `First Draft CLI @@ -329,7 +330,7 @@ const PLAN_COMPILE_ANALYSIS_NOT_VALID_DETAIL = const PLAN_COMPILE_LOCAL_PLAN_CHANGED_DETAIL = "The local Foundation Plan changed after validation. Run 'firstdraft plan compile' again to submit the current bytes."; const PLAN_COMPILE_DIRECT_REQUEST_OUTCOME_UNKNOWN_DETAIL = - "A Compilation may have started, but its response could not be verified. Do not rerun 'firstdraft plan compile --output' or start another Compilation until the current Project is reconciled."; + "A Compilation may have started, but its response could not be verified. Do not rerun 'firstdraft plan compile' or start another Compilation until the current Project is reconciled."; const PLAN_COMPILE_DIRECT_START_REJECTED_DETAIL = "First Draft rejected the direct Compilation request."; const PLAN_COMPILE_DIRECT_STATUS_UNAVAILABLE_DETAIL = @@ -339,7 +340,7 @@ const PLAN_COMPILE_DIRECT_STATUS_INVALID_DETAIL = const PLAN_COMPILE_DIRECT_CHANGED_DETAIL = "The pinned Compilation changed while being polled. The command stopped without downloading an artifact."; const PLAN_COMPILE_DIRECT_TIMEOUT_DETAIL = - "The retained Compilation is still processing after the bounded ten-minute wait. Use current.compilation.id with 'firstdraft compilation status'; do not rerun 'firstdraft plan compile --output' or start another Compilation."; + "The retained Compilation is still processing after the bounded ten-minute wait. Use current.compilation.id with 'firstdraft compilation status'; do not rerun 'firstdraft plan compile' or start another Compilation."; const PLAN_COMPILE_DIRECT_FAILED_DETAIL = "The pinned Compilation failed. No artifact was downloaded or materialized."; const PLAN_COMPILE_DIRECT_CANCELLED_DETAIL = @@ -349,19 +350,19 @@ const PLAN_PUBLISH_INCOMPATIBLE_STATE_DETAIL = const PLAN_PUBLISH_NOT_PUSHED_DETAIL = "The current Foundation Plan was not retained before the Publication request."; const PLAN_PUBLISH_LOCAL_PLAN_CHANGED_DETAIL = - "The local Foundation Plan changed after validation. Run 'firstdraft plan compile' again to submit the current bytes."; + "The local Foundation Plan changed after validation. Run 'firstdraft plan compile --github' again to submit the current bytes."; const PLAN_PUBLISH_REQUEST_OUTCOME_UNKNOWN_DETAIL = - "The Publication may have started, but its retained singleton status could not be verified. No mutation was retried. Do not run concurrent Compile commands. Wait, then rerun 'firstdraft plan compile' with unchanged Plan bytes to safely reconcile or resume the retained singleton."; + "The Publication may have started, but its retained singleton status could not be verified. No mutation was retried. Do not run concurrent Compile commands. Wait, then rerun 'firstdraft plan compile --github' with unchanged Plan bytes to safely reconcile or resume the retained singleton."; const PLAN_PUBLISH_START_REJECTED_DETAIL = "First Draft rejected the publication request."; const PLAN_PUBLISH_STATUS_UNAVAILABLE_DETAIL = - "Could not read the retained Publication status. The command stopped without starting another Publication. Do not run concurrent Compile commands. Wait, then rerun 'firstdraft plan compile' with unchanged Plan bytes to safely resume the retained singleton."; + "Could not read the retained Publication status. The command stopped without starting another Publication. Do not run concurrent Compile commands. Wait, then rerun 'firstdraft plan compile --github' with unchanged Plan bytes to safely resume the retained singleton."; const PLAN_PUBLISH_STATUS_INVALID_DETAIL = "First Draft returned an invalid publication status response. Retrying unchanged will not repair this protocol mismatch."; const PLAN_PUBLISH_CHANGED_DETAIL = "The pinned Publication changed while being polled. The command stopped without following a replacement."; const PLAN_PUBLISH_TIMEOUT_DETAIL = - "The retained Publication is still processing after the bounded ten-minute wait. This invocation stopped waiting, but retained work may continue. Do not run concurrent Compile commands. Wait, then rerun 'firstdraft plan compile' with unchanged Plan bytes to safely resume the retained singleton."; + "The retained Publication is still processing after the bounded ten-minute wait. This invocation stopped waiting, but retained work may continue. Do not run concurrent Compile commands. Wait, then rerun 'firstdraft plan compile --github' with unchanged Plan bytes to safely resume the retained singleton."; const PLAN_PUBLISH_FAILED_DETAIL = "The pinned Publication failed. Its validated status identifies the failed phase."; const PLAN_PUBLISH_CANCELLED_DETAIL = "The pinned Publication was cancelled."; @@ -1527,6 +1528,7 @@ async function runPlanCompile({ args: [...argv], options: { output: { type: "string" }, + github: { type: "boolean" }, help: { type: "boolean", short: "h" }, }, allowPositionals: false, @@ -1548,8 +1550,8 @@ async function runPlanCompile({ return 0; } - const output = parsed.values.output; - if (output !== undefined && output.length === 0) { + const { output, github } = parsed.values; + if (output !== undefined && (output.length === 0 || github)) { writeJson(stderr, { error: "invalid_arguments", detail: PLAN_COMPILE_INVALID_ARGUMENTS_DETAIL, @@ -1577,11 +1579,11 @@ async function runPlanCompile({ readStatus: planCompileReadStatus, onProgress: reportProgress, }; - if (output !== undefined) { + if (!github) { try { const result = await compilePlanToDirectory({ ...shared, - output, + output: output ?? ".", compilationSleep, compilationNow, compile: planCompileDownload, @@ -1594,7 +1596,7 @@ async function runPlanCompile({ } try { - const result = await compilePlan({ + const result = await compilePlanToGitHub({ ...shared, publicationSleep: planPublishSleep, publicationNow: planPublishNow, diff --git a/src/commands/plan-compile.js b/src/commands/plan-compile.js index c13258e..1eec749 100644 --- a/src/commands/plan-compile.js +++ b/src/commands/plan-compile.js @@ -80,7 +80,7 @@ export class PlanCompileAnalysisInvalidError extends Error { * * @param {CompilePlanOptions} options */ -export async function compilePlan({ +export async function compilePlanToGitHub({ cwd, apiUrl, fetchFunction, @@ -134,8 +134,8 @@ export async function compilePlan({ /** * Submit and analyze the exact current local Plan, then start one direct * Compilation and materialize its verified artifact into an absent directory - * or the eligible current directory. GitHub Publication remains the no-output - * mode owned by compilePlan. + * or the eligible current directory. GitHub Publication is the explicit + * --github mode owned by compilePlanToGitHub. * * @param {CompilePlanToDirectoryOptions} options */ diff --git a/test/package.test.js b/test/package.test.js index 44ae034..cadf200 100644 --- a/test/package.test.js +++ b/test/package.test.js @@ -101,39 +101,28 @@ test("ordinary pre-1.0 versions use the approval-gated distribution channel", () access: "public", provenance: true, registry: "https://registry.npmjs.org/", - tag: "next", + tag: "latest", }); }); -test("stable release completion requires qualified latest promotion", () => { +test("approved releases publish directly to latest and preserve immutable versions", () => { assert.match( agentInstructions, - /publication under `next` as candidate availability, not a completed stable release[\s\S]*?explicitly named release-specific qualification[\s\S]*?separately approved[\s\S]*?npm's `latest`/, + /Publish approved versions directly to `latest`/, ); assert.match( releasingGuide, - /Release-specific qualification means the exact gate named for that candidate; it does not imply unrelated or full[\s\S]*?service qualification/, - ); - assert.match( - releaseHistory, - /Later on August 7, 2026,[\s\S]*?`next`, while `latest`[\s\S]*?continued to identify `0\.1\.0-alpha\.2`[\s\S]*?On August 12, 2026,[\s\S]*?selected bounded CLI `0\.1\.0` user-journey smoke passed[\s\S]*?separate promotion approval[\s\S]*?both `next` and `latest` then identified ordinary version `0\.1\.0`[\s\S]*?Full\s+v14 service qualification remained separate and incomplete/, - ); - assert.match( - releasingGuide, - /Until promotion, `latest` remains the supported stable release; a distinct `next` candidate\s+is supported only for its named qualification\. When both tags identify one version, that version fills both roles\./, + /publication reuses successful CI for the exact source/, ); + assert.match(releasingGuide, /approval already given for that scope/); assert.match( securityGuide, - /stable release currently identified by npm's `latest` tag receives security fixes[\s\S]*?different version under the approval-gated `next` tag is supported only for its explicitly named release-specific[\s\S]*?does not displace the stable release before separate promotion approval[\s\S]*?When `next` and `latest`\s+identify the same version, that release fills both roles/, + /release currently identified by npm's `latest` tag receives security fixes/, ); assert.match( releaseHistory, /Protected tag `v0\.1\.0` and package version `0\.1\.0` were consumed and immutable/, ); - assert.match( - releaseHistory, - /Package version `0\.2\.1` and protected tag `v0\.2\.1` are consumed and immutable[\s\S]*?As observed on August 29, 2026,[\s\S]*?current-directory root-output[\s\S]*?`4352f64baf673ad93457e8bc84273e9d1d9a9501`[\s\S]*?`b43ba6de98e27328e548cc3410ba9f39dfa9fcee`[\s\S]*?was not part of those registry bytes/, - ); const consumedVersions = [ ...releaseHistory.matchAll( /(?:Package|package) version `([^`]+)`(?=[\s\S]{0,120}?consumed and immutable)/g, @@ -146,7 +135,7 @@ test("stable release completion requires qualified latest promotion", () => { ); assert.match( releasingGuide, - /If either identity is already[\s\S]*?consumed, prepare the next version required by the pre-1\.0 policy rather than moving or reusing it/, + /If either identity is already\s+consumed, prepare the next version/, ); assert.doesNotMatch( `${readme}\n${releasingGuide}`, @@ -154,13 +143,27 @@ test("stable release completion requires qualified latest promotion", () => { ); assert.doesNotMatch( releasingGuide, - /npm trust github '@firstdraft\.com\/cli'/, - "routine release instructions must not recreate trusted publishing", + /npm trust github|npm access grant|npm whoami|npm trust list/, ); +}); + +test("publication reuses successful exact-source CI instead of rerunning the suite", () => { + const verifyJob = workflowJob(publishWorkflow, "verify"); + assert.match(verifyJob, /actions: read/); + assert.match(verifyJob, /GH_TOKEN: \$\{\{ github\.token \}\}/); + assert.match( + verifyJob, + /gh run list --repo "\$GITHUB_REPOSITORY" --workflow ci\.yml/, + ); + assert.match( + verifyJob, + /--branch main --event push --commit "\$release_sha" --status success/, + ); + assert.match(verifyJob, /test -n "\$ci_url"/); + assert.match(verifyJob, /npm run pack:check/); assert.doesNotMatch( - releasingGuide, - /npm access grant/, - "routine release instructions must not mutate package access", + publishWorkflow, + /npm ci|npm audit|npm run check|npm test/, ); }); @@ -180,7 +183,7 @@ test("OIDC publication repeats every release source check", () => { (line) => line === npmApprovalGate, ); const publishCommand = - "npm publish --access public --tag next --provenance --ignore-scripts"; + "npm publish --access public --tag latest --provenance --ignore-scripts"; const publishCommandIndex = publishJob.indexOf(publishCommand); const publishInvocation = "npm publish"; const oidcPermission = "\n id-token: write\n"; diff --git a/test/plan-compile.test.js b/test/plan-compile.test.js index 71a1d8b..1d9fd2a 100644 --- a/test/plan-compile.test.js +++ b/test/plan-compile.test.js @@ -76,7 +76,7 @@ test("plan compile submits exact bytes, waits for valid analysis, and publishes }); const apiUrl = await listen(context, server); const cwd = localDirectory(context, PLAN_SOURCE); - const result = await invoke(["plan", "compile"], { cwd, apiUrl }); + const result = await invoke(["plan", "compile", "--github"], { cwd, apiUrl }); assert.equal(result.status, 0); assert.equal(result.stderr, SUCCESS_PROGRESS); @@ -205,7 +205,7 @@ test("plan compile may push unchanged bytes before analysis and Publication", as /** @type {unknown[]} */ const order = []; const expected = publicationBody(); - const result = await invoke(["plan", "compile"], { + const result = await invoke(["plan", "compile", "--github"], { cwd, planCompilePush: async (/** @type {{cwd: string}} */ options) => { order.push(["push", options.cwd]); @@ -335,90 +335,95 @@ test("plan compile root output locks before push and releases after invalid anal ); }); -test("plan compile root output materializes directly without Publication", async (context) => { - if (process.platform === "win32") return context.skip(); - const cwd = localDirectory(context, PLAN_SOURCE, { - api_url: "https://api.example.test", - foundation_plan_etag: ETAG, - }); - writeFileSync(path.join(cwd, "product-notes.md"), "Design notes\n"); - const artifact = directArtifactFixture(true); - /** @type {{input: string | URL | Request, init: RequestInit}[]} */ - const calls = []; - const result = await invoke(["plan", "compile", "--output", "."], { - cwd, - planCompilePush: successfulPush, - planCompileReadStatus: async () => ({ - status: 200, - body: analysisBody("valid"), - }), - planCompilePublish: async () => { - throw new Error("Publication must remain untouched"); - }, - fetchFunction: sequenceFetch( +for (const outputArgs of [[], ["--output", "."]]) { + test(`plan compile ${outputArgs.join(" ")} materializes root output without Publication`, async (context) => { + if (process.platform === "win32") return context.skip(); + const cwd = localDirectory(context, PLAN_SOURCE, { + api_url: "https://api.example.test", + foundation_plan_etag: ETAG, + }); + writeFileSync(path.join(cwd, "product-notes.md"), "Design notes\n"); + const artifact = directArtifactFixture(true); + /** @type {{input: string | URL | Request, init: RequestInit}[]} */ + const calls = []; + const result = await invoke(["plan", "compile", ...outputArgs], { + cwd, + planCompilePush: successfulPush, + planCompileReadStatus: async () => ({ + status: 200, + body: analysisBody("valid"), + }), + planCompilePublish: async () => { + throw new Error("Publication must remain untouched"); + }, + fetchFunction: sequenceFetch( + [ + jsonResponse(directCompilationBody("succeeded", artifact), 202, { + Location: directCompilationPath(), + }), + new Response(artifact.source, { + status: 200, + headers: { + "Content-Type": ARTIFACT_MEDIA_TYPE, + "Content-Length": String(artifact.source.byteLength), + "Cache-Control": "no-store, no-transform", + ETag: `"sha256:${artifact.sha256}"`, + }, + }), + ], + calls, + ), + }); + + assert.equal(result.status, 0, result.stderr); + assert.deepEqual( + calls.map((call) => [call.init.method, String(call.input)]), [ - jsonResponse(directCompilationBody("succeeded", artifact), 202, { - Location: directCompilationPath(), - }), - new Response(artifact.source, { - status: 200, - headers: { - "Content-Type": ARTIFACT_MEDIA_TYPE, - "Content-Length": String(artifact.source.byteLength), - "Cache-Control": "no-store, no-transform", - ETag: `"sha256:${artifact.sha256}"`, - }, - }), + ["POST", `https://api.example.test${compilationCollectionPath()}`], + ["GET", `https://api.example.test${directArtifactPath()}`], ], - calls, - ), + ); + assert.equal( + readFileSync(path.join(cwd, "README.md"), "utf8"), + "Movie Catalog\n", + ); + assert.equal( + readFileSync( + path.join(cwd, ".firstdraft/design/product-notes.md"), + "utf8", + ), + "Design notes\n", + ); + assert.equal( + readFileSync( + path.join(cwd, ".firstdraft/design/.firstdraft/foundation-plan.json"), + "utf8", + ), + PLAN_SOURCE.toString("utf8"), + ); + assert.equal(existsSync(path.join(cwd, ROOT_TRANSACTION_NAME)), false); + const output = JSON.parse(result.stdout).output; + assert.equal(output.path, realpathSync(cwd)); + assert.equal( + output.root_adoption.design_path, + path.join(realpathSync(cwd), ".firstdraft/design"), + ); + assert.equal(output.root_adoption.moved_entry_count, 2); + assert.equal( + readFileSync( + path.join(cwd, ".firstdraft/submitted-foundation-plan.json"), + "utf8", + ), + PLAN_SOURCE.toString("utf8"), + ); + assert.equal( + readFileSync(path.join(cwd, ".firstdraft/gaps.json"), "utf8"), + '{"gaps":[]}\n', + ); + assert.equal(existsSync(path.join(cwd, ".firstdraft/state.json")), false); + assert.equal(existsSync(path.join(cwd, "design")), false); }); - - assert.equal(result.status, 0, result.stderr); - assert.deepEqual( - calls.map((call) => [call.init.method, String(call.input)]), - [ - ["POST", `https://api.example.test${compilationCollectionPath()}`], - ["GET", `https://api.example.test${directArtifactPath()}`], - ], - ); - assert.equal( - readFileSync(path.join(cwd, "README.md"), "utf8"), - "Movie Catalog\n", - ); - assert.equal( - readFileSync(path.join(cwd, ".firstdraft/design/product-notes.md"), "utf8"), - "Design notes\n", - ); - assert.equal( - readFileSync( - path.join(cwd, ".firstdraft/design/.firstdraft/foundation-plan.json"), - "utf8", - ), - PLAN_SOURCE.toString("utf8"), - ); - assert.equal(existsSync(path.join(cwd, ROOT_TRANSACTION_NAME)), false); - const output = JSON.parse(result.stdout).output; - assert.equal(output.path, realpathSync(cwd)); - assert.equal( - output.root_adoption.design_path, - path.join(realpathSync(cwd), ".firstdraft/design"), - ); - assert.equal(output.root_adoption.moved_entry_count, 2); - assert.equal( - readFileSync( - path.join(cwd, ".firstdraft/submitted-foundation-plan.json"), - "utf8", - ), - PLAN_SOURCE.toString("utf8"), - ); - assert.equal( - readFileSync(path.join(cwd, ".firstdraft/gaps.json"), "utf8"), - '{"gaps":[]}\n', - ); - assert.equal(existsSync(path.join(cwd, ".firstdraft/state.json")), false); - assert.equal(existsSync(path.join(cwd, "design")), false); -}); +} test("plan compile --output rejects an existing destination before Plan mutation", async (context) => { const cwd = localDirectory(context, PLAN_SOURCE); @@ -810,10 +815,7 @@ test("direct Compilation timeout names read-only retained-ID recovery", async (c assert.equal(envelope.current.compilation.id, COMPILATION_ID); assert.equal(envelope.current.compilation.status, "queued"); assert.match(envelope.detail, /firstdraft compilation status/); - assert.match( - envelope.detail, - /do not rerun 'firstdraft plan compile --output'/, - ); + assert.match(envelope.detail, /do not rerun 'firstdraft plan compile'/); assert.equal(calls.length, 1); }); @@ -825,7 +827,7 @@ test("plan compile waits past a terminal analysis for the prior graph version", /** @type {unknown[]} */ const calls = []; let publications = 0; - const result = await invoke(["plan", "compile"], { + const result = await invoke(["plan", "compile", "--github"], { cwd, planCompilePush: async () => ({ status: 200, @@ -859,7 +861,7 @@ test("plan compile rejects an older Head at the accepted graph version", async ( }); const olderHead = "0".repeat(64); let publications = 0; - const result = await invoke(["plan", "compile"], { + const result = await invoke(["plan", "compile", "--github"], { cwd, planCompilePush: successfulPush, fetchFunction: sequenceFetch([ @@ -887,7 +889,7 @@ test("invalid JSON and schema diagnostics stop before analysis or Publication", const cwd = localDirectory(context, source); /** @type {unknown[]} */ const calls = []; - const result = await invoke(["plan", "compile"], { + const result = await invoke(["plan", "compile", "--github"], { cwd, apiUrl: "https://api.example.test", fetchFunction: sequenceFetch( @@ -921,7 +923,7 @@ test("semantic and failed analysis stop before Publication with structured statu }); let publications = 0; const current = analysisBody(status); - const result = await invoke(["plan", "compile"], { + const result = await invoke(["plan", "compile", "--github"], { cwd, planCompilePush: successfulPush, planCompileReadStatus: async () => ({ status: 200, body: current }), @@ -955,7 +957,7 @@ test("recurring diagnostics remain repairable and never trigger Publication", as }; for (let attempt = 0; attempt < 2; attempt += 1) { - const result = await invoke(["plan", "compile"], options); + const result = await invoke(["plan", "compile", "--github"], options); assertHandledFailure(result, "plan_not_valid"); assert.equal( errorEnvelope(result.stderr).current.analysis.diagnostics[0].code, @@ -971,7 +973,7 @@ test("the final local-byte check stops a stale analyzed Plan before Publication" foundation_plan_etag: ETAG, }); let networkRequests = 0; - const result = await invoke(["plan", "compile"], { + const result = await invoke(["plan", "compile", "--github"], { cwd, planCompilePush: successfulPush, planCompileReadStatus: async () => { @@ -1006,7 +1008,7 @@ test("the final local-byte check stops a stale analyzed Plan before Publication" test("push ambiguity, analysis failures, and rejected reads have distinct errors", async (context) => { const pushCwd = localDirectory(context, PLAN_SOURCE); - const push = await invoke(["plan", "compile"], { + const push = await invoke(["plan", "compile", "--github"], { cwd: pushCwd, apiUrl: "https://api.example.test", fetchFunction: async () => { @@ -1039,7 +1041,7 @@ test("push ambiguity, analysis failures, and rejected reads have distinct errors ]; for (const [response, error] of analysisFailures) { const cwd = localDirectory(context, PLAN_SOURCE); - const result = await invoke(["plan", "compile"], { + const result = await invoke(["plan", "compile", "--github"], { cwd, apiUrl: "https://api.example.test", fetchFunction: sequenceFetch([ @@ -1072,6 +1074,8 @@ test("help and invalid direct-output syntax have no prerequisites", async () => ["plan", "compile", "--output"], ["plan", "compile", "--output", "one", "--output", "two"], ["plan", "compile", "application"], + ["plan", "compile", "--github", "--output", "."], + ["plan", "compile", "--output", "app", "--github"], ]) { const invalid = await invoke(argv, { cwd: process.cwd(), diff --git a/test/plan-publish.test.js b/test/plan-publish.test.js index aa322d0..e18bb62 100644 --- a/test/plan-publish.test.js +++ b/test/plan-publish.test.js @@ -86,9 +86,11 @@ const PLAN_COMPILE_HELP = `First Draft CLI Usage: firstdraft plan compile firstdraft plan compile --output + firstdraft plan compile --github Options: - --output Materialize the generated application here + --output Materialize here (default: .) + --github Publish to a private GitHub repository -h, --help Show help Environment: @@ -96,12 +98,11 @@ Environment: FIRSTDRAFT_API_URL Override the initial API origin The command submits the exact current whole-file Plan, waits for its analysis, -and proceeds only when that analysis is valid. Without --output, it conditionally -creates or replays the internal GitHub Publication lifecycle and prints the -private repository URL. With --output, it starts one direct Compilation and -atomically materializes the verified artifact into an absent directory, or -preserves existing root material under .firstdraft/design when the output is ., without -starting GitHub Publication. Progress is written to stderr. +and proceeds only when that analysis is valid. By default it materializes the +verified application in the current directory, preserving existing root material +under .firstdraft/design. --output can select another absent directory. +--github selects the GitHub Publication lifecycle and prints the private repository +URL; it cannot be combined with --output. Progress is written to stderr. `; test("plan compile invokes Publication and one conditional singleton PUT and polls sequentially", async (context) => { @@ -151,7 +152,7 @@ test("plan compile invokes Publication and one conditional singleton PUT and pol const timeouts = []; /** @type {number[]} */ const delays = []; - const result = await invoke(["plan", "compile"], { + const result = await invoke(["plan", "compile", "--github"], { cwd, apiUrl: "https://canary-secret.example", createRequestSignal: (/** @type {number} */ timeoutMs) => { @@ -331,7 +332,7 @@ test("progress reports each safe GitHub phase, scheduled retry, and parked retry jsonResponse(publicationBody("succeeded")), ]; - const result = await invoke(["plan", "compile"], { + const result = await invoke(["plan", "compile", "--github"], { cwd, fetchFunction: sequenceFetch(responses), planPublishSleep: async () => {}, @@ -392,7 +393,7 @@ test("progress accepts every coordinated safe reason code", async (context) => { ); responses.push(jsonResponse(publicationBody("succeeded"))); - const result = await invoke(["plan", "compile"], { + const result = await invoke(["plan", "compile", "--github"], { cwd, fetchFunction: sequenceFetch(responses), planPublishSleep: async () => {}, @@ -419,7 +420,7 @@ test("a repeated singleton PUT accepts provenance matching local Plan state", as const cwd = remoteDirectory(context, "https://api.example.test"); /** @type {FetchCall[]} */ const calls = []; - const result = await invoke(["plan", "compile"], { + const result = await invoke(["plan", "compile", "--github"], { cwd, fetchFunction: sequenceFetch( [jsonResponse(publicationBody("succeeded"), 200)], @@ -439,7 +440,7 @@ test("an ambiguous PUT is reconciled by one safe singleton GET", async (context) const cwd = remoteDirectory(context, "https://api.example.test"); /** @type {FetchCall[]} */ const calls = []; - const result = await invoke(["plan", "compile"], { + const result = await invoke(["plan", "compile", "--github"], { cwd, fetchFunction: sequenceFetch( [ @@ -469,7 +470,7 @@ test("an ambiguous PUT does not adopt a singleton from a different Plan Head", a const retainedHead = "a".repeat(64); /** @type {FetchCall[]} */ const calls = []; - const result = await invoke(["plan", "compile"], { + const result = await invoke(["plan", "compile", "--github"], { cwd, fetchFunction: sequenceFetch( [ @@ -509,7 +510,7 @@ test("an invalid successful PUT response can reconcile to the exact singleton", const cwd = remoteDirectory(context, "https://api.example.test"); /** @type {FetchCall[]} */ const calls = []; - const result = await invoke(["plan", "compile"], { + const result = await invoke(["plan", "compile", "--github"], { cwd, fetchFunction: sequenceFetch( [ @@ -539,7 +540,7 @@ test("an unresolved ambiguous PUT remains outcome unknown without replaying the const cwd = remoteDirectory(context, "https://api.example.test"); /** @type {FetchCall[]} */ const calls = []; - const result = await invoke(["plan", "compile"], { + const result = await invoke(["plan", "compile", "--github"], { cwd, fetchFunction: sequenceFetch( [ @@ -620,7 +621,7 @@ test("local prerequisites reject before publication network access", async (cont project_id: PROJECT_ID, }); assertHandledFailure( - await invoke(["plan", "compile"], { + await invoke(["plan", "compile", "--github"], { cwd: unpushed, fetchFunction: inaccessible, }), @@ -634,7 +635,7 @@ test("local prerequisites reject before publication network access", async (cont foundation_plan_etag: '"opaque"', }); assertHandledFailure( - await invoke(["plan", "compile"], { + await invoke(["plan", "compile", "--github"], { cwd: opaque, fetchFunction: inaccessible, planCompilePush: async () => ({ @@ -657,7 +658,7 @@ test("local prerequisites reject before publication network access", async (cont Buffer.concat([PLAN_SOURCE, Buffer.from(" ")]), ); assertHandledFailure( - await invoke(["plan", "compile"], { + await invoke(["plan", "compile", "--github"], { cwd: changed, fetchFunction: inaccessible, }), @@ -667,7 +668,7 @@ test("local prerequisites reject before publication network access", async (cont const missingPlan = remoteDirectory(context, "https://api.example.test"); rmSync(planPath(missingPlan)); assertHandledFailure( - await invoke(["plan", "compile"], { + await invoke(["plan", "compile", "--github"], { cwd: missingPlan, fetchFunction: inaccessible, }), @@ -677,14 +678,14 @@ test("local prerequisites reject before publication network access", async (cont test("missing and rejected credentials use the stable authentication error", async (context) => { const cwd = remoteDirectory(context, "https://api.example.test"); - const missing = await invoke(["plan", "compile"], { + const missing = await invoke(["plan", "compile", "--github"], { cwd, apiToken: undefined, fetchFunction: inaccessibleFetch(), }); assertHandledFailure(missing, "authentication_required"); - const rejected = await invoke(["plan", "compile"], { + const rejected = await invoke(["plan", "compile", "--github"], { cwd, fetchFunction: sequenceFetch([ problemResponse( @@ -697,7 +698,7 @@ test("missing and rejected credentials use the stable authentication error", asy assertHandledFailure(rejected, "authentication_required"); assert.equal(errorEnvelope(rejected.stderr).status, 401); - const reconciliation = await invoke(["plan", "compile"], { + const reconciliation = await invoke(["plan", "compile", "--github"], { cwd, fetchFunction: sequenceFetch([ async () => { @@ -716,7 +717,7 @@ test("missing and rejected credentials use the stable authentication error", asy test("validated start rejections are distinct from unknown mutation outcomes", async (context) => { const cwd = remoteDirectory(context, "https://api.example.test"); - const rejected = await invoke(["plan", "compile"], { + const rejected = await invoke(["plan", "compile", "--github"], { cwd, fetchFunction: sequenceFetch([ problemResponse(412, "precondition_failed", "The Plan changed."), @@ -736,7 +737,7 @@ test("validated start rejections are distinct from unknown mutation outcomes", a }, }); - const malformed = await invoke(["plan", "compile"], { + const malformed = await invoke(["plan", "compile", "--github"], { cwd, fetchFunction: sequenceFetch([ new Response("canary-secret", { status: 500 }), @@ -752,7 +753,7 @@ test("validated timeout and server errors reconcile without replaying the PUT", const cwd = remoteDirectory(context, "https://api.example.test"); /** @type {FetchCall[]} */ const calls = []; - const result = await invoke(["plan", "compile"], { + const result = await invoke(["plan", "compile", "--github"], { cwd, fetchFunction: sequenceFetch( [ @@ -775,7 +776,7 @@ test("validated timeout and server errors reconcile without replaying the PUT", } const cwd = remoteDirectory(context, "https://api.example.test"); - const unresolved = await invoke(["plan", "compile"], { + const unresolved = await invoke(["plan", "compile", "--github"], { cwd, fetchFunction: sequenceFetch([ problemResponse(503, "publication_delayed", "Publication is delayed."), @@ -797,7 +798,7 @@ test("validated timeout and server errors reconcile without replaying the PUT", test("polling distinguishes unavailable and invalid status responses", async (context) => { const unavailableCwd = remoteDirectory(context, "https://api.example.test"); - const unavailable = await invoke(["plan", "compile"], { + const unavailable = await invoke(["plan", "compile", "--github"], { cwd: unavailableCwd, fetchFunction: sequenceFetch([ jsonResponse(publicationBody("compiling"), 201), @@ -828,7 +829,7 @@ First Draft: Compiling application... ...publicationBody("provisioning_repository"), canary: "canary-secret", }; - const invalid = await invoke(["plan", "compile"], { + const invalid = await invoke(["plan", "compile", "--github"], { cwd: invalidCwd, fetchFunction: sequenceFetch([ jsonResponse(publicationBody("compiling"), 201), @@ -880,7 +881,7 @@ test("polling rejects replacement identities, regressions, and repository mutati for (const { initial, changed } of cases) { const cwd = remoteDirectory(context, "https://api.example.test"); - const result = await invoke(["plan", "compile"], { + const result = await invoke(["plan", "compile", "--github"], { cwd, fetchFunction: sequenceFetch([ jsonResponse(initial, 201), @@ -899,7 +900,7 @@ test("polling rejects replacement identities, regressions, and repository mutati test("the bounded wait reports its last validated status", async (context) => { const cwd = remoteDirectory(context, "https://api.example.test"); let clock = 0; - const result = await invoke(["plan", "compile"], { + const result = await invoke(["plan", "compile", "--github"], { cwd, fetchFunction: sequenceFetch([ jsonResponse(publicationBody("compiling"), 201), @@ -963,7 +964,7 @@ test("terminal progress distinguishes Compilation outcomes from later GitHub out for (const [status, expectedError, changes, terminalProgress] of cases) { const cwd = remoteDirectory(context, "https://api.example.test"); - const result = await invoke(["plan", "compile"], { + const result = await invoke(["plan", "compile", "--github"], { cwd, fetchFunction: sequenceFetch([ jsonResponse(publicationBody(status, changes), 201), @@ -988,7 +989,7 @@ ${compiled ? "First Draft: Application compiled.\n" : ""}First Draft: ${terminal for (const status of ["failed", "cancelled"]) { const cwd = remoteDirectory(context, "https://api.example.test"); - const result = await invoke(["plan", "compile"], { + const result = await invoke(["plan", "compile", "--github"], { cwd, fetchFunction: sequenceFetch([ jsonResponse( @@ -1154,7 +1155,7 @@ test("exact response shapes and coherent terminal projections are required", asy for (const [index, body] of invalidBodies.entries()) { const cwd = remoteDirectory(context, "https://api.example.test"); - const result = await invoke(["plan", "compile"], { + const result = await invoke(["plan", "compile", "--github"], { cwd, fetchFunction: sequenceFetch([ jsonResponse(body, 201),