From 3b158b8814c4eedefceb44a17b82b8e4c34e6c78 Mon Sep 17 00:00:00 2001 From: leyoonafr Date: Sun, 23 Aug 2026 22:35:18 +0800 Subject: [PATCH 01/22] docs: define GitHub release notes policy --- AGENTS.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index b0eb454..a426361 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,6 +28,14 @@ Use `node:test` with `node:assert/strict`. Write behavior-focused test names and History uses Conventional Commit subjects such as `feat: complete human mode audit wizard (#56)` and `fix: support exact Node 20.12 runtime`. Keep commits scoped and imperative. Open feature and fix PRs against `dev`; only release promotion PRs target `main`. Include a concise rationale, linked issue, validation commands/results, and screenshots or terminal output for user-visible CLI changes. +## GitHub Release Notes + +Build every GitHub Release body from the exact previous-tag-to-current-tag range. Write a version-specific summary from that range; treat unchanged prose copied from an earlier release as a failed release check. + +Include a complete, deduplicated inventory of every issue resolved by the release and every pull request merged in the release range. Link each issue and pull request, and list every contributing developer by GitHub handle with the pull requests they contributed. When a category has no entries, state `None` explicitly. + +Use URLs that resolve from the published GitHub Release page. For repository files, prefer absolute GitHub URLs pinned to the release tag so the target is immutable. Before publishing, test every release-note link in its final rendered form and verify that each target exists and returns a non-404 response. Release-note preparation is complete only when the version-specific summary, full issue and pull-request inventory, contributor attribution, and link check all match the release tag. + ## Security & Generated Files Preserve local-first permission boundaries and never commit credentials, private repository data, reports, evidence, deployment identifiers, or personal data. Report vulnerabilities through `SECURITY.md`. Edit the canonical skill first, run `npm run build`, and commit synchronized adapter copies. From 8ff957719dee4ce75cba8688d99de4dfbfaae671 Mon Sep 17 00:00:00 2001 From: leyoonafr Date: Sun, 23 Aug 2026 22:37:24 +0800 Subject: [PATCH 02/22] docs: add project toolchain migration guide (#205) --- .../launchrally/skills/launchrally/SKILL.md | 1 + .../launchrally/skills/launchrally/SKILL.md | 1 + docs/getting-started/install.md | 4 + docs/getting-started/quickstart.md | 5 + .../experimental-0.4.2-announcement.md | 5 +- docs/maintainers/p1-migration-notes.md | 204 ++++++++++++++++++ packages/cli/README.md | 5 +- skills/launchrally/SKILL.md | 1 + test/phase-1-documentation.test.js | 61 ++++++ 9 files changed, 284 insertions(+), 3 deletions(-) diff --git a/adapters/claude/launchrally/skills/launchrally/SKILL.md b/adapters/claude/launchrally/skills/launchrally/SKILL.md index 14dc67f..f3729a1 100644 --- a/adapters/claude/launchrally/skills/launchrally/SKILL.md +++ b/adapters/claude/launchrally/skills/launchrally/SKILL.md @@ -20,6 +20,7 @@ Use the local CLI as the only authority for Checks, Evidence, Severity, release - When Audit or Verify returns an authenticated Core Journey request, read [references/protected-journeys.md](references/protected-journeys.md). - When a completed Audit or Verify contains `provider_tool_recoveries`, read [references/provider-tool-recovery.md](references/provider-tool-recovery.md). - When invoking the CLI or handling its states, read [references/cli-contract.md](references/cli-contract.md). +- For an exact Experimental 0.4.1-to-0.4.2 Project Toolchain update, follow the [version-specific migration authority](https://github.com/codeacme17/launchrally/blob/main/docs/maintainers/p1-migration-notes.md#project-toolchain-migration-041-to-042) and the typed lifecycle router in [references/cli-contract.md](references/cli-contract.md). - When inspecting or exchanging a Phase 1 architecture record, read [references/phase-1-contracts.md](references/phase-1-contracts.md). Contract availability alone does not make a Phase 1 operation executable. - When a typed `architect` interaction enters Product Intent discovery, read [references/product-intent.md](references/product-intent.md) before presenting semantic-analysis permission or confirmation. - When presenting a Capability Catalog, derived obligations, a Capability Graph, or an Integration Contract, read [references/capability-model.md](references/capability-model.md). diff --git a/adapters/codex/launchrally/skills/launchrally/SKILL.md b/adapters/codex/launchrally/skills/launchrally/SKILL.md index 0a42a11..0e7d181 100644 --- a/adapters/codex/launchrally/skills/launchrally/SKILL.md +++ b/adapters/codex/launchrally/skills/launchrally/SKILL.md @@ -19,6 +19,7 @@ Use the local CLI as the only authority for Checks, Evidence, Severity, release - When Audit or Verify returns an authenticated Core Journey request, read [references/protected-journeys.md](references/protected-journeys.md). - When a completed Audit or Verify contains `provider_tool_recoveries`, read [references/provider-tool-recovery.md](references/provider-tool-recovery.md). - When invoking the CLI or handling its states, read [references/cli-contract.md](references/cli-contract.md). +- For an exact Experimental 0.4.1-to-0.4.2 Project Toolchain update, follow the [version-specific migration authority](https://github.com/codeacme17/launchrally/blob/main/docs/maintainers/p1-migration-notes.md#project-toolchain-migration-041-to-042) and the typed lifecycle router in [references/cli-contract.md](references/cli-contract.md). - When inspecting or exchanging a Phase 1 architecture record, read [references/phase-1-contracts.md](references/phase-1-contracts.md). Contract availability alone does not make a Phase 1 operation executable. - When a typed `architect` interaction enters Product Intent discovery, read [references/product-intent.md](references/product-intent.md) before presenting semantic-analysis permission or confirmation. - When presenting a Capability Catalog, derived obligations, a Capability Graph, or an Integration Contract, read [references/capability-model.md](references/capability-model.md). diff --git a/docs/getting-started/install.md b/docs/getting-started/install.md index 8ce3633..336f3cf 100644 --- a/docs/getting-started/install.md +++ b/docs/getting-started/install.md @@ -2,6 +2,10 @@ LaunchRally `0.3.2` is the exact Stable release used by this guide. Stable availability follows the reviewed P0 Validated decision and satisfied Quality Floor. +Updating an initialized Experimental `0.4.1` project is a separate path. Follow +the [exact 0.4.1-to-0.4.2 Project Toolchain migration](../maintainers/p1-migration-notes.md#project-toolchain-migration-041-to-042) +without relabeling this Stable `0.3.2` installation path. + ## Supported environments The CLI requires Node.js 20.12.0 or newer and is verified with Node.js 20.12, 22, and 24 on macOS, Linux, and Windows. The commands work in POSIX shells, PowerShell, and `cmd.exe`; path syntax and manual removal commands differ by shell. diff --git a/docs/getting-started/quickstart.md b/docs/getting-started/quickstart.md index e27055a..fe79d1b 100644 --- a/docs/getting-started/quickstart.md +++ b/docs/getting-started/quickstart.md @@ -2,6 +2,11 @@ LaunchRally `0.3.2` is a public Stable release. Run it against a repository you control and review every disclosed read or write boundary before confirming it. Stable means the reviewed P0 Validated decision and Quality Floor requirements are satisfied. +Already using an initialized Experimental `0.4.1` project? Use the separate +[exact 0.4.1-to-0.4.2 Project Toolchain migration](../maintainers/p1-migration-notes.md#project-toolchain-migration-041-to-042). +That path keeps Launcher, selected Engine, project pin, Plugin, and release +channel distinct; it does not change the Stable quickstart below. + ## Direct CLI Quickstart LaunchRally's default interactive journey uses a user-managed PATH installation. Install the exact Launcher through your current npm prefix, then verify structured output before entering a repository: diff --git a/docs/maintainers/experimental-0.4.2-announcement.md b/docs/maintainers/experimental-0.4.2-announcement.md index c6080bb..7418c71 100644 --- a/docs/maintainers/experimental-0.4.2-announcement.md +++ b/docs/maintainers/experimental-0.4.2-announcement.md @@ -15,6 +15,7 @@ Phase 0 remains independently Product Complete, P0 Validated, and Stable at 0.3.2. npm `latest` continues to resolve to 0.3.2; 0.4.2 is available only from `experimental` or by exact version. -Review the [0.4.2 changelog](../../CHANGELOG.md), [migration and failed-release -guidance](p1-migration-notes.md), and [external verification +Review the [0.4.2 changelog](../../CHANGELOG.md), [exact 0.4.1-to-0.4.2 Project +Toolchain migration](p1-migration-notes.md#project-toolchain-migration-041-to-042), +[failed-release guidance](p1-migration-notes.md#failed-experimental-publication), and [external verification record](experimental-0.4.2-p1-evidence.md) before adoption. diff --git a/docs/maintainers/p1-migration-notes.md b/docs/maintainers/p1-migration-notes.md index ecb0e22..4da962c 100644 --- a/docs/maintainers/p1-migration-notes.md +++ b/docs/maintainers/p1-migration-notes.md @@ -3,6 +3,210 @@ LaunchRally 0.4.2 continues the additive Phase 1 layer. Existing Phase 0 projects and the public 0.3.2 Stable line remain independently valid. +## Project Toolchain migration: 0.4.1 to 0.4.2 + +This procedure is only for an initialized project whose established Engine pin +is `0.4.1`. LaunchRally 0.4.2 remains **Experimental** on npm's +`experimental` channel; completing this migration does not make Phase 1 P1 +Validated or Stable. Phase 0 `0.3.2` remains the release on npm `latest`. + +Treat these as five separate facts and lifecycles: + +- the **Launcher** is the user-managed `rally` dispatcher installed through the + current npm prefix; +- the selected **Engine** executes repository operations; +- the **project pin** and rebuildable Engine materialization make up the + repository-owned Project Toolchain; +- a Codex or Claude **Plugin** supplies host interaction guidance and never + selects or replaces the Engine; and +- `0.4.2` is on the **Experimental** channel, independently of those versions. + +Updating the Launcher does not update an initialized project's valid pin. Thus, +after the first install below, structured version output with +`launcher_version: "0.4.2"`, `cli_version: "0.4.1"`, and +`authority.source: "project_toolchain"` is expected. It proves that the newer +Launcher followed the established Engine; it is not a failed installation. +Only an explicitly confirmed `toolchain migrate` changes that pin. `init` is +not a migration mechanism. + +Before confirmation, review the complete preview. Migration changes only the +owned Project Toolchain package, lock, authority descriptor, and rebuildable +materialization. It preserves the Manifest, immutable Reports, Evidence, +Architecture history, Provider intent, host application source, application +dependencies, and external saved Reports. It marks the prior current Report +non-current with `execution_authority_changed`, so a fresh full Verify is +required after migration. + +Keep the original Manifest-bound source Audit Report used by confirmed Init. +In the examples, set `SOURCE_REPORT` to that external JSON file, not to the +prior or new `.launchrally/reports//record.json` current Report. + +### Current 0.4.2 confirmation behavior + +The shipped 0.4.2 `toolchain migrate` command does not provide a styled +in-process prompt. In default TTY Human Mode it emits the typed JSON permission +or migration preview and exits. Review that complete JSON, preserve its opaque +`interaction.resume_token`, and run the explicit resume command shown below. +The optional registry permission and the migration confirmation are separate +decisions. This is the current 0.4.2 behavior; it does not promise the future +in-process interaction tracked by issue #204. + +Agent/CI Mode may add `--json` and follow the same +`launchrally.dev/toolchain-lifecycle/v1` states. An Agent must validate typed +fields and preserve tokens verbatim rather than parse Human prose or infer a +permission or confirmation. + +### POSIX + +Set the two paths first. Keep the response files outside `.launchrally` and +delete them yourself after the migration if they are no longer needed. + +```sh +PROJECT_ROOT=/path/to/project +SOURCE_REPORT=/path/to/original-manifest-bound-audit-report.json +MIGRATION_RESPONSE=./launchrally-0.4.2-migration-response.json + +npm install --global @launchrally/cli@0.4.2 +rally --version --json +rally --version --json --cwd "$PROJECT_ROOT" +rally toolchain status --json --cwd "$PROJECT_ROOT" + +rally toolchain migrate --to 0.4.2 --cwd "$PROJECT_ROOT" > "$MIGRATION_RESPONSE" +node -e 'const fs = require("node:fs"); const value = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); console.log(value.status); console.log(JSON.stringify(value.request ?? {}, null, 2));' "$MIGRATION_RESPONSE" +``` + +Before migrating, require the repository-scoped version and status results to +identify the established 0.4.1 project pin and selected Engine separately from +the 0.4.2 Launcher. + +If the status is `needs_permission`, review the exact `npm_registry_read` +request. To deny it, replace `approved` with `denied`; denial preserves the +0.4.1 pin and returns `registry_permission_denied`. To approve only that bounded +read and obtain the migration preview: + +```sh +MIGRATION_TOKEN="$(node -e 'const fs = require("node:fs"); const value = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); process.stdout.write(value.interaction.resume_token);' "$MIGRATION_RESPONSE")" +rally toolchain migrate --to 0.4.2 --cwd "$PROJECT_ROOT" --resume "$MIGRATION_TOKEN" --permissions '{"npm_registry_read":"approved"}' > "$MIGRATION_RESPONSE" +node -e 'const fs = require("node:fs"); const value = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); console.log(value.status); console.log(JSON.stringify(value.preview ?? {}, null, 2));' "$MIGRATION_RESPONSE" +``` + +When the response is `needs_confirmation`, inspect every `preview.changes` +entry. Resume with `decline` to leave the pin and project unchanged, or confirm +that exact preview: + +```sh +MIGRATION_TOKEN="$(node -e 'const fs = require("node:fs"); const value = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); process.stdout.write(value.interaction.resume_token);' "$MIGRATION_RESPONSE")" +rally toolchain migrate --to 0.4.2 --cwd "$PROJECT_ROOT" --resume "$MIGRATION_TOKEN" --confirm confirm + +rally --version --json --cwd "$PROJECT_ROOT" +rally verify --cwd "$PROJECT_ROOT" --report "$SOURCE_REPORT" --scope full +``` + +Require `authority.state: "ready"`, `authority.source: "project_toolchain"`, +`launcher_version: "0.4.2"`, and `cli_version: "0.4.2"` before Verify. A +completed Verify creates a new current Report. Use the exact current Report path +printed by completion for Plan or Architect; retain `SOURCE_REPORT` as the +Manifest-bound input to future whole-release Verify runs. + +### PowerShell + +```powershell +$ProjectRoot = 'C:\path\to\project' +$SourceReport = 'C:\path\to\original-manifest-bound-audit-report.json' +$MigrationResponse = '.\launchrally-0.4.2-migration-response.json' + +npm install --global @launchrally/cli@0.4.2 +rally --version --json +rally --version --json --cwd $ProjectRoot +rally toolchain status --json --cwd $ProjectRoot + +rally toolchain migrate --to 0.4.2 --cwd $ProjectRoot | Set-Content -Encoding utf8 $MigrationResponse +$Migration = Get-Content -Raw $MigrationResponse | ConvertFrom-Json +$Migration.status +$Migration.request | ConvertTo-Json -Depth 20 +``` + +If `$Migration.status` is `needs_permission`, review the exact +`npm_registry_read` request. Use `denied` to preserve the 0.4.1 pin, or approve +only that request and obtain the preview: + +```powershell +rally toolchain migrate --to 0.4.2 --cwd $ProjectRoot --resume $Migration.interaction.resume_token --permissions '{"npm_registry_read":"approved"}' | Set-Content -Encoding utf8 $MigrationResponse +$Migration = Get-Content -Raw $MigrationResponse | ConvertFrom-Json +$Migration.status +$Migration.preview | ConvertTo-Json -Depth 20 +``` + +When the response is `needs_confirmation`, inspect every +`$Migration.preview.changes` entry. Resume with `decline` to change nothing, or +confirm the exact preview: + +```powershell +rally toolchain migrate --to 0.4.2 --cwd $ProjectRoot --resume $Migration.interaction.resume_token --confirm confirm + +rally --version --json --cwd $ProjectRoot +rally verify --cwd $ProjectRoot --report $SourceReport --scope full +``` + +Apply the same postconditions as the POSIX path. Use the new current Report for +Plan or Architect, and keep `$SourceReport` for future whole-release Verify. + +### Denial, recovery, restore, and downgrade + +- A denied registry read, `--confirm decline`, an abandoned preview, or Ctrl-C + before adoption preserves the 0.4.1 project authority. Start a new migrate + operation if an opaque token is lost or invalid; never reconstruct it. +- Missing registry access is not permission to use `sudo`, a floating version, + a silent npm action, a changed npm prefix or shell profile, a copied + unvalidated Engine, Provider installation or login, or a mutated application + dependency. Retry after the exact package is available in an authorized npm + cache, or explicitly approve the typed bounded registry read. +- A later lifecycle command recovers a safely recognizable interrupted + transaction before proceeding. Inconsistent or malformed transaction state + fails closed; preserve it and inspect `rally toolchain status --json --cwd + ` rather than editing owned files or invoking an Engine directly. +- `rally toolchain restore --cwd ` only rebuilds the established exact + pin; it does not undo a completed migration. It may independently request the + same bounded registry read. +- A deliberate supported downgrade uses the same preview/resume protocol, for + example `rally toolchain migrate --to 0.4.1 --cwd `. It again makes + the prior current Report non-current and requires fresh full Verify. There is + no automatic rollback or unsupported-version bypass. + +### Update a Plugin separately + +The migration does not update either host Plugin. If one is installed, update +it only after the Launcher and Project Toolchain checks above. + +#### Codex Plugin + +Replace the installed Codex Plugin and pin the marketplace checkout to the +exact release tag: + +```sh +codex plugin remove launchrally@launchrally +codex plugin marketplace remove launchrally +codex plugin marketplace add codeacme17/launchrally --ref v0.4.2 +codex plugin add launchrally@launchrally +codex plugin list --json +``` + +#### Claude Plugin + +For Claude Code, refresh the catalog and update the user-scoped Claude Plugin: + +```sh +claude plugin marketplace update launchrally +claude plugin update launchrally@launchrally --scope user +claude plugin list --json +``` + +Plugin update or removal leaves the Launcher, Project Toolchain, Manifest, +Reports, Evidence, Architecture history, Provider intent, application source, +and owner-restricted host resume registry unchanged. Removing that separate +host registry invalidates retained resumable host artifacts; it is not part of +Project Toolchain migration. + ## Adoption Install 0.4.2 only by selecting the non-stable `experimental` channel or the diff --git a/packages/cli/README.md b/packages/cli/README.md index 210d360..8bcce1e 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -32,12 +32,14 @@ TTY Human Verify keeps fresh Evidence review in the same process. It shows every Ordinary Init preserves an existing Manifest while it may preview adoption of a newer Report into immutable history. To replace incorrect or outdated release intent with a current corrected Audit, run `rally init --cwd . --report ./corrected-audit-report.json --rebind`, review the old/new source identities and exact Manifest diff, and confirm that separate preview. Rebind preserves the exact Project Toolchain and all prior Reports and Evidence. -`rally architect` is the read-only Phase 1 whole-product decision flow. It consumes a current full Report plus confirmed Product Intent, Capability Catalog, and Capability Graph files, returns a typed Blueprint before confirmation, and then accepts independent decision responses. A completed confirmed set includes an immutable Architecture Package bundle. Use `rally architecture-package --package --output ` to write only an explicitly selected pre-Init output. After Init, omit `--output` to inspect the exact local-history preview, then repeat with its exact `--resume --confirm confirm` to append the digest-bound package transactionally. Neither command performs Provider writes, stages files, commits files, or turns the Manifest into reasoning history. +`rally architect` is the read-only Phase 1 whole-product decision flow. It consumes a current full Report plus confirmed Product Intent, Capability Catalog, and Capability Graph files, returns a typed Blueprint before confirmation, and then accepts independent decision responses. A completed confirmed set includes an immutable Architecture Package bundle. Use `rally architecture-package --package --output ` to write only an explicitly selected pre-Init output. After Init, omit `--output`; TTY Human Mode shows the exact local-history preview and confirms the digest-bound append in the same process. Agent/CI Mode retains the explicit `--resume --confirm confirm` protocol. Neither command performs Provider writes, stages files, commits files, or turns the Manifest into reasoning history. `rally handoff` is the typed external Executor coordination flow for the current Task Graph frontier. It accepts exact reviewed Executor Descriptors and observed tool versions, groups compatible Tasks by their real authority boundary, and creates an unapproved Handoff Package for one selected batch. Only `--confirm confirm` approves that exact package; the CLI still performs no installation, login, credential collection, Provider write, deployment, or external execution. A supplied normalized Execution Receipt remains an unverified claim and routes to fresh Verify. Exact-version npm-exec remains a no-install trial and CI fallback. Keep its full prefix on every follow-up; see the [Quickstart](https://github.com/codeacme17/launchrally/blob/main/docs/getting-started/quickstart.md) for the directly executable sequence. +For an initialized Experimental 0.4.1 project, follow the [exact 0.4.1-to-0.4.2 Project Toolchain migration](https://github.com/codeacme17/launchrally/blob/main/docs/maintainers/p1-migration-notes.md#project-toolchain-migration-041-to-042). Updating the Launcher, project pin, and optional Plugin are separate actions. + ## Compatibility and boundaries The CLI requires Node.js 20.12.0 or newer and is verified on Node.js 20, 22, and 24. A supported Launcher follows a valid project Engine through versioned Execution Authority and never silently falls back when project authority is unavailable or invalid. @@ -50,6 +52,7 @@ When an approved read cannot find its official Provider executable, the Report p - [Install, lifecycle, and troubleshooting](https://github.com/codeacme17/launchrally/blob/main/docs/getting-started/install.md) - [Quickstart](https://github.com/codeacme17/launchrally/blob/main/docs/getting-started/quickstart.md) +- [0.4.1-to-0.4.2 Project Toolchain migration](https://github.com/codeacme17/launchrally/blob/main/docs/maintainers/p1-migration-notes.md#project-toolchain-migration-041-to-042) - [Privacy boundary](https://github.com/codeacme17/launchrally/blob/main/docs/concepts/privacy.md) - [Project data model](https://github.com/codeacme17/launchrally/blob/main/docs/concepts/data-model.md) diff --git a/skills/launchrally/SKILL.md b/skills/launchrally/SKILL.md index 0a42a11..0e7d181 100644 --- a/skills/launchrally/SKILL.md +++ b/skills/launchrally/SKILL.md @@ -19,6 +19,7 @@ Use the local CLI as the only authority for Checks, Evidence, Severity, release - When Audit or Verify returns an authenticated Core Journey request, read [references/protected-journeys.md](references/protected-journeys.md). - When a completed Audit or Verify contains `provider_tool_recoveries`, read [references/provider-tool-recovery.md](references/provider-tool-recovery.md). - When invoking the CLI or handling its states, read [references/cli-contract.md](references/cli-contract.md). +- For an exact Experimental 0.4.1-to-0.4.2 Project Toolchain update, follow the [version-specific migration authority](https://github.com/codeacme17/launchrally/blob/main/docs/maintainers/p1-migration-notes.md#project-toolchain-migration-041-to-042) and the typed lifecycle router in [references/cli-contract.md](references/cli-contract.md). - When inspecting or exchanging a Phase 1 architecture record, read [references/phase-1-contracts.md](references/phase-1-contracts.md). Contract availability alone does not make a Phase 1 operation executable. - When a typed `architect` interaction enters Product Intent discovery, read [references/product-intent.md](references/product-intent.md) before presenting semantic-analysis permission or confirmation. - When presenting a Capability Catalog, derived obligations, a Capability Graph, or an Integration Contract, read [references/capability-model.md](references/capability-model.md). diff --git a/test/phase-1-documentation.test.js b/test/phase-1-documentation.test.js index 984b2ab..138f32e 100644 --- a/test/phase-1-documentation.test.js +++ b/test/phase-1-documentation.test.js @@ -181,3 +181,64 @@ test("the canonical Skill routes the complete Phase 1 typed journey", async () = assert.match(journey, /Human Mode/iu); assert.match(journey, /cross-host/iu); }); + +test("the published Experimental migration path tracks the exact release version", async () => { + const [rootPackage, release] = await Promise.all([ + text("package.json").then(JSON.parse), + text("release/p1.json").then(JSON.parse), + ]); + const [ + migrationNotes, + announcement, + install, + quickstart, + skill, + codexSkill, + claudeSkill, + ] = await Promise.all([ + text(release.experimental_publication.migration_notes), + text(release.experimental_publication.announcement), + text("docs/getting-started/install.md"), + text("docs/getting-started/quickstart.md"), + text("skills/launchrally/SKILL.md"), + text("adapters/codex/launchrally/skills/launchrally/SKILL.md"), + text("adapters/claude/launchrally/skills/launchrally/SKILL.md"), + ]); + const version = rootPackage.version; + const heading = `Project Toolchain migration: 0.4.1 to ${version}`; + const migration = section(migrationNotes, heading); + const anchor = `p1-migration-notes.md#project-toolchain-migration-041-to-${version.replaceAll(".", "")}`; + + assert.equal(release.release_status, "experimental"); + assert.equal(release.experimental_publication.candidate_tag, `v${version}`); + assert.equal( + release.experimental_publication.migration_notes, + "docs/maintainers/p1-migration-notes.md", + ); + assert.match(migration, new RegExp(`@launchrally/cli@${version.replaceAll(".", "\\.")}`, "u")); + assert.match(migration, new RegExp(`toolchain migrate --to ${version.replaceAll(".", "\\.")}`, "u")); + assert.match( + migration, + new RegExp( + `launcher_version[^\\n]*${version.replaceAll(".", "\\.")}[\\s\\S]*cli_version[^\\n]*0\\.4\\.1`, + "u", + ), + ); + assert.match(migration, /### POSIX/u); + assert.match(migration, /### PowerShell/u); + assert.match(migration, /npm_registry_read/u); + assert.match(migration, /--resume/u); + assert.match(migration, /--confirm confirm/u); + assert.match(migration, /execution_authority_changed/u); + assert.match(migration, /Manifest-bound source Audit Report/iu); + assert.match(migration, /new current Report/iu); + assert.match(migration, /Codex Plugin/u); + assert.match(migration, /Claude Plugin/u); + + for (const navigation of [announcement, install, quickstart]) { + assert.ok(navigation.includes(anchor), `missing exact migration link: ${anchor}`); + } + for (const routedSkill of [skill, codexSkill, claudeSkill]) { + assert.match(routedSkill, /p1-migration-notes\.md#project-toolchain-migration-041-to-042/u); + } +}); From 81e1cb352dc89cfff9ba588ab25197571a655e0f Mon Sep 17 00:00:00 2001 From: leyoonafr Date: Sun, 23 Aug 2026 22:55:07 +0800 Subject: [PATCH 03/22] fix: complete remaining human mode flows (#187) --- packages/cli/bin/engine.js | 91 +++++- packages/cli/bin/human-handoff.js | 220 ++++++++++++++ packages/cli/bin/human-version.js | 28 ++ packages/cli/bin/prompt-adapters.js | 135 +++++++++ packages/cli/bin/rally.js | 5 + release/artifacts.json | 2 + scripts/test-release-artifacts.mjs | 55 ++++ test/handoff.test.js | 16 +- test/launcher.test.js | 455 +++++++++++++++++++++++++++- test/release.test.js | 3 + 10 files changed, 986 insertions(+), 24 deletions(-) create mode 100644 packages/cli/bin/human-handoff.js create mode 100644 packages/cli/bin/human-version.js diff --git a/packages/cli/bin/engine.js b/packages/cli/bin/engine.js index 7a6ccba..8715569 100644 --- a/packages/cli/bin/engine.js +++ b/packages/cli/bin/engine.js @@ -50,6 +50,11 @@ import { renderHumanArchitecturePackageOutcome, runHumanArchitecturePackage, } from "./human-architecture-package.js"; +import { renderHumanVersion } from "./human-version.js"; +import { + renderHumanHandoffOutcome, + runHumanHandoff, +} from "./human-handoff.js"; import { createSystemFilePicker } from "./system-file-picker.js"; import { consumeInvocationContext, @@ -448,6 +453,19 @@ function print(value) { return; } + if ( + value.operation === "version" + && ["completed", "unavailable", "execution_error"].includes(value.status) + ) { + const presentation = humanAuditPresentationOptions({ + args, + env: process.env, + output: process.stdout, + }); + process.stdout.write(`${renderHumanVersion(value, { styled: presentation.styled })}\n`); + return; + } + if ( value.operation === "audit" && ["needs_input", "needs_confirmation", "needs_permission"].includes(value.status) @@ -601,7 +619,7 @@ async function main() { if (command === "version") { const authority = await resolveExecutionAuthority({ cwd: optionValue("--cwd"), - launcher_version: VERSION, + launcher_version: invocationContext.launcher_version, }); const ready = authority.state === "ready"; const invalid = authority.state === "invalid_toolchain"; @@ -610,7 +628,7 @@ async function main() { status: ready ? "completed" : invalid ? "execution_error" : "unavailable", operation: "version", ...(ready ? { cli_version: authority.engine.version } : {}), - launcher_version: VERSION, + launcher_version: invocationContext.launcher_version, authority, ...(!ready ? { error: authority.state, @@ -1224,13 +1242,15 @@ async function main() { } } } catch { - print({ + const failure = { contract: CLI_INTERACTION_CONTRACT, status: "execution_error", operation: "handoff", error: "invalid_handoff_input_file", message: "Handoff requires readable Task Graph, Executor, tool, and review JSON files.", - }); + }; + if (json) print(failure); + else process.stdout.write(`${renderHumanHandoffOutcome(failure)}\n`); return 2; } let receipt; @@ -1239,13 +1259,74 @@ async function main() { try { receipt = JSON.parse(await readFile(receiptPath, "utf8")); } catch { - print({ + const failure = { contract: CLI_INTERACTION_CONTRACT, status: "execution_error", operation: "handoff", error: "invalid_execution_receipt_file", message: "The execution receipt JSON could not be read and parsed.", + }; + if (json) print(failure); + else process.stdout.write(`${renderHumanHandoffOutcome(failure)}\n`); + return 2; + } + } + if (!json) { + if (["--resume", "--select", "--confirm", "--choice"].some((option) => + args.includes(option))) { + process.stderr.write([ + "Human Mode does not accept structured Handoff decisions.", + "Use rally handoff --json with explicit resume and decision options for the Agent/CI protocol.", + ].join("\n") + "\n"); + return 2; + } + if (process.stdin.isTTY !== true) { + process.stderr.write([ + "Non-TTY Human Mode cannot coordinate Handoff decisions safely.", + "Use rally handoff --json for the resumable Agent/CI protocol.", + ].join("\n") + "\n"); + return 2; + } + const { createClackPromptAdapter, createPlainPromptAdapter } = await import( + "./prompt-adapters.js" + ); + const presentation = humanAuditPresentationOptions({ + args, + env: process.env, + output: process.stdout, + }); + const prompt = presentation.plain + ? createPlainPromptAdapter({ input: process.stdin, output: process.stderr }) + : await createClackPromptAdapter({ input: process.stdin, output: process.stderr }); + try { + const result = await runHumanHandoff({ + source, + receipt, + prompt, + runHandoff, + loadReceipt: async (receiptFile) => { + try { + return JSON.parse(await readFile(path.resolve(receiptFile), "utf8")); + } catch { + const error = new Error("The execution receipt JSON could not be read and parsed."); + error.code = "invalid_execution_receipt_file"; + throw error; + } + }, }); + return ["unavailable", "execution_error", "stale_input"].includes(result.status) + ? 2 + : 0; + } catch (error) { + if (error instanceof PromptCancelledError) { + process.stderr.write("Handoff cancelled. LaunchRally executed no external Task.\n"); + return 130; + } + process.stdout.write(`${renderHumanHandoffOutcome({ + status: "execution_error", + error: error.code ?? "handoff_human_mode_failed", + message: error.message, + })}\n`); return 2; } } diff --git a/packages/cli/bin/human-handoff.js b/packages/cli/bin/human-handoff.js new file mode 100644 index 0000000..c548ec7 --- /dev/null +++ b/packages/cli/bin/human-handoff.js @@ -0,0 +1,220 @@ +import { terminalSafeText } from "./human-architect.js"; + +function list(values, render = terminalSafeText, fallback = "None") { + return values?.length > 0 + ? values.map((value) => ` - ${render(value)}`) + : [` - ${fallback}`]; +} + +function candidateLines(candidate) { + return [ + `${candidate.recommended ? "Recommended" : "Alternative"}: ${terminalSafeText(candidate.batch_id)}`, + ` Executor: ${terminalSafeText(candidate.executor_id)}`, + ` Tasks: ${candidate.task_ids.map(terminalSafeText).join(", ")}`, + ` Authority: ${terminalSafeText(candidate.effect_class)} on ${terminalSafeText(candidate.target)}`, + ` Availability: ${candidate.available ? "available" : terminalSafeText(candidate.unavailable_reason)}`, + ` Tools: ${candidate.tools.map((tool) => `${terminalSafeText(tool.executable)}@${terminalSafeText(tool.exact_version)}`).join(", ")}`, + ` Authentication: ${terminalSafeText(candidate.authentication_state)} (${candidate.auth_assumptions.map(terminalSafeText).join(", ")})`, + ` Secret handling: ${terminalSafeText(candidate.secret_handling)}`, + ` Cancellation: ${terminalSafeText(candidate.cancellation)}`, + ` Partial failure: ${terminalSafeText(candidate.partial_failure)}`, + ]; +} + +export function renderHumanHandoffDiscovery(result) { + const lines = [ + "External Executor Discovery", + "LaunchRally does not install, log in, request credentials, or execute external Tasks.", + "Ready authority boundaries:", + ...list(result.preview?.user_visible_effects), + ]; + if (result.candidates?.length > 0) { + lines.push( + "Reviewed Executor candidates:", + ...result.candidates.flatMap(candidateLines), + ); + } + if (result.recovery) { + lines.push( + "Executor recovery:", + ` Reason: ${terminalSafeText(result.recovery.reason)}`, + ` Tool: ${terminalSafeText(result.recovery.tool.executable)}@${terminalSafeText(result.recovery.tool.exact_version)}`, + ` Official manual: ${terminalSafeText(result.recovery.official_manual.url)}`, + ...(result.recovery.installation_instructions?.length > 0 + ? [ + " User-managed commands (not executed):", + ...result.recovery.installation_instructions.map(({ command }) => + ` - ${[command.executable, ...command.arguments].map(terminalSafeText).join(" ")}`), + ] + : []), + ); + } + if (result.manual_path) { + lines.push( + "Manual or custom Executor path is available without granting authority.", + `Required capabilities: ${result.manual_path.required_capabilities?.map(terminalSafeText).join(", ") || "none"}`, + ); + } + return lines.join("\n"); +} + +export function renderHumanHandoffAuthority(result) { + const handoff = result.handoff_package; + const authority = handoff.authority_batch; + return [ + "Exact External Authority Preview", + `Handoff Package: ${terminalSafeText(handoff.handoff_id)}`, + `Environment: ${terminalSafeText(handoff.environment)}`, + `Executor: ${terminalSafeText(handoff.executor.id)}`, + "Tasks:", + ...list(handoff.task_ids), + "Allowed effects:", + ...list(authority.allowed_effects), + "Prohibited effects:", + ...list(authority.prohibited_effects), + "User-visible effects:", + ...list(authority.user_visible_effects), + `Authentication: ${terminalSafeText(authority.executor_requirements.authentication_state)} (${authority.executor_requirements.auth_assumptions.map(terminalSafeText).join(", ")})`, + `Secret handling: ${terminalSafeText(authority.executor_requirements.secret_handling)}`, + `Cancellation: ${terminalSafeText(authority.coordination.cancellation)}`, + `Partial failure: ${terminalSafeText(authority.coordination.partial_failure)}`, + "Confirmation grants only this exact external authority. LaunchRally still performs no external execution.", + ].join("\n"); +} + +export function renderHumanHandoffReceipt(result) { + const receipt = result.execution_receipt; + const outcomes = result.execution_outcomes ?? []; + return [ + receipt ? "Execution Receipt Review" : "Approved Handoff Package", + `Handoff Package: ${terminalSafeText(result.handoff_package?.handoff_id)}`, + `Approval: ${terminalSafeText(result.handoff_package?.approval?.state)}`, + ...(receipt ? [ + `Receipt: ${terminalSafeText(receipt.receipt_id)}`, + "Classification: claim only; not Machine Evidence; verification remains unverified.", + "Reported Task outcomes:", + ...list(outcomes, (outcome) => [ + terminalSafeText(outcome.task_id), + terminalSafeText(outcome.receipt_state), + outcome.remaining_work?.state === "required" + ? `remaining work ${terminalSafeText(outcome.remaining_work.coordination)}` + : "no remaining work reported", + ].join(" — ")), + ] : [ + "Run the approved package with the selected external Executor outside LaunchRally, then submit its normalized Execution Receipt.", + "The receipt must be secret-free and exactly bound to this Handoff Package.", + ]), + ].join("\n"); +} + +export function renderHumanHandoffOutcome(result) { + if (result.status === "completed" && result.next?.operation === "verify") { + return [ + "Handoff Ready for Fresh Verify", + "The Execution Receipt remains a claim and has not been promoted to Machine Evidence.", + `Verify scope: ${terminalSafeText(result.next.scope)}`, + "Task requests:", + ...list(result.next.task_requests, (request) => + `${terminalSafeText(request.task_id)} — ${terminalSafeText(request.scope)}; Evidence: ${request.evidence_targets.map(terminalSafeText).join(", ")}`), + "Next action: run fresh independent Verify for these exact targets.", + ].join("\n"); + } + if (result.outcome === "manual_or_custom_selected") { + return [ + "Manual or Custom Executor Selected", + "No external authority was granted and no external execution was started.", + ].join("\n"); + } + if ( + ["deferred", "receipt_deferred"].includes(result.outcome) + || (result.status === "resumable" && result.state === "verification_pending") + ) { + let detail = "No external authority was granted."; + if (result.state === "verification_pending") { + detail = "The Execution Receipt remains a claim and fresh Verify remains pending."; + } else if (result.outcome === "receipt_deferred") { + detail = "The approved package remains external; no receipt was accepted."; + } + return [ + "Handoff Deferred", + detail, + "Run Handoff again with current inputs when you are ready to continue.", + ].join("\n"); + } + if (result.status === "denied") { + return [ + "External Authority Declined", + "No external authority was granted and no external Task was executed by LaunchRally.", + ].join("\n"); + } + if (result.status === "cancelled") { + return [ + "Handoff Cancelled", + `Authority granted before cancellation: ${result.safety?.authority_granted ? "yes" : "no"}`, + "LaunchRally did not execute an external Task.", + ].join("\n"); + } + if (result.status === "stale_input") { + return [ + "Handoff Inputs Are Stale", + "Refresh the Task Graph and reviewed Executor inputs before granting authority.", + ].join("\n"); + } + if (result.status === "execution_error" || result.status === "unavailable") { + return [ + "Handoff Could Not Continue", + `Error: ${terminalSafeText(result.error)}`, + ...(result.message ? [`Message: ${terminalSafeText(result.message)}`] : []), + "No additional authority was granted by this failure.", + ].join("\n"); + } + return [ + "Handoff Complete", + `Outcome: ${terminalSafeText(result.outcome ?? result.status)}`, + "LaunchRally did not execute an external Task.", + ].join("\n"); +} + +export async function runHumanHandoff({ + source, + receipt, + prompt, + runHandoff, + loadReceipt, +}) { + let result; + let suppliedReceipt = receipt; + try { + await prompt.start?.("handoff"); + result = await runHandoff(source, {}); + while (["needs_input", "needs_confirmation", "resumable", "partial_completion"] + .includes(result.status)) { + const response = await prompt.respondHandoff(result, { + receiptAvailable: suppliedReceipt !== undefined, + }); + const options = { resume_token: result.resume_token }; + if (result.request?.kind === "executor_selection") { + options.selection = response.selection; + } else if (result.request?.kind === "authority_confirmation") { + options.confirmation = response.confirmation; + } else if (result.request?.kind === "execution_receipt") { + if (response.choice === "submit") { + if (suppliedReceipt === undefined) { + suppliedReceipt = await loadReceipt(response.receipt_path); + } + options.receipt = suppliedReceipt; + } else { + options.choice = response.choice; + } + } else { + options.choice = response.choice; + } + result = await runHandoff({}, options); + if (response.choice === "defer") break; + } + await prompt.finishHandoff(result); + return result; + } finally { + await prompt.close(); + } +} diff --git a/packages/cli/bin/human-version.js b/packages/cli/bin/human-version.js new file mode 100644 index 0000000..31f6c0a --- /dev/null +++ b/packages/cli/bin/human-version.js @@ -0,0 +1,28 @@ +import { terminalSafeText } from "./human-architect.js"; + +function styledText(value, style, enabled) { + return enabled ? `\u001B[${style}m${value}\u001B[0m` : value; +} + +function nextActionLabel(authority) { + const action = authority?.next_action; + if (!action) return "unavailable"; + if (action.operation === "none") return "none"; + return action.operation ?? "unavailable"; +} + +export function renderHumanVersion(result, { styled = false } = {}) { + const authority = result.authority ?? {}; + const engineLabel = authority.source === "project_toolchain" + ? "Project Engine" + : "Selected Engine"; + return [ + styledText("LaunchRally Version", "1;36", styled), + `Launcher: ${styledText(terminalSafeText(result.launcher_version), "1", styled)}`, + `${engineLabel}: ${styledText(terminalSafeText(result.cli_version ?? authority.engine?.version), "1", styled)}`, + `Authority: ${styledText(terminalSafeText(authority.state), "1", styled)} (${terminalSafeText(authority.source)})`, + `Compatibility: ${terminalSafeText(authority.engine?.compatibility)}`, + `Materialization: ${terminalSafeText(authority.materialization?.state)}`, + `Next action: ${terminalSafeText(nextActionLabel(authority))}`, + ].join("\n"); +} diff --git a/packages/cli/bin/prompt-adapters.js b/packages/cli/bin/prompt-adapters.js index 8080f8b..dd40280 100644 --- a/packages/cli/bin/prompt-adapters.js +++ b/packages/cli/bin/prompt-adapters.js @@ -22,6 +22,12 @@ import { renderHumanArchitecturePackageOutcome, renderHumanArchitecturePackagePreview, } from "./human-architecture-package.js"; +import { + renderHumanHandoffAuthority, + renderHumanHandoffDiscovery, + renderHumanHandoffOutcome, + renderHumanHandoffReceipt, +} from "./human-handoff.js"; import { renderHumanInit, renderHumanInitFullPreview } from "./human-init.js"; function styleTextSupportsArrays() { @@ -237,9 +243,79 @@ function operationTitle(operation) { if (operation === "verify") return "LaunchRally Verify"; if (operation === "architect") return "LaunchRally Architect"; if (operation === "architecture-package") return "LaunchRally Architecture Package"; + if (operation === "handoff") return "LaunchRally External Executor Handoff"; return "LaunchRally Audit"; } +function handoffChoiceOptions(result, { receiptAvailable = false } = {}) { + if (result.request?.kind === "executor_selection") { + return result.candidates.filter(({ available }) => available).map((candidate) => ({ + label: `${candidate.recommended ? "Recommended — " : ""}${candidate.executor_id}: ${candidate.effect_class} on ${candidate.target}`, + value: candidate.batch_id, + })); + } + if (result.request?.kind === "authority_confirmation") { + return [ + { label: "Confirm this exact external authority", value: "confirm" }, + { label: "Decline", value: "deny" }, + { label: "Cancel", value: "cancel" }, + ]; + } + if (result.request?.kind === "execution_receipt") { + return [ + { + label: receiptAvailable + ? "Submit the supplied normalized Execution Receipt" + : "Submit a normalized Execution Receipt file", + value: "submit", + }, + { label: "Defer receipt review", value: "defer" }, + { label: "Cancel external coordination", value: "cancel" }, + ]; + } + if (result.request?.kind === "fresh_verification") { + return [ + { label: "Continue to fresh Verify", value: "verify" }, + { label: "Defer fresh Verify", value: "defer" }, + { label: "Cancel", value: "cancel" }, + ]; + } + const labels = { + show_install_instructions: "Show reviewed user-managed installation instructions", + open_official_manual: "Show the official manual", + manual_or_custom: "Use a manual or custom Executor", + defer: "Defer Handoff", + cancel: "Cancel", + refresh: "Refresh Handoff inputs", + }; + return (result.request?.choices ?? []).map((choice) => ({ + label: labels[choice] ?? choice, + value: choice, + })); +} + +function handoffInitialValue(result) { + if (result.request?.kind === "executor_selection") { + return result.candidates.find(({ available, recommended }) => available && recommended) + ?.batch_id; + } + if (result.request?.kind === "authority_confirmation") return "deny"; + if (["execution_receipt", "fresh_verification"].includes(result.request?.kind)) { + return "defer"; + } + return result.request?.choices?.includes("defer") ? "defer" : undefined; +} + +function handoffPresentation(result) { + if (result.request?.kind === "authority_confirmation") { + return { title: "Exact authority preview", content: renderHumanHandoffAuthority(result) }; + } + if (["execution_receipt", "fresh_verification"].includes(result.request?.kind)) { + return { title: "Receipt and verification", content: renderHumanHandoffReceipt(result) }; + } + return { title: "Executor discovery", content: renderHumanHandoffDiscovery(result) }; +} + function terminalSafeText(value) { return stripVTControlCharacters(String(value)) .replace(/[\u0000-\u001f\u007f-\u009f]/gu, " ") @@ -1088,6 +1164,35 @@ export function createPlainPromptAdapter({ async finishArchitecturePackage(result, bundle) { write(output, renderHumanArchitecturePackageOutcome(result, bundle)); }, + async respondHandoff(result, context = {}) { + const presentation = handoffPresentation(result); + write(output, presentation.content); + const value = await choose( + result.request?.kind === "authority_confirmation" + ? "Grant only this exact external authority?" + : result.request?.kind === "execution_receipt" + ? "Review the external execution receipt:" + : result.request?.kind === "fresh_verification" + ? "Choose the fresh verification action:" + : "Choose how to continue:", + handoffChoiceOptions(result, context), + handoffInitialValue(result), + ); + if (result.request?.kind === "executor_selection") return { selection: value }; + if (result.request?.kind === "authority_confirmation") return { confirmation: value }; + if (result.request?.kind === "execution_receipt" && value === "submit") { + return { + choice: value, + ...(context.receiptAvailable + ? {} + : { receipt_path: (await ask("Execution Receipt JSON path:")).trim() }), + }; + } + return { choice: value }; + }, + async finishHandoff(result) { + write(output, renderHumanHandoffOutcome(result)); + }, async close() { signals.off("SIGINT", handleInterrupt); readline.close(); @@ -1524,6 +1629,36 @@ export async function createClackPromptAdapter({ common, ); }, + async respondHandoff(result, context = {}) { + const presentation = handoffPresentation(result); + clack.note(presentation.content, presentation.title, common); + const value = cancelled(await clack.select({ + ...common, + message: result.request?.kind === "authority_confirmation" + ? "Grant only this exact external authority?" + : result.request?.kind === "execution_receipt" + ? "Review the external execution receipt" + : result.request?.kind === "fresh_verification" + ? "Choose the fresh verification action" + : "Choose how to continue", + options: handoffChoiceOptions(result, context), + initialValue: handoffInitialValue(result), + }), clack, output, "Handoff"); + if (result.request?.kind === "executor_selection") return { selection: value }; + if (result.request?.kind === "authority_confirmation") return { confirmation: value }; + if (result.request?.kind === "execution_receipt" && value === "submit") { + return { + choice: value, + ...(context.receiptAvailable + ? {} + : { receipt_path: (await ask("Execution Receipt JSON path:", { required: true })).trim() }), + }; + } + return { choice: value }; + }, + async finishHandoff(result) { + clack.note(renderHumanHandoffOutcome(result), "Handoff outcome", common); + }, async close() { signals.off("SIGINT", handleInterrupt); }, diff --git a/packages/cli/bin/rally.js b/packages/cli/bin/rally.js index 35b11f0..1abef38 100755 --- a/packages/cli/bin/rally.js +++ b/packages/cli/bin/rally.js @@ -19,10 +19,12 @@ const REPOSITORY_OPERATIONS = new Set([ "audit", "architect", "architecture-package", + "handoff", "init", "plan", "providers", "verify", + "version", ]); const BUNDLED_ENGINE = fileURLToPath(new URL("./engine.js", import.meta.url)); @@ -134,6 +136,9 @@ async function main() { launcher_version: VERSION, }); if (authority.state !== "ready") { + if (command === "version") { + return await runEngine(BUNDLED_ENGINE, arguments_, invocationContext); + } const cwd = path.resolve(optionValue(arguments_, "--cwd") ?? process.cwd()); printAuthorityFailure( command, diff --git a/release/artifacts.json b/release/artifacts.json index bb5d323..cda9532 100644 --- a/release/artifacts.json +++ b/release/artifacts.json @@ -166,8 +166,10 @@ "bin/human-architecture-package.js", "bin/human-authenticated-journey.js", "bin/human-audit.js", + "bin/human-handoff.js", "bin/human-init.js", "bin/human-verify.js", + "bin/human-version.js", "bin/invocation-context.js", "bin/prompt-adapters.js", "bin/rally.js", diff --git a/scripts/test-release-artifacts.mjs b/scripts/test-release-artifacts.mjs index 05f670a..51c87bb 100644 --- a/scripts/test-release-artifacts.mjs +++ b/scripts/test-release-artifacts.mjs @@ -113,6 +113,32 @@ async function runNpm(arguments_, options = {}) { : run("npm", arguments_, options); } +async function runInPty(command, arguments_, options = {}) { + if (process.platform === "win32") return null; + const runner = [ + "import errno, fcntl, os, pty, struct, subprocess, sys, termios", + "master, slave = pty.openpty()", + "fcntl.ioctl(slave, termios.TIOCSWINSZ, struct.pack('HHHH', 30, 100, 0, 0))", + "child = subprocess.Popen(sys.argv[1:], stdin=slave, stdout=slave, stderr=slave, close_fds=True)", + "os.close(slave)", + "chunks = []", + "while True:", + " try:", + " chunk = os.read(master, 4096)", + " except OSError as error:", + " if error.errno == errno.EIO:", + " break", + " raise", + " if not chunk:", + " break", + " chunks.append(chunk)", + "os.close(master)", + "sys.stdout.buffer.write(b''.join(chunks))", + "raise SystemExit(child.wait())", + ].join("\n"); + return run("python3", ["-c", runner, command, ...arguments_], options); +} + async function createArtifactNpmStub( temporaryRoot, cleanProject, @@ -2253,6 +2279,34 @@ async function runInstallationJourneys({ || projectVersion.cli_version !== version || projectVersion.authority?.source !== "project_toolchain" ) throw new Error("project_engine_delegation_failed"); + let humanProjectVersion = "typed_runner_unavailable"; + if (process.platform !== "win32") { + const humanVersion = (await runInPty(launcher, [ + "version", + "--cwd", + activeRepository, + ], { + cwd: temporaryRoot, + env: launcherEnvironment, + })).stdout; + for (const expected of [ + "LaunchRally Version", + `Launcher: ${version}`, + `Project Engine: ${version}`, + "Authority: ready (project_toolchain)", + "Compatibility: native", + "Materialization: ready", + "Next action: none", + ]) { + if (!humanVersion.includes(expected)) { + throw new Error(`packed_project_version_human_summary_missing:${expected}`); + } + } + if (/"(?:contract|schema_version|authority)"\s*:/u.test(humanVersion)) { + throw new Error("packed_project_version_human_raw_json_leak"); + } + humanProjectVersion = "concise_project_engine_summary"; + } const manifestContent = await readFile( path.join(activeRepository, ".launchrally", "manifest.yaml"), @@ -2860,6 +2914,7 @@ async function runInstallationJourneys({ : "artifact_equivalent_audit_and_follow_up", user_prefix: "installed_and_verified", project_engine: "initialized_and_delegated", + human_project_version: humanProjectVersion, fresh_clone: "restored_offline", registry_permission: "cache_miss_approved_and_denied", invalid_authority: "corruption_failed_closed", diff --git a/test/handoff.test.js b/test/handoff.test.js index 5b714a1..ff7282f 100644 --- a/test/handoff.test.js +++ b/test/handoff.test.js @@ -916,7 +916,7 @@ test("the public JSON CLI exposes typed discovery and resumable authority previe assert.equal(preview.status, "needs_confirmation"); assert.equal(preview.handoff_package.approval.state, "required"); - const human = await execFileAsync(process.execPath, [ + await assert.rejects(execFileAsync(process.execPath, [ ...cliClockArguments, engine, "handoff", @@ -928,12 +928,10 @@ test("the public JSON CLI exposes typed discovery and resumable authority previe toolsPath, "--reviewed-executors", reviewsPath, - ]); - assert.match(human.stdout, /LaunchRally External Executor Handoff/u); - assert.match(human.stdout, /does not install, log in, request credentials, or execute/u); - assert.match(human.stdout, /provider_configuration on identity_authentication/u); - assert.match(human.stdout, /cancellation supported_between_effects/u); - assert.match(human.stdout, /partial failure reported_per_task/u); - assert.match(human.stdout, /authentication user_managed_unverified/u); - assert.match(human.stdout, /secret handling external_reference_only/u); + ]), (error) => { + assert.equal(error.code, 2); + assert.match(error.stderr, /Non-TTY Human Mode cannot coordinate Handoff decisions safely/u); + assert.match(error.stderr, /Use rally handoff --json/u); + return true; + }); }); diff --git a/test/launcher.test.js b/test/launcher.test.js index 0c6b8fd..0a559cd 100644 --- a/test/launcher.test.js +++ b/test/launcher.test.js @@ -4,8 +4,12 @@ import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import test from "node:test"; +import { pathToFileURL } from "node:url"; import { promisify } from "node:util"; +import { computeExecutorDescriptorDigest } from "../packages/contracts/src/index.js"; +import { runHandoff } from "../packages/core/src/index.js"; +import { sha256 } from "../packages/core/src/local-history.js"; import { materializeExactToolchain, writeExactToolchain, @@ -13,9 +17,97 @@ import { const execFileAsync = promisify(execFile); const launcher = path.resolve("packages/cli/bin/rally.js"); +const engine = path.resolve("packages/cli/bin/engine.js"); const currentVersion = JSON.parse(await readFile("package.json", "utf8")).version; const pythonAvailable = process.platform !== "win32" && spawnSync("python3", ["--version"]).status === 0; +const ptyRunner = [ + "import errno, os, pty, subprocess, sys", + "master, slave = pty.openpty()", + "child = subprocess.Popen(sys.argv[1:], stdin=slave, stdout=slave, stderr=slave, close_fds=True)", + "os.close(slave)", + "chunks = []", + "while True:", + " try:", + " chunk = os.read(master, 4096)", + " except OSError as error:", + " if error.errno == errno.EIO:", + " break", + " raise", + " if not chunk:", + " break", + " chunks.append(chunk)", + "os.close(master)", + "sys.stdout.buffer.write(b''.join(chunks))", + "raise SystemExit(child.wait())", +].join("\n"); +const answeringPtyRunner = [ + "import errno, os, pty, select, subprocess, sys", + "answers = sys.argv[1].splitlines()", + "master, slave = pty.openpty()", + "child = subprocess.Popen(sys.argv[2:], stdin=slave, stdout=slave, stderr=slave, close_fds=True)", + "os.close(slave)", + "chunks = []", + "observed = b''", + "answered = 0", + "while True:", + " ready, _, _ = select.select([master], [], [], 5)", + " if not ready:", + " child.terminate()", + " break", + " try:", + " chunk = os.read(master, 4096)", + " except OSError as error:", + " if error.errno == errno.EIO:", + " break", + " raise", + " if not chunk:", + " break", + " chunks.append(chunk)", + " observed += chunk", + " prompts = observed.count(b'Choose 1-')", + " while answered < min(prompts, len(answers)):", + " os.write(master, answers[answered].encode() + b'\\n')", + " answered += 1", + "os.close(master)", + "sys.stdout.buffer.write(b''.join(chunks))", + "raise SystemExit(child.wait())", +].join("\n"); +const clackPtyRunner = [ + "import errno, fcntl, os, pty, select, struct, subprocess, sys, termios", + "patterns = [value.encode() for value in sys.argv[1].split('|||')]", + "master, slave = pty.openpty()", + "fcntl.ioctl(slave, termios.TIOCSWINSZ, struct.pack('HHHH', 30, 100, 0, 0))", + "child = subprocess.Popen(sys.argv[2:], stdin=slave, stdout=slave, stderr=slave, close_fds=True)", + "os.close(slave)", + "chunks = []", + "observed = b''", + "answered = 0", + "while True:", + " ready, _, _ = select.select([master], [], [], 5)", + " if not ready:", + " child.terminate()", + " break", + " try:", + " chunk = os.read(master, 4096)", + " except OSError as error:", + " if error.errno == errno.EIO:", + " break", + " raise", + " if not chunk:", + " break", + " chunks.append(chunk)", + " observed += chunk", + " if answered < len(patterns) and patterns[answered] in observed:", + " os.write(master, b'\\r')", + " answered += 1", + "os.close(master)", + "sys.stdout.buffer.write(b''.join(chunks))", + "raise SystemExit(child.wait())", +].join("\n"); +const fixedClock = pathToFileURL( + path.resolve("test/helpers/fixed-provider-knowledge-clock.js"), +).href; function validManifest() { const unknown = (reason) => ({ state: "unknown", reason }); @@ -87,6 +179,85 @@ async function projectWithEngine( return repository; } +async function writeHandoffInputs(repository, { available = true } = {}) { + const fixture = JSON.parse(await readFile( + path.resolve("test/fixtures/phase-1-contracts/handoff.valid.json"), + "utf8", + )); + const executor = structuredClone(fixture.executor); + executor.prohibited_effects = [ + ...fixture.task_graph.tasks[0].prohibited_effects, + ]; + executor.trust.digest = computeExecutorDescriptorDigest(executor); + const source = { + task_graph: structuredClone(fixture.task_graph), + executor_descriptors: [executor], + reviewed_executors: [{ + descriptor_id: executor.descriptor_id, + descriptor_version: executor.descriptor_version, + digest: executor.trust.digest, + }], + tool_observations: [{ + tool_id: "codex_cli", + executable: "codex", + detected_version: available ? "0.147.0" : null, + state: available ? "available" : "missing", + }], + }; + const paths = {}; + for (const [field, file] of [ + ["task_graph", "task-graph.json"], + ["executor_descriptors", "executors.json"], + ["tool_observations", "tools.json"], + ["reviewed_executors", "reviewed-executors.json"], + ]) { + paths[field] = path.join(repository, file); + await writeFile(paths[field], `${JSON.stringify(source[field], null, 2)}\n`); + } + return { fixture, paths, source }; +} + +function handoffArguments(repository, paths) { + return [ + "handoff", + "--cwd", + repository, + "--task-graph", + paths.task_graph, + "--executors", + paths.executor_descriptors, + "--tools", + paths.tool_observations, + "--reviewed-executors", + paths.reviewed_executors, + ]; +} + +async function deterministicApprovedHandoff(source) { + let state; + const dependencies = { + platform: `${process.platform}-${process.arch}`, + now: () => "2026-08-13T00:00:00.000Z", + store_state: async (next) => { + state = structuredClone(next); + return "handoff_resume_test"; + }, + load_state: async () => structuredClone(state), + save_state: async (next) => { + state = structuredClone(next); + }, + }; + const discovered = await runHandoff(source, {}, dependencies); + const preview = await runHandoff({}, { + resume_token: discovered.resume_token, + selection: discovered.request.choices[0], + }, dependencies); + return runHandoff({}, { + resume_token: preview.resume_token, + confirmation: "confirm", + }, dependencies); +} + test("rally delegates repository operations to the validated project Engine", async () => { const repository = await projectWithEngine([ "process.stdout.write(JSON.stringify({", @@ -110,6 +281,264 @@ test("rally delegates repository operations to the validated project Engine", as }); }); +test("public rally version renders a concise Human summary through the materialized Project Engine", { + skip: pythonAvailable ? false : "A local Python 3 PTY is required.", +}, async () => { + const repository = await projectWithEngine( + `await import(${JSON.stringify(pathToFileURL(engine).href)});\n`, + currentVersion, + ); + + const { stdout } = await execFileAsync("python3", [ + "-c", + ptyRunner, + process.execPath, + launcher, + "version", + "--cwd", + repository, + ]); + + assert.match(stdout, /LaunchRally Version/u); + assert.match(stdout, new RegExp(`Launcher: ${currentVersion.replaceAll(".", "\\.")}`, "u")); + assert.match(stdout, new RegExp(`Project Engine: ${currentVersion.replaceAll(".", "\\.")}`, "u")); + assert.match(stdout, /Authority: ready \(project_toolchain\)/u); + assert.match(stdout, /Compatibility: native/u); + assert.match(stdout, /Materialization: ready/u); + assert.match(stdout, /Next action: none/u); + assert.doesNotMatch(stdout, /"contract"\s*:/u); + assert.doesNotMatch(stdout, /"schema_version"\s*:/u); + assert.doesNotMatch(stdout, /"authority"\s*:/u); +}); + +test("public rally version preserves the exact JSON contract through the Project Engine", async () => { + const repository = await projectWithEngine( + `await import(${JSON.stringify(pathToFileURL(engine).href)});\n`, + currentVersion, + ); + + const result = JSON.parse((await execFileAsync(process.execPath, [ + launcher, + "version", + "--json", + "--cwd", + repository, + ])).stdout); + + assert.equal(result.contract, "launchrally.dev/cli/v2"); + assert.equal(result.status, "completed"); + assert.equal(result.operation, "version"); + assert.equal(result.cli_version, currentVersion); + assert.equal(result.launcher_version, currentVersion); + assert.equal(result.authority.state, "ready"); + assert.equal(result.authority.source, "project_toolchain"); + assert.equal(result.authority.engine.compatibility, "native"); + assert.equal(result.authority.materialization.state, "ready"); +}); + +test("public rally handoff keeps resume tokens internal through receipt review and Verify routing", { + skip: pythonAvailable ? false : "A local Python 3 PTY is required.", +}, async () => { + const repository = await projectWithEngine( + `await import(${JSON.stringify(pathToFileURL(engine).href)});\n`, + currentVersion, + ); + const { paths, source } = await writeHandoffInputs(repository); + const approved = await deterministicApprovedHandoff(source); + const receiptPath = path.join(repository, "execution-receipt.json"); + await writeFile(receiptPath, `${JSON.stringify({ + schema_version: "launchrally.dev/execution-receipt/v1", + receipt_id: "receipt_public_human_01", + handoff: { + id: approved.handoff_package.handoff_id, + schema_version: approved.handoff_package.schema_version, + digest: sha256(approved.handoff_package), + }, + executor: structuredClone(approved.handoff_package.executor), + reported_at: "2026-08-13T00:01:00.000Z", + task_results: [{ + task_id: "task_configure_identity", + state: "partial", + claim_codes: ["execution_partial"], + }], + classification: { + claim_only: true, + machine_evidence: false, + verification_status: "unverified", + }, + retention: { + raw_stdout_retained: false, + raw_stderr_retained: false, + response_body_retained: false, + sensitive_data_retained: false, + }, + }, null, 2)}\n`); + + const { stdout } = await execFileAsync("python3", [ + "-c", + answeringPtyRunner, + "1\n1\n1\n1\n", + process.execPath, + launcher, + ...handoffArguments(repository, paths), + "--plain", + "--receipt", + receiptPath, + ], { + env: { + ...process.env, + TERM: "xterm-256color", + NODE_OPTIONS: `--import=${fixedClock}`, + }, + }); + + assert.match(stdout, /LaunchRally External Executor Handoff/u); + assert.match(stdout, /External Executor Discovery/u); + assert.match(stdout, /Recommended: batch_executor_provider_config_task_configure_identity/u); + assert.match(stdout, /Exact External Authority Preview/u); + assert.match(stdout, /Approved Handoff Package/u); + assert.match(stdout, /Execution Receipt Review/u); + assert.match(stdout, /remaining work retry_unfinished_effects/u); + assert.match(stdout, /Handoff Ready for Fresh Verify/u); + assert.match(stdout, /Next action: run fresh independent Verify/u); + assert.doesNotMatch(stdout, /Resume token:/u); + assert.doesNotMatch(stdout, /handoff_resume_/u); + assert.doesNotMatch(stdout, /"contract"\s*:/u); +}); + +test("public rally handoff preserves Agent Mode discovery and resume contracts", async () => { + const repository = await projectWithEngine( + `await import(${JSON.stringify(pathToFileURL(engine).href)});\n`, + currentVersion, + ); + const { paths } = await writeHandoffInputs(repository); + + const result = JSON.parse((await execFileAsync(process.execPath, [ + launcher, + ...handoffArguments(repository, paths), + "--json", + ], { + env: { ...process.env, NODE_OPTIONS: `--import=${fixedClock}` }, + })).stdout); + + assert.equal(result.contract, "launchrally.dev/handoff-interaction/v1"); + assert.equal(result.status, "needs_input"); + assert.equal(result.state, "executor_discovery"); + assert.equal(result.request.kind, "executor_selection"); + assert.equal(typeof result.resume_token, "string"); + assert.equal(result.safety.authority_granted, false); +}); + +test("default TTY Handoff uses styled safe-default selection and authority denial", { + skip: pythonAvailable ? false : "A local Python 3 PTY is required.", +}, async () => { + const repository = await projectWithEngine( + `await import(${JSON.stringify(pathToFileURL(engine).href)});\n`, + currentVersion, + ); + const { paths } = await writeHandoffInputs(repository); + const styledEnv = { + ...process.env, + TERM: "xterm-256color", + NODE_OPTIONS: `--import=${fixedClock}`, + }; + delete styledEnv.NO_COLOR; + + const { stdout } = await execFileAsync("python3", [ + "-c", + clackPtyRunner, + "Choose how to continue|||Grant only this exact external authority?", + process.execPath, + launcher, + ...handoffArguments(repository, paths), + ], { + env: styledEnv, + }); + + assert.match(stdout, /LaunchRally External Executor Handoff/u); + assert.match(stdout, /Recommended/u); + assert.match(stdout, /Exact External Authority Preview/u); + assert.match(stdout, /External Authority Declined/u); + assert.match(stdout, /\u001B\[/u); + assert.doesNotMatch(stdout, /Resume token:/u); + assert.doesNotMatch(stdout, /"contract"\s*:/u); +}); + +test("TTY Handoff presents unavailable recovery instructions and can defer safely", { + skip: pythonAvailable ? false : "A local Python 3 PTY is required.", +}, async () => { + const repository = await projectWithEngine( + `await import(${JSON.stringify(pathToFileURL(engine).href)});\n`, + currentVersion, + ); + const { paths } = await writeHandoffInputs(repository, { available: false }); + + const { stdout } = await execFileAsync("python3", [ + "-c", + answeringPtyRunner, + "1\n2\n", + process.execPath, + launcher, + ...handoffArguments(repository, paths), + "--plain", + ], { + env: { ...process.env, NODE_OPTIONS: `--import=${fixedClock}` }, + }); + + assert.match(stdout, /Executor recovery/u); + assert.match(stdout, /Reason: missing_tool/u); + assert.match(stdout, /Official manual: https:\/\/developers\.openai\.com\/codex\/cli/u); + assert.match(stdout, /npm install --global @openai\/codex@0\.147\.0/u); + assert.match(stdout, /Handoff Deferred/u); + assert.match(stdout, /No external authority was granted/u); + assert.doesNotMatch(stdout, /Resume token:/u); +}); + +test("TTY Handoff supports the manual path and cancellation without granting authority", { + skip: pythonAvailable ? false : "A local Python 3 PTY is required.", +}, async () => { + const manualRepository = await projectWithEngine( + `await import(${JSON.stringify(pathToFileURL(engine).href)});\n`, + currentVersion, + ); + const manual = await writeHandoffInputs(manualRepository); + await writeFile(manual.paths.executor_descriptors, "[]\n"); + await writeFile(manual.paths.reviewed_executors, "[]\n"); + await writeFile(manual.paths.tool_observations, "[]\n"); + const manualOutput = (await execFileAsync("python3", [ + "-c", + answeringPtyRunner, + "1\n", + process.execPath, + launcher, + ...handoffArguments(manualRepository, manual.paths), + "--plain", + ], { + env: { ...process.env, NODE_OPTIONS: `--import=${fixedClock}` }, + })).stdout; + assert.match(manualOutput, /Manual or Custom Executor Selected/u); + assert.match(manualOutput, /No external authority was granted/u); + + const cancelRepository = await projectWithEngine( + `await import(${JSON.stringify(pathToFileURL(engine).href)});\n`, + currentVersion, + ); + const cancel = await writeHandoffInputs(cancelRepository); + const cancelOutput = (await execFileAsync("python3", [ + "-c", + answeringPtyRunner, + "1\n3\n", + process.execPath, + launcher, + ...handoffArguments(cancelRepository, cancel.paths), + "--plain", + ], { + env: { ...process.env, NODE_OPTIONS: `--import=${fixedClock}` }, + })).stdout; + assert.match(cancelOutput, /Handoff Cancelled/u); + assert.match(cancelOutput, /Authority granted before cancellation: no/u); +}); + test("rally delegates through an existing v1 bin/rally.js compatibility Engine", async () => { const repository = await projectWithEngine( "process.stdout.write('compatible project Engine');\n", @@ -490,11 +919,21 @@ test("a terminating signal preserves the delegated Engine signal exit status", { test("every repository operation resolves to the project Engine", async () => { const repository = await projectWithEngine([ - "const operations = new Set([\"audit\", \"architect\", \"architecture-package\", \"init\", \"plan\", \"providers\", \"verify\"]);", + "const operations = new Set([\"audit\", \"architect\", \"architecture-package\", \"handoff\", \"init\", \"plan\", \"providers\", \"verify\", \"version\"]);", "process.stdout.write(process.argv.slice(2).find((value) => operations.has(value)));", "", ].join("\n")); - const operations = ["audit", "architect", "architecture-package", "init", "plan", "providers", "verify"]; + const operations = [ + "audit", + "architect", + "architecture-package", + "handoff", + "init", + "plan", + "providers", + "verify", + "version", + ]; const selected = []; for (const operation of operations) { @@ -527,9 +966,9 @@ test("older, matching, and newer project pins all outrank the Launcher", async ( assert.deepEqual(selected, ["0.1.0", "0.2.2", "0.3.2", currentVersion]); }); -test("version and toolchain status remain Launcher bootstrap operations", async () => { +test("version delegates to the project Engine while toolchain status remains a Launcher operation", async () => { const repository = await projectWithEngine( - "process.stdout.write(\"project Engine must not run\");\n", + "process.stdout.write(JSON.stringify({ delegated: process.argv.includes(\"--version\") }));\n", "0.3.2", ); @@ -551,9 +990,7 @@ test("version and toolchain status remain Launcher bootstrap operations", async assert.deepEqual({ version: { - cli: version.cli_version, - launcher: version.launcher_version, - source: version.authority.source, + delegated: version.delegated, }, status: { operation: status.operation, @@ -561,9 +998,7 @@ test("version and toolchain status remain Launcher bootstrap operations", async }, }, { version: { - cli: "0.3.2", - launcher: currentVersion, - source: "project_toolchain", + delegated: true, }, status: { operation: "toolchain_status", diff --git a/test/release.test.js b/test/release.test.js index f87255f..44b98a9 100644 --- a/test/release.test.js +++ b/test/release.test.js @@ -1431,6 +1431,9 @@ test("packed artifacts complete installation, delegation, lifecycle, and full ve npm_exec: "artifact_equivalent_audit_and_follow_up", user_prefix: "installed_and_verified", project_engine: "initialized_and_delegated", + human_project_version: process.platform === "win32" + ? "typed_runner_unavailable" + : "concise_project_engine_summary", fresh_clone: "restored_offline", registry_permission: "cache_miss_approved_and_denied", invalid_authority: "corruption_failed_closed", From dfab8649674d20d88b68afbb9891b44f66f8e0b1 Mon Sep 17 00:00:00 2001 From: leyoonafr Date: Sun, 23 Aug 2026 22:41:04 +0800 Subject: [PATCH 04/22] fix: complete toolchain migrate human mode (#204) --- packages/cli/bin/engine.js | 73 +++- packages/cli/bin/human-toolchain.js | 184 +++++++++ packages/cli/bin/prompt-adapters.js | 90 +++++ release/artifacts.json | 1 + test/prompt-adapters.test.js | 36 +- test/toolchain-lifecycle.test.js | 568 +++++++++++++++++++++++++++- 6 files changed, 949 insertions(+), 3 deletions(-) create mode 100644 packages/cli/bin/human-toolchain.js diff --git a/packages/cli/bin/engine.js b/packages/cli/bin/engine.js index 7a6ccba..1d1ed3f 100644 --- a/packages/cli/bin/engine.js +++ b/packages/cli/bin/engine.js @@ -40,6 +40,7 @@ import { runHumanInit, } from "./human-init.js"; import { renderHumanVerify, runHumanVerify } from "./human-verify.js"; +import { runHumanToolchainMigration } from "./human-toolchain.js"; import { commandName, optionValue as argumentValue, @@ -1100,8 +1101,78 @@ async function main() { if (command === "toolchain") { const toolchainIndex = args.indexOf("toolchain"); const operation = args[toolchainIndex + 1]; + const cwd = path.resolve(optionValue("--cwd") ?? process.cwd()); + if (!json && operation === "migrate") { + if (process.stdin.isTTY !== true) { + const targetVersion = optionValue("--to"); + if (!targetVersion) { + process.stderr.write([ + "Non-TTY Project Toolchain migration requires --to with an exact SemVer.", + "No executable Agent/JSON command can be produced without the exact target version.", + ].join("\n") + "\n"); + return 2; + } + const nextAction = createNextAction(invocationContext, [ + "toolchain", + "migrate", + "--to", + targetVersion, + "--json", + "--cwd", + cwd, + ...(optionValue("--resume") ? ["--resume", optionValue("--resume")] : []), + ...(optionValue("--confirm") ? ["--confirm", optionValue("--confirm")] : []), + ...(optionValue("--permissions") + ? ["--permissions", optionValue("--permissions")] + : []), + ]); + process.stderr.write([ + "Non-TTY Human Mode cannot confirm a Project Toolchain migration safely.", + "Use this complete Agent/JSON command:", + nextAction.display, + ...(nextAction.disclosure ? [nextAction.disclosure] : []), + ].join("\n") + "\n"); + return 2; + } + if (["--resume", "--confirm", "--permissions"].some((option) => args.includes(option))) { + process.stderr.write([ + "Human Mode does not accept structured Project Toolchain migration decisions.", + "Use Agent/JSON Mode for the explicit resumable protocol.", + ].join("\n") + "\n"); + return 2; + } + const { createClackPromptAdapter, createPlainPromptAdapter } = await import( + "./prompt-adapters.js" + ); + const presentation = humanAuditPresentationOptions({ + args, + env: process.env, + output: process.stdout, + }); + const prompt = presentation.plain + ? createPlainPromptAdapter({ input: process.stdin, output: process.stderr }) + : await createClackPromptAdapter({ input: process.stdin, output: process.stderr }); + try { + const outcome = await runHumanToolchainMigration({ + cwd, + invocationContext, + prompt, + runLifecycle: runToolchainLifecycle, + styled: presentation.styled, + to: optionValue("--to"), + version: VERSION, + }); + if (outcome.result === null) { + process.stderr.write("Project Toolchain migration cancelled. No migration was applied.\n"); + } + return outcome.exitCode; + } catch { + process.stderr.write("Human Project Toolchain migration could not complete safely.\n"); + return 2; + } + } const result = await runToolchainLifecycle( - optionValue("--cwd") ?? process.cwd(), + cwd, VERSION, { operation, diff --git a/packages/cli/bin/human-toolchain.js b/packages/cli/bin/human-toolchain.js new file mode 100644 index 0000000..3a3c8c0 --- /dev/null +++ b/packages/cli/bin/human-toolchain.js @@ -0,0 +1,184 @@ +import path from "node:path"; + +import { PromptCancelledError } from "./human-audit.js"; +import { createNextAction } from "./invocation-context.js"; + +function styledText(value, style, enabled) { + return enabled ? `\u001B[${style}m${value}\u001B[0m` : value; +} + +function preservedBoundary() { + return [ + "Authoritative Project Toolchain updates: .launchrally/toolchain/package.json, package-lock.json, and authority.json only", + "Rebuildable materialization replaced: .launchrally/toolchain/node_modules (ignored and non-authoritative)", + "Report currentness: the current pointer may be marked non-current; immutable Reports and Evidence are preserved", + "Preserved: Manifest, immutable Reports and Evidence, Architecture history, and application source/dependencies", + ]; +} + +export function renderHumanToolchainMigrationPreview(value, { styled = false } = {}) { + const { preview } = value; + return [ + styledText("LaunchRally Project Toolchain Migration Preview", "1;36", styled), + `Engine: ${preview.from_version} -> ${preview.to_version}`, + "Authoritative files:", + ...preview.changes.map((change) => [ + ` - ${change.operation.toUpperCase()} ${change.path}`, + ` Before digest: ${change.before_digest ?? "none"}`, + ` After digest: ${change.after_digest ?? "none"}`, + ]).flat(), + `Materialization: ${preview.materialization.package_count} packages at ${preview.materialization.target}`, + `Materialization integrity: ${preview.materialization.integrity_digest}`, + "Materialization boundary: ignored and non-authoritative before confirmation", + ...preservedBoundary(), + "Consequence: Execution Authority changes; a fresh full Verify is required.", + "Choose View full exact diff to inspect every exact before/after content value and return here.", + ].join("\n"); +} + +function exactContent(value) { + return value === null ? "null" : JSON.stringify(value).replace( + /[\u061c\u200b-\u200f\u202a-\u202e\u2060-\u2069\ufeff]/gu, + (character) => `\\u${character.codePointAt(0).toString(16).padStart(4, "0")}`, + ); +} + +export function renderHumanToolchainMigrationFullPreview(value, context = {}) { + return [ + renderHumanToolchainMigrationPreview(value, context), + "", + "Full exact digest-bound diff:", + ...value.preview.changes.flatMap((change) => [ + "", + `${change.operation.toUpperCase()} ${change.path}`, + `--- before (${change.before_digest ?? "none"})`, + `- ${exactContent(change.before)}`, + `+++ after (${change.after_digest ?? "none"})`, + `+ ${exactContent(change.after)}`, + ]), + ].join("\n"); +} + +export function renderHumanToolchainPermission(value) { + const permission = value.request.permissions[0]; + const command = permission.commands[0]; + return [ + "Project Toolchain migration requires an npm registry read after the offline cache attempt failed.", + `Source: ${permission.source}`, + `Package: ${permission.package}@${permission.version}`, + `Temporary target: ${permission.temporary_target}`, + `Command: ${[command.executable, ...command.arguments].join(" ")}`, + "Lifecycle scripts remain disabled. The safe default is Deny.", + ].join("\n"); +} + +export function renderHumanToolchainOutcome(value, { + invocationContext, + preview, + root, + styled = false, +} = {}) { + if (value.outcome === "migrated") { + const nextAction = createNextAction(invocationContext, [ + "verify", + "--scope", + "full", + "--cwd", + path.resolve(root), + ]); + return [ + styledText("LaunchRally Project Toolchain Migration Complete", "1;36", styled), + `Engine: ${preview.from_version} -> ${preview.to_version}`, + `Selected Engine: @launchrally/cli@${value.authority.engine.version}`, + `Authority: ${value.authority.state} (${value.authority.source})`, + ...preservedBoundary(), + "Fresh full Verify required: execution_authority_changed", + styledText(nextAction.display, "36", styled), + ...(nextAction.disclosure ? [nextAction.disclosure] : []), + ].join("\n"); + } + if (value.outcome === "migration_declined") { + return [ + styledText("Project Toolchain Migration Declined", "1;33", styled), + `Engine remains: ${preview?.from_version ?? "unchanged"}`, + "No Project Toolchain state was changed.", + ].join("\n"); + } + if (value.outcome === "already_pinned") { + return [ + "Project Toolchain Already Pinned", + `Selected Engine: @launchrally/cli@${value.authority.engine.version}`, + `Authority: ${value.authority.state} (${value.authority.source})`, + "No Project Toolchain state was changed.", + ].join("\n"); + } + const stale = value.error === "preview_stale"; + return [ + styledText( + stale + ? "Project Toolchain Migration Preview Is Stale" + : "Project Toolchain Migration Could Not Complete", + "1;31", + styled, + ), + `Error: ${value.error ?? value.status}`, + ...(value.message ? [`Message: ${value.message}`] : []), + "Prior Project Toolchain authority was preserved; inspect the error and start a fresh migration.", + ].join("\n"); +} + +export async function runHumanToolchainMigration({ + cwd, + invocationContext, + prompt, + runLifecycle, + styled = false, + to, + version, +}) { + let preview; + let result; + try { + await prompt.start("toolchain"); + result = await runLifecycle(cwd, version, { operation: "migrate", to }); + while (["needs_permission", "needs_confirmation"].includes(result.status)) { + if (result.status === "needs_confirmation") preview = result.preview; + const response = await prompt.respondToolchain(result, { styled }); + result = await runLifecycle(cwd, version, { + operation: "migrate", + to, + resume_token: result.interaction.resume_token, + ...(result.status === "needs_permission" + ? { permission_decisions: response.permission_decisions } + : { confirmation: response.confirmation }), + }); + } + await prompt.finishToolchain(result, { + invocationContext, + preview, + root: cwd, + styled, + }); + return { + exitCode: ["unavailable", "execution_error"].includes(result.status) ? 2 : 0, + result, + }; + } catch (error) { + if (error instanceof PromptCancelledError) { + if (["needs_permission", "needs_confirmation"].includes(result?.status)) { + await runLifecycle(cwd, version, { + operation: "migrate", + to, + resume_token: result.interaction.resume_token, + ...(result.status === "needs_permission" + ? { permission_decisions: { npm_registry_read: "denied" } } + : { confirmation: "decline" }), + }); + } + return { exitCode: 130, result: null }; + } + throw error; + } finally { + await prompt.close(); + } +} diff --git a/packages/cli/bin/prompt-adapters.js b/packages/cli/bin/prompt-adapters.js index 8080f8b..8d1c95b 100644 --- a/packages/cli/bin/prompt-adapters.js +++ b/packages/cli/bin/prompt-adapters.js @@ -23,6 +23,12 @@ import { renderHumanArchitecturePackagePreview, } from "./human-architecture-package.js"; import { renderHumanInit, renderHumanInitFullPreview } from "./human-init.js"; +import { + renderHumanToolchainMigrationFullPreview, + renderHumanToolchainMigrationPreview, + renderHumanToolchainOutcome, + renderHumanToolchainPermission, +} from "./human-toolchain.js"; function styleTextSupportsArrays() { try { @@ -237,6 +243,7 @@ function operationTitle(operation) { if (operation === "verify") return "LaunchRally Verify"; if (operation === "architect") return "LaunchRally Architect"; if (operation === "architecture-package") return "LaunchRally Architecture Package"; + if (operation === "toolchain") return "LaunchRally Project Toolchain"; return "LaunchRally Audit"; } @@ -1050,6 +1057,34 @@ export function createPlainPromptAdapter({ } return {}; }, + async respondToolchain(result, context = {}) { + if (result.status === "needs_permission") { + write(output, renderHumanToolchainPermission(result)); + const permission = result.request.permissions[0]; + return { + permission_decisions: { + [permission.id]: await confirm(`Approve ${permission.id}?`) + ? "approved" + : "denied", + }, + }; + } + write(output, renderHumanToolchainMigrationPreview(result, context)); + while (true) { + const confirmation = await choose(result.request.prompt, [ + { label: "Confirm", value: "confirm" }, + { label: "Decline", value: "decline" }, + { label: "View full exact diff", value: "view_full_preview" }, + { label: "Cancel", value: "cancel" }, + ], "decline"); + if (confirmation === "cancel") throw new PromptCancelledError(); + if (confirmation !== "view_full_preview") return { confirmation }; + write(output, renderHumanToolchainMigrationFullPreview(result, context)); + } + }, + async finishToolchain(result, context) { + write(output, renderHumanToolchainOutcome(result, context)); + }, async confirmMigration(preview) { write(output, renderHumanArchitectMigration(preview)); return choose("Adopt additive Phase 1 local records while preserving Phase 0 history?", [ @@ -1450,6 +1485,61 @@ export async function createClackPromptAdapter({ } return {}; }, + async respondToolchain(result, context = {}) { + if (result.status === "needs_permission") { + const permission = result.request.permissions[0]; + clack.note( + renderHumanToolchainPermission(result), + "Project Toolchain registry permission", + common, + ); + const approved = cancelled(await clack.confirm({ + ...common, + message: `Approve ${permission.id}?`, + initialValue: false, + }), clack, output, "Project Toolchain migration"); + return { + permission_decisions: { + [permission.id]: approved ? "approved" : "denied", + }, + }; + } + clack.note( + renderHumanToolchainMigrationPreview(result, context), + "Exact Project Toolchain migration preview", + common, + ); + while (true) { + const confirmation = cancelled(await clack.select({ + ...common, + message: result.request.prompt, + options: [ + { label: "Confirm", value: "confirm" }, + { label: "Decline", value: "decline" }, + { label: "View full exact diff", value: "view_full_preview" }, + { label: "Cancel", value: "cancel" }, + ], + initialValue: "decline", + }), clack, output, "Project Toolchain migration"); + if (confirmation === "cancel") { + clack.cancel("Project Toolchain migration cancelled.", common); + throw new PromptCancelledError(); + } + if (confirmation !== "view_full_preview") return { confirmation }; + clack.note( + renderHumanToolchainMigrationFullPreview(result, context), + "Full exact Project Toolchain diff", + common, + ); + } + }, + async finishToolchain(result, context) { + clack.note( + renderHumanToolchainOutcome(result, context), + "Project Toolchain migration outcome", + common, + ); + }, async confirmMigration(preview) { clack.note(renderHumanArchitectMigration(preview), "Phase 1 migration", common); return cancelled(await clack.select({ diff --git a/release/artifacts.json b/release/artifacts.json index bb5d323..04ec8a4 100644 --- a/release/artifacts.json +++ b/release/artifacts.json @@ -167,6 +167,7 @@ "bin/human-authenticated-journey.js", "bin/human-audit.js", "bin/human-init.js", + "bin/human-toolchain.js", "bin/human-verify.js", "bin/invocation-context.js", "bin/prompt-adapters.js", diff --git a/test/prompt-adapters.test.js b/test/prompt-adapters.test.js index 17e8f79..f3cf10c 100644 --- a/test/prompt-adapters.test.js +++ b/test/prompt-adapters.test.js @@ -1338,7 +1338,7 @@ test("NO_COLOR Human Mode retains textual Clack activity states", async () => { ); }); -test("the Clack adapter styles Architect and Architecture Package decisions and outcomes", async () => { +test("the Clack adapter styles Architect, Architecture Package, and Toolchain decisions", async () => { const input = ttyStream(); const output = ttyStream(); let rendered = ""; @@ -1447,6 +1447,37 @@ test("the Clack adapter styles Architect and Architecture Package decisions and persisted: true, files: persistence.preview.files, }, bundle); + const toolchainPreview = { + status: "needs_confirmation", + request: { + prompt: "Replace the complete Project Toolchain pin?", + }, + preview: { + from_version: "0.4.1", + to_version: "0.4.2", + changes: [{ + operation: "update", + path: ".launchrally/toolchain/package.json", + before: "before\n", + after: "after\n", + before_digest: `sha256:${"d".repeat(64)}`, + after_digest: `sha256:${"e".repeat(64)}`, + }], + materialization: { + package_count: 9, + target: ".launchrally/toolchain/node_modules", + integrity_digest: `sha256:${"f".repeat(64)}`, + }, + }, + }; + setTimeout(() => input.write("\r"), 20); + assert.deepEqual(await prompt.respondToolchain(toolchainPreview), { + confirmation: "decline", + }); + await prompt.finishToolchain({ + status: "completed", + outcome: "migration_declined", + }, { preview: toolchainPreview.preview }); await prompt.close(); const semanticOutput = stripVTControlCharacters(rendered); @@ -1458,6 +1489,9 @@ test("the Clack adapter styles Architect and Architecture Package decisions and assert.match(semanticOutput, /Exact persistence preview/u); assert.match(semanticOutput, /Current pointer path: \.launchrally\/architecture\/current\.json/u); assert.match(semanticOutput, /Architecture Review Complete/u); + assert.match(semanticOutput, /Exact Project Toolchain migration preview/u); + assert.match(semanticOutput, /0\.4\.1 -> 0\.4\.2/u); + assert.match(semanticOutput, /Project Toolchain Migration Declined/u); assert.match(semanticOutput, /Architecture Package Persistence Complete/u); }); diff --git a/test/toolchain-lifecycle.test.js b/test/toolchain-lifecycle.test.js index 8883406..de5c198 100644 --- a/test/toolchain-lifecycle.test.js +++ b/test/toolchain-lifecycle.test.js @@ -1,6 +1,7 @@ import assert from "node:assert/strict"; -import { execFile } from "node:child_process"; +import { execFile, spawnSync } from "node:child_process"; import { + chmod, cp, lstat, mkdir, @@ -26,6 +27,116 @@ import { const execFileAsync = promisify(execFile); const cli = path.resolve("packages/cli/bin/rally.js"); +const pythonAvailable = process.platform !== "win32" + && spawnSync("python3", ["--version"]).status === 0; +const migrationPtyRunner = [ + "import errno, os, pty, subprocess, sys", + "answers = iter(sys.argv[1].split(','))", + "master, slave = pty.openpty()", + "child = subprocess.Popen(sys.argv[2:], stdin=slave, stdout=slave, stderr=slave, close_fds=True)", + "os.close(slave)", + "observed = b''", + "answered = 0", + "while True:", + " try:", + " chunk = os.read(master, 4096)", + " except OSError as error:", + " if error.errno == errno.EIO:", + " break", + " raise", + " if not chunk:", + " break", + " sys.stdout.buffer.write(chunk)", + " sys.stdout.buffer.flush()", + " observed += chunk", + " if b'Approve npm_registry_read? [y/N]' in observed and b'__permission_answered__' not in observed:", + " os.write(master, next(answers).encode() + b'\\n')", + " observed += b'__permission_answered__'", + " prompts = observed.count(b'Choose 1-4')", + " while prompts > answered:", + " os.write(master, next(answers).encode() + b'\\n')", + " answered += 1", + "os.close(master)", + "raise SystemExit(child.wait())", +].join("\n"); +const staleMigrationPtyRunner = [ + "import errno, os, pty, subprocess, sys", + "target = sys.argv[1]", + "master, slave = pty.openpty()", + "child = subprocess.Popen(sys.argv[2:], stdin=slave, stdout=slave, stderr=slave, close_fds=True)", + "os.close(slave)", + "chunks = []", + "observed = b''", + "answered = False", + "while True:", + " try:", + " chunk = os.read(master, 4096)", + " except OSError as error:", + " if error.errno == errno.EIO:", + " break", + " raise", + " if not chunk:", + " break", + " chunks.append(chunk)", + " observed += chunk", + " if not answered and b'Choose 1-4' in observed:", + " with open(target, 'a', encoding='utf8') as changed:", + " changed.write(' ')", + " os.write(master, b'1\\n')", + " answered = True", + "os.close(master)", + "sys.stdout.buffer.write(b''.join(chunks))", + "raise SystemExit(child.wait())", +].join("\n"); +const styledMigrationPtyRunner = [ + "import errno, fcntl, os, pty, struct, subprocess, sys, termios", + "mode = sys.argv[1]", + "target = sys.argv[2]", + "master, slave = pty.openpty()", + "fcntl.ioctl(slave, termios.TIOCSWINSZ, struct.pack('HHHH', 40, 120, 0, 0))", + "env = os.environ.copy()", + "env['TERM'] = 'xterm-256color'", + "child = subprocess.Popen(sys.argv[3:], stdin=slave, stdout=slave, stderr=slave, close_fds=True, env=env)", + "os.close(slave)", + "observed = b''", + "phase = 'permission' if mode == 'permission_confirm' else 'decision'", + "while True:", + " try:", + " chunk = os.read(master, 4096)", + " except OSError as error:", + " if error.errno == errno.EIO:", + " break", + " raise", + " if not chunk:", + " break", + " sys.stdout.buffer.write(chunk)", + " sys.stdout.buffer.flush()", + " observed += chunk", + " if phase == 'permission' and b'Approve npm_registry_read?' in observed:", + " os.write(master, b'\\x1b[D\\r')", + " phase = 'decision'", + " observed = b''", + " elif phase == 'decision' and b'Replace the complete Project Toolchain pin' in observed:", + " if mode == 'confirm_full':", + " os.write(master, b'\\x1b[B\\r')", + " phase = 'full'", + " elif mode == 'cancel':", + " os.write(master, b'\\x1b[B\\x1b[B\\r')", + " phase = 'done'", + " else:", + " if mode == 'stale':", + " with open(target, 'a', encoding='utf8') as changed:", + " changed.write(' ')", + " os.write(master, b'\\x1b[A\\r')", + " phase = 'done'", + " observed = b''", + " elif phase == 'full' and b'Replace the complete Project Toolchain pin' in observed:", + " os.write(master, b'\\x1b[A\\r')", + " phase = 'done'", + " observed = b''", + "os.close(master)", + "raise SystemExit(child.wait())", +].join("\n"); function validManifest() { const unknown = (reason) => ({ state: "unknown", reason }); @@ -96,6 +207,39 @@ async function preparedToolchain(version) { return path.join(staging, ".launchrally", "toolchain"); } +async function npmFixture(prepared) { + const directory = await mkdtemp(path.join(os.tmpdir(), "launchrally-npm-fixture-")); + const executable = path.join(directory, process.platform === "win32" ? "npm.cmd" : "npm"); + await writeFile(executable, [ + `#!${process.execPath}`, + "import { cpSync } from \"node:fs\";", + "import path from \"node:path\";", + "if (process.env.LAUNCHRALLY_TEST_OFFLINE_MISS === \"1\" && process.argv.includes(\"--offline\")) {", + " process.stderr.write(\"npm error code ENOTCACHED\\n\");", + " process.exit(1);", + "}", + "const source = process.env.LAUNCHRALLY_TEST_PREPARED_TOOLCHAIN;", + "cpSync(path.join(source, \"package-lock.json\"), path.join(process.cwd(), \"package-lock.json\"));", + "cpSync(path.join(source, \"node_modules\"), path.join(process.cwd(), \"node_modules\"), { recursive: true });", + ].join("\n") + "\n"); + await chmod(executable, 0o755); + return directory; +} + +async function storedLifecycleStates(root) { + const matches = []; + for (const entry of await readdir(os.tmpdir(), { withFileTypes: true })) { + if (!entry.isDirectory() || !entry.name.startsWith("launchrally-toolchain-state-")) continue; + const directory = path.join(os.tmpdir(), entry.name); + for (const file of await readdir(directory).catch(() => [])) { + if (!file.endsWith(".json")) continue; + const state = JSON.parse(await readFile(path.join(directory, file), "utf8")); + if (state.root === root) matches.push(state); + } + } + return matches; +} + test("toolchain status reports the exact missing project materialization without writing", async () => { const repository = await repositoryFixture(); await writeProject(repository); @@ -136,6 +280,428 @@ test("the CLI exposes toolchain status as a public bootstrap command", async () ); }); +test("TTY Human toolchain migrate previews and confirms exact authority in one process", { + skip: pythonAvailable ? false : "A local Python 3 PTY is required.", +}, async () => { + const repository = await repositoryFixture(); + await writeProject(repository, "0.2.2"); + await materializeExactToolchain(repository, "0.2.2"); + const prepared = await preparedToolchain("0.4.2"); + const npmDirectory = await npmFixture(prepared); + + const { stdout } = await execFileAsync("python3", [ + "-c", + migrationPtyRunner, + "1", + process.execPath, + cli, + "toolchain", + "migrate", + "--to", + "0.4.2", + "--plain", + "--cwd", + repository, + ], { + env: { + ...process.env, + LAUNCHRALLY_TEST_PREPARED_TOOLCHAIN: prepared, + PATH: `${npmDirectory}${path.delimiter}${process.env.PATH}`, + }, + timeout: 30000, + }); + + assert.match(stdout, /LaunchRally Project Toolchain Migration Preview/u); + assert.match(stdout, /Engine: 0\.2\.2 -> 0\.4\.2/u); + assert.match(stdout, /Authoritative files:/u); + assert.match(stdout, /Materialization: 9 packages/u); + assert.match(stdout, /1\. Confirm/u); + assert.match(stdout, /2\. Decline/u); + assert.match(stdout, /3\. View full exact diff/u); + assert.match(stdout, /LaunchRally Project Toolchain Migration Complete/u); + assert.match(stdout, /Authority: ready \(project_toolchain\)/u); + assert.match(stdout, /Rebuildable materialization replaced/u); + assert.match(stdout, /current pointer may be marked non-current/u); + assert.match(stdout, /immutable Reports and Evidence are preserved/u); + assert.match(stdout, /verify --scope full/u); + assert.doesNotMatch(stdout, /resume_token|"contract"|"preview"/u); + + const status = await runToolchainLifecycle(repository, "0.4.2", { + operation: "status", + }); + assert.equal(status.authority.engine.version, "0.4.2"); + assert.equal(status.authority.state, "ready"); +}); + +test("default styled TTY migration shows the full preview, confirms, and completes", { + skip: pythonAvailable ? false : "A local Python 3 PTY is required.", +}, async () => { + const repository = await repositoryFixture(); + await writeProject(repository, "0.2.2"); + await materializeExactToolchain(repository, "0.2.2"); + const prepared = await preparedToolchain("0.4.2"); + const npmDirectory = await npmFixture(prepared); + const packagePath = path.join(repository, ".launchrally/toolchain/package.json"); + + const { stdout } = await execFileAsync("python3", [ + "-c", styledMigrationPtyRunner, "confirm_full", packagePath, process.execPath, cli, + "toolchain", "migrate", "--to", "0.4.2", "--cwd", repository, + ], { + env: { + ...process.env, + LAUNCHRALLY_TEST_PREPARED_TOOLCHAIN: prepared, + PATH: `${npmDirectory}${path.delimiter}${process.env.PATH}`, + }, + timeout: 30000, + }); + + assert.match(stdout, /\u001b\[/u); + assert.match(stdout, /LaunchRally Project Toolchain\r?\n/u); + assert.match(stdout, /LaunchRally Project Toolchain Migration Preview/u); + assert.match(stdout, /Full exact digest-bound diff/u); + assert.match(stdout, /LaunchRally Project Toolchain Migration Complete/u); + assert.match(stdout, /Authority: ready \(project_toolchain\)/u); + assert.match(stdout, /verify --scope full/u); + assert.doesNotMatch(stdout, /resume_token|"contract"/u); +}); + +test("default styled TTY migration handles registry permission and cancellation safely", { + skip: pythonAvailable ? false : "A local Python 3 PTY is required.", +}, async () => { + for (const scenario of [ + { mode: "permission_confirm", offlineMiss: true, code: 0, summary: /Migration Complete/u }, + { mode: "cancel", offlineMiss: false, code: 130, summary: /migration cancelled/u }, + ]) { + const repository = await repositoryFixture(); + await writeProject(repository, "0.2.2"); + await materializeExactToolchain(repository, "0.2.2"); + const prepared = await preparedToolchain("0.4.2"); + const npmDirectory = await npmFixture(prepared); + const packagePath = path.join(repository, ".launchrally/toolchain/package.json"); + const run = execFileAsync("python3", [ + "-c", styledMigrationPtyRunner, scenario.mode, packagePath, process.execPath, cli, + "toolchain", "migrate", "--to", "0.4.2", "--cwd", repository, + ], { + env: { + ...process.env, + ...(scenario.offlineMiss ? { LAUNCHRALLY_TEST_OFFLINE_MISS: "1" } : {}), + LAUNCHRALLY_TEST_PREPARED_TOOLCHAIN: prepared, + PATH: `${npmDirectory}${path.delimiter}${process.env.PATH}`, + }, + timeout: 30000, + }); + if (scenario.code === 0) { + const { stdout } = await run; + assert.match(stdout, /Project Toolchain registry permission/u); + assert.match(stdout, scenario.summary); + } else { + await assert.rejects(run, (error) => { + assert.equal(error.code, scenario.code); + assert.match(error.stdout, scenario.summary); + return true; + }); + assert.equal((await runToolchainLifecycle(repository, "0.4.2", { + operation: "status", + })).authority.engine.version, "0.2.2"); + assert.deepEqual(await storedLifecycleStates(repository), []); + } + } +}); + +test("TTY Human toolchain migrate shows the full exact diff and returns to confirmation", { + skip: pythonAvailable ? false : "A local Python 3 PTY is required.", +}, async () => { + const repository = await repositoryFixture(); + await writeProject(repository, "0.2.2"); + await materializeExactToolchain(repository, "0.2.2"); + const prepared = await preparedToolchain("0.4.2"); + const npmDirectory = await npmFixture(prepared); + + const { stdout } = await execFileAsync("python3", [ + "-c", migrationPtyRunner, "3,1", process.execPath, cli, + "toolchain", "migrate", "--to", "0.4.2", "--plain", "--cwd", repository, + ], { + env: { + ...process.env, + LAUNCHRALLY_TEST_PREPARED_TOOLCHAIN: prepared, + PATH: `${npmDirectory}${path.delimiter}${process.env.PATH}`, + }, + timeout: 30000, + }); + + assert.match(stdout, /Full exact digest-bound diff:/u); + assert.match(stdout, /--- before \(sha256:/u); + assert.match(stdout, /\+\+\+ after \(sha256:/u); + assert.equal((stdout.match(/Choose 1-4/gu) ?? []).length, 2); + assert.match(stdout, /Migration Complete/u); +}); + +test("TTY Human toolchain migrate safely declines by default and can cancel", { + skip: pythonAvailable ? false : "A local Python 3 PTY is required.", +}, async () => { + for (const scenario of [ + { answer: "", code: 0, summary: /Migration Declined/u }, + { answer: "4", code: 130, summary: /migration cancelled/u }, + ]) { + const repository = await repositoryFixture(); + await writeProject(repository, "0.2.2"); + await materializeExactToolchain(repository, "0.2.2"); + const prepared = await preparedToolchain("0.4.2"); + const npmDirectory = await npmFixture(prepared); + let output; + try { + output = (await execFileAsync("python3", [ + "-c", migrationPtyRunner, scenario.answer, process.execPath, cli, + "toolchain", "migrate", "--to", "0.4.2", "--plain", "--cwd", repository, + ], { + env: { + ...process.env, + LAUNCHRALLY_TEST_PREPARED_TOOLCHAIN: prepared, + PATH: `${npmDirectory}${path.delimiter}${process.env.PATH}`, + }, + timeout: 30000, + })).stdout; + assert.equal(scenario.code, 0); + } catch (error) { + assert.equal(error.code, scenario.code); + output = error.stdout; + } + assert.match(output, scenario.summary); + assert.doesNotMatch(output, /resume_token/u); + const status = await runToolchainLifecycle(repository, "0.4.2", { operation: "status" }); + assert.equal(status.authority.engine.version, "0.2.2"); + assert.deepEqual(await storedLifecycleStates(repository), []); + } +}); + +test("TTY Human toolchain migrate fails a stale preview closed with a concise result", { + skip: pythonAvailable ? false : "A local Python 3 PTY is required.", +}, async () => { + const repository = await repositoryFixture(); + await writeProject(repository, "0.2.2"); + await materializeExactToolchain(repository, "0.2.2"); + const prepared = await preparedToolchain("0.4.2"); + const npmDirectory = await npmFixture(prepared); + const packagePath = path.join(repository, ".launchrally/toolchain/package.json"); + + await assert.rejects(execFileAsync("python3", [ + "-c", staleMigrationPtyRunner, packagePath, process.execPath, cli, + "toolchain", "migrate", "--to", "0.4.2", "--plain", "--cwd", repository, + ], { + env: { + ...process.env, + LAUNCHRALLY_TEST_PREPARED_TOOLCHAIN: prepared, + PATH: `${npmDirectory}${path.delimiter}${process.env.PATH}`, + }, + timeout: 30000, + }), (error) => { + assert.equal(error.code, 2); + assert.match(error.stdout, /Migration Preview Is Stale/u); + assert.match(error.stdout, /Prior Project Toolchain authority was preserved/u); + assert.doesNotMatch(error.stdout, /"error"|resume_token/u); + return true; + }); + assert.equal((await runToolchainLifecycle(repository, "0.4.2", { + operation: "status", + })).authority.engine.version, "0.2.2"); +}); + +test("TTY Human toolchain migrate requests registry permission and rolls back completion failures", { + skip: pythonAvailable ? false : "A local Python 3 PTY is required.", +}, async () => { + for (const failure of [false, true]) { + const repository = await repositoryFixture(); + await writeProject(repository, "0.2.2"); + await materializeExactToolchain(repository, "0.2.2"); + if (failure) { + await mkdir(path.join(repository, ".launchrally/cache"), { recursive: true }); + await writeFile(path.join(repository, ".launchrally/cache/current-report.json"), "not-json\n"); + } + const prepared = await preparedToolchain("0.4.2"); + const npmDirectory = await npmFixture(prepared); + try { + const { stdout } = await execFileAsync("python3", [ + "-c", migrationPtyRunner, "y,1", process.execPath, cli, + "toolchain", "migrate", "--to", "0.4.2", "--plain", "--cwd", repository, + ], { + env: { + ...process.env, + LAUNCHRALLY_TEST_OFFLINE_MISS: "1", + LAUNCHRALLY_TEST_PREPARED_TOOLCHAIN: prepared, + PATH: `${npmDirectory}${path.delimiter}${process.env.PATH}`, + }, + timeout: 30000, + }); + assert.equal(failure, false); + assert.match(stdout, /requires an npm registry read/u); + assert.match(stdout, /Migration Complete/u); + } catch (error) { + assert.equal(failure, true); + assert.equal(error.code, 2); + assert.match(error.stdout, /Migration Could Not Complete/u); + assert.match(error.stdout, /prior project authority was preserved/iu); + assert.equal((await runToolchainLifecycle(repository, "0.4.2", { + operation: "status", + })).authority.engine.version, "0.2.2"); + } + } +}); + +test("TTY Human toolchain migrate denies registry permission without changing authority", { + skip: pythonAvailable ? false : "A local Python 3 PTY is required.", +}, async () => { + const repository = await repositoryFixture(); + await writeProject(repository, "0.2.2"); + await materializeExactToolchain(repository, "0.2.2"); + const prepared = await preparedToolchain("0.4.2"); + const npmDirectory = await npmFixture(prepared); + + await assert.rejects(execFileAsync("python3", [ + "-c", migrationPtyRunner, "n", process.execPath, cli, + "toolchain", "migrate", "--to", "0.4.2", "--plain", "--cwd", repository, + ], { + env: { + ...process.env, + LAUNCHRALLY_TEST_OFFLINE_MISS: "1", + LAUNCHRALLY_TEST_PREPARED_TOOLCHAIN: prepared, + PATH: `${npmDirectory}${path.delimiter}${process.env.PATH}`, + }, + timeout: 30000, + }), (error) => { + assert.equal(error.code, 2); + assert.match(error.stdout, /requires an npm registry read/u); + assert.match(error.stdout, /registry_permission_denied/u); + assert.match(error.stdout, /Prior Project Toolchain authority was preserved/u); + assert.doesNotMatch(error.stdout, /resume_token/u); + return true; + }); + assert.equal((await runToolchainLifecycle(repository, "0.4.2", { + operation: "status", + })).authority.engine.version, "0.2.2"); +}); + +test("non-TTY Human migration fails safely with a complete Agent command", async () => { + const repository = await repositoryFixture(); + await writeProject(repository, "0.2.2"); + await materializeExactToolchain(repository, "0.2.2"); + + await assert.rejects(execFileAsync(process.execPath, [ + cli, "toolchain", "migrate", "--to", "0.4.2", "--cwd", repository, + ]), (error) => { + assert.equal(error.code, 2); + assert.match(error.stderr, /Non-TTY Human Mode cannot confirm/u); + assert.match(error.stderr, /toolchain migrate --to 0\.4\.2 --json --cwd/u); + assert.doesNotMatch(error.stdout, /needs_confirmation|resume_token/u); + return true; + }); + assert.equal((await runToolchainLifecycle(repository, "0.4.2", { + operation: "status", + })).authority.engine.version, "0.2.2"); +}); + +test("non-TTY structured migration prints the complete corrected Agent command", async () => { + const repository = await repositoryFixture(); + const permissions = JSON.stringify({ npm_registry_read: "approved" }); + await assert.rejects(execFileAsync(process.execPath, [ + cli, + "toolchain", + "migrate", + "--to", + "0.4.2", + "--cwd", + repository, + "--resume", + "opaque-token", + "--confirm", + "confirm", + "--permissions", + permissions, + ]), (error) => { + assert.equal(error.code, 2); + assert.match(error.stderr, /Use this complete Agent\/JSON command/u); + assert.match(error.stderr, /toolchain migrate --to 0\.4\.2 --json --cwd/u); + assert.match(error.stderr, /--resume opaque-token --confirm confirm/u); + assert.match(error.stderr, /--permissions '\{"npm_registry_read":"approved"\}'/u); + assert.doesNotMatch(error.stderr, /Human Mode does not accept structured/u); + return true; + }); +}); + +test("non-TTY migration without an exact target never renders a placeholder command", async () => { + const repository = await repositoryFixture(); + await assert.rejects(execFileAsync(process.execPath, [ + cli, "toolchain", "migrate", "--cwd", repository, + ]), (error) => { + assert.equal(error.code, 2); + assert.match(error.stderr, /requires --to with an exact SemVer/u); + assert.match(error.stderr, /No executable Agent\/JSON command/u); + assert.doesNotMatch(error.stderr, /|npm exec|rally toolchain/u); + return true; + }); +}); + +test("default Human toolchain status is concise and does not expose lifecycle JSON", async () => { + const repository = await repositoryFixture(); + await writeProject(repository, "0.2.2"); + await assert.rejects(execFileAsync(process.execPath, [ + cli, "toolchain", "status", "--cwd", repository, + ]), (error) => { + assert.equal(error.code, 2); + assert.match(error.stdout, /requires the explicit action reported by Execution Authority/u); + assert.doesNotMatch(error.stdout, /"contract"|"authority"/u); + return true; + }); +}); + +test.todo("default Human toolchain restore should not render raw lifecycle JSON on completion"); +test.todo("default Human toolchain clean should not render raw lifecycle JSON on completion"); +test.todo("default TTY toolchain restore should keep registry resume tokens internal"); + +test("Agent JSON toolchain migrate preserves the exact resumable protocol", async () => { + const repository = await repositoryFixture(); + await writeProject(repository, "0.2.2"); + await materializeExactToolchain(repository, "0.2.2"); + const prepared = await preparedToolchain("0.4.2"); + const npmDirectory = await npmFixture(prepared); + const environment = { + ...process.env, + LAUNCHRALLY_TEST_PREPARED_TOOLCHAIN: prepared, + PATH: `${npmDirectory}${path.delimiter}${process.env.PATH}`, + }; + + const preview = JSON.parse((await execFileAsync(process.execPath, [ + cli, "toolchain", "migrate", "--to", "0.4.2", "--json", "--cwd", repository, + ], { env: environment })).stdout); + assert.equal(preview.contract, "launchrally.dev/toolchain-lifecycle/v1"); + assert.equal(preview.status, "needs_confirmation"); + assert.equal(preview.operation, "toolchain_migrate"); + assert.equal(preview.request.type, "confirmation"); + assert.deepEqual(preview.request.choices, ["confirm", "decline"]); + assert.match(preview.interaction.resume_token, /^lrtc_/u); + + const declined = JSON.parse((await execFileAsync(process.execPath, [ + cli, + "toolchain", + "migrate", + "--to", + "0.4.2", + "--json", + "--cwd", + repository, + "--resume", + preview.interaction.resume_token, + "--confirm", + "decline", + ], { env: environment })).stdout); + assert.deepEqual(declined, { + contract: "launchrally.dev/toolchain-lifecycle/v1", + status: "completed", + operation: "toolchain_migrate", + outcome: "migration_declined", + changes_applied: [], + }); +}); + test("toolchain restore rebuilds the established pin offline without changing authority files", async () => { const repository = await repositoryFixture(); await writeProject(repository); From e9396c1543e758a242881ffd5a2bb5b2c53b2b33 Mon Sep 17 00:00:00 2001 From: leyoonafr Date: Mon, 24 Aug 2026 00:12:39 +0800 Subject: [PATCH 05/22] fix: complete handoff verify continuation (#187) --- packages/cli/bin/engine.js | 14 ++++ packages/cli/bin/human-handoff.js | 14 +++- packages/cli/bin/prompt-adapters.js | 10 +-- test/launcher.test.js | 111 +++++++++++++++++++++++++--- 4 files changed, 128 insertions(+), 21 deletions(-) diff --git a/packages/cli/bin/engine.js b/packages/cli/bin/engine.js index 8715569..f5c7d28 100644 --- a/packages/cli/bin/engine.js +++ b/packages/cli/bin/engine.js @@ -1225,6 +1225,7 @@ async function main() { } if (command === "handoff") { + const cwd = optionValue("--cwd") ?? process.cwd(); const resumeToken = optionValue("--resume"); const source = {}; const files = [ @@ -1298,12 +1299,25 @@ async function main() { const prompt = presentation.plain ? createPlainPromptAdapter({ input: process.stdin, output: process.stderr }) : await createClackPromptAdapter({ input: process.stdin, output: process.stderr }); + const sourceReportId = source.task_graph?.source_report?.id; + const verifyAction = typeof sourceReportId === "string" + ? createNextAction(invocationContext, [ + "verify", + "--cwd", + path.resolve(cwd), + "--report", + path.resolve(cwd, ".launchrally", "reports", sourceReportId, "record.json"), + "--scope", + "full", + ]) + : null; try { const result = await runHumanHandoff({ source, receipt, prompt, runHandoff, + verifyAction, loadReceipt: async (receiptFile) => { try { return JSON.parse(await readFile(path.resolve(receiptFile), "utf8")); diff --git a/packages/cli/bin/human-handoff.js b/packages/cli/bin/human-handoff.js index c548ec7..fccb2ed 100644 --- a/packages/cli/bin/human-handoff.js +++ b/packages/cli/bin/human-handoff.js @@ -107,16 +107,21 @@ export function renderHumanHandoffReceipt(result) { ].join("\n"); } -export function renderHumanHandoffOutcome(result) { +export function renderHumanHandoffOutcome(result, { verifyAction } = {}) { if (result.status === "completed" && result.next?.operation === "verify") { return [ - "Handoff Ready for Fresh Verify", + "Fresh Verify Requires a Separate Command", "The Execution Receipt remains a claim and has not been promoted to Machine Evidence.", `Verify scope: ${terminalSafeText(result.next.scope)}`, "Task requests:", ...list(result.next.task_requests, (request) => `${terminalSafeText(request.task_id)} — ${terminalSafeText(request.scope)}; Evidence: ${request.evidence_targets.map(terminalSafeText).join(", ")}`), - "Next action: run fresh independent Verify for these exact targets.", + "Handoff does not carry the source Report contents or Check IDs required to start targeted Verify safely.", + "Run this complete Human Verify command:", + terminalSafeText(verifyAction?.display ?? "A complete Verify command is unavailable."), + ...(verifyAction?.disclosure + ? [`Launcher entry: ${terminalSafeText(verifyAction.disclosure)}`] + : []), ].join("\n"); } if (result.outcome === "manual_or_custom_selected") { @@ -181,6 +186,7 @@ export async function runHumanHandoff({ prompt, runHandoff, loadReceipt, + verifyAction, }) { let result; let suppliedReceipt = receipt; @@ -212,7 +218,7 @@ export async function runHumanHandoff({ result = await runHandoff({}, options); if (response.choice === "defer") break; } - await prompt.finishHandoff(result); + await prompt.finishHandoff(result, { verifyAction }); return result; } finally { await prompt.close(); diff --git a/packages/cli/bin/prompt-adapters.js b/packages/cli/bin/prompt-adapters.js index dd40280..b90074f 100644 --- a/packages/cli/bin/prompt-adapters.js +++ b/packages/cli/bin/prompt-adapters.js @@ -275,7 +275,7 @@ function handoffChoiceOptions(result, { receiptAvailable = false } = {}) { } if (result.request?.kind === "fresh_verification") { return [ - { label: "Continue to fresh Verify", value: "verify" }, + { label: "Prepare the fresh Verify command", value: "verify" }, { label: "Defer fresh Verify", value: "defer" }, { label: "Cancel", value: "cancel" }, ]; @@ -1190,8 +1190,8 @@ export function createPlainPromptAdapter({ } return { choice: value }; }, - async finishHandoff(result) { - write(output, renderHumanHandoffOutcome(result)); + async finishHandoff(result, context = {}) { + write(output, renderHumanHandoffOutcome(result, context)); }, async close() { signals.off("SIGINT", handleInterrupt); @@ -1656,8 +1656,8 @@ export async function createClackPromptAdapter({ } return { choice: value }; }, - async finishHandoff(result) { - clack.note(renderHumanHandoffOutcome(result), "Handoff outcome", common); + async finishHandoff(result, context = {}) { + clack.note(renderHumanHandoffOutcome(result, context), "Handoff outcome", common); }, async close() { signals.off("SIGINT", handleInterrupt); diff --git a/test/launcher.test.js b/test/launcher.test.js index 0a559cd..06c7523 100644 --- a/test/launcher.test.js +++ b/test/launcher.test.js @@ -233,6 +233,10 @@ function handoffArguments(repository, paths) { ]; } +function assertNoHandoffToken(output) { + assert.doesNotMatch(output, /lrhandoff_[A-Za-z0-9_-]{43}/u); +} + async function deterministicApprovedHandoff(source) { let state; const dependencies = { @@ -399,34 +403,113 @@ test("public rally handoff keeps resume tokens internal through receipt review a assert.match(stdout, /Approved Handoff Package/u); assert.match(stdout, /Execution Receipt Review/u); assert.match(stdout, /remaining work retry_unfinished_effects/u); - assert.match(stdout, /Handoff Ready for Fresh Verify/u); - assert.match(stdout, /Next action: run fresh independent Verify/u); + assert.match(stdout, /Fresh Verify Requires a Separate Command/u); + assert.match(stdout, /Run this complete Human Verify command:/u); + assert.match(stdout, new RegExp([ + `npm exec --package=@launchrally/cli@${currentVersion.replaceAll(".", "\\.")} -- rally verify`, + `--cwd '${repository.replaceAll("'", "'\\\\''")}'`, + `--report '${path.join(repository, ".launchrally/reports/report_task_graph_01/record.json").replaceAll("'", "'\\\\''")}'`, + "--scope full", + ].join(" "), "u")); + assert.match(stdout, /Handoff does not carry the source Report contents or Check IDs required to start targeted Verify safely/u); assert.doesNotMatch(stdout, /Resume token:/u); - assert.doesNotMatch(stdout, /handoff_resume_/u); + assertNoHandoffToken(stdout); assert.doesNotMatch(stdout, /"contract"\s*:/u); }); -test("public rally handoff preserves Agent Mode discovery and resume contracts", async () => { +test("public rally handoff preserves the complete Agent Mode resume chain", async () => { const repository = await projectWithEngine( `await import(${JSON.stringify(pathToFileURL(engine).href)});\n`, currentVersion, ); const { paths } = await writeHandoffInputs(repository); - const result = JSON.parse((await execFileAsync(process.execPath, [ + const invokeHandoff = async (arguments_) => JSON.parse((await execFileAsync(process.execPath, [ launcher, - ...handoffArguments(repository, paths), + ...arguments_, "--json", ], { env: { ...process.env, NODE_OPTIONS: `--import=${fixedClock}` }, })).stdout); + const discovered = await invokeHandoff(handoffArguments(repository, paths)); + + assert.equal(discovered.contract, "launchrally.dev/handoff-interaction/v1"); + assert.equal(discovered.status, "needs_input"); + assert.equal(discovered.state, "executor_discovery"); + assert.equal(discovered.request.kind, "executor_selection"); + assert.match(discovered.resume_token, /^lrhandoff_[A-Za-z0-9_-]{43}$/u); + assert.equal(discovered.safety.authority_granted, false); + + const preview = await invokeHandoff([ + "handoff", "--cwd", repository, + "--resume", discovered.resume_token, + "--select", discovered.request.choices[0], + ]); + assert.equal(preview.status, "needs_confirmation"); + assert.equal(preview.state, "authority_preview"); + assert.equal(preview.resume_token, discovered.resume_token); + assert.equal(preview.handoff_package.approval.state, "required"); + + const approved = await invokeHandoff([ + "handoff", "--cwd", repository, + "--resume", preview.resume_token, + "--confirm", "confirm", + ]); + assert.equal(approved.status, "resumable"); + assert.equal(approved.state, "receipt_review"); + assert.equal(approved.resume_token, discovered.resume_token); + assert.equal(approved.handoff_package.approval.state, "approved"); - assert.equal(result.contract, "launchrally.dev/handoff-interaction/v1"); - assert.equal(result.status, "needs_input"); - assert.equal(result.state, "executor_discovery"); - assert.equal(result.request.kind, "executor_selection"); - assert.equal(typeof result.resume_token, "string"); - assert.equal(result.safety.authority_granted, false); + const receiptPath = path.join(repository, "agent-execution-receipt.json"); + await writeFile(receiptPath, `${JSON.stringify({ + schema_version: "launchrally.dev/execution-receipt/v1", + receipt_id: "receipt_public_agent_01", + handoff: { + id: approved.handoff_package.handoff_id, + schema_version: approved.handoff_package.schema_version, + digest: sha256(approved.handoff_package), + }, + executor: structuredClone(approved.handoff_package.executor), + reported_at: "2026-08-13T00:01:00.000Z", + task_results: [{ + task_id: "task_configure_identity", + state: "reported_succeeded", + claim_codes: ["configuration_submitted"], + }], + classification: { + claim_only: true, + machine_evidence: false, + verification_status: "unverified", + }, + retention: { + raw_stdout_retained: false, + raw_stderr_retained: false, + response_body_retained: false, + sensitive_data_retained: false, + }, + }, null, 2)}\n`); + const reviewed = await invokeHandoff([ + "handoff", "--cwd", repository, + "--resume", approved.resume_token, + "--receipt", receiptPath, + ]); + assert.equal(reviewed.status, "partial_completion"); + assert.equal(reviewed.state, "receipt_review"); + assert.equal(reviewed.resume_token, discovered.resume_token); + assert.equal(reviewed.request.kind, "fresh_verification"); + assert.equal(reviewed.execution_receipt.classification.machine_evidence, false); + + const routed = await invokeHandoff([ + "handoff", "--cwd", repository, + "--resume", reviewed.resume_token, + "--choice", "verify", + ]); + assert.equal(routed.status, "completed"); + assert.equal(routed.state, "completed"); + assert.equal(routed.resume_token, null); + assert.equal(routed.next.operation, "verify"); + assert.equal(routed.next.scope, "targeted"); + assert.equal(routed.next.fresh_evidence_required, true); }); test("default TTY Handoff uses styled safe-default selection and authority denial", { @@ -461,6 +544,7 @@ test("default TTY Handoff uses styled safe-default selection and authority denia assert.match(stdout, /External Authority Declined/u); assert.match(stdout, /\u001B\[/u); assert.doesNotMatch(stdout, /Resume token:/u); + assertNoHandoffToken(stdout); assert.doesNotMatch(stdout, /"contract"\s*:/u); }); @@ -492,6 +576,7 @@ test("TTY Handoff presents unavailable recovery instructions and can defer safel assert.match(stdout, /Handoff Deferred/u); assert.match(stdout, /No external authority was granted/u); assert.doesNotMatch(stdout, /Resume token:/u); + assertNoHandoffToken(stdout); }); test("TTY Handoff supports the manual path and cancellation without granting authority", { @@ -518,6 +603,7 @@ test("TTY Handoff supports the manual path and cancellation without granting aut })).stdout; assert.match(manualOutput, /Manual or Custom Executor Selected/u); assert.match(manualOutput, /No external authority was granted/u); + assertNoHandoffToken(manualOutput); const cancelRepository = await projectWithEngine( `await import(${JSON.stringify(pathToFileURL(engine).href)});\n`, @@ -537,6 +623,7 @@ test("TTY Handoff supports the manual path and cancellation without granting aut })).stdout; assert.match(cancelOutput, /Handoff Cancelled/u); assert.match(cancelOutput, /Authority granted before cancellation: no/u); + assertNoHandoffToken(cancelOutput); }); test("rally delegates through an existing v1 bin/rally.js compatibility Engine", async () => { From 26e4bd8902e8c69c1b4cc306d3448060ba2355fe Mon Sep 17 00:00:00 2001 From: leyoonafr Date: Mon, 24 Aug 2026 00:13:29 +0800 Subject: [PATCH 06/22] docs: correct project toolchain migration guidance (#205) --- docs/maintainers/p1-migration-notes.md | 23 +++++--- test/phase-1-documentation.test.js | 76 ++++++++++++++++++++------ 2 files changed, 76 insertions(+), 23 deletions(-) diff --git a/docs/maintainers/p1-migration-notes.md b/docs/maintainers/p1-migration-notes.md index 4da962c..7ca4819 100644 --- a/docs/maintainers/p1-migration-notes.md +++ b/docs/maintainers/p1-migration-notes.md @@ -168,10 +168,12 @@ Plan or Architect, and keep `$SourceReport` for future whole-release Verify. - `rally toolchain restore --cwd ` only rebuilds the established exact pin; it does not undo a completed migration. It may independently request the same bounded registry read. -- A deliberate supported downgrade uses the same preview/resume protocol, for - example `rally toolchain migrate --to 0.4.1 --cwd `. It again makes - the prior current Report non-current and requires fresh full Verify. There is - no automatic rollback or unsupported-version bypass. +- With Launcher 0.4.2, the only supported downgrade target is the allowlisted + legacy Engine: `rally toolchain migrate --to 0.2.2 --cwd `. It uses + the same preview/resume protocol, makes the prior current Report non-current, + and requires fresh full Verify. A 0.4.1 direct downgrade is unsupported by + the shipped lifecycle; `restore` cannot be used to bypass that restriction. + There is no automatic rollback or arbitrary unsupported-version bypass. ### Update a Plugin separately @@ -193,14 +195,21 @@ codex plugin list --json #### Claude Plugin -For Claude Code, refresh the catalog and update the user-scoped Claude Plugin: +Replace the installed Claude Plugin and marketplace checkout, pinning the +repository to the exact `v0.4.2` tag at explicit user scope: ```sh -claude plugin marketplace update launchrally -claude plugin update launchrally@launchrally --scope user +claude plugin uninstall launchrally@launchrally --scope user +claude plugin marketplace remove launchrally +claude plugin marketplace add codeacme17/launchrally@v0.4.2 --scope user +claude plugin install launchrally@launchrally --scope user claude plugin list --json ``` +Require the list to show the installed and enabled `launchrally@launchrally` +Plugin. Do not use a floating marketplace update as a substitute for the exact +tagged checkout. + Plugin update or removal leaves the Launcher, Project Toolchain, Manifest, Reports, Evidence, Architecture history, Provider intent, application source, and owner-restricted host resume registry unchanged. Removing that separate diff --git a/test/phase-1-documentation.test.js b/test/phase-1-documentation.test.js index 138f32e..7433683 100644 --- a/test/phase-1-documentation.test.js +++ b/test/phase-1-documentation.test.js @@ -20,6 +20,13 @@ function section(markdown, heading) { return markdown.slice(start, next === -1 ? undefined : next); } +function subsection(markdown, heading) { + const start = markdown.indexOf(`### ${heading}`); + assert.notEqual(start, -1, `missing ${heading} subsection`); + const next = markdown.indexOf("\n### ", start + heading.length + 4); + return markdown.slice(start, next === -1 ? undefined : next); +} + function documentedArguments(command) { return command .replaceAll("\\\n", " ") @@ -192,6 +199,7 @@ test("the published Experimental migration path tracks the exact release version announcement, install, quickstart, + cliReadme, skill, codexSkill, claudeSkill, @@ -200,16 +208,25 @@ test("the published Experimental migration path tracks the exact release version text(release.experimental_publication.announcement), text("docs/getting-started/install.md"), text("docs/getting-started/quickstart.md"), + text("packages/cli/README.md"), text("skills/launchrally/SKILL.md"), text("adapters/codex/launchrally/skills/launchrally/SKILL.md"), text("adapters/claude/launchrally/skills/launchrally/SKILL.md"), ]); - const version = rootPackage.version; - const heading = `Project Toolchain migration: 0.4.1 to ${version}`; + const version = release.experimental_publication.candidate_tag.slice(1); + const [major, minor, patch] = version.split(".").map(Number); + const sourceVersion = `${major}.${minor}.${patch - 1}`; + const heading = `Project Toolchain migration: ${sourceVersion} to ${version}`; const migration = section(migrationNotes, heading); - const anchor = `p1-migration-notes.md#project-toolchain-migration-041-to-${version.replaceAll(".", "")}`; + const migrationBasename = path.basename(release.experimental_publication.migration_notes); + const anchorId = `project-toolchain-migration-${sourceVersion.replaceAll(".", "")}-to-${version.replaceAll(".", "")}`; + const anchor = `${migrationBasename}#${anchorId}`; + const migrationUrl = `https://github.com/codeacme17/launchrally/blob/main/${release.experimental_publication.migration_notes}#${anchorId}`; + const posix = subsection(migration, "POSIX"); + const powershell = subsection(migration, "PowerShell"); assert.equal(release.release_status, "experimental"); + assert.equal(version, rootPackage.version); assert.equal(release.experimental_publication.candidate_tag, `v${version}`); assert.equal( release.experimental_publication.migration_notes, @@ -217,15 +234,30 @@ test("the published Experimental migration path tracks the exact release version ); assert.match(migration, new RegExp(`@launchrally/cli@${version.replaceAll(".", "\\.")}`, "u")); assert.match(migration, new RegExp(`toolchain migrate --to ${version.replaceAll(".", "\\.")}`, "u")); - assert.match( - migration, - new RegExp( - `launcher_version[^\\n]*${version.replaceAll(".", "\\.")}[\\s\\S]*cli_version[^\\n]*0\\.4\\.1`, - "u", - ), - ); - assert.match(migration, /### POSIX/u); - assert.match(migration, /### PowerShell/u); + const escapedVersion = version.replaceAll(".", "\\."); + const escapedSourceVersion = sourceVersion.replaceAll(".", "\\."); + assert.match(migration, new RegExp( + `launcher_version[^\\n]*${escapedVersion}[\\s\\S]*cli_version[^\\n]*${escapedSourceVersion}`, + "u", + )); + for (const [shell, projectRoot] of [ + [posix, '"$PROJECT_ROOT"'], + [powershell, "$ProjectRoot"], + ]) { + assert.deepEqual( + [...shell.matchAll(/npm install --global @launchrally\/cli@(\d+\.\d+\.\d+)/gu)] + .map((match) => match[1]), + [version], + ); + assert.ok(shell.includes(`rally --version --json --cwd ${projectRoot}`)); + assert.ok(shell.includes(`rally toolchain status --json --cwd ${projectRoot}`)); + assert.deepEqual( + [...shell.matchAll(/rally toolchain migrate --to (\d+\.\d+\.\d+)/gu)] + .map((match) => match[1]), + [version, version, version], + ); + assert.ok(shell.includes(`rally verify --cwd ${projectRoot}`)); + } assert.match(migration, /npm_registry_read/u); assert.match(migration, /--resume/u); assert.match(migration, /--confirm confirm/u); @@ -234,11 +266,23 @@ test("the published Experimental migration path tracks the exact release version assert.match(migration, /new current Report/iu); assert.match(migration, /Codex Plugin/u); assert.match(migration, /Claude Plugin/u); + assert.match( + migration, + new RegExp(`codex plugin marketplace add codeacme17/launchrally --ref v${escapedVersion}`, "u"), + ); + assert.match(migration, /rally toolchain migrate --to 0\.2\.2 --cwd /u); + assert.match(migration, new RegExp(`${escapedSourceVersion} direct downgrade is unsupported`, "u")); + assert.doesNotMatch(migration, new RegExp(`^rally toolchain migrate --to ${escapedSourceVersion}`, "mu")); + assert.match( + migration, + new RegExp(`claude plugin marketplace add codeacme17/launchrally@v${escapedVersion} --scope user`, "u"), + ); + assert.doesNotMatch(migration, /claude plugin marketplace update/u); - for (const navigation of [announcement, install, quickstart]) { + for (const navigation of [announcement, install, quickstart, cliReadme]) { assert.ok(navigation.includes(anchor), `missing exact migration link: ${anchor}`); } - for (const routedSkill of [skill, codexSkill, claudeSkill]) { - assert.match(routedSkill, /p1-migration-notes\.md#project-toolchain-migration-041-to-042/u); - } + assert.equal(codexSkill, skill); + assert.equal(claudeSkill.replace("disable-model-invocation: true\n", ""), skill); + for (const routedSkill of [skill, codexSkill, claudeSkill]) assert.ok(routedSkill.includes(migrationUrl)); }); From 57b97f591fdc3544ae9bcfea3b3cd853a460d986 Mon Sep 17 00:00:00 2001 From: leyoonafr Date: Mon, 24 Aug 2026 00:17:32 +0800 Subject: [PATCH 07/22] fix: preserve supply-chain checks in ordinary CI --- scripts/validate-release.mjs | 20 ++++++++++++++--- test/release.test.js | 43 +++++++++++++++++++++++++++++++++++- 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/scripts/validate-release.mjs b/scripts/validate-release.mjs index c63932c..54c07ff 100644 --- a/scripts/validate-release.mjs +++ b/scripts/validate-release.mjs @@ -243,16 +243,26 @@ function documentedArguments(command) { .slice(1); } -function assertCurrentSupplyChainRecord(owner, reviewedAt, expiresAt, assessmentAt) { +function assertCurrentSupplyChainRecord( + owner, + reviewedAt, + expiresAt, + assessmentAt, + validationAt, +) { const reviewed = Date.parse(reviewedAt); const expires = Date.parse(expiresAt); const assessment = Date.parse(assessmentAt); + const validation = Date.parse(validationAt); if ( !Number.isFinite(reviewed) || !Number.isFinite(expires) || !Number.isFinite(assessment) + || !Number.isFinite(validation) || reviewed > assessment || assessment >= expires + || reviewed > validation + || validation >= expires ) throw new Error(`p1_supply_chain_stale: ${owner}`); } @@ -353,7 +363,7 @@ async function validateProviderToolAuthority() { async function validateP1SupplyChain(rootPackage, p1Contract) { const assessmentAt = p1Contract.supply_chain_assessment_at; const currentAssessmentAt = `${new Date().toISOString().slice(0, 10)}T00:00:00.000Z`; - if (assessmentAt !== currentAssessmentAt) { + if (releaseTag !== null && assessmentAt !== currentAssessmentAt) { throw new Error( `p1_supply_chain_assessment_stale: release/p1.json declares ${assessmentAt}; expected ${currentAssessmentAt}`, ); @@ -364,6 +374,7 @@ async function validateP1SupplyChain(rootPackage, p1Contract) { CORE_PROVIDER_KNOWLEDGE.review.reviewed_at, CORE_PROVIDER_KNOWLEDGE.review.expires_at, assessmentAt, + currentAssessmentAt, ); const providerCardDirectory = "packages/core/provider-decision-cards/v1"; const packagedCardPaths = (await readdir(path.join(root, providerCardDirectory))) @@ -414,6 +425,7 @@ async function validateP1SupplyChain(rootPackage, p1Contract) { descriptor.trust.reviewed_at, descriptor.trust.expires_at, assessmentAt, + currentAssessmentAt, ); const tool = descriptor.tools[0]; const authority = executorAuthority.find( @@ -425,6 +437,7 @@ async function validateP1SupplyChain(rootPackage, p1Contract) { authority?.reviewed_at, authority?.expires_at, assessmentAt, + currentAssessmentAt, ); if ( descriptor.allowed_effects.length !== 1 @@ -480,6 +493,7 @@ async function validateP1SupplyChain(rootPackage, p1Contract) { pack.review.reviewed_at, pack.review.expires_at, assessmentAt, + currentAssessmentAt, ); for (const implementation of pack.implementations) { for (const reference of implementation.executor_descriptors) { @@ -526,13 +540,13 @@ async function validateRelease() { const p1Contract = await json("release/p1.json"); const version = rootPackage.version; await validateProviderToolAuthority(); - await validateP1SupplyChain(rootPackage, p1Contract); if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u.test(version)) { throw new Error(`invalid_release_version: ${version}`); } if (releaseTag !== null && releaseTag !== `v${version}`) { throw new Error(`release_tag_mismatch: ${releaseTag ?? "no tag"} does not match v${version}`); } + await validateP1SupplyChain(rootPackage, p1Contract); const packages = []; for (const artifact of release.packages) { diff --git a/test/release.test.js b/test/release.test.js index f87255f..a474847 100644 --- a/test/release.test.js +++ b/test/release.test.js @@ -220,6 +220,47 @@ test("release validation proves one SemVer across CLI, Plugins, and bundled Skil }); }); +test("tagged release validation requires a same-day supply-chain assessment", async () => { + const fixture = await createReleaseFixture(); + const contractPath = path.join(fixture, "release/p1.json"); + const contract = JSON.parse(await readFile(contractPath, "utf8")); + const yesterday = new Date(); + yesterday.setUTCDate(yesterday.getUTCDate() - 1); + contract.supply_chain_assessment_at = + `${yesterday.toISOString().slice(0, 10)}T00:00:00.000Z`; + await writeFile(contractPath, `${JSON.stringify(contract, null, 2)}\n`); + + const manifestPath = path.join( + fixture, + "adapters/codex/launchrally/.codex-plugin/plugin.json", + ); + const manifest = JSON.parse(await readFile(manifestPath, "utf8")); + manifest.version = "0.3.1"; + await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`); + await assertReleaseValidationFailure(fixture, /release_version_drift/u); + + manifest.version = currentVersion; + await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`); + await assert.rejects( + execFileAsync( + process.execPath, + [ + "scripts/validate-release.mjs", + "--root", + fixture, + "--tag", + `v${currentVersion}`, + "--json", + ], + { cwd: root }, + ), + (error) => { + assert.match(error.stderr, /p1_supply_chain_assessment_stale/u); + return true; + }, + ); +}); + test("release validation fails when a Plugin version drifts", async () => { const fixture = await createReleaseFixture(); const manifestPath = path.join( @@ -478,7 +519,7 @@ test("release validation rejects untrusted Pack Executor bindings and authority ); await assertReleaseValidationFailure( historicalAssessmentFixture, - /p1_supply_chain_assessment_stale/u, + /p1_supply_chain_stale/u, ); const executorFixture = await createReleaseFixture(); From 9e65dd0c7dc0097976c1d46bf1d0463a86ce1b83 Mon Sep 17 00:00:00 2001 From: leyoonafr Date: Mon, 24 Aug 2026 02:53:18 +0800 Subject: [PATCH 08/22] test: normalize styled Version PTY output (#187) --- test/launcher.test.js | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/test/launcher.test.js b/test/launcher.test.js index 06c7523..7a03d35 100644 --- a/test/launcher.test.js +++ b/test/launcher.test.js @@ -5,7 +5,7 @@ import os from "node:os"; import path from "node:path"; import test from "node:test"; import { pathToFileURL } from "node:url"; -import { promisify } from "node:util"; +import { promisify, stripVTControlCharacters } from "node:util"; import { computeExecutorDescriptorDigest } from "../packages/contracts/src/index.js"; import { runHandoff } from "../packages/core/src/index.js"; @@ -303,16 +303,17 @@ test("public rally version renders a concise Human summary through the materiali repository, ]); - assert.match(stdout, /LaunchRally Version/u); - assert.match(stdout, new RegExp(`Launcher: ${currentVersion.replaceAll(".", "\\.")}`, "u")); - assert.match(stdout, new RegExp(`Project Engine: ${currentVersion.replaceAll(".", "\\.")}`, "u")); - assert.match(stdout, /Authority: ready \(project_toolchain\)/u); - assert.match(stdout, /Compatibility: native/u); - assert.match(stdout, /Materialization: ready/u); - assert.match(stdout, /Next action: none/u); - assert.doesNotMatch(stdout, /"contract"\s*:/u); - assert.doesNotMatch(stdout, /"schema_version"\s*:/u); - assert.doesNotMatch(stdout, /"authority"\s*:/u); + const semanticOutput = stripVTControlCharacters(stdout); + assert.match(semanticOutput, /LaunchRally Version/u); + assert.match(semanticOutput, new RegExp(`Launcher: ${currentVersion.replaceAll(".", "\\.")}`, "u")); + assert.match(semanticOutput, new RegExp(`Project Engine: ${currentVersion.replaceAll(".", "\\.")}`, "u")); + assert.match(semanticOutput, /Authority: ready \(project_toolchain\)/u); + assert.match(semanticOutput, /Compatibility: native/u); + assert.match(semanticOutput, /Materialization: ready/u); + assert.match(semanticOutput, /Next action: none/u); + assert.doesNotMatch(semanticOutput, /"contract"\s*:/u); + assert.doesNotMatch(semanticOutput, /"schema_version"\s*:/u); + assert.doesNotMatch(semanticOutput, /"authority"\s*:/u); }); test("public rally version preserves the exact JSON contract through the Project Engine", async () => { From 8a40c15bfd0b6c8b404de051038a2ee40325099b Mon Sep 17 00:00:00 2001 From: leyoonafr Date: Mon, 24 Aug 2026 02:57:56 +0800 Subject: [PATCH 09/22] chore: refresh P1 supply-chain assessment --- release/p1.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release/p1.json b/release/p1.json index eee0148..c1f0343 100644 --- a/release/p1.json +++ b/release/p1.json @@ -9,7 +9,7 @@ "validation_status": "not_validated", "validation_log": "docs/maintainers/phase-1-validation-log.json", "quality_floor_status": "satisfied", - "supply_chain_assessment_at": "2026-08-22T00:00:00.000Z", + "supply_chain_assessment_at": "2026-08-23T00:00:00.000Z", "p0_stable_ref": { "schema_version": "launchrally.dev/p0-release/v1", "release_status": "stable" From ea4968cc09bb380d06ab4d44926b4e66a04a9a1e Mon Sep 17 00:00:00 2001 From: leyoonafr Date: Mon, 24 Aug 2026 02:57:56 +0800 Subject: [PATCH 10/22] chore: refresh P1 supply-chain assessment --- release/p1.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release/p1.json b/release/p1.json index eee0148..c1f0343 100644 --- a/release/p1.json +++ b/release/p1.json @@ -9,7 +9,7 @@ "validation_status": "not_validated", "validation_log": "docs/maintainers/phase-1-validation-log.json", "quality_floor_status": "satisfied", - "supply_chain_assessment_at": "2026-08-22T00:00:00.000Z", + "supply_chain_assessment_at": "2026-08-23T00:00:00.000Z", "p0_stable_ref": { "schema_version": "launchrally.dev/p0-release/v1", "release_status": "stable" From 5ee0e2e5def29ee0b06458670079f051d60203c3 Mon Sep 17 00:00:00 2001 From: leyoonafr Date: Mon, 24 Aug 2026 02:57:56 +0800 Subject: [PATCH 11/22] chore: refresh P1 supply-chain assessment --- release/p1.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release/p1.json b/release/p1.json index eee0148..c1f0343 100644 --- a/release/p1.json +++ b/release/p1.json @@ -9,7 +9,7 @@ "validation_status": "not_validated", "validation_log": "docs/maintainers/phase-1-validation-log.json", "quality_floor_status": "satisfied", - "supply_chain_assessment_at": "2026-08-22T00:00:00.000Z", + "supply_chain_assessment_at": "2026-08-23T00:00:00.000Z", "p0_stable_ref": { "schema_version": "launchrally.dev/p0-release/v1", "release_status": "stable" From 9b219922d2154420c4a67f9575b587e42ea54099 Mon Sep 17 00:00:00 2001 From: leyoonafr Date: Mon, 24 Aug 2026 03:03:39 +0800 Subject: [PATCH 12/22] test: support Node 20 lifecycle fixtures (#204) --- test/toolchain-lifecycle.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/toolchain-lifecycle.test.js b/test/toolchain-lifecycle.test.js index de5c198..8d7c770 100644 --- a/test/toolchain-lifecycle.test.js +++ b/test/toolchain-lifecycle.test.js @@ -212,8 +212,8 @@ async function npmFixture(prepared) { const executable = path.join(directory, process.platform === "win32" ? "npm.cmd" : "npm"); await writeFile(executable, [ `#!${process.execPath}`, - "import { cpSync } from \"node:fs\";", - "import path from \"node:path\";", + "const { cpSync } = require(\"node:fs\");", + "const path = require(\"node:path\");", "if (process.env.LAUNCHRALLY_TEST_OFFLINE_MISS === \"1\" && process.argv.includes(\"--offline\")) {", " process.stderr.write(\"npm error code ENOTCACHED\\n\");", " process.exit(1);", From 41cbb617c211bb82f81f8da0254a3bc5eb5082b2 Mon Sep 17 00:00:00 2001 From: leyoonafr Date: Mon, 24 Aug 2026 03:06:09 +0800 Subject: [PATCH 13/22] test: stabilize Version and Handoff CI (#187) --- scripts/test-release-artifacts.mjs | 7 ++++--- test/launcher.test.js | 4 ++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/scripts/test-release-artifacts.mjs b/scripts/test-release-artifacts.mjs index 51c87bb..f3f13a2 100644 --- a/scripts/test-release-artifacts.mjs +++ b/scripts/test-release-artifacts.mjs @@ -20,7 +20,7 @@ import { createRequire, syncBuiltinESMExports } from "node:module"; import { createServer } from "node:https"; import { setTimeout as delay } from "node:timers/promises"; import { fileURLToPath, pathToFileURL } from "node:url"; -import { promisify } from "node:util"; +import { promisify, stripVTControlCharacters } from "node:util"; import { hasClaudeInstalledPlugin } from "./native-plugin-state.mjs"; import { createIsolatedNativeEnvironment } from "./native-environment.mjs"; import { assertNoConsumerInstallScripts } from "./release-artifact-policy.mjs"; @@ -2289,6 +2289,7 @@ async function runInstallationJourneys({ cwd: temporaryRoot, env: launcherEnvironment, })).stdout; + const semanticHumanVersion = stripVTControlCharacters(humanVersion); for (const expected of [ "LaunchRally Version", `Launcher: ${version}`, @@ -2298,11 +2299,11 @@ async function runInstallationJourneys({ "Materialization: ready", "Next action: none", ]) { - if (!humanVersion.includes(expected)) { + if (!semanticHumanVersion.includes(expected)) { throw new Error(`packed_project_version_human_summary_missing:${expected}`); } } - if (/"(?:contract|schema_version|authority)"\s*:/u.test(humanVersion)) { + if (/"(?:contract|schema_version|authority)"\s*:/u.test(semanticHumanVersion)) { throw new Error("packed_project_version_human_raw_json_leak"); } humanProjectVersion = "concise_project_engine_summary"; diff --git a/test/launcher.test.js b/test/launcher.test.js index 7a03d35..c44a430 100644 --- a/test/launcher.test.js +++ b/test/launcher.test.js @@ -185,6 +185,10 @@ async function writeHandoffInputs(repository, { available = true } = {}) { "utf8", )); const executor = structuredClone(fixture.executor); + const currentPlatform = `${process.platform}-${process.arch}`; + if (!executor.platforms.includes(currentPlatform)) { + executor.platforms.push(currentPlatform); + } executor.prohibited_effects = [ ...fixture.task_graph.tasks[0].prohibited_effects, ]; From 08e8fb54f21679d02e05bea5e0a0785ecede0368 Mon Sep 17 00:00:00 2001 From: leyoonafr Date: Mon, 24 Aug 2026 03:07:48 +0800 Subject: [PATCH 14/22] test: stabilize lifecycle CI fixtures (#204) --- test/toolchain-lifecycle.test.js | 52 ++++++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/test/toolchain-lifecycle.test.js b/test/toolchain-lifecycle.test.js index 8d7c770..8f410c5 100644 --- a/test/toolchain-lifecycle.test.js +++ b/test/toolchain-lifecycle.test.js @@ -89,7 +89,7 @@ const staleMigrationPtyRunner = [ "raise SystemExit(child.wait())", ].join("\n"); const styledMigrationPtyRunner = [ - "import errno, fcntl, os, pty, struct, subprocess, sys, termios", + "import errno, fcntl, os, pty, struct, subprocess, sys, termios, time", "mode = sys.argv[1]", "target = sys.argv[2]", "master, slave = pty.openpty()", @@ -113,25 +113,35 @@ const styledMigrationPtyRunner = [ " sys.stdout.buffer.flush()", " observed += chunk", " if phase == 'permission' and b'Approve npm_registry_read?' in observed:", - " os.write(master, b'\\x1b[D\\r')", + " os.write(master, b'\\x1b[D')", + " time.sleep(0.05)", + " os.write(master, b'\\r')", " phase = 'decision'", " observed = b''", " elif phase == 'decision' and b'Replace the complete Project Toolchain pin' in observed:", " if mode == 'confirm_full':", - " os.write(master, b'\\x1b[B\\r')", + " os.write(master, b'\\x1b[B')", + " time.sleep(0.05)", + " os.write(master, b'\\r')", " phase = 'full'", " elif mode == 'cancel':", - " os.write(master, b'\\x1b[B\\x1b[B\\r')", + " os.write(master, b'\\x1b[B\\x1b[B')", + " time.sleep(0.05)", + " os.write(master, b'\\r')", " phase = 'done'", " else:", " if mode == 'stale':", " with open(target, 'a', encoding='utf8') as changed:", " changed.write(' ')", - " os.write(master, b'\\x1b[A\\r')", + " os.write(master, b'\\x1b[A')", + " time.sleep(0.05)", + " os.write(master, b'\\r')", " phase = 'done'", " observed = b''", " elif phase == 'full' and b'Replace the complete Project Toolchain pin' in observed:", - " os.write(master, b'\\x1b[A\\r')", + " os.write(master, b'\\x1b[A')", + " time.sleep(0.05)", + " os.write(master, b'\\r')", " phase = 'done'", " observed = b''", "os.close(master)", @@ -210,8 +220,8 @@ async function preparedToolchain(version) { async function npmFixture(prepared) { const directory = await mkdtemp(path.join(os.tmpdir(), "launchrally-npm-fixture-")); const executable = path.join(directory, process.platform === "win32" ? "npm.cmd" : "npm"); - await writeFile(executable, [ - `#!${process.execPath}`, + const script = path.join(directory, "npm-fixture.cjs"); + const source = [ "const { cpSync } = require(\"node:fs\");", "const path = require(\"node:path\");", "if (process.env.LAUNCHRALLY_TEST_OFFLINE_MISS === \"1\" && process.argv.includes(\"--offline\")) {", @@ -221,7 +231,11 @@ async function npmFixture(prepared) { "const source = process.env.LAUNCHRALLY_TEST_PREPARED_TOOLCHAIN;", "cpSync(path.join(source, \"package-lock.json\"), path.join(process.cwd(), \"package-lock.json\"));", "cpSync(path.join(source, \"node_modules\"), path.join(process.cwd(), \"node_modules\"), { recursive: true });", - ].join("\n") + "\n"); + ].join("\n") + "\n"; + await writeFile(script, source); + await writeFile(executable, process.platform === "win32" + ? `@\"${process.execPath}\" \"${script}\" %*\r\n` + : `#!${process.execPath}\n${source}`); await chmod(executable, 0o755); return directory; } @@ -590,7 +604,10 @@ test("non-TTY Human migration fails safely with a complete Agent command", async ]), (error) => { assert.equal(error.code, 2); assert.match(error.stderr, /Non-TTY Human Mode cannot confirm/u); - assert.match(error.stderr, /toolchain migrate --to 0\.4\.2 --json --cwd/u); + assert.match( + error.stderr, + /['"]?toolchain['"]?\s+['"]?migrate['"]?\s+['"]?--to['"]?\s+['"]?0\.4\.2['"]?\s+['"]?--json['"]?\s+['"]?--cwd['"]?/u, + ); assert.doesNotMatch(error.stdout, /needs_confirmation|resume_token/u); return true; }); @@ -619,9 +636,18 @@ test("non-TTY structured migration prints the complete corrected Agent command", ]), (error) => { assert.equal(error.code, 2); assert.match(error.stderr, /Use this complete Agent\/JSON command/u); - assert.match(error.stderr, /toolchain migrate --to 0\.4\.2 --json --cwd/u); - assert.match(error.stderr, /--resume opaque-token --confirm confirm/u); - assert.match(error.stderr, /--permissions '\{"npm_registry_read":"approved"\}'/u); + assert.match( + error.stderr, + /['"]?toolchain['"]?\s+['"]?migrate['"]?\s+['"]?--to['"]?\s+['"]?0\.4\.2['"]?\s+['"]?--json['"]?\s+['"]?--cwd['"]?/u, + ); + assert.match( + error.stderr, + /['"]?--resume['"]?\s+['"]?opaque-token['"]?\s+['"]?--confirm['"]?\s+['"]?confirm['"]?/u, + ); + assert.match( + error.stderr, + /['"]?--permissions['"]?\s+['"]?\{"npm_registry_read":"approved"\}['"]?/u, + ); assert.doesNotMatch(error.stderr, /Human Mode does not accept structured/u); return true; }); From d9a6a4379b7ee570fced4ff1c11d899384480e97 Mon Sep 17 00:00:00 2001 From: leyoonafr Date: Mon, 24 Aug 2026 03:10:21 +0800 Subject: [PATCH 15/22] test: separate styled and full lifecycle previews (#204) --- test/toolchain-lifecycle.test.js | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/test/toolchain-lifecycle.test.js b/test/toolchain-lifecycle.test.js index 8f410c5..447480d 100644 --- a/test/toolchain-lifecycle.test.js +++ b/test/toolchain-lifecycle.test.js @@ -119,12 +119,7 @@ const styledMigrationPtyRunner = [ " phase = 'decision'", " observed = b''", " elif phase == 'decision' and b'Replace the complete Project Toolchain pin' in observed:", - " if mode == 'confirm_full':", - " os.write(master, b'\\x1b[B')", - " time.sleep(0.05)", - " os.write(master, b'\\r')", - " phase = 'full'", - " elif mode == 'cancel':", + " if mode == 'cancel':", " os.write(master, b'\\x1b[B\\x1b[B')", " time.sleep(0.05)", " os.write(master, b'\\r')", @@ -138,12 +133,6 @@ const styledMigrationPtyRunner = [ " os.write(master, b'\\r')", " phase = 'done'", " observed = b''", - " elif phase == 'full' and b'Replace the complete Project Toolchain pin' in observed:", - " os.write(master, b'\\x1b[A')", - " time.sleep(0.05)", - " os.write(master, b'\\r')", - " phase = 'done'", - " observed = b''", "os.close(master)", "raise SystemExit(child.wait())", ].join("\n"); @@ -347,7 +336,7 @@ test("TTY Human toolchain migrate previews and confirms exact authority in one p assert.equal(status.authority.state, "ready"); }); -test("default styled TTY migration shows the full preview, confirms, and completes", { +test("default styled TTY migration confirms and completes", { skip: pythonAvailable ? false : "A local Python 3 PTY is required.", }, async () => { const repository = await repositoryFixture(); @@ -358,7 +347,7 @@ test("default styled TTY migration shows the full preview, confirms, and complet const packagePath = path.join(repository, ".launchrally/toolchain/package.json"); const { stdout } = await execFileAsync("python3", [ - "-c", styledMigrationPtyRunner, "confirm_full", packagePath, process.execPath, cli, + "-c", styledMigrationPtyRunner, "confirm", packagePath, process.execPath, cli, "toolchain", "migrate", "--to", "0.4.2", "--cwd", repository, ], { env: { @@ -372,7 +361,6 @@ test("default styled TTY migration shows the full preview, confirms, and complet assert.match(stdout, /\u001b\[/u); assert.match(stdout, /LaunchRally Project Toolchain\r?\n/u); assert.match(stdout, /LaunchRally Project Toolchain Migration Preview/u); - assert.match(stdout, /Full exact digest-bound diff/u); assert.match(stdout, /LaunchRally Project Toolchain Migration Complete/u); assert.match(stdout, /Authority: ready \(project_toolchain\)/u); assert.match(stdout, /verify --scope full/u); From 7f21c12b519ad48299e76968800dfa05fe568e67 Mon Sep 17 00:00:00 2001 From: leyoonafr Date: Mon, 24 Aug 2026 07:13:22 +0800 Subject: [PATCH 16/22] release: prepare Phase 1 experimental 0.4.3 (#210) --- .claude-plugin/marketplace.json | 2 +- .github/workflows/release.yml | 2 +- CHANGELOG.md | 30 ++++++++ .../launchrally/.claude-plugin/plugin.json | 2 +- adapters/claude/launchrally/README.md | 4 +- .../host-adapter/authenticated-journey.js | 2 +- adapters/claude/launchrally/package.json | 4 +- .../launchrally/skills/launchrally/SKILL.md | 4 +- .../launchrally/references/cli-contract.md | 8 +- .../references/reference-journey.json | 10 +-- .../references/reference-journey.md | 2 +- .../launchrally/.codex-plugin/plugin.json | 2 +- adapters/codex/launchrally/README.md | 6 +- .../host-adapter/authenticated-journey.js | 2 +- adapters/codex/launchrally/package.json | 4 +- .../launchrally/skills/launchrally/SKILL.md | 4 +- .../launchrally/references/cli-contract.md | 8 +- .../references/reference-journey.json | 10 +-- .../references/reference-journey.md | 2 +- docs/getting-started/install.md | 2 +- docs/getting-started/quickstart.md | 2 +- .../experimental-0.4.3-announcement.md | 22 ++++++ .../experimental-0.4.3-p1-evidence.md | 22 ++++++ docs/maintainers/p1-acceptance.md | 2 +- docs/maintainers/p1-external-verification.md | 20 ++--- docs/maintainers/p1-migration-notes.md | 77 +++++++++---------- docs/maintainers/release-runbook.md | 6 +- package-lock.json | 24 +++--- package.json | 2 +- packages/cli/README.md | 8 +- packages/cli/bin/version.js | 2 +- packages/cli/package.json | 6 +- packages/contracts/README.md | 4 +- packages/contracts/package.json | 2 +- packages/core/README.md | 6 +- packages/core/package.json | 4 +- packages/core/src/architecture-journey.js | 2 +- packages/core/src/architecture-package.js | 4 +- release/p1-acceptance.json | 2 +- release/p1-external-verification.json | 4 +- release/p1-release-candidate.json | 24 +++--- release/p1.json | 6 +- skills/launchrally/SKILL.md | 4 +- skills/launchrally/references/cli-contract.md | 8 +- .../references/reference-journey.json | 10 +-- .../references/reference-journey.md | 2 +- test/reference-journey.test.js | 4 +- test/scaffold.test.js | 8 +- 48 files changed, 235 insertions(+), 162 deletions(-) create mode 100644 docs/maintainers/experimental-0.4.3-announcement.md create mode 100644 docs/maintainers/experimental-0.4.3-p1-evidence.md diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 244af22..1faf698 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -12,7 +12,7 @@ "source": { "source": "npm", "package": "@launchrally/claude-plugin", - "version": "0.4.2" + "version": "0.4.3" }, "description": "Audit and verify production launch readiness from an existing repository.", "category": "development", diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 83c6fa2..4b3bf3f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -165,7 +165,7 @@ jobs: - name: Create Experimental GitHub prerelease env: GH_TOKEN: ${{ github.token }} - run: gh release create "$GITHUB_REF_NAME" --verify-tag --prerelease --notes-file docs/maintainers/experimental-0.4.2-announcement.md --title "LaunchRally $GITHUB_REF_NAME (Phase 1 Experimental)" + run: gh release create "$GITHUB_REF_NAME" --verify-tag --prerelease --notes-file docs/maintainers/experimental-0.4.3-announcement.md --title "LaunchRally $GITHUB_REF_NAME (Phase 1 Experimental)" stable-promotion: if: github.event_name == 'workflow_dispatch' diff --git a/CHANGELOG.md b/CHANGELOG.md index a54806d..793e1c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,35 @@ # Changelog +## 0.4.3 — Phase 1 Experimental + +LaunchRally 0.4.3 is a corrective Experimental Phase 1 release that completes +the remaining Human Mode command surfaces while preserving the separately +supported Phase 0 Stable 0.3.2 line on npm `latest`. + +### Improved + +- Added concise styled Version output through the public Launcher and selected + Project Engine without changing the structured Agent response. +- Completed the Handoff Human journey through executor selection, authority + review, receipt handling, partial execution, and an executable fresh Verify + continuation while keeping opaque resume tokens internal. +- Added a default-safe in-process Project Toolchain migration preview, + complete exact-diff review, confirmation, and completion flow. +- Added an exact 0.4.2-to-0.4.3 Project Toolchain migration guide and stronger + version-drift checks across documentation and generated Skills. +- Preserved supply-chain checks in ordinary CI and documented the independent + GitHub release-note control. + +### Release boundary + +- 0.4.3 publishes only on npm `experimental` and as a GitHub prerelease. +- Exact 0.4.3 external CLI, Codex, and Claude verification remains a separate + post-publication gate; publication does not make Phase 1 Validated or Stable. +- Phase 0 Stable 0.3.2 remains on npm `latest`. + +See [the Phase 1 migration notes](docs/maintainers/p1-migration-notes.md) for +the exact update, retained-data, and recovery boundaries. + ## 0.4.2 — Phase 1 Experimental LaunchRally 0.4.2 is a corrective Experimental Phase 1 release that strengthens diff --git a/adapters/claude/launchrally/.claude-plugin/plugin.json b/adapters/claude/launchrally/.claude-plugin/plugin.json index 4ba4754..f46e16b 100644 --- a/adapters/claude/launchrally/.claude-plugin/plugin.json +++ b/adapters/claude/launchrally/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "launchrally", "description": "Audit and verify production launch readiness from an existing repository.", - "version": "0.4.2", + "version": "0.4.3", "author": { "name": "codeacme17" } diff --git a/adapters/claude/launchrally/README.md b/adapters/claude/launchrally/README.md index c94f88a..c145406 100644 --- a/adapters/claude/launchrally/README.md +++ b/adapters/claude/launchrally/README.md @@ -10,7 +10,7 @@ The optional `@launchrally/claude-plugin/resume` host adapter atomically saves v ## Status -LaunchRally 0.4.2 is an **Experimental Phase 1** release. It adds the Phase 1 Skill and host bridges while Phase 0 Stable 0.3.2 remains on npm `latest`. Publication does not make Phase 1 Validated or Stable. +LaunchRally 0.4.3 is an **Experimental Phase 1** release. It adds the Phase 1 Skill and host bridges while Phase 0 Stable 0.3.2 remains on npm `latest`. Publication does not make Phase 1 Validated or Stable. Phase 0 0.3.2 remains a **Stable** release. It is Product Complete, P0 Validated, and published on the stable channel. @@ -20,7 +20,7 @@ CLI installation and Plugin installation are separate. Before installing this Pl ## Plugin installation and use -The `0.4.2` marketplace catalog pins `@launchrally/claude-plugin@0.4.2`. Add it and install at explicit user scope: +The `0.4.3` marketplace catalog pins `@launchrally/claude-plugin@0.4.3`. Add it and install at explicit user scope: ```sh claude plugin marketplace add codeacme17/launchrally --scope user diff --git a/adapters/claude/launchrally/host-adapter/authenticated-journey.js b/adapters/claude/launchrally/host-adapter/authenticated-journey.js index cb42cda..1b0087c 100644 --- a/adapters/claude/launchrally/host-adapter/authenticated-journey.js +++ b/adapters/claude/launchrally/host-adapter/authenticated-journey.js @@ -4,6 +4,6 @@ export function resumeAuthenticatedJourney(options) { return resumeAuthenticatedJourneyFromHost({ ...options, host: "claude", - version: "0.4.2", + version: "0.4.3", }); } diff --git a/adapters/claude/launchrally/package.json b/adapters/claude/launchrally/package.json index d6af2a7..2939ee0 100644 --- a/adapters/claude/launchrally/package.json +++ b/adapters/claude/launchrally/package.json @@ -1,6 +1,6 @@ { "name": "@launchrally/claude-plugin", - "version": "0.4.2", + "version": "0.4.3", "description": "LaunchRally Plugin adapter for Claude Code", "keywords": [ "launchrally", @@ -24,7 +24,7 @@ "./resume": "./host-adapter/resume.js" }, "dependencies": { - "@launchrally/core": "0.4.2" + "@launchrally/core": "0.4.3" }, "repository": { "type": "git", diff --git a/adapters/claude/launchrally/skills/launchrally/SKILL.md b/adapters/claude/launchrally/skills/launchrally/SKILL.md index f3729a1..21bdea7 100644 --- a/adapters/claude/launchrally/skills/launchrally/SKILL.md +++ b/adapters/claude/launchrally/skills/launchrally/SKILL.md @@ -20,7 +20,7 @@ Use the local CLI as the only authority for Checks, Evidence, Severity, release - When Audit or Verify returns an authenticated Core Journey request, read [references/protected-journeys.md](references/protected-journeys.md). - When a completed Audit or Verify contains `provider_tool_recoveries`, read [references/provider-tool-recovery.md](references/provider-tool-recovery.md). - When invoking the CLI or handling its states, read [references/cli-contract.md](references/cli-contract.md). -- For an exact Experimental 0.4.1-to-0.4.2 Project Toolchain update, follow the [version-specific migration authority](https://github.com/codeacme17/launchrally/blob/main/docs/maintainers/p1-migration-notes.md#project-toolchain-migration-041-to-042) and the typed lifecycle router in [references/cli-contract.md](references/cli-contract.md). +- For an exact Experimental 0.4.2-to-0.4.3 Project Toolchain update, follow the [version-specific migration authority](https://github.com/codeacme17/launchrally/blob/main/docs/maintainers/p1-migration-notes.md#project-toolchain-migration-042-to-043) and the typed lifecycle router in [references/cli-contract.md](references/cli-contract.md). - When inspecting or exchanging a Phase 1 architecture record, read [references/phase-1-contracts.md](references/phase-1-contracts.md). Contract availability alone does not make a Phase 1 operation executable. - When a typed `architect` interaction enters Product Intent discovery, read [references/product-intent.md](references/product-intent.md) before presenting semantic-analysis permission or confirmation. - When presenting a Capability Catalog, derived obligations, a Capability Graph, or an Integration Contract, read [references/capability-model.md](references/capability-model.md). @@ -55,7 +55,7 @@ Use the local CLI as the only authority for Checks, Evidence, Severity, release 1. Resolve the exact repository root without changing files. 2. Make `rally --version --json --cwd ` the first discovery operation. Do not probe for or invoke a Project Toolchain Engine directly. -3. If `rally` is absent, explain that CLI installation and Plugin installation are separate, link to the repository's single Install authority, present exactly `npm install --global @launchrally/cli@0.4.2` and the structured verification command, then stop before Audit and wait for the user. Do not run the install, automatically substitute the separate exact-version npm-exec trial/CI entry, or change npm prefixes or shell profiles. +3. If `rally` is absent, explain that CLI installation and Plugin installation are separate, link to the repository's single Install authority, present exactly `npm install --global @launchrally/cli@0.4.3` and the structured verification command, then stop before Audit and wait for the user. Do not run the install, automatically substitute the separate exact-version npm-exec trial/CI entry, or change npm prefixes or shell profiles. 4. Validate the complete version response and `launchrally.dev/execution-authority/v1` object through the compatibility matrix and typed authority router in [references/cli-contract.md](references/cli-contract.md). Plugin, Launcher, selected Engine, and project-pin versions are separate facts and need not be equal when each is supported. 5. For `ready`, invoke every repository operation through `rally` so the Launcher follows the selected Engine. For restore, migrate, or clean, show the exact operation, permissions, targets, and effects and wait for explicit user approval before Agent execution. 6. Invoke Agent Mode with structured output and handle the returned state. Stop on unknown contracts or versions, invalid descriptors or paths, and malformed output. diff --git a/adapters/claude/launchrally/skills/launchrally/references/cli-contract.md b/adapters/claude/launchrally/skills/launchrally/references/cli-contract.md index 064cc0e..62d66cb 100644 --- a/adapters/claude/launchrally/skills/launchrally/references/cli-contract.md +++ b/adapters/claude/launchrally/skills/launchrally/references/cli-contract.md @@ -7,7 +7,7 @@ Invoke Agent Mode with `--json`. Require `contract: "launchrally.dev/cli/v2"`, t CLI installation is a prerequisite separate from Plugin installation. Invoke `rally --version --json --cwd ` as the first discovery operation. If spawning `rally` reports that the executable is missing, present the exact user-managed PATH installation and verification commands below, then stop before Audit and wait: ```bash -npm install --global @launchrally/cli@0.4.2 +npm install --global @launchrally/cli@0.4.3 rally --version --json --cwd ``` @@ -17,10 +17,10 @@ This Skill release uses this explicit compatibility matrix: | Layer | Supported value | Meaning | | --- | --- | --- | -| Plugin version | `0.4.2` | Interaction guidance only; never an execution candidate. | -| Launcher version | `0.3.0`, `0.3.1`, `0.3.2`, `0.4.0`, `0.4.1`, or `0.4.2`, only when the running implementation declares v1 | A supported user-managed `rally` dispatcher implementing the contracts below. Historical published `0.2.2` direct binaries predate v1 interception and do not qualify. | +| Plugin version | `0.4.3` | Interaction guidance only; never an execution candidate. | +| Launcher version | `0.3.0`, `0.3.1`, `0.3.2`, `0.4.0`, `0.4.1`, `0.4.2`, or `0.4.3`, only when the running implementation declares v1 | A supported user-managed `rally` dispatcher implementing the contracts below. Historical published `0.2.2` direct binaries predate v1 interception and do not qualify. | | Execution Authority contract | `launchrally.dev/execution-authority/v1` | The only supported Engine-selection contract. | -| Selected Engine version and contract | `0.2.2`, `0.3.0`, `0.3.1`, `0.3.2`, `0.4.0`, `0.4.1`, or `0.4.2` with the declared compatibility path and CLI interaction `launchrally.dev/cli/v2` | The Engine selected by validated authority. A descriptor-free `0.2.2` project is accepted only through the legacy row below. | +| Selected Engine version and contract | `0.2.2`, `0.3.0`, `0.3.1`, `0.3.2`, `0.4.0`, `0.4.1`, `0.4.2`, or `0.4.3` with the declared compatibility path and CLI interaction `launchrally.dev/cli/v2` | The Engine selected by validated authority. A descriptor-free `0.2.2` project is accepted only through the legacy row below. | | Legacy project pin | `0.2.2`, authority descriptor absent, `compatibility: "legacy_adapter"` | Supported only through the Launcher's allowlisted legacy adapter after explicit restore when materialization is missing. | Keep the Plugin version, Launcher version, selected Engine version, and project pin as separate facts. Read `launcher_version` from the version result, the selected Engine from `cli_version` and `authority.engine`, and the project pin from the validated `project_toolchain` authority. Do not require these versions to be equal; continue only when each value and contract is explicitly supported by the matrix. Any unknown or malformed version must stop before a journey operation. diff --git a/adapters/claude/launchrally/skills/launchrally/references/reference-journey.json b/adapters/claude/launchrally/skills/launchrally/references/reference-journey.json index a5d7480..8b8f24d 100644 --- a/adapters/claude/launchrally/skills/launchrally/references/reference-journey.json +++ b/adapters/claude/launchrally/skills/launchrally/references/reference-journey.json @@ -2,19 +2,19 @@ "schema_version": "launchrally.dev/reference-journey/v3", "compatibility": { "plugin": { - "version": "0.4.2", + "version": "0.4.3", "role": "interaction_only" }, "launcher": { "package": "@launchrally/cli", - "supported_versions": ["0.3.0", "0.3.1", "0.3.2", "0.4.0", "0.4.1", "0.4.2"] + "supported_versions": ["0.3.0", "0.3.1", "0.3.2", "0.4.0", "0.4.1", "0.4.2", "0.4.3"] }, "execution_authority": { "supported_contracts": ["launchrally.dev/execution-authority/v1"] }, "engine": { "package": "@launchrally/cli", - "supported_versions": ["0.2.2", "0.3.0", "0.3.1", "0.3.2", "0.4.0", "0.4.1", "0.4.2"], + "supported_versions": ["0.2.2", "0.3.0", "0.3.1", "0.3.2", "0.4.0", "0.4.1", "0.4.2", "0.4.3"], "authority_contracts": ["launchrally.dev/execution-authority/v1"], "interaction_contracts": ["launchrally.dev/cli/v2"] }, @@ -31,7 +31,7 @@ "installation": { "owner": "user", "executable": "npm", - "arguments": ["install", "--global", "@launchrally/cli@0.4.2"] + "arguments": ["install", "--global", "@launchrally/cli@0.4.3"] }, "verification": { "arguments": ["--version", "--json", "--cwd", "{repository_root}"] @@ -57,7 +57,7 @@ }, "cli": { "package": "@launchrally/cli", - "version": "0.4.2", + "version": "0.4.3", "contract": "launchrally.dev/cli/v2" }, "protected_journeys": { diff --git a/adapters/claude/launchrally/skills/launchrally/references/reference-journey.md b/adapters/claude/launchrally/skills/launchrally/references/reference-journey.md index fb0633a..361fec6 100644 --- a/adapters/claude/launchrally/skills/launchrally/references/reference-journey.md +++ b/adapters/claude/launchrally/skills/launchrally/references/reference-journey.md @@ -9,7 +9,7 @@ The user-managed `rally` Launcher is a prerequisite separate from the Codex or C If `rally` is absent, present these exact user-managed commands, stop before Audit, and wait: ```bash -npm install --global @launchrally/cli@0.4.2 +npm install --global @launchrally/cli@0.4.3 rally --version --json --cwd ``` diff --git a/adapters/codex/launchrally/.codex-plugin/plugin.json b/adapters/codex/launchrally/.codex-plugin/plugin.json index ffa2db2..1201e42 100644 --- a/adapters/codex/launchrally/.codex-plugin/plugin.json +++ b/adapters/codex/launchrally/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "launchrally", - "version": "0.4.2", + "version": "0.4.3", "description": "Audit and verify production launch readiness from an existing repository.", "author": { "name": "codeacme17", diff --git a/adapters/codex/launchrally/README.md b/adapters/codex/launchrally/README.md index 97951a5..e4e92d8 100644 --- a/adapters/codex/launchrally/README.md +++ b/adapters/codex/launchrally/README.md @@ -10,7 +10,7 @@ The optional `@launchrally/codex-plugin/resume` host adapter atomically saves va ## Status -LaunchRally 0.4.2 is an **Experimental Phase 1** release. It adds the Phase 1 Skill and host bridges while Phase 0 Stable 0.3.2 remains on npm `latest`. Publication does not make Phase 1 Validated or Stable. +LaunchRally 0.4.3 is an **Experimental Phase 1** release. It adds the Phase 1 Skill and host bridges while Phase 0 Stable 0.3.2 remains on npm `latest`. Publication does not make Phase 1 Validated or Stable. Phase 0 0.3.2 remains a **Stable** release. It is Product Complete, P0 Validated, and published on the stable channel. @@ -23,7 +23,7 @@ CLI installation and Plugin installation are separate. Before installing this Pl Codex installs Plugins at user scope. Pin the marketplace to the exact release tag, then install LaunchRally: ```sh -codex plugin marketplace add codeacme17/launchrally --ref v0.4.2 +codex plugin marketplace add codeacme17/launchrally --ref v0.4.3 codex plugin add launchrally@launchrally codex plugin list --json ``` @@ -37,7 +37,7 @@ Replace the installed Plugin and exact marketplace checkout deliberately: ```sh codex plugin remove launchrally@launchrally codex plugin marketplace remove launchrally -codex plugin marketplace add codeacme17/launchrally --ref v0.4.2 +codex plugin marketplace add codeacme17/launchrally --ref v0.4.3 codex plugin add launchrally@launchrally ``` diff --git a/adapters/codex/launchrally/host-adapter/authenticated-journey.js b/adapters/codex/launchrally/host-adapter/authenticated-journey.js index b4d64e9..f2c55eb 100644 --- a/adapters/codex/launchrally/host-adapter/authenticated-journey.js +++ b/adapters/codex/launchrally/host-adapter/authenticated-journey.js @@ -4,6 +4,6 @@ export function resumeAuthenticatedJourney(options) { return resumeAuthenticatedJourneyFromHost({ ...options, host: "codex", - version: "0.4.2", + version: "0.4.3", }); } diff --git a/adapters/codex/launchrally/package.json b/adapters/codex/launchrally/package.json index 218b7da..beae143 100644 --- a/adapters/codex/launchrally/package.json +++ b/adapters/codex/launchrally/package.json @@ -1,6 +1,6 @@ { "name": "@launchrally/codex-plugin", - "version": "0.4.2", + "version": "0.4.3", "description": "LaunchRally Plugin adapter for Codex", "keywords": [ "launchrally", @@ -24,7 +24,7 @@ "./resume": "./host-adapter/resume.js" }, "dependencies": { - "@launchrally/core": "0.4.2" + "@launchrally/core": "0.4.3" }, "repository": { "type": "git", diff --git a/adapters/codex/launchrally/skills/launchrally/SKILL.md b/adapters/codex/launchrally/skills/launchrally/SKILL.md index 0e7d181..40d4fb8 100644 --- a/adapters/codex/launchrally/skills/launchrally/SKILL.md +++ b/adapters/codex/launchrally/skills/launchrally/SKILL.md @@ -19,7 +19,7 @@ Use the local CLI as the only authority for Checks, Evidence, Severity, release - When Audit or Verify returns an authenticated Core Journey request, read [references/protected-journeys.md](references/protected-journeys.md). - When a completed Audit or Verify contains `provider_tool_recoveries`, read [references/provider-tool-recovery.md](references/provider-tool-recovery.md). - When invoking the CLI or handling its states, read [references/cli-contract.md](references/cli-contract.md). -- For an exact Experimental 0.4.1-to-0.4.2 Project Toolchain update, follow the [version-specific migration authority](https://github.com/codeacme17/launchrally/blob/main/docs/maintainers/p1-migration-notes.md#project-toolchain-migration-041-to-042) and the typed lifecycle router in [references/cli-contract.md](references/cli-contract.md). +- For an exact Experimental 0.4.2-to-0.4.3 Project Toolchain update, follow the [version-specific migration authority](https://github.com/codeacme17/launchrally/blob/main/docs/maintainers/p1-migration-notes.md#project-toolchain-migration-042-to-043) and the typed lifecycle router in [references/cli-contract.md](references/cli-contract.md). - When inspecting or exchanging a Phase 1 architecture record, read [references/phase-1-contracts.md](references/phase-1-contracts.md). Contract availability alone does not make a Phase 1 operation executable. - When a typed `architect` interaction enters Product Intent discovery, read [references/product-intent.md](references/product-intent.md) before presenting semantic-analysis permission or confirmation. - When presenting a Capability Catalog, derived obligations, a Capability Graph, or an Integration Contract, read [references/capability-model.md](references/capability-model.md). @@ -54,7 +54,7 @@ Use the local CLI as the only authority for Checks, Evidence, Severity, release 1. Resolve the exact repository root without changing files. 2. Make `rally --version --json --cwd ` the first discovery operation. Do not probe for or invoke a Project Toolchain Engine directly. -3. If `rally` is absent, explain that CLI installation and Plugin installation are separate, link to the repository's single Install authority, present exactly `npm install --global @launchrally/cli@0.4.2` and the structured verification command, then stop before Audit and wait for the user. Do not run the install, automatically substitute the separate exact-version npm-exec trial/CI entry, or change npm prefixes or shell profiles. +3. If `rally` is absent, explain that CLI installation and Plugin installation are separate, link to the repository's single Install authority, present exactly `npm install --global @launchrally/cli@0.4.3` and the structured verification command, then stop before Audit and wait for the user. Do not run the install, automatically substitute the separate exact-version npm-exec trial/CI entry, or change npm prefixes or shell profiles. 4. Validate the complete version response and `launchrally.dev/execution-authority/v1` object through the compatibility matrix and typed authority router in [references/cli-contract.md](references/cli-contract.md). Plugin, Launcher, selected Engine, and project-pin versions are separate facts and need not be equal when each is supported. 5. For `ready`, invoke every repository operation through `rally` so the Launcher follows the selected Engine. For restore, migrate, or clean, show the exact operation, permissions, targets, and effects and wait for explicit user approval before Agent execution. 6. Invoke Agent Mode with structured output and handle the returned state. Stop on unknown contracts or versions, invalid descriptors or paths, and malformed output. diff --git a/adapters/codex/launchrally/skills/launchrally/references/cli-contract.md b/adapters/codex/launchrally/skills/launchrally/references/cli-contract.md index 064cc0e..62d66cb 100644 --- a/adapters/codex/launchrally/skills/launchrally/references/cli-contract.md +++ b/adapters/codex/launchrally/skills/launchrally/references/cli-contract.md @@ -7,7 +7,7 @@ Invoke Agent Mode with `--json`. Require `contract: "launchrally.dev/cli/v2"`, t CLI installation is a prerequisite separate from Plugin installation. Invoke `rally --version --json --cwd ` as the first discovery operation. If spawning `rally` reports that the executable is missing, present the exact user-managed PATH installation and verification commands below, then stop before Audit and wait: ```bash -npm install --global @launchrally/cli@0.4.2 +npm install --global @launchrally/cli@0.4.3 rally --version --json --cwd ``` @@ -17,10 +17,10 @@ This Skill release uses this explicit compatibility matrix: | Layer | Supported value | Meaning | | --- | --- | --- | -| Plugin version | `0.4.2` | Interaction guidance only; never an execution candidate. | -| Launcher version | `0.3.0`, `0.3.1`, `0.3.2`, `0.4.0`, `0.4.1`, or `0.4.2`, only when the running implementation declares v1 | A supported user-managed `rally` dispatcher implementing the contracts below. Historical published `0.2.2` direct binaries predate v1 interception and do not qualify. | +| Plugin version | `0.4.3` | Interaction guidance only; never an execution candidate. | +| Launcher version | `0.3.0`, `0.3.1`, `0.3.2`, `0.4.0`, `0.4.1`, `0.4.2`, or `0.4.3`, only when the running implementation declares v1 | A supported user-managed `rally` dispatcher implementing the contracts below. Historical published `0.2.2` direct binaries predate v1 interception and do not qualify. | | Execution Authority contract | `launchrally.dev/execution-authority/v1` | The only supported Engine-selection contract. | -| Selected Engine version and contract | `0.2.2`, `0.3.0`, `0.3.1`, `0.3.2`, `0.4.0`, `0.4.1`, or `0.4.2` with the declared compatibility path and CLI interaction `launchrally.dev/cli/v2` | The Engine selected by validated authority. A descriptor-free `0.2.2` project is accepted only through the legacy row below. | +| Selected Engine version and contract | `0.2.2`, `0.3.0`, `0.3.1`, `0.3.2`, `0.4.0`, `0.4.1`, `0.4.2`, or `0.4.3` with the declared compatibility path and CLI interaction `launchrally.dev/cli/v2` | The Engine selected by validated authority. A descriptor-free `0.2.2` project is accepted only through the legacy row below. | | Legacy project pin | `0.2.2`, authority descriptor absent, `compatibility: "legacy_adapter"` | Supported only through the Launcher's allowlisted legacy adapter after explicit restore when materialization is missing. | Keep the Plugin version, Launcher version, selected Engine version, and project pin as separate facts. Read `launcher_version` from the version result, the selected Engine from `cli_version` and `authority.engine`, and the project pin from the validated `project_toolchain` authority. Do not require these versions to be equal; continue only when each value and contract is explicitly supported by the matrix. Any unknown or malformed version must stop before a journey operation. diff --git a/adapters/codex/launchrally/skills/launchrally/references/reference-journey.json b/adapters/codex/launchrally/skills/launchrally/references/reference-journey.json index a5d7480..8b8f24d 100644 --- a/adapters/codex/launchrally/skills/launchrally/references/reference-journey.json +++ b/adapters/codex/launchrally/skills/launchrally/references/reference-journey.json @@ -2,19 +2,19 @@ "schema_version": "launchrally.dev/reference-journey/v3", "compatibility": { "plugin": { - "version": "0.4.2", + "version": "0.4.3", "role": "interaction_only" }, "launcher": { "package": "@launchrally/cli", - "supported_versions": ["0.3.0", "0.3.1", "0.3.2", "0.4.0", "0.4.1", "0.4.2"] + "supported_versions": ["0.3.0", "0.3.1", "0.3.2", "0.4.0", "0.4.1", "0.4.2", "0.4.3"] }, "execution_authority": { "supported_contracts": ["launchrally.dev/execution-authority/v1"] }, "engine": { "package": "@launchrally/cli", - "supported_versions": ["0.2.2", "0.3.0", "0.3.1", "0.3.2", "0.4.0", "0.4.1", "0.4.2"], + "supported_versions": ["0.2.2", "0.3.0", "0.3.1", "0.3.2", "0.4.0", "0.4.1", "0.4.2", "0.4.3"], "authority_contracts": ["launchrally.dev/execution-authority/v1"], "interaction_contracts": ["launchrally.dev/cli/v2"] }, @@ -31,7 +31,7 @@ "installation": { "owner": "user", "executable": "npm", - "arguments": ["install", "--global", "@launchrally/cli@0.4.2"] + "arguments": ["install", "--global", "@launchrally/cli@0.4.3"] }, "verification": { "arguments": ["--version", "--json", "--cwd", "{repository_root}"] @@ -57,7 +57,7 @@ }, "cli": { "package": "@launchrally/cli", - "version": "0.4.2", + "version": "0.4.3", "contract": "launchrally.dev/cli/v2" }, "protected_journeys": { diff --git a/adapters/codex/launchrally/skills/launchrally/references/reference-journey.md b/adapters/codex/launchrally/skills/launchrally/references/reference-journey.md index fb0633a..361fec6 100644 --- a/adapters/codex/launchrally/skills/launchrally/references/reference-journey.md +++ b/adapters/codex/launchrally/skills/launchrally/references/reference-journey.md @@ -9,7 +9,7 @@ The user-managed `rally` Launcher is a prerequisite separate from the Codex or C If `rally` is absent, present these exact user-managed commands, stop before Audit, and wait: ```bash -npm install --global @launchrally/cli@0.4.2 +npm install --global @launchrally/cli@0.4.3 rally --version --json --cwd ``` diff --git a/docs/getting-started/install.md b/docs/getting-started/install.md index 336f3cf..21add4e 100644 --- a/docs/getting-started/install.md +++ b/docs/getting-started/install.md @@ -3,7 +3,7 @@ LaunchRally `0.3.2` is the exact Stable release used by this guide. Stable availability follows the reviewed P0 Validated decision and satisfied Quality Floor. Updating an initialized Experimental `0.4.1` project is a separate path. Follow -the [exact 0.4.1-to-0.4.2 Project Toolchain migration](../maintainers/p1-migration-notes.md#project-toolchain-migration-041-to-042) +the [exact 0.4.2-to-0.4.3 Project Toolchain migration](../maintainers/p1-migration-notes.md#project-toolchain-migration-042-to-043) without relabeling this Stable `0.3.2` installation path. ## Supported environments diff --git a/docs/getting-started/quickstart.md b/docs/getting-started/quickstart.md index fe79d1b..3036afa 100644 --- a/docs/getting-started/quickstart.md +++ b/docs/getting-started/quickstart.md @@ -3,7 +3,7 @@ LaunchRally `0.3.2` is a public Stable release. Run it against a repository you control and review every disclosed read or write boundary before confirming it. Stable means the reviewed P0 Validated decision and Quality Floor requirements are satisfied. Already using an initialized Experimental `0.4.1` project? Use the separate -[exact 0.4.1-to-0.4.2 Project Toolchain migration](../maintainers/p1-migration-notes.md#project-toolchain-migration-041-to-042). +[exact 0.4.2-to-0.4.3 Project Toolchain migration](../maintainers/p1-migration-notes.md#project-toolchain-migration-042-to-043). That path keeps Launcher, selected Engine, project pin, Plugin, and release channel distinct; it does not change the Stable quickstart below. diff --git a/docs/maintainers/experimental-0.4.3-announcement.md b/docs/maintainers/experimental-0.4.3-announcement.md new file mode 100644 index 0000000..e4951f9 --- /dev/null +++ b/docs/maintainers/experimental-0.4.3-announcement.md @@ -0,0 +1,22 @@ +# LaunchRally 0.4.3 — Phase 1 Experimental + +LaunchRally 0.4.3 publishes a corrective Phase 1 candidate on npm's non-stable +`experimental` channel. It completes styled Human Mode for Version, Handoff, +and Project Toolchain migration while preserving the exact Agent/JSON resume +contracts. It also adds an exact 0.4.2-to-0.4.3 migration path and strengthens +release-note and supply-chain validation. + +At publication time, this exact Phase 1 candidate remains **Product Incomplete** +until its independent external verification through separate clean CLI, Codex, +and Claude journeys is reviewed and linked. The release is **Experimental**, +**not P1 Validated**, and **not Stable**. Publication, CI, downloads, and +elapsed time do not change those states. + +Phase 0 remains independently Product Complete, P0 Validated, and Stable at +0.3.2. npm `latest` continues to resolve to 0.3.2; 0.4.3 is available only from +`experimental` or by exact version. + +Review the [0.4.3 changelog](../../CHANGELOG.md), [exact 0.4.2-to-0.4.3 Project +Toolchain migration](p1-migration-notes.md#project-toolchain-migration-042-to-043), +and [external verification record](experimental-0.4.3-p1-evidence.md) before +adoption. diff --git a/docs/maintainers/experimental-0.4.3-p1-evidence.md b/docs/maintainers/experimental-0.4.3-p1-evidence.md new file mode 100644 index 0000000..31a0d9c --- /dev/null +++ b/docs/maintainers/experimental-0.4.3-p1-evidence.md @@ -0,0 +1,22 @@ +# Phase 1 Experimental 0.4.3 external verification + +Status: pending publication and independent external verification. + +This record is intentionally incomplete in the Release Candidate. It must be +updated only after the protected `v0.4.3` workflow publishes all five exact +artifacts on npm `experimental`, verifies their candidate digests and +provenance, and the clean direct CLI, Codex, and Claude journeys complete. + +The final record will contain only aggregate, non-sensitive conclusions and +public release links. Raw host transcripts, resume tokens, Reports, Evidence, +repository paths, target details, credentials, and personal data must not be +committed or uploaded. + +The machine-readable aggregate is `release/p1-external-verification.json`. +While publication or any external host journey is incomplete, that record +must remain `pending`; it can become `completed` only from three distinct +signed CLI, Codex, and Claude host envelopes accepted by +`scripts/verify-p1-external-results.mjs`. + +Publication does not imply Phase 1 Validated or Stable. Phase 0 Stable 0.3.2 +must remain on npm `latest` throughout the Experimental publication. diff --git a/docs/maintainers/p1-acceptance.md b/docs/maintainers/p1-acceptance.md index 865cfec..ac62b3a 100644 --- a/docs/maintainers/p1-acceptance.md +++ b/docs/maintainers/p1-acceptance.md @@ -49,7 +49,7 @@ open requirement or pending mandatory gate. | P1-HOST-01 | Codex and Claude resume exact Architecture and Handoff state without prose reconstruction | Claude Handoff state resumes in Codex from one validated local artifact | #136 | Complete | | P1-COVERAGE-01 | Representative Provider-neutral Packs preserve honest depth for managed and generic implementations | reference packs cover every product shape and integration family without Provider semantics | #137 | Complete | | P1-DOCS-01 | Agent and Human users can follow the complete authority-aware Phase 1 journey | documented Phase 1 commands are equivalent across POSIX and PowerShell | #138 | Complete | -| P1-RELEASE-01 | Exact P1 artifacts publish only as Experimental before separate external verification and Stable promotion | P1 Experimental publication readiness permits only external verification to remain open; P1 external verification cannot complete without its signed aggregate record; published P1 artifacts match candidate digests, provenance, and the preserved latest line; the P1 announcement and external Agent procedure preserve every lifecycle boundary; external Phase 1 results require machine-checked CLI, Codex, and Claude scenario coverage; packed artifacts complete installation, delegation, lifecycle, and full verification journeys | #201 | Open | +| P1-RELEASE-01 | Exact P1 artifacts publish only as Experimental before separate external verification and Stable promotion | P1 Experimental publication readiness permits only external verification to remain open; P1 external verification cannot complete without its signed aggregate record; published P1 artifacts match candidate digests, provenance, and the preserved latest line; the P1 announcement and external Agent procedure preserve every lifecycle boundary; external Phase 1 results require machine-checked CLI, Codex, and Claude scenario coverage; packed artifacts complete installation, delegation, lifecycle, and full verification journeys | #210 | Open | | P1-VALIDATION-01 | Phase 1 qualitative learning remains append-only, telemetry-free, aggregate, and lifecycle-separated | the Phase 1 Validation Log remains collecting, not validated, Experimental, and not approved | #195 | Complete | ## Independent lifecycle diff --git a/docs/maintainers/p1-external-verification.md b/docs/maintainers/p1-external-verification.md index 1892687..64fe8a8 100644 --- a/docs/maintainers/p1-external-verification.md +++ b/docs/maintainers/p1-external-verification.md @@ -1,6 +1,6 @@ # Phase 1 external verification procedure -Run this procedure only after the protected `v0.4.2` workflow has published +Run this procedure only after the protected `v0.4.3` workflow has published all five packages and its public smoke plus provenance checks are green. Use a fresh local account or disposable VM for each host. Do not copy tokens, cookies, configuration, Reports, Evidence, repository data, or host state from @@ -16,7 +16,7 @@ authored `$PHASE1_FIXTURE` is accepted. ```bash PHASE1_RELEASE="$(mktemp -d)/launchrally" -git clone --depth 1 --branch v0.4.2 https://github.com/codeacme17/launchrally.git "$PHASE1_RELEASE" +git clone --depth 1 --branch v0.4.3 https://github.com/codeacme17/launchrally.git "$PHASE1_RELEASE" cd "$PHASE1_RELEASE" npm ci --ignore-scripts node scripts/verify-experimental-release.mjs --phase published --json @@ -27,7 +27,7 @@ node scripts/record-p1-external-host.mjs \ --output /tmp/launchrally-p1-cli.json ``` -The result must identify 0.4.2 on `experimental`, retain 0.3.2 on `latest`, and +The result must identify 0.4.3 on `experimental`, retain 0.3.2 on `latest`, and match all five committed integrity values and the GitHub release-workflow SLSA provenance. Stop if any identity, digest, tag, commit, or channel differs. @@ -37,7 +37,7 @@ Use the clean host's own authenticated Codex session; never export its credential into the fixture or transcript. ```bash -codex plugin marketplace add codeacme17/launchrally --ref v0.4.2 +codex plugin marketplace add codeacme17/launchrally --ref v0.4.3 codex plugin add launchrally@launchrally CODEX_CHALLENGE="$(openssl rand -hex 32)" codex -C "$PHASE1_RELEASE" "Use the installed LaunchRally Skill. Run exactly: node scripts/record-p1-external-host.mjs --host codex --challenge $CODEX_CHALLENGE --output /tmp/launchrally-p1-codex.json. Do not replace the command with prose. Report success only if the command exits zero." @@ -51,7 +51,7 @@ Use the clean host's own authenticated Claude Code session with the Plugin installed at explicit user scope. ```bash -claude plugin marketplace add codeacme17/launchrally@v0.4.2 --scope user +claude plugin marketplace add codeacme17/launchrally@v0.4.3 --scope user claude plugin install launchrally@launchrally --scope user cd "$PHASE1_RELEASE" CLAUDE_CHALLENGE="$(openssl rand -hex 32)" @@ -77,7 +77,7 @@ Generate the exact review statement from all three envelopes: ```bash node scripts/verify-p1-external-results.mjs \ - --version 0.4.2 \ + --version 0.4.3 \ --cli /tmp/launchrally-p1-cli.json \ --codex /tmp/launchrally-p1-codex.json \ --claude /tmp/launchrally-p1-claude.json \ @@ -85,7 +85,7 @@ node scripts/verify-p1-external-results.mjs \ ``` An independent external reviewer must have observed the three native -invocations and post that exact statement as a comment on issue #141. The +invocations and post that exact statement as a comment on issue #210. The reviewer must not be the actor who triggered the protected release workflow. The final verifier reads both identities from GitHub and rejects self-review. @@ -94,12 +94,12 @@ prose. Replace both placeholders with exact public URLs: ```bash node scripts/verify-p1-external-results.mjs \ - --version 0.4.2 \ + --version 0.4.3 \ --cli /tmp/launchrally-p1-cli.json \ --codex /tmp/launchrally-p1-codex.json \ --claude /tmp/launchrally-p1-claude.json \ --workflow-url https://github.com/codeacme17/launchrally/actions/runs/REPLACE_WITH_RUN_ID \ - --release-url https://github.com/codeacme17/launchrally/releases/tag/v0.4.2 \ + --release-url https://github.com/codeacme17/launchrally/releases/tag/v0.4.3 \ --review-url https://github.com/codeacme17/launchrally/issues/141#issuecomment-REPLACE_WITH_COMMENT_ID \ --output /tmp/launchrally-p1-external-verification.json \ --json @@ -120,7 +120,7 @@ Receipt cannot replace independently observed zero-exit Agent invocations and their machine-checked envelopes. Record only these non-sensitive conclusions in -`experimental-0.4.2-p1-evidence.md`: host and exact version, public workflow +`experimental-0.4.3-p1-evidence.md`: host and exact version, public workflow URL, public release/package links, scenario status, typed boundary reached, and whether the fresh Verify result was qualifying. Independently review that record before changing `P1-RELEASE-01`, `p1_external_verification`, or Product diff --git a/docs/maintainers/p1-migration-notes.md b/docs/maintainers/p1-migration-notes.md index 7ca4819..96ad7b5 100644 --- a/docs/maintainers/p1-migration-notes.md +++ b/docs/maintainers/p1-migration-notes.md @@ -1,12 +1,12 @@ # Phase 1 Experimental migration notes -LaunchRally 0.4.2 continues the additive Phase 1 layer. Existing Phase 0 +LaunchRally 0.4.3 continues the additive Phase 1 layer. Existing Phase 0 projects and the public 0.3.2 Stable line remain independently valid. -## Project Toolchain migration: 0.4.1 to 0.4.2 +## Project Toolchain migration: 0.4.2 to 0.4.3 This procedure is only for an initialized project whose established Engine pin -is `0.4.1`. LaunchRally 0.4.2 remains **Experimental** on npm's +is `0.4.2`. LaunchRally 0.4.3 remains **Experimental** on npm's `experimental` channel; completing this migration does not make Phase 1 P1 Validated or Stable. Phase 0 `0.3.2` remains the release on npm `latest`. @@ -19,11 +19,11 @@ Treat these as five separate facts and lifecycles: repository-owned Project Toolchain; - a Codex or Claude **Plugin** supplies host interaction guidance and never selects or replaces the Engine; and -- `0.4.2` is on the **Experimental** channel, independently of those versions. +- `0.4.3` is on the **Experimental** channel, independently of those versions. Updating the Launcher does not update an initialized project's valid pin. Thus, after the first install below, structured version output with -`launcher_version: "0.4.2"`, `cli_version: "0.4.1"`, and +`launcher_version: "0.4.3"`, `cli_version: "0.4.2"`, and `authority.source: "project_toolchain"` is expected. It proves that the newer Launcher followed the established Engine; it is not a failed installation. Only an explicitly confirmed `toolchain migrate` changes that pin. `init` is @@ -41,20 +41,19 @@ Keep the original Manifest-bound source Audit Report used by confirmed Init. In the examples, set `SOURCE_REPORT` to that external JSON file, not to the prior or new `.launchrally/reports//record.json` current Report. -### Current 0.4.2 confirmation behavior +### Current 0.4.3 confirmation behavior -The shipped 0.4.2 `toolchain migrate` command does not provide a styled -in-process prompt. In default TTY Human Mode it emits the typed JSON permission -or migration preview and exits. Review that complete JSON, preserve its opaque -`interaction.resume_token`, and run the explicit resume command shown below. -The optional registry permission and the migration confirmation are separate -decisions. This is the current 0.4.2 behavior; it does not promise the future -in-process interaction tracked by issue #204. +In an interactive TTY, the shipped 0.4.3 `toolchain migrate` command keeps its +resume token internal, renders a styled concise preview, defaults to decline, +allows the complete exact diff to be reviewed, and returns to the same prompt +before completing a confirmed migration in-process. Cancellation, denial, or a +stale preview changes no Project Toolchain state. -Agent/CI Mode may add `--json` and follow the same -`launchrally.dev/toolchain-lifecycle/v1` states. An Agent must validate typed -fields and preserve tokens verbatim rather than parse Human prose or infer a -permission or confirmation. +The copy-pasteable procedures below intentionally use Agent/CI `--json` so the +complete `launchrally.dev/toolchain-lifecycle/v1` protocol remains explicit +and shell-equivalent. Validate every typed field, preserve each opaque token +verbatim, and keep the optional registry permission separate from migration +confirmation. Never parse Human prose or infer either decision. ### POSIX @@ -64,29 +63,29 @@ delete them yourself after the migration if they are no longer needed. ```sh PROJECT_ROOT=/path/to/project SOURCE_REPORT=/path/to/original-manifest-bound-audit-report.json -MIGRATION_RESPONSE=./launchrally-0.4.2-migration-response.json +MIGRATION_RESPONSE=./launchrally-0.4.3-migration-response.json -npm install --global @launchrally/cli@0.4.2 +npm install --global @launchrally/cli@0.4.3 rally --version --json rally --version --json --cwd "$PROJECT_ROOT" rally toolchain status --json --cwd "$PROJECT_ROOT" -rally toolchain migrate --to 0.4.2 --cwd "$PROJECT_ROOT" > "$MIGRATION_RESPONSE" +rally toolchain migrate --to 0.4.3 --json --cwd "$PROJECT_ROOT" > "$MIGRATION_RESPONSE" node -e 'const fs = require("node:fs"); const value = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); console.log(value.status); console.log(JSON.stringify(value.request ?? {}, null, 2));' "$MIGRATION_RESPONSE" ``` Before migrating, require the repository-scoped version and status results to -identify the established 0.4.1 project pin and selected Engine separately from -the 0.4.2 Launcher. +identify the established 0.4.2 project pin and selected Engine separately from +the 0.4.3 Launcher. If the status is `needs_permission`, review the exact `npm_registry_read` request. To deny it, replace `approved` with `denied`; denial preserves the -0.4.1 pin and returns `registry_permission_denied`. To approve only that bounded +0.4.2 pin and returns `registry_permission_denied`. To approve only that bounded read and obtain the migration preview: ```sh MIGRATION_TOKEN="$(node -e 'const fs = require("node:fs"); const value = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); process.stdout.write(value.interaction.resume_token);' "$MIGRATION_RESPONSE")" -rally toolchain migrate --to 0.4.2 --cwd "$PROJECT_ROOT" --resume "$MIGRATION_TOKEN" --permissions '{"npm_registry_read":"approved"}' > "$MIGRATION_RESPONSE" +rally toolchain migrate --to 0.4.3 --json --cwd "$PROJECT_ROOT" --resume "$MIGRATION_TOKEN" --permissions '{"npm_registry_read":"approved"}' > "$MIGRATION_RESPONSE" node -e 'const fs = require("node:fs"); const value = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); console.log(value.status); console.log(JSON.stringify(value.preview ?? {}, null, 2));' "$MIGRATION_RESPONSE" ``` @@ -96,14 +95,14 @@ that exact preview: ```sh MIGRATION_TOKEN="$(node -e 'const fs = require("node:fs"); const value = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); process.stdout.write(value.interaction.resume_token);' "$MIGRATION_RESPONSE")" -rally toolchain migrate --to 0.4.2 --cwd "$PROJECT_ROOT" --resume "$MIGRATION_TOKEN" --confirm confirm +rally toolchain migrate --to 0.4.3 --json --cwd "$PROJECT_ROOT" --resume "$MIGRATION_TOKEN" --confirm confirm rally --version --json --cwd "$PROJECT_ROOT" rally verify --cwd "$PROJECT_ROOT" --report "$SOURCE_REPORT" --scope full ``` Require `authority.state: "ready"`, `authority.source: "project_toolchain"`, -`launcher_version: "0.4.2"`, and `cli_version: "0.4.2"` before Verify. A +`launcher_version: "0.4.3"`, and `cli_version: "0.4.3"` before Verify. A completed Verify creates a new current Report. Use the exact current Report path printed by completion for Plan or Architect; retain `SOURCE_REPORT` as the Manifest-bound input to future whole-release Verify runs. @@ -113,25 +112,25 @@ Manifest-bound input to future whole-release Verify runs. ```powershell $ProjectRoot = 'C:\path\to\project' $SourceReport = 'C:\path\to\original-manifest-bound-audit-report.json' -$MigrationResponse = '.\launchrally-0.4.2-migration-response.json' +$MigrationResponse = '.\launchrally-0.4.3-migration-response.json' -npm install --global @launchrally/cli@0.4.2 +npm install --global @launchrally/cli@0.4.3 rally --version --json rally --version --json --cwd $ProjectRoot rally toolchain status --json --cwd $ProjectRoot -rally toolchain migrate --to 0.4.2 --cwd $ProjectRoot | Set-Content -Encoding utf8 $MigrationResponse +rally toolchain migrate --to 0.4.3 --json --cwd $ProjectRoot | Set-Content -Encoding utf8 $MigrationResponse $Migration = Get-Content -Raw $MigrationResponse | ConvertFrom-Json $Migration.status $Migration.request | ConvertTo-Json -Depth 20 ``` If `$Migration.status` is `needs_permission`, review the exact -`npm_registry_read` request. Use `denied` to preserve the 0.4.1 pin, or approve +`npm_registry_read` request. Use `denied` to preserve the 0.4.2 pin, or approve only that request and obtain the preview: ```powershell -rally toolchain migrate --to 0.4.2 --cwd $ProjectRoot --resume $Migration.interaction.resume_token --permissions '{"npm_registry_read":"approved"}' | Set-Content -Encoding utf8 $MigrationResponse +rally toolchain migrate --to 0.4.3 --json --cwd $ProjectRoot --resume $Migration.interaction.resume_token --permissions '{"npm_registry_read":"approved"}' | Set-Content -Encoding utf8 $MigrationResponse $Migration = Get-Content -Raw $MigrationResponse | ConvertFrom-Json $Migration.status $Migration.preview | ConvertTo-Json -Depth 20 @@ -142,7 +141,7 @@ When the response is `needs_confirmation`, inspect every confirm the exact preview: ```powershell -rally toolchain migrate --to 0.4.2 --cwd $ProjectRoot --resume $Migration.interaction.resume_token --confirm confirm +rally toolchain migrate --to 0.4.3 --json --cwd $ProjectRoot --resume $Migration.interaction.resume_token --confirm confirm rally --version --json --cwd $ProjectRoot rally verify --cwd $ProjectRoot --report $SourceReport --scope full @@ -154,7 +153,7 @@ Plan or Architect, and keep `$SourceReport` for future whole-release Verify. ### Denial, recovery, restore, and downgrade - A denied registry read, `--confirm decline`, an abandoned preview, or Ctrl-C - before adoption preserves the 0.4.1 project authority. Start a new migrate + before adoption preserves the 0.4.2 project authority. Start a new migrate operation if an opaque token is lost or invalid; never reconstruct it. - Missing registry access is not permission to use `sudo`, a floating version, a silent npm action, a changed npm prefix or shell profile, a copied @@ -168,10 +167,10 @@ Plan or Architect, and keep `$SourceReport` for future whole-release Verify. - `rally toolchain restore --cwd ` only rebuilds the established exact pin; it does not undo a completed migration. It may independently request the same bounded registry read. -- With Launcher 0.4.2, the only supported downgrade target is the allowlisted +- With Launcher 0.4.3, the only supported downgrade target is the allowlisted legacy Engine: `rally toolchain migrate --to 0.2.2 --cwd `. It uses the same preview/resume protocol, makes the prior current Report non-current, - and requires fresh full Verify. A 0.4.1 direct downgrade is unsupported by + and requires fresh full Verify. A 0.4.2 direct downgrade is unsupported by the shipped lifecycle; `restore` cannot be used to bypass that restriction. There is no automatic rollback or arbitrary unsupported-version bypass. @@ -188,7 +187,7 @@ exact release tag: ```sh codex plugin remove launchrally@launchrally codex plugin marketplace remove launchrally -codex plugin marketplace add codeacme17/launchrally --ref v0.4.2 +codex plugin marketplace add codeacme17/launchrally --ref v0.4.3 codex plugin add launchrally@launchrally codex plugin list --json ``` @@ -196,12 +195,12 @@ codex plugin list --json #### Claude Plugin Replace the installed Claude Plugin and marketplace checkout, pinning the -repository to the exact `v0.4.2` tag at explicit user scope: +repository to the exact `v0.4.3` tag at explicit user scope: ```sh claude plugin uninstall launchrally@launchrally --scope user claude plugin marketplace remove launchrally -claude plugin marketplace add codeacme17/launchrally@v0.4.2 --scope user +claude plugin marketplace add codeacme17/launchrally@v0.4.3 --scope user claude plugin install launchrally@launchrally --scope user claude plugin list --json ``` @@ -218,7 +217,7 @@ Project Toolchain migration. ## Adoption -Install 0.4.2 only by selecting the non-stable `experimental` channel or the +Install 0.4.3 only by selecting the non-stable `experimental` channel or the exact version. Architect previews a versioned Phase 1 adoption before any project write. Confirmation creates only the disclosed `.launchrally/phase-1` records and retains existing Phase 0 bytes. Denial and interruption leave the diff --git a/docs/maintainers/release-runbook.md b/docs/maintainers/release-runbook.md index f402a9e..14b67f2 100644 --- a/docs/maintainers/release-runbook.md +++ b/docs/maintainers/release-runbook.md @@ -66,8 +66,8 @@ Create a protected annotated tag on that exact `main` commit and push only the tag: ```bash -git tag --annotate v0.4.2 --message "LaunchRally 0.4.2 Phase 1 Experimental" -git push origin v0.4.2 +git tag --annotate v0.4.3 --message "LaunchRally 0.4.3 Phase 1 Experimental" +git push origin v0.4.3 ``` The release workflow independently rejects a lightweight tag, a tag whose @@ -93,7 +93,7 @@ that every new page shows its package-specific README and keywords during the public smoke check; source changes alone do not update the registry pages. Attach the successful workflow URL, five public package URLs, attestation -result, exact CLI result, and both Plugin results to issue #201. Keep Phase 1 +result, exact CLI result, and both Plugin results to issue #210. Keep Phase 1 Incomplete, Experimental, and Not Validated until those external results have been independently reviewed and merged into the P1 evidence and governance records. Publication never moves npm `latest`; Phase 0 Stable 0.3.2 remains diff --git a/package-lock.json b/package-lock.json index a48b110..1bae098 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "launchrally", - "version": "0.4.2", + "version": "0.4.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "launchrally", - "version": "0.4.2", + "version": "0.4.3", "license": "Apache-2.0", "workspaces": [ "packages/*", @@ -22,18 +22,18 @@ }, "adapters/claude/launchrally": { "name": "@launchrally/claude-plugin", - "version": "0.4.2", + "version": "0.4.3", "license": "Apache-2.0", "dependencies": { - "@launchrally/core": "0.4.2" + "@launchrally/core": "0.4.3" } }, "adapters/codex/launchrally": { "name": "@launchrally/codex-plugin", - "version": "0.4.2", + "version": "0.4.3", "license": "Apache-2.0", "dependencies": { - "@launchrally/core": "0.4.2" + "@launchrally/core": "0.4.3" } }, "node_modules/@anthropic-ai/claude-code": { @@ -393,13 +393,13 @@ }, "packages/cli": { "name": "@launchrally/cli", - "version": "0.4.2", + "version": "0.4.3", "license": "Apache-2.0", "dependencies": { "@clack/core": "1.4.3", "@clack/prompts": "1.7.0", - "@launchrally/contracts": "0.4.2", - "@launchrally/core": "0.4.2" + "@launchrally/contracts": "0.4.3", + "@launchrally/core": "0.4.3" }, "bin": { "rally": "bin/rally.js" @@ -410,15 +410,15 @@ }, "packages/contracts": { "name": "@launchrally/contracts", - "version": "0.4.2", + "version": "0.4.3", "license": "Apache-2.0" }, "packages/core": { "name": "@launchrally/core", - "version": "0.4.2", + "version": "0.4.3", "license": "Apache-2.0", "dependencies": { - "@launchrally/contracts": "0.4.2" + "@launchrally/contracts": "0.4.3" } } } diff --git a/package.json b/package.json index f54d8b5..cb7d9cf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "launchrally", - "version": "0.4.2", + "version": "0.4.3", "private": true, "description": "Local-first launch readiness audit and verification", "license": "Apache-2.0", diff --git a/packages/cli/README.md b/packages/cli/README.md index 8bcce1e..1d972cb 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -4,7 +4,7 @@ The LaunchRally CLI provides the `rally` Launcher and deterministic Engine for a ## Status -LaunchRally 0.4.2 is an **Experimental Phase 1** release. Phase 1 adds Product Intent, Provider-neutral Architecture, bounded Executor coordination, and fresh assurance without relabeling Phase 0 Stable 0.3.2 on npm `latest`. Phase 1 publication does not imply Validated or Stable. Review every disclosed permission and preview before continuing. +LaunchRally 0.4.3 is an **Experimental Phase 1** release. Phase 1 adds Product Intent, Provider-neutral Architecture, bounded Executor coordination, and fresh assurance without relabeling Phase 0 Stable 0.3.2 on npm `latest`. Phase 1 publication does not imply Validated or Stable. Review every disclosed permission and preview before continuing. Phase 0 0.3.2 remains a **Stable** release. P0 is Product Complete and P0 Validated with the Quality Floor satisfied. @@ -13,7 +13,7 @@ Phase 0 0.3.2 remains a **Stable** release. P0 is Product Complete and P0 Valida Install the exact Launcher through your current user-writable npm prefix and verify it before entering a repository: ```sh -npm install --global @launchrally/cli@0.4.2 +npm install --global @launchrally/cli@0.4.3 rally --version --json ``` @@ -38,7 +38,7 @@ Ordinary Init preserves an existing Manifest while it may preview adoption of a Exact-version npm-exec remains a no-install trial and CI fallback. Keep its full prefix on every follow-up; see the [Quickstart](https://github.com/codeacme17/launchrally/blob/main/docs/getting-started/quickstart.md) for the directly executable sequence. -For an initialized Experimental 0.4.1 project, follow the [exact 0.4.1-to-0.4.2 Project Toolchain migration](https://github.com/codeacme17/launchrally/blob/main/docs/maintainers/p1-migration-notes.md#project-toolchain-migration-041-to-042). Updating the Launcher, project pin, and optional Plugin are separate actions. +For an initialized Experimental 0.4.2 project, follow the [exact 0.4.2-to-0.4.3 Project Toolchain migration](https://github.com/codeacme17/launchrally/blob/main/docs/maintainers/p1-migration-notes.md#project-toolchain-migration-042-to-043). Updating the Launcher, project pin, and optional Plugin are separate actions. ## Compatibility and boundaries @@ -52,7 +52,7 @@ When an approved read cannot find its official Provider executable, the Report p - [Install, lifecycle, and troubleshooting](https://github.com/codeacme17/launchrally/blob/main/docs/getting-started/install.md) - [Quickstart](https://github.com/codeacme17/launchrally/blob/main/docs/getting-started/quickstart.md) -- [0.4.1-to-0.4.2 Project Toolchain migration](https://github.com/codeacme17/launchrally/blob/main/docs/maintainers/p1-migration-notes.md#project-toolchain-migration-041-to-042) +- [0.4.2-to-0.4.3 Project Toolchain migration](https://github.com/codeacme17/launchrally/blob/main/docs/maintainers/p1-migration-notes.md#project-toolchain-migration-042-to-043) - [Privacy boundary](https://github.com/codeacme17/launchrally/blob/main/docs/concepts/privacy.md) - [Project data model](https://github.com/codeacme17/launchrally/blob/main/docs/concepts/data-model.md) diff --git a/packages/cli/bin/version.js b/packages/cli/bin/version.js index 2391cca..bf562a1 100644 --- a/packages/cli/bin/version.js +++ b/packages/cli/bin/version.js @@ -1 +1 @@ -export const VERSION = "0.4.2"; +export const VERSION = "0.4.3"; diff --git a/packages/cli/package.json b/packages/cli/package.json index f1c4be6..8d596ad 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@launchrally/cli", - "version": "0.4.2", + "version": "0.4.3", "description": "LaunchRally local-first audit CLI", "keywords": [ "launch-readiness", @@ -28,8 +28,8 @@ "dependencies": { "@clack/core": "1.4.3", "@clack/prompts": "1.7.0", - "@launchrally/contracts": "0.4.2", - "@launchrally/core": "0.4.2" + "@launchrally/contracts": "0.4.3", + "@launchrally/core": "0.4.3" }, "repository": { "type": "git", diff --git a/packages/contracts/README.md b/packages/contracts/README.md index 5b02160..b11c2fc 100644 --- a/packages/contracts/README.md +++ b/packages/contracts/README.md @@ -14,14 +14,14 @@ The authenticated Journey Evidence contract qualifies only structurally validate ## Status -LaunchRally 0.4.2 is an **Experimental Phase 1** release. Its public contracts are versioned independently from package release status. Phase 0 Stable 0.3.2 remains on npm `latest`; publishing Phase 1 does not make it Validated or Stable. Consumers should reject unsupported contract major versions and pin the exact package version they have tested. +LaunchRally 0.4.3 is an **Experimental Phase 1** release. Its public contracts are versioned independently from package release status. Phase 0 Stable 0.3.2 remains on npm `latest`; publishing Phase 1 does not make it Validated or Stable. Consumers should reject unsupported contract major versions and pin the exact package version they have tested. Phase 0 0.3.2 remains a **Stable** release. Stable availability follows the reviewed P0 Validated decision and satisfied Quality Floor. ## Install and import ```sh -npm install @launchrally/contracts@0.4.2 +npm install @launchrally/contracts@0.4.3 ``` ```js diff --git a/packages/contracts/package.json b/packages/contracts/package.json index 0bd5145..106364c 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -1,6 +1,6 @@ { "name": "@launchrally/contracts", - "version": "0.4.2", + "version": "0.4.3", "description": "Versioned LaunchRally CLI and report contracts", "keywords": [ "launchrally", diff --git a/packages/core/README.md b/packages/core/README.md index c7f4520..13af036 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -4,7 +4,7 @@ ## Status -LaunchRally 0.4.2 is an **Experimental Phase 1** release. It adds the local-first Phase 1 decision, coordination, and assurance layer while Phase 0 Stable 0.3.2 remains available on npm `latest`. Publication does not make Phase 1 Validated or Stable. Pin the exact version and review release changes before upgrading. +LaunchRally 0.4.3 is an **Experimental Phase 1** release. It adds the local-first Phase 1 decision, coordination, and assurance layer while Phase 0 Stable 0.3.2 remains available on npm `latest`. Publication does not make Phase 1 Validated or Stable. Pin the exact version and review release changes before upgrading. Phase 0 0.3.2 remains a **Stable** release. The package is Product Complete, P0 Validated, and published on the stable channel. @@ -31,13 +31,13 @@ Confirmed decisions can be materialized with `createArchitecturePackageBundle`. ## Install and use ```sh -npm install @launchrally/core@0.4.2 +npm install @launchrally/core@0.4.3 ``` ```js import { runAudit } from "@launchrally/core"; -const interaction = await runAudit(process.cwd(), "0.4.2"); +const interaction = await runAudit(process.cwd(), "0.4.3"); console.log(interaction.status, interaction.next); ``` diff --git a/packages/core/package.json b/packages/core/package.json index 6947b85..b24214e 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@launchrally/core", - "version": "0.4.2", + "version": "0.4.3", "description": "Deterministic LaunchRally audit and verification core", "keywords": [ "launchrally", @@ -21,7 +21,7 @@ "src/" ], "dependencies": { - "@launchrally/contracts": "0.4.2" + "@launchrally/contracts": "0.4.3" }, "repository": { "type": "git", diff --git a/packages/core/src/architecture-journey.js b/packages/core/src/architecture-journey.js index 4a65c16..6794afa 100644 --- a/packages/core/src/architecture-journey.js +++ b/packages/core/src/architecture-journey.js @@ -175,7 +175,7 @@ async function applyAdoption(state, fileOperations = {}) { export async function runArchitectureJourney(cwd, source = {}, options = {}, dependencies = {}) { const selectedRoot = path.resolve(cwd); const root = await realpath(selectedRoot); - const launcherVersion = options.launcher_version ?? "0.4.2"; + const launcherVersion = options.launcher_version ?? "0.4.3"; if (options.resume_token) { const candidate = (dependencies.load_state ?? loadArchitectureState)(options.resume_token); const state = candidate?.state_version === STATE_VERSION ? candidate : null; diff --git a/packages/core/src/architecture-package.js b/packages/core/src/architecture-package.js index 3a008a8..db60c0b 100644 --- a/packages/core/src/architecture-package.js +++ b/packages/core/src/architecture-package.js @@ -540,7 +540,7 @@ export async function previewArchitecturePackagePersistence(root, bundle, option const resolvedRoot = await repositoryRoot(root); const initialized = await initializedProject( resolvedRoot, - options.launcher_version ?? "0.4.2", + options.launcher_version ?? "0.4.3", ); if (!initialized && !options.output_path) { return { mode: "output_only", requires_confirmation: false, files: [] }; @@ -748,7 +748,7 @@ async function persistInitializedArchitecturePackage( preview, options, ) { - if (!await initializedProject(resolvedRoot, options.launcher_version ?? "0.4.2")) { + if (!await initializedProject(resolvedRoot, options.launcher_version ?? "0.4.3")) { const error = new Error("The repository was no longer initialized at confirmation time."); error.code = "architecture_persistence_scope_changed"; throw error; diff --git a/release/p1-acceptance.json b/release/p1-acceptance.json index 76bd946..ecfc4b2 100644 --- a/release/p1-acceptance.json +++ b/release/p1-acceptance.json @@ -115,7 +115,7 @@ "id": "P1-DOCS-01", "requirement": "Agent and Human users can follow the complete authority-aware Phase 1 journey", "contracts": ["skills/launchrally/references/phase-1-command-examples.json"], "implementation": ["docs/getting-started/phase-1.md"], "tests": [{"path": "test/phase-1-documentation.test.js", "name": "documented Phase 1 commands are equivalent across POSIX and PowerShell"}], "tracking": "#138", "status": "complete", "gates": ["p1_traceability", "p1_quality_floor", "p1_exact_artifacts"] }, { - "id": "P1-RELEASE-01", "requirement": "Exact P1 artifacts publish only as Experimental before separate external verification and Stable promotion", "contracts": ["release/p1.json", "release/p1-regression-registry.json", "release/p1-release-candidate.json", "release/p1-external-verification.json"], "implementation": ["scripts/release-version.mjs", "scripts/validate-p0.mjs", "scripts/validate-p1.mjs", "scripts/test-release-artifacts.mjs", "scripts/verify-experimental-release.mjs", "scripts/record-p1-external-host.mjs", "scripts/verify-p1-external-results.mjs", ".github/workflows/ci.yml", ".github/workflows/release.yml", "CHANGELOG.md", "docs/maintainers/p1-migration-notes.md", "docs/maintainers/p1-external-verification.md", "docs/maintainers/experimental-0.4.2-announcement.md", "docs/maintainers/experimental-0.4.2-p1-evidence.md"], "tests": [{"path": "test/p0-release.test.js", "name": "P0 Stable remains valid only when a later Experimental P1 candidate preserves its exact line"}, {"path": "test/p1-governance.test.js", "name": "P1 Experimental publication readiness permits only external verification to remain open"}, {"path": "test/p1-governance.test.js", "name": "P0 Stable never promotes P1 beyond Experimental without separate approval"}, {"path": "test/p1-governance.test.js", "name": "P1 regression assignments remain append-only against the reviewed Git baseline"}, {"path": "test/p1-governance.test.js", "name": "CI compares the P1 regression registry with every reviewed base"}, {"path": "test/p1-governance.test.js", "name": "P1 external verification cannot complete without its signed aggregate record"}, {"path": "test/release.test.js", "name": "the P1 artifact matrix rejects a CI target that does not match the runtime"}, {"path": "test/release.test.js", "name": "release CI runs every exact P1 platform and shell target"}, {"path": "test/release.test.js", "name": "packed artifacts complete installation, delegation, lifecycle, and full verification journeys"}, {"path": "test/release.test.js", "name": "Experimental P1 publication is gated independently from the P0 Stable channel"}, {"path": "test/release.test.js", "name": "the P1 Experimental candidate advances coherently without moving P0 latest"}, {"path": "test/release.test.js", "name": "published P1 artifacts match candidate digests, provenance, and the preserved latest line"}, {"path": "test/release.test.js", "name": "the P1 announcement and external Agent procedure preserve every lifecycle boundary"}, {"path": "test/release.test.js", "name": "external Phase 1 results require machine-checked CLI, Codex, and Claude scenario coverage"}], "tracking": "#201", "status": "open", "gates": ["p1_traceability", "p1_quality_floor", "p1_supply_chain", "p1_exact_artifacts", "p1_external_verification"] + "id": "P1-RELEASE-01", "requirement": "Exact P1 artifacts publish only as Experimental before separate external verification and Stable promotion", "contracts": ["release/p1.json", "release/p1-regression-registry.json", "release/p1-release-candidate.json", "release/p1-external-verification.json"], "implementation": ["scripts/release-version.mjs", "scripts/validate-p0.mjs", "scripts/validate-p1.mjs", "scripts/test-release-artifacts.mjs", "scripts/verify-experimental-release.mjs", "scripts/record-p1-external-host.mjs", "scripts/verify-p1-external-results.mjs", ".github/workflows/ci.yml", ".github/workflows/release.yml", "CHANGELOG.md", "docs/maintainers/p1-migration-notes.md", "docs/maintainers/p1-external-verification.md", "docs/maintainers/experimental-0.4.3-announcement.md", "docs/maintainers/experimental-0.4.3-p1-evidence.md"], "tests": [{"path": "test/p0-release.test.js", "name": "P0 Stable remains valid only when a later Experimental P1 candidate preserves its exact line"}, {"path": "test/p1-governance.test.js", "name": "P1 Experimental publication readiness permits only external verification to remain open"}, {"path": "test/p1-governance.test.js", "name": "P0 Stable never promotes P1 beyond Experimental without separate approval"}, {"path": "test/p1-governance.test.js", "name": "P1 regression assignments remain append-only against the reviewed Git baseline"}, {"path": "test/p1-governance.test.js", "name": "CI compares the P1 regression registry with every reviewed base"}, {"path": "test/p1-governance.test.js", "name": "P1 external verification cannot complete without its signed aggregate record"}, {"path": "test/release.test.js", "name": "the P1 artifact matrix rejects a CI target that does not match the runtime"}, {"path": "test/release.test.js", "name": "release CI runs every exact P1 platform and shell target"}, {"path": "test/release.test.js", "name": "packed artifacts complete installation, delegation, lifecycle, and full verification journeys"}, {"path": "test/release.test.js", "name": "Experimental P1 publication is gated independently from the P0 Stable channel"}, {"path": "test/release.test.js", "name": "the P1 Experimental candidate advances coherently without moving P0 latest"}, {"path": "test/release.test.js", "name": "published P1 artifacts match candidate digests, provenance, and the preserved latest line"}, {"path": "test/release.test.js", "name": "the P1 announcement and external Agent procedure preserve every lifecycle boundary"}, {"path": "test/release.test.js", "name": "external Phase 1 results require machine-checked CLI, Codex, and Claude scenario coverage"}], "tracking": "#210", "status": "open", "gates": ["p1_traceability", "p1_quality_floor", "p1_supply_chain", "p1_exact_artifacts", "p1_external_verification"] }, { "id": "P1-VALIDATION-01", "requirement": "Phase 1 qualitative learning remains append-only, telemetry-free, aggregate, and lifecycle-separated", "contracts": ["release/p1.json", "release/p1-regression-registry.json", "docs/maintainers/phase-1-validation-log.json"], "implementation": ["scripts/p1-validation-log-contract.mjs", "scripts/validation-log-shared.mjs", "scripts/validate-p1.mjs", "docs/maintainers/phase-1-validation.md"], "tests": [{"path": "test/p1-validation.test.js", "name": "the Phase 1 Validation Log remains collecting, not validated, Experimental, and not approved"}, {"path": "test/p1-validation.test.js", "name": "the Phase 1 Validation Log rejects historical mutation against the reviewed Git baseline"}, {"path": "test/p1-validation.test.js", "name": "a Phase 1 Quality Floor regression suspends only its declared authority and preserves P0 Stable"}], "tracking": "#195", "status": "complete", "gates": ["p1_traceability", "p1_quality_floor"] diff --git a/release/p1-external-verification.json b/release/p1-external-verification.json index fd3bbff..6316fa4 100644 --- a/release/p1-external-verification.json +++ b/release/p1-external-verification.json @@ -1,8 +1,8 @@ { "schema_version": "launchrally.dev/p1-external-verification/v1", "status": "pending", - "version": "0.4.2", - "tag": "v0.4.2", + "version": "0.4.3", + "tag": "v0.4.3", "channel": "experimental", "verified_at": null, "workflow_url": null, diff --git a/release/p1-release-candidate.json b/release/p1-release-candidate.json index 6fc8107..0c16e40 100644 --- a/release/p1-release-candidate.json +++ b/release/p1-release-candidate.json @@ -1,35 +1,35 @@ { "schema_version": "launchrally.dev/p1-release-candidate/v1", - "version": "0.4.2", - "tag": "v0.4.2", + "version": "0.4.3", + "tag": "v0.4.3", "channel": "experimental", "stable_channel": "latest", "p0_stable_version": "0.3.2", "packages": [ { "name": "@launchrally/contracts", - "integrity": "sha512-CoLhtUkm9WOvPVxBPpR+K5RSJ7IzD9jWrnBv+fi4iUjmV1ZUMcelYcuwbHcaf8q0AOa9ZOOuUcViZq0AkL2w3A==", - "shasum": "39730edc2edd1f8c4e380f8971accfa92eb1f00e" + "integrity": "sha512-Fq3WbaAUuOBfgbqTd4jcPGuBllom0yiLuhEremTJTY3PdZlMNS+R//z3VeclSsKyd6r9QOFjyzYlZfWWk08uwA==", + "shasum": "1c82c3b8ea3240a0eb5cc3b11659489f72528c26" }, { "name": "@launchrally/core", - "integrity": "sha512-sm+0w859ZwCA64WqF2ITZJZwCvCy+g/Q/IyZnHgePCJKrHqFj4XMpmh70kXmgLj8W3YGI+CKmSXXeK+LK6Wq7A==", - "shasum": "b15be4d57b37d14f1ae71d70ce7d465bdd9834ea" + "integrity": "sha512-4a+H4qoWvni3hfymrjaS12OMX3zH6YNQoHxus+b9BJZcFG0ZmW0WtVPourOVeQPFXRpVX3ixXEI7moLqP9ndWQ==", + "shasum": "65e0e341af2ad354bda4e527ee0d211d68fe6491" }, { "name": "@launchrally/cli", - "integrity": "sha512-sz4t58uCztF8GKXTp6OunFGhcilftKODgO5/+D24vIlTsBaA6Jg9ct4W0ODonXFBuoR5mzNvyMPp60SyJIVRBA==", - "shasum": "ec514c0f4b12a02cad87b9d050c2d81f3e567f3c" + "integrity": "sha512-SnIEcpfi7BBEbg2wzkqaECEHPQLEwQtcA9SY0oQcu7PlFY4iKXlkpJfF4X1D/6g79jYmYsHFVXLuMYwVV+jFlw==", + "shasum": "356de9dea048332a44c3b40febb6aab6bad94afb" }, { "name": "@launchrally/codex-plugin", - "integrity": "sha512-vKp+tv+cPXixoF+82qeHrFRw/LkA+MCQEFWxmO4nUxlrac8fDoF10IXDk/tl2dxLGQfMGcMwKLqhfTu8kHI1cQ==", - "shasum": "3535b401a968d9d644e078b38f6f252a78c153a4" + "integrity": "sha512-0v3ufyUbXmV1gt5ALwwBSxL6Ls1cQEkERqOvmbRGOwW1JKICGJT7EEeZXAXLlhJyXdjkSYbfoHwVQ3SbH7gsaA==", + "shasum": "bd4c84d4c049ff4f7672965e984f75a7a6547b59" }, { "name": "@launchrally/claude-plugin", - "integrity": "sha512-x68P7ATFRAhN7Br6LhozCKpBJW/U1//8/gL2Pu5+X9tk0/kd0J3RPxPPVQ9EbFdGg4dNHxhn4sT7TAkOETSu0g==", - "shasum": "747fd623cbd821c7c1cf365abb8b494da610c9c5" + "integrity": "sha512-QB3T4XTPCl1+UeUCIyS3ARS5WBzFzqqU/i1cMudlJvWlCZi3a2cj0461A6z1guEA+sNXGC4Mxp1LK0/Hujotiw==", + "shasum": "76691e766732cabbd7df7375b7f66201b9abe08c" } ] } diff --git a/release/p1.json b/release/p1.json index c1f0343..a62eb33 100644 --- a/release/p1.json +++ b/release/p1.json @@ -19,15 +19,15 @@ "approved_tag": null }, "experimental_publication": { - "announcement": "docs/maintainers/experimental-0.4.2-announcement.md", - "candidate_tag": "v0.4.2", + "announcement": "docs/maintainers/experimental-0.4.3-announcement.md", + "candidate_tag": "v0.4.3", "channel": "experimental", "stable_channel": "latest", "p0_stable_tag": "v0.3.2", "candidate_manifest": "release/p1-release-candidate.json", "changelog": "CHANGELOG.md", "migration_notes": "docs/maintainers/p1-migration-notes.md", - "evidence_record": "docs/maintainers/experimental-0.4.2-p1-evidence.md" + "evidence_record": "docs/maintainers/experimental-0.4.3-p1-evidence.md" }, "acceptance_requirement_ids": [ "P1-INTENT-01", "P1-INTENT-02", "P1-INTENT-03", diff --git a/skills/launchrally/SKILL.md b/skills/launchrally/SKILL.md index 0e7d181..40d4fb8 100644 --- a/skills/launchrally/SKILL.md +++ b/skills/launchrally/SKILL.md @@ -19,7 +19,7 @@ Use the local CLI as the only authority for Checks, Evidence, Severity, release - When Audit or Verify returns an authenticated Core Journey request, read [references/protected-journeys.md](references/protected-journeys.md). - When a completed Audit or Verify contains `provider_tool_recoveries`, read [references/provider-tool-recovery.md](references/provider-tool-recovery.md). - When invoking the CLI or handling its states, read [references/cli-contract.md](references/cli-contract.md). -- For an exact Experimental 0.4.1-to-0.4.2 Project Toolchain update, follow the [version-specific migration authority](https://github.com/codeacme17/launchrally/blob/main/docs/maintainers/p1-migration-notes.md#project-toolchain-migration-041-to-042) and the typed lifecycle router in [references/cli-contract.md](references/cli-contract.md). +- For an exact Experimental 0.4.2-to-0.4.3 Project Toolchain update, follow the [version-specific migration authority](https://github.com/codeacme17/launchrally/blob/main/docs/maintainers/p1-migration-notes.md#project-toolchain-migration-042-to-043) and the typed lifecycle router in [references/cli-contract.md](references/cli-contract.md). - When inspecting or exchanging a Phase 1 architecture record, read [references/phase-1-contracts.md](references/phase-1-contracts.md). Contract availability alone does not make a Phase 1 operation executable. - When a typed `architect` interaction enters Product Intent discovery, read [references/product-intent.md](references/product-intent.md) before presenting semantic-analysis permission or confirmation. - When presenting a Capability Catalog, derived obligations, a Capability Graph, or an Integration Contract, read [references/capability-model.md](references/capability-model.md). @@ -54,7 +54,7 @@ Use the local CLI as the only authority for Checks, Evidence, Severity, release 1. Resolve the exact repository root without changing files. 2. Make `rally --version --json --cwd ` the first discovery operation. Do not probe for or invoke a Project Toolchain Engine directly. -3. If `rally` is absent, explain that CLI installation and Plugin installation are separate, link to the repository's single Install authority, present exactly `npm install --global @launchrally/cli@0.4.2` and the structured verification command, then stop before Audit and wait for the user. Do not run the install, automatically substitute the separate exact-version npm-exec trial/CI entry, or change npm prefixes or shell profiles. +3. If `rally` is absent, explain that CLI installation and Plugin installation are separate, link to the repository's single Install authority, present exactly `npm install --global @launchrally/cli@0.4.3` and the structured verification command, then stop before Audit and wait for the user. Do not run the install, automatically substitute the separate exact-version npm-exec trial/CI entry, or change npm prefixes or shell profiles. 4. Validate the complete version response and `launchrally.dev/execution-authority/v1` object through the compatibility matrix and typed authority router in [references/cli-contract.md](references/cli-contract.md). Plugin, Launcher, selected Engine, and project-pin versions are separate facts and need not be equal when each is supported. 5. For `ready`, invoke every repository operation through `rally` so the Launcher follows the selected Engine. For restore, migrate, or clean, show the exact operation, permissions, targets, and effects and wait for explicit user approval before Agent execution. 6. Invoke Agent Mode with structured output and handle the returned state. Stop on unknown contracts or versions, invalid descriptors or paths, and malformed output. diff --git a/skills/launchrally/references/cli-contract.md b/skills/launchrally/references/cli-contract.md index 064cc0e..62d66cb 100644 --- a/skills/launchrally/references/cli-contract.md +++ b/skills/launchrally/references/cli-contract.md @@ -7,7 +7,7 @@ Invoke Agent Mode with `--json`. Require `contract: "launchrally.dev/cli/v2"`, t CLI installation is a prerequisite separate from Plugin installation. Invoke `rally --version --json --cwd ` as the first discovery operation. If spawning `rally` reports that the executable is missing, present the exact user-managed PATH installation and verification commands below, then stop before Audit and wait: ```bash -npm install --global @launchrally/cli@0.4.2 +npm install --global @launchrally/cli@0.4.3 rally --version --json --cwd ``` @@ -17,10 +17,10 @@ This Skill release uses this explicit compatibility matrix: | Layer | Supported value | Meaning | | --- | --- | --- | -| Plugin version | `0.4.2` | Interaction guidance only; never an execution candidate. | -| Launcher version | `0.3.0`, `0.3.1`, `0.3.2`, `0.4.0`, `0.4.1`, or `0.4.2`, only when the running implementation declares v1 | A supported user-managed `rally` dispatcher implementing the contracts below. Historical published `0.2.2` direct binaries predate v1 interception and do not qualify. | +| Plugin version | `0.4.3` | Interaction guidance only; never an execution candidate. | +| Launcher version | `0.3.0`, `0.3.1`, `0.3.2`, `0.4.0`, `0.4.1`, `0.4.2`, or `0.4.3`, only when the running implementation declares v1 | A supported user-managed `rally` dispatcher implementing the contracts below. Historical published `0.2.2` direct binaries predate v1 interception and do not qualify. | | Execution Authority contract | `launchrally.dev/execution-authority/v1` | The only supported Engine-selection contract. | -| Selected Engine version and contract | `0.2.2`, `0.3.0`, `0.3.1`, `0.3.2`, `0.4.0`, `0.4.1`, or `0.4.2` with the declared compatibility path and CLI interaction `launchrally.dev/cli/v2` | The Engine selected by validated authority. A descriptor-free `0.2.2` project is accepted only through the legacy row below. | +| Selected Engine version and contract | `0.2.2`, `0.3.0`, `0.3.1`, `0.3.2`, `0.4.0`, `0.4.1`, `0.4.2`, or `0.4.3` with the declared compatibility path and CLI interaction `launchrally.dev/cli/v2` | The Engine selected by validated authority. A descriptor-free `0.2.2` project is accepted only through the legacy row below. | | Legacy project pin | `0.2.2`, authority descriptor absent, `compatibility: "legacy_adapter"` | Supported only through the Launcher's allowlisted legacy adapter after explicit restore when materialization is missing. | Keep the Plugin version, Launcher version, selected Engine version, and project pin as separate facts. Read `launcher_version` from the version result, the selected Engine from `cli_version` and `authority.engine`, and the project pin from the validated `project_toolchain` authority. Do not require these versions to be equal; continue only when each value and contract is explicitly supported by the matrix. Any unknown or malformed version must stop before a journey operation. diff --git a/skills/launchrally/references/reference-journey.json b/skills/launchrally/references/reference-journey.json index a5d7480..8b8f24d 100644 --- a/skills/launchrally/references/reference-journey.json +++ b/skills/launchrally/references/reference-journey.json @@ -2,19 +2,19 @@ "schema_version": "launchrally.dev/reference-journey/v3", "compatibility": { "plugin": { - "version": "0.4.2", + "version": "0.4.3", "role": "interaction_only" }, "launcher": { "package": "@launchrally/cli", - "supported_versions": ["0.3.0", "0.3.1", "0.3.2", "0.4.0", "0.4.1", "0.4.2"] + "supported_versions": ["0.3.0", "0.3.1", "0.3.2", "0.4.0", "0.4.1", "0.4.2", "0.4.3"] }, "execution_authority": { "supported_contracts": ["launchrally.dev/execution-authority/v1"] }, "engine": { "package": "@launchrally/cli", - "supported_versions": ["0.2.2", "0.3.0", "0.3.1", "0.3.2", "0.4.0", "0.4.1", "0.4.2"], + "supported_versions": ["0.2.2", "0.3.0", "0.3.1", "0.3.2", "0.4.0", "0.4.1", "0.4.2", "0.4.3"], "authority_contracts": ["launchrally.dev/execution-authority/v1"], "interaction_contracts": ["launchrally.dev/cli/v2"] }, @@ -31,7 +31,7 @@ "installation": { "owner": "user", "executable": "npm", - "arguments": ["install", "--global", "@launchrally/cli@0.4.2"] + "arguments": ["install", "--global", "@launchrally/cli@0.4.3"] }, "verification": { "arguments": ["--version", "--json", "--cwd", "{repository_root}"] @@ -57,7 +57,7 @@ }, "cli": { "package": "@launchrally/cli", - "version": "0.4.2", + "version": "0.4.3", "contract": "launchrally.dev/cli/v2" }, "protected_journeys": { diff --git a/skills/launchrally/references/reference-journey.md b/skills/launchrally/references/reference-journey.md index fb0633a..361fec6 100644 --- a/skills/launchrally/references/reference-journey.md +++ b/skills/launchrally/references/reference-journey.md @@ -9,7 +9,7 @@ The user-managed `rally` Launcher is a prerequisite separate from the Codex or C If `rally` is absent, present these exact user-managed commands, stop before Audit, and wait: ```bash -npm install --global @launchrally/cli@0.4.2 +npm install --global @launchrally/cli@0.4.3 rally --version --json --cwd ``` diff --git a/test/reference-journey.test.js b/test/reference-journey.test.js index b192346..c64273f 100644 --- a/test/reference-journey.test.js +++ b/test/reference-journey.test.js @@ -854,14 +854,14 @@ test("the canonical Skill declares Launcher compatibility and typed authority li }, launcher: { package: "@launchrally/cli", - supported_versions: ["0.3.0", "0.3.1", "0.3.2", "0.4.0", "0.4.1", cliPackage.version], + supported_versions: ["0.3.0", "0.3.1", "0.3.2", "0.4.0", "0.4.1", "0.4.2", cliPackage.version], }, execution_authority: { supported_contracts: ["launchrally.dev/execution-authority/v1"], }, engine: { package: "@launchrally/cli", - supported_versions: ["0.2.2", "0.3.0", "0.3.1", "0.3.2", "0.4.0", "0.4.1", cliPackage.version], + supported_versions: ["0.2.2", "0.3.0", "0.3.1", "0.3.2", "0.4.0", "0.4.1", "0.4.2", cliPackage.version], authority_contracts: ["launchrally.dev/execution-authority/v1"], interaction_contracts: ["launchrally.dev/cli/v2"], }, diff --git a/test/scaffold.test.js b/test/scaffold.test.js index a231607..6a57b36 100644 --- a/test/scaffold.test.js +++ b/test/scaffold.test.js @@ -545,8 +545,8 @@ test("audit excludes Architecture history without hiding protected LaunchRally i "schema_version: launchrally.dev/manifest/v2\n", ); - const initial = await runAudit(fixture, "0.4.2"); - const confirmation = await runAudit(fixture, "0.4.2", { + const initial = await runAudit(fixture, "0.4.3"); + const confirmation = await runAudit(fixture, "0.4.3", { resume_token: initial.interaction.resume_token, answers: { intended_environment: "production", @@ -556,11 +556,11 @@ test("audit excludes Architecture history without hiding protected LaunchRally i support_layers: [], }, }); - const permission = await runAudit(fixture, "0.4.2", { + const permission = await runAudit(fixture, "0.4.3", { resume_token: confirmation.interaction.resume_token, confirmation: "confirm", }); - const result = await runAudit(fixture, "0.4.2", { + const result = await runAudit(fixture, "0.4.3", { resume_token: permission.interaction.resume_token, permission_decisions: { public_verification: "denied" }, }); From 720fb42ed6b7d1305d232299d1bda2eb35a1e48f Mon Sep 17 00:00:00 2001 From: leyoonafr Date: Mon, 24 Aug 2026 07:34:59 +0800 Subject: [PATCH 17/22] docs: complete 0.4.3 release inventory (#210) --- .../experimental-0.4.3-announcement.md | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/docs/maintainers/experimental-0.4.3-announcement.md b/docs/maintainers/experimental-0.4.3-announcement.md index e4951f9..f086fc7 100644 --- a/docs/maintainers/experimental-0.4.3-announcement.md +++ b/docs/maintainers/experimental-0.4.3-announcement.md @@ -16,7 +16,25 @@ Phase 0 remains independently Product Complete, P0 Validated, and Stable at 0.3.2. npm `latest` continues to resolve to 0.3.2; 0.4.3 is available only from `experimental` or by exact version. -Review the [0.4.3 changelog](../../CHANGELOG.md), [exact 0.4.2-to-0.4.3 Project -Toolchain migration](p1-migration-notes.md#project-toolchain-migration-042-to-043), -and [external verification record](experimental-0.4.3-p1-evidence.md) before -adoption. +Review the [0.4.3 changelog](https://github.com/codeacme17/launchrally/blob/v0.4.3/CHANGELOG.md), +[exact 0.4.2-to-0.4.3 Project Toolchain migration](https://github.com/codeacme17/launchrally/blob/v0.4.3/docs/maintainers/p1-migration-notes.md#project-toolchain-migration-042-to-043), +and [external verification record](https://github.com/codeacme17/launchrally/blob/v0.4.3/docs/maintainers/experimental-0.4.3-p1-evidence.md) +before adoption. + +## Resolved issues + +- [#187 — Style the complete Architect Human Mode decision flow](https://github.com/codeacme17/launchrally/issues/187) +- [#204 — Complete Project Toolchain lifecycle Human Mode interactions](https://github.com/codeacme17/launchrally/issues/204) +- [#205 — Add an exact 0.4.1-to-0.4.2 Project Toolchain migration guide](https://github.com/codeacme17/launchrally/issues/205) +- [#210 — Publish and verify Phase 1 Experimental 0.4.3](https://github.com/codeacme17/launchrally/issues/210) + +## Merged pull requests + +- [#206 — Keep release validation current and document release notes](https://github.com/codeacme17/launchrally/pull/206) +- [#207 — Complete Project Toolchain migrate Human Mode](https://github.com/codeacme17/launchrally/pull/207) +- [#208 — Add exact 0.4.1-to-0.4.2 migration guide](https://github.com/codeacme17/launchrally/pull/208) +- [#209 — Complete remaining Phase 1 Human Mode flows](https://github.com/codeacme17/launchrally/pull/209) + +## Contributors + +- [@codeacme17](https://github.com/codeacme17) — [#206](https://github.com/codeacme17/launchrally/pull/206), [#207](https://github.com/codeacme17/launchrally/pull/207), [#208](https://github.com/codeacme17/launchrally/pull/208), [#209](https://github.com/codeacme17/launchrally/pull/209) From 1f27a6ef2151765e50d07cfbf1276a548bdeb3f6 Mon Sep 17 00:00:00 2001 From: leyoonafr Date: Mon, 24 Aug 2026 07:36:55 +0800 Subject: [PATCH 18/22] docs: preserve historical migration guidance (#210) --- docs/getting-started/install.md | 8 +- docs/getting-started/quickstart.md | 5 +- docs/maintainers/p1-migration-notes.md | 121 +++++++++++++++++++++++++ 3 files changed, 129 insertions(+), 5 deletions(-) diff --git a/docs/getting-started/install.md b/docs/getting-started/install.md index 21add4e..9f79a6b 100644 --- a/docs/getting-started/install.md +++ b/docs/getting-started/install.md @@ -2,9 +2,11 @@ LaunchRally `0.3.2` is the exact Stable release used by this guide. Stable availability follows the reviewed P0 Validated decision and satisfied Quality Floor. -Updating an initialized Experimental `0.4.1` project is a separate path. Follow -the [exact 0.4.2-to-0.4.3 Project Toolchain migration](../maintainers/p1-migration-notes.md#project-toolchain-migration-042-to-043) -without relabeling this Stable `0.3.2` installation path. +Updating an initialized Experimental project is a separate path. Follow the +exact Project Toolchain migration for the established Engine pin: +[0.4.1 to 0.4.2](../maintainers/p1-migration-notes.md#project-toolchain-migration-041-to-042) +or [0.4.2 to 0.4.3](../maintainers/p1-migration-notes.md#project-toolchain-migration-042-to-043). +Neither path relabels this Stable `0.3.2` installation guide. ## Supported environments diff --git a/docs/getting-started/quickstart.md b/docs/getting-started/quickstart.md index 3036afa..e361c57 100644 --- a/docs/getting-started/quickstart.md +++ b/docs/getting-started/quickstart.md @@ -2,8 +2,9 @@ LaunchRally `0.3.2` is a public Stable release. Run it against a repository you control and review every disclosed read or write boundary before confirming it. Stable means the reviewed P0 Validated decision and Quality Floor requirements are satisfied. -Already using an initialized Experimental `0.4.1` project? Use the separate -[exact 0.4.2-to-0.4.3 Project Toolchain migration](../maintainers/p1-migration-notes.md#project-toolchain-migration-042-to-043). +Already using an initialized Experimental project? Use the separate exact +Project Toolchain migration for [0.4.1 to 0.4.2](../maintainers/p1-migration-notes.md#project-toolchain-migration-041-to-042) +or [0.4.2 to 0.4.3](../maintainers/p1-migration-notes.md#project-toolchain-migration-042-to-043). That path keeps Launcher, selected Engine, project pin, Plugin, and release channel distinct; it does not change the Stable quickstart below. diff --git a/docs/maintainers/p1-migration-notes.md b/docs/maintainers/p1-migration-notes.md index 96ad7b5..f4d45b9 100644 --- a/docs/maintainers/p1-migration-notes.md +++ b/docs/maintainers/p1-migration-notes.md @@ -215,6 +215,127 @@ and owner-restricted host resume registry unchanged. Removing that separate host registry invalidates retained resumable host artifacts; it is not part of Project Toolchain migration. +## Project Toolchain migration: 0.4.1 to 0.4.2 + +This historical procedure remains available for an initialized project whose +established Engine pin is `0.4.1`. LaunchRally 0.4.2 is Experimental; Phase 0 +Stable remains `0.3.2` on npm `latest`. Updating the global Launcher does not +change a valid project pin, so `launcher_version: "0.4.2"` together with +`cli_version: "0.4.1"` and `authority.source: "project_toolchain"` is expected +before migration. Only an explicitly confirmed `toolchain migrate` changes the +pin; `init` is not a migration mechanism. + +Migration changes only the owned Project Toolchain package, lock, authority +descriptor, and rebuildable materialization. It preserves the Manifest, +immutable Reports, Evidence, Architecture history, Provider intent, +application dependencies and source, and external saved Reports. It marks the +prior current Report non-current with `execution_authority_changed`, requiring +a fresh full Verify against the original Manifest-bound source Audit Report. + +The shipped 0.4.2 default TTY flow emits typed permission and confirmation +states and requires explicit resume commands. Review every field, keep opaque +resume tokens outside the repository, and treat registry permission and +migration confirmation as separate decisions. + +### POSIX + +```sh +PROJECT_ROOT=/path/to/project +SOURCE_REPORT=/path/to/original-manifest-bound-audit-report.json +MIGRATION_RESPONSE=./launchrally-0.4.2-migration-response.json + +npm install --global @launchrally/cli@0.4.2 +rally --version --json +rally --version --json --cwd "$PROJECT_ROOT" +rally toolchain status --json --cwd "$PROJECT_ROOT" + +rally toolchain migrate --to 0.4.2 --cwd "$PROJECT_ROOT" > "$MIGRATION_RESPONSE" +node -e 'const fs = require("node:fs"); const value = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); console.log(value.status); console.log(JSON.stringify(value.request ?? {}, null, 2));' "$MIGRATION_RESPONSE" +``` + +If the response is `needs_permission`, review the exact `npm_registry_read` +request. Denial preserves the 0.4.1 pin. Approval authorizes only that bounded +read and returns the exact migration preview: + +```sh +MIGRATION_TOKEN="$(node -e 'const fs = require("node:fs"); const value = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); process.stdout.write(value.interaction.resume_token);' "$MIGRATION_RESPONSE")" +rally toolchain migrate --to 0.4.2 --cwd "$PROJECT_ROOT" --resume "$MIGRATION_TOKEN" --permissions '{"npm_registry_read":"approved"}' > "$MIGRATION_RESPONSE" +node -e 'const fs = require("node:fs"); const value = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); console.log(value.status); console.log(JSON.stringify(value.preview ?? {}, null, 2));' "$MIGRATION_RESPONSE" +``` + +When the response is `needs_confirmation`, inspect every +`preview.changes` entry. Resume with `decline` to change nothing, or confirm the +exact preview and perform the mandatory full Verify: + +```sh +MIGRATION_TOKEN="$(node -e 'const fs = require("node:fs"); const value = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); process.stdout.write(value.interaction.resume_token);' "$MIGRATION_RESPONSE")" +rally toolchain migrate --to 0.4.2 --cwd "$PROJECT_ROOT" --resume "$MIGRATION_TOKEN" --confirm confirm +rally --version --json --cwd "$PROJECT_ROOT" +rally verify --cwd "$PROJECT_ROOT" --report "$SOURCE_REPORT" --scope full +``` + +### PowerShell + +```powershell +$ProjectRoot = 'C:\path\to\project' +$SourceReport = 'C:\path\to\original-manifest-bound-audit-report.json' +$MigrationResponse = '.\launchrally-0.4.2-migration-response.json' + +npm install --global @launchrally/cli@0.4.2 +rally --version --json +rally --version --json --cwd $ProjectRoot +rally toolchain status --json --cwd $ProjectRoot + +rally toolchain migrate --to 0.4.2 --cwd $ProjectRoot | Set-Content -Encoding utf8 $MigrationResponse +$Migration = Get-Content -Raw $MigrationResponse | ConvertFrom-Json +$Migration.status +$Migration.request | ConvertTo-Json -Depth 20 +``` + +Approve only the exact registry request when required, then inspect and confirm +the exact preview: + +```powershell +rally toolchain migrate --to 0.4.2 --cwd $ProjectRoot --resume $Migration.interaction.resume_token --permissions '{"npm_registry_read":"approved"}' | Set-Content -Encoding utf8 $MigrationResponse +$Migration = Get-Content -Raw $MigrationResponse | ConvertFrom-Json +$Migration.preview | ConvertTo-Json -Depth 20 + +rally toolchain migrate --to 0.4.2 --cwd $ProjectRoot --resume $Migration.interaction.resume_token --confirm confirm +rally --version --json --cwd $ProjectRoot +rally verify --cwd $ProjectRoot --report $SourceReport --scope full +``` + +After migration require ready `project_toolchain` authority and both Launcher +and selected Engine at 0.4.2. Use the new current Report printed by Verify for +Plan or Architect, while retaining the Manifest-bound source Report for future +whole-release Verify runs. + +Denial, cancellation, abandoned or stale previews, and missing registry access +preserve the 0.4.1 pin. Interrupted transactions recover only when their state +is safely recognizable; malformed state fails closed. `restore` rebuilds the +established pin and is not a downgrade. Launcher 0.4.2 supports only the +allowlisted legacy downgrade target `0.2.2`; direct downgrade to 0.4.1 is +unsupported. + +Update an installed Plugin separately and pin its checkout to the exact tag: + +```sh +codex plugin remove launchrally@launchrally +codex plugin marketplace remove launchrally +codex plugin marketplace add codeacme17/launchrally --ref v0.4.2 +codex plugin add launchrally@launchrally +codex plugin list --json + +claude plugin uninstall launchrally@launchrally --scope user +claude plugin marketplace remove launchrally +claude plugin marketplace add codeacme17/launchrally@v0.4.2 --scope user +claude plugin install launchrally@launchrally --scope user +claude plugin list --json +``` + +Plugin update or removal does not change the Launcher, Project Toolchain, +project-owned data, application source, or owner-restricted host resume state. + ## Adoption Install 0.4.3 only by selecting the non-stable `experimental` channel or the From 99aa0c5a9e5b3abe545fb0214a4fb82d700e3ca8 Mon Sep 17 00:00:00 2001 From: leyoonafr Date: Mon, 24 Aug 2026 07:38:46 +0800 Subject: [PATCH 19/22] docs: include release promotion pull requests (#210) --- docs/maintainers/experimental-0.4.3-announcement.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/maintainers/experimental-0.4.3-announcement.md b/docs/maintainers/experimental-0.4.3-announcement.md index f086fc7..32ba6b9 100644 --- a/docs/maintainers/experimental-0.4.3-announcement.md +++ b/docs/maintainers/experimental-0.4.3-announcement.md @@ -34,7 +34,9 @@ before adoption. - [#207 — Complete Project Toolchain migrate Human Mode](https://github.com/codeacme17/launchrally/pull/207) - [#208 — Add exact 0.4.1-to-0.4.2 migration guide](https://github.com/codeacme17/launchrally/pull/208) - [#209 — Complete remaining Phase 1 Human Mode flows](https://github.com/codeacme17/launchrally/pull/209) +- [#211 — Prepare Phase 1 Experimental 0.4.3](https://github.com/codeacme17/launchrally/pull/211) +- [#212 — Promote Phase 1 Experimental 0.4.3](https://github.com/codeacme17/launchrally/pull/212) ## Contributors -- [@codeacme17](https://github.com/codeacme17) — [#206](https://github.com/codeacme17/launchrally/pull/206), [#207](https://github.com/codeacme17/launchrally/pull/207), [#208](https://github.com/codeacme17/launchrally/pull/208), [#209](https://github.com/codeacme17/launchrally/pull/209) +- [@codeacme17](https://github.com/codeacme17) — [#206](https://github.com/codeacme17/launchrally/pull/206), [#207](https://github.com/codeacme17/launchrally/pull/207), [#208](https://github.com/codeacme17/launchrally/pull/208), [#209](https://github.com/codeacme17/launchrally/pull/209), [#211](https://github.com/codeacme17/launchrally/pull/211), [#212](https://github.com/codeacme17/launchrally/pull/212) From 82c9c0bb41e4c43ada6adeecb58adf06c7f0e9fa Mon Sep 17 00:00:00 2001 From: leyoonafr Date: Mon, 24 Aug 2026 07:43:56 +0800 Subject: [PATCH 20/22] test: derive current lifecycle version (#210) --- test/reference-journey.test.js | 2 +- test/toolchain-lifecycle.test.js | 66 ++++++++++++++++---------------- 2 files changed, 35 insertions(+), 33 deletions(-) diff --git a/test/reference-journey.test.js b/test/reference-journey.test.js index c64273f..be62e6e 100644 --- a/test/reference-journey.test.js +++ b/test/reference-journey.test.js @@ -873,7 +873,7 @@ test("the canonical Skill declares Launcher compatibility and typed authority li }); assert.deepEqual( journey.compatibility.launcher.supported_versions, - ["0.3.0", "0.3.1", "0.3.2", "0.4.0", "0.4.1", journey.compatibility.plugin.version], + ["0.3.0", "0.3.1", "0.3.2", "0.4.0", "0.4.1", "0.4.2", journey.compatibility.plugin.version], "a pre-authority direct binary cannot be treated as a supported Launcher", ); assert.notEqual( diff --git a/test/toolchain-lifecycle.test.js b/test/toolchain-lifecycle.test.js index 447480d..462780c 100644 --- a/test/toolchain-lifecycle.test.js +++ b/test/toolchain-lifecycle.test.js @@ -20,6 +20,7 @@ import { promisify } from "node:util"; import { assertValidToolchainLifecycle } from "../packages/contracts/src/index.js"; import { runToolchainLifecycle } from "../packages/core/src/index.js"; +import { VERSION } from "../packages/cli/bin/version.js"; import { materializeExactToolchain, writeExactToolchain, @@ -27,6 +28,7 @@ import { const execFileAsync = promisify(execFile); const cli = path.resolve("packages/cli/bin/rally.js"); +const escapedVersion = VERSION.replaceAll(".", "\\."); const pythonAvailable = process.platform !== "win32" && spawnSync("python3", ["--version"]).status === 0; const migrationPtyRunner = [ @@ -289,7 +291,7 @@ test("TTY Human toolchain migrate previews and confirms exact authority in one p const repository = await repositoryFixture(); await writeProject(repository, "0.2.2"); await materializeExactToolchain(repository, "0.2.2"); - const prepared = await preparedToolchain("0.4.2"); + const prepared = await preparedToolchain(VERSION); const npmDirectory = await npmFixture(prepared); const { stdout } = await execFileAsync("python3", [ @@ -301,7 +303,7 @@ test("TTY Human toolchain migrate previews and confirms exact authority in one p "toolchain", "migrate", "--to", - "0.4.2", + VERSION, "--plain", "--cwd", repository, @@ -315,7 +317,7 @@ test("TTY Human toolchain migrate previews and confirms exact authority in one p }); assert.match(stdout, /LaunchRally Project Toolchain Migration Preview/u); - assert.match(stdout, /Engine: 0\.2\.2 -> 0\.4\.2/u); + assert.match(stdout, new RegExp(`Engine: 0\\.2\\.2 -> ${escapedVersion}`, "u")); assert.match(stdout, /Authoritative files:/u); assert.match(stdout, /Materialization: 9 packages/u); assert.match(stdout, /1\. Confirm/u); @@ -329,10 +331,10 @@ test("TTY Human toolchain migrate previews and confirms exact authority in one p assert.match(stdout, /verify --scope full/u); assert.doesNotMatch(stdout, /resume_token|"contract"|"preview"/u); - const status = await runToolchainLifecycle(repository, "0.4.2", { + const status = await runToolchainLifecycle(repository, VERSION, { operation: "status", }); - assert.equal(status.authority.engine.version, "0.4.2"); + assert.equal(status.authority.engine.version, VERSION); assert.equal(status.authority.state, "ready"); }); @@ -342,13 +344,13 @@ test("default styled TTY migration confirms and completes", { const repository = await repositoryFixture(); await writeProject(repository, "0.2.2"); await materializeExactToolchain(repository, "0.2.2"); - const prepared = await preparedToolchain("0.4.2"); + const prepared = await preparedToolchain(VERSION); const npmDirectory = await npmFixture(prepared); const packagePath = path.join(repository, ".launchrally/toolchain/package.json"); const { stdout } = await execFileAsync("python3", [ "-c", styledMigrationPtyRunner, "confirm", packagePath, process.execPath, cli, - "toolchain", "migrate", "--to", "0.4.2", "--cwd", repository, + "toolchain", "migrate", "--to", VERSION, "--cwd", repository, ], { env: { ...process.env, @@ -377,12 +379,12 @@ test("default styled TTY migration handles registry permission and cancellation const repository = await repositoryFixture(); await writeProject(repository, "0.2.2"); await materializeExactToolchain(repository, "0.2.2"); - const prepared = await preparedToolchain("0.4.2"); + const prepared = await preparedToolchain(VERSION); const npmDirectory = await npmFixture(prepared); const packagePath = path.join(repository, ".launchrally/toolchain/package.json"); const run = execFileAsync("python3", [ "-c", styledMigrationPtyRunner, scenario.mode, packagePath, process.execPath, cli, - "toolchain", "migrate", "--to", "0.4.2", "--cwd", repository, + "toolchain", "migrate", "--to", VERSION, "--cwd", repository, ], { env: { ...process.env, @@ -402,7 +404,7 @@ test("default styled TTY migration handles registry permission and cancellation assert.match(error.stdout, scenario.summary); return true; }); - assert.equal((await runToolchainLifecycle(repository, "0.4.2", { + assert.equal((await runToolchainLifecycle(repository, VERSION, { operation: "status", })).authority.engine.version, "0.2.2"); assert.deepEqual(await storedLifecycleStates(repository), []); @@ -416,12 +418,12 @@ test("TTY Human toolchain migrate shows the full exact diff and returns to confi const repository = await repositoryFixture(); await writeProject(repository, "0.2.2"); await materializeExactToolchain(repository, "0.2.2"); - const prepared = await preparedToolchain("0.4.2"); + const prepared = await preparedToolchain(VERSION); const npmDirectory = await npmFixture(prepared); const { stdout } = await execFileAsync("python3", [ "-c", migrationPtyRunner, "3,1", process.execPath, cli, - "toolchain", "migrate", "--to", "0.4.2", "--plain", "--cwd", repository, + "toolchain", "migrate", "--to", VERSION, "--plain", "--cwd", repository, ], { env: { ...process.env, @@ -448,13 +450,13 @@ test("TTY Human toolchain migrate safely declines by default and can cancel", { const repository = await repositoryFixture(); await writeProject(repository, "0.2.2"); await materializeExactToolchain(repository, "0.2.2"); - const prepared = await preparedToolchain("0.4.2"); + const prepared = await preparedToolchain(VERSION); const npmDirectory = await npmFixture(prepared); let output; try { output = (await execFileAsync("python3", [ "-c", migrationPtyRunner, scenario.answer, process.execPath, cli, - "toolchain", "migrate", "--to", "0.4.2", "--plain", "--cwd", repository, + "toolchain", "migrate", "--to", VERSION, "--plain", "--cwd", repository, ], { env: { ...process.env, @@ -470,7 +472,7 @@ test("TTY Human toolchain migrate safely declines by default and can cancel", { } assert.match(output, scenario.summary); assert.doesNotMatch(output, /resume_token/u); - const status = await runToolchainLifecycle(repository, "0.4.2", { operation: "status" }); + const status = await runToolchainLifecycle(repository, VERSION, { operation: "status" }); assert.equal(status.authority.engine.version, "0.2.2"); assert.deepEqual(await storedLifecycleStates(repository), []); } @@ -482,13 +484,13 @@ test("TTY Human toolchain migrate fails a stale preview closed with a concise re const repository = await repositoryFixture(); await writeProject(repository, "0.2.2"); await materializeExactToolchain(repository, "0.2.2"); - const prepared = await preparedToolchain("0.4.2"); + const prepared = await preparedToolchain(VERSION); const npmDirectory = await npmFixture(prepared); const packagePath = path.join(repository, ".launchrally/toolchain/package.json"); await assert.rejects(execFileAsync("python3", [ "-c", staleMigrationPtyRunner, packagePath, process.execPath, cli, - "toolchain", "migrate", "--to", "0.4.2", "--plain", "--cwd", repository, + "toolchain", "migrate", "--to", VERSION, "--plain", "--cwd", repository, ], { env: { ...process.env, @@ -503,7 +505,7 @@ test("TTY Human toolchain migrate fails a stale preview closed with a concise re assert.doesNotMatch(error.stdout, /"error"|resume_token/u); return true; }); - assert.equal((await runToolchainLifecycle(repository, "0.4.2", { + assert.equal((await runToolchainLifecycle(repository, VERSION, { operation: "status", })).authority.engine.version, "0.2.2"); }); @@ -519,12 +521,12 @@ test("TTY Human toolchain migrate requests registry permission and rolls back co await mkdir(path.join(repository, ".launchrally/cache"), { recursive: true }); await writeFile(path.join(repository, ".launchrally/cache/current-report.json"), "not-json\n"); } - const prepared = await preparedToolchain("0.4.2"); + const prepared = await preparedToolchain(VERSION); const npmDirectory = await npmFixture(prepared); try { const { stdout } = await execFileAsync("python3", [ "-c", migrationPtyRunner, "y,1", process.execPath, cli, - "toolchain", "migrate", "--to", "0.4.2", "--plain", "--cwd", repository, + "toolchain", "migrate", "--to", VERSION, "--plain", "--cwd", repository, ], { env: { ...process.env, @@ -542,7 +544,7 @@ test("TTY Human toolchain migrate requests registry permission and rolls back co assert.equal(error.code, 2); assert.match(error.stdout, /Migration Could Not Complete/u); assert.match(error.stdout, /prior project authority was preserved/iu); - assert.equal((await runToolchainLifecycle(repository, "0.4.2", { + assert.equal((await runToolchainLifecycle(repository, VERSION, { operation: "status", })).authority.engine.version, "0.2.2"); } @@ -555,12 +557,12 @@ test("TTY Human toolchain migrate denies registry permission without changing au const repository = await repositoryFixture(); await writeProject(repository, "0.2.2"); await materializeExactToolchain(repository, "0.2.2"); - const prepared = await preparedToolchain("0.4.2"); + const prepared = await preparedToolchain(VERSION); const npmDirectory = await npmFixture(prepared); await assert.rejects(execFileAsync("python3", [ "-c", migrationPtyRunner, "n", process.execPath, cli, - "toolchain", "migrate", "--to", "0.4.2", "--plain", "--cwd", repository, + "toolchain", "migrate", "--to", VERSION, "--plain", "--cwd", repository, ], { env: { ...process.env, @@ -577,7 +579,7 @@ test("TTY Human toolchain migrate denies registry permission without changing au assert.doesNotMatch(error.stdout, /resume_token/u); return true; }); - assert.equal((await runToolchainLifecycle(repository, "0.4.2", { + assert.equal((await runToolchainLifecycle(repository, VERSION, { operation: "status", })).authority.engine.version, "0.2.2"); }); @@ -588,18 +590,18 @@ test("non-TTY Human migration fails safely with a complete Agent command", async await materializeExactToolchain(repository, "0.2.2"); await assert.rejects(execFileAsync(process.execPath, [ - cli, "toolchain", "migrate", "--to", "0.4.2", "--cwd", repository, + cli, "toolchain", "migrate", "--to", VERSION, "--cwd", repository, ]), (error) => { assert.equal(error.code, 2); assert.match(error.stderr, /Non-TTY Human Mode cannot confirm/u); assert.match( error.stderr, - /['"]?toolchain['"]?\s+['"]?migrate['"]?\s+['"]?--to['"]?\s+['"]?0\.4\.2['"]?\s+['"]?--json['"]?\s+['"]?--cwd['"]?/u, + new RegExp(String.raw`['"]?toolchain['"]?\s+['"]?migrate['"]?\s+['"]?--to['"]?\s+['"]?${escapedVersion}['"]?\s+['"]?--json['"]?\s+['"]?--cwd['"]?`, "u"), ); assert.doesNotMatch(error.stdout, /needs_confirmation|resume_token/u); return true; }); - assert.equal((await runToolchainLifecycle(repository, "0.4.2", { + assert.equal((await runToolchainLifecycle(repository, VERSION, { operation: "status", })).authority.engine.version, "0.2.2"); }); @@ -612,7 +614,7 @@ test("non-TTY structured migration prints the complete corrected Agent command", "toolchain", "migrate", "--to", - "0.4.2", + VERSION, "--cwd", repository, "--resume", @@ -626,7 +628,7 @@ test("non-TTY structured migration prints the complete corrected Agent command", assert.match(error.stderr, /Use this complete Agent\/JSON command/u); assert.match( error.stderr, - /['"]?toolchain['"]?\s+['"]?migrate['"]?\s+['"]?--to['"]?\s+['"]?0\.4\.2['"]?\s+['"]?--json['"]?\s+['"]?--cwd['"]?/u, + new RegExp(String.raw`['"]?toolchain['"]?\s+['"]?migrate['"]?\s+['"]?--to['"]?\s+['"]?${escapedVersion}['"]?\s+['"]?--json['"]?\s+['"]?--cwd['"]?`, "u"), ); assert.match( error.stderr, @@ -675,7 +677,7 @@ test("Agent JSON toolchain migrate preserves the exact resumable protocol", asyn const repository = await repositoryFixture(); await writeProject(repository, "0.2.2"); await materializeExactToolchain(repository, "0.2.2"); - const prepared = await preparedToolchain("0.4.2"); + const prepared = await preparedToolchain(VERSION); const npmDirectory = await npmFixture(prepared); const environment = { ...process.env, @@ -684,7 +686,7 @@ test("Agent JSON toolchain migrate preserves the exact resumable protocol", asyn }; const preview = JSON.parse((await execFileAsync(process.execPath, [ - cli, "toolchain", "migrate", "--to", "0.4.2", "--json", "--cwd", repository, + cli, "toolchain", "migrate", "--to", VERSION, "--json", "--cwd", repository, ], { env: environment })).stdout); assert.equal(preview.contract, "launchrally.dev/toolchain-lifecycle/v1"); assert.equal(preview.status, "needs_confirmation"); @@ -698,7 +700,7 @@ test("Agent JSON toolchain migrate preserves the exact resumable protocol", asyn "toolchain", "migrate", "--to", - "0.4.2", + VERSION, "--json", "--cwd", repository, From 08907b8c21d0f21a5f022b4740271dd153943583 Mon Sep 17 00:00:00 2001 From: leyoonafr Date: Mon, 7 Sep 2026 11:46:40 +0800 Subject: [PATCH 21/22] chore: refresh P1 release assessment (#210) --- release/p1.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release/p1.json b/release/p1.json index a62eb33..a82b284 100644 --- a/release/p1.json +++ b/release/p1.json @@ -9,7 +9,7 @@ "validation_status": "not_validated", "validation_log": "docs/maintainers/phase-1-validation-log.json", "quality_floor_status": "satisfied", - "supply_chain_assessment_at": "2026-08-23T00:00:00.000Z", + "supply_chain_assessment_at": "2026-09-07T00:00:00.000Z", "p0_stable_ref": { "schema_version": "launchrally.dev/p0-release/v1", "release_status": "stable" From 257c6ecaac75a75e649ce404b3ba322c455cff42 Mon Sep 17 00:00:00 2001 From: leyoonafr Date: Mon, 7 Sep 2026 12:17:14 +0800 Subject: [PATCH 22/22] docs: complete 0.4.3 release inventory (#210) --- .../experimental-0.4.3-announcement.md | 7 +- .../experimental-0.4.3-prepublication.md | 75 +++++++++++++++++++ 2 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 docs/maintainers/experimental-0.4.3-prepublication.md diff --git a/docs/maintainers/experimental-0.4.3-announcement.md b/docs/maintainers/experimental-0.4.3-announcement.md index 32ba6b9..d996e5e 100644 --- a/docs/maintainers/experimental-0.4.3-announcement.md +++ b/docs/maintainers/experimental-0.4.3-announcement.md @@ -21,6 +21,9 @@ Review the [0.4.3 changelog](https://github.com/codeacme17/launchrally/blob/v0.4 and [external verification record](https://github.com/codeacme17/launchrally/blob/v0.4.3/docs/maintainers/experimental-0.4.3-p1-evidence.md) before adoption. +The [prepublication checks](https://github.com/codeacme17/launchrally/blob/v0.4.3/docs/maintainers/experimental-0.4.3-prepublication.md) +record the assessed candidate and publisher controls separately from public verification. + ## Resolved issues - [#187 — Style the complete Architect Human Mode decision flow](https://github.com/codeacme17/launchrally/issues/187) @@ -36,7 +39,9 @@ before adoption. - [#209 — Complete remaining Phase 1 Human Mode flows](https://github.com/codeacme17/launchrally/pull/209) - [#211 — Prepare Phase 1 Experimental 0.4.3](https://github.com/codeacme17/launchrally/pull/211) - [#212 — Promote Phase 1 Experimental 0.4.3](https://github.com/codeacme17/launchrally/pull/212) +- [#213 — Refresh P1 release assessment for 0.4.3](https://github.com/codeacme17/launchrally/pull/213) +- [#214 — Complete 0.4.3 release inventory and prepublication evidence](https://github.com/codeacme17/launchrally/pull/214) ## Contributors -- [@codeacme17](https://github.com/codeacme17) — [#206](https://github.com/codeacme17/launchrally/pull/206), [#207](https://github.com/codeacme17/launchrally/pull/207), [#208](https://github.com/codeacme17/launchrally/pull/208), [#209](https://github.com/codeacme17/launchrally/pull/209), [#211](https://github.com/codeacme17/launchrally/pull/211), [#212](https://github.com/codeacme17/launchrally/pull/212) +- [@codeacme17](https://github.com/codeacme17) — [#206](https://github.com/codeacme17/launchrally/pull/206), [#207](https://github.com/codeacme17/launchrally/pull/207), [#208](https://github.com/codeacme17/launchrally/pull/208), [#209](https://github.com/codeacme17/launchrally/pull/209), [#211](https://github.com/codeacme17/launchrally/pull/211), [#212](https://github.com/codeacme17/launchrally/pull/212), [#213](https://github.com/codeacme17/launchrally/pull/213), [#214](https://github.com/codeacme17/launchrally/pull/214) diff --git a/docs/maintainers/experimental-0.4.3-prepublication.md b/docs/maintainers/experimental-0.4.3-prepublication.md new file mode 100644 index 0000000..08699e9 --- /dev/null +++ b/docs/maintainers/experimental-0.4.3-prepublication.md @@ -0,0 +1,75 @@ +# Phase 1 Experimental 0.4.3 prepublication checks + +Assessment date: 2026-09-07 UTC. + +This record covers checks performed before publishing the 0.4.3 candidate. +It does not establish public availability, independent external verification, +P1 Validated status, or Stable-promotion approval. + +## Candidate identity + +- Version: `0.4.3` across all five packages. +- Candidate manifest: `release/p1-release-candidate.json`. +- Assessed correction commit: `08907b8c21d0f21a5f022b4740271dd153943583`. +- Dev integration commit: `4f5b5a033899e3ee9323e1c087df08e8d31955d6`. +- Correction: refresh the supply-chain assessment from August 23 to September 7. +- The correction changes no candidate package digest. +- Release channel: `experimental`; preserved Stable channel: `latest` at `0.3.2`. + +The final release tag must identify the approved main promotion commit. +Neither commit above is a substitute for that final main identity. + +## npm identity and publisher controls + +Authenticated package-access checks confirmed write access to every package. +All five `npm trust list` checks returned the following exact configuration: + +| Package | Provider | Repository | Workflow | Environment | +| --- | --- | --- | --- | --- | +| `@launchrally/contracts` | GitHub | `codeacme17/launchrally` | `release.yml` | `npm` | +| `@launchrally/core` | GitHub | `codeacme17/launchrally` | `release.yml` | `npm` | +| `@launchrally/cli` | GitHub | `codeacme17/launchrally` | `release.yml` | `npm` | +| `@launchrally/codex-plugin` | GitHub | `codeacme17/launchrally` | `release.yml` | `npm` | +| `@launchrally/claude-plugin` | GitHub | `codeacme17/launchrally` | `release.yml` | `npm` | + +Each publisher allows `publish` and `stage publish`. The release workflow uses +the approved direct publication path; no publisher configuration was changed. +The GitHub `npm` environment requires deployment review and permits `v*.*.*` +tags. An active tag ruleset protects `refs/tags/v*.*.*`. + +At assessment time all five public `experimental` tags resolved to `0.4.2`, +and all five `latest` tags resolved to `0.3.2`. + +## Verification results + +| Check | Result | +| --- | --- | +| Locked dependency installation with scripts disabled | Passed | +| Build and generated Skill synchronization | Passed; no generated diff | +| Supply-chain test suite | 68 passed, 0 failed | +| Full test suite after the assessment correction | 849 passed, 0 failed, 3 existing TODOs | +| Exact packed-artifact journeys within the full suite | Passed | +| Acceptance traceability | 25 P0 requirements, 39 P1 requirements, 17 release gates validated | +| P0 governance | Passed | +| P1 publication readiness | 39 requirements and 5 mandatory gates validated | +| Tagged release validation for `v0.4.3` | Passed on the assessment date | +| Candidate digest verification | All five npm pack digests match the committed manifest | +| Independent standards and specification review of the correction | No findings on either axis | +| Correction PR CI | All nine executed jobs passed; branch-policy job skipped | + +The separate optional preflight fan-out was declined by the maintainer. +Same-day tagged-release validation must be repeated if publication moves to +another UTC date; this record does not extend the assessment's validity. + +## Public references and remaining gates + +- [Assessment correction PR #213](https://github.com/codeacme17/launchrally/pull/213). +- [Correction CI](https://github.com/codeacme17/launchrally/actions/runs/34081920547). +- [Main promotion PR #212](https://github.com/codeacme17/launchrally/pull/212). +- [Release tracker #210](https://github.com/codeacme17/launchrally/issues/210). + +Main promotion, protected annotated-tag publication, public digest/provenance +smoke, and the published-artifact Human Mode checks required by #187 remain +separate gates. Independent CLI, Codex, and Claude external verification must +be recorded through the existing signed-result procedure after publication. +No public-release or external-verification gate is marked complete here.