diff --git a/CHANGELOG.md b/CHANGELOG.md index 492d01d..709b707 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,20 @@ All notable changes to this project are documented in this file. The format foll ## [Unreleased] +### Fixed + +- The shared Participant workflow told a pane to add `--resume` to its `crew join` when + "the pane is recovering a clean stop" — a state a Participant CLI cannot observe — while + its own parse rule admitted only ` [id]` and so hid the appended `--resume` token + that is the pane's only reliable signal. A pane following that rule literally dropped the + flag and joined without it, and the roster gate then failed the relaunch with + `LAUNCH_FAILED` because the planned archived ids never came back active. The parse rule + now reads ` [id] [--resume]` and the join step keys off that token; the Claude Code + and Pi/Little Coder artifacts widen their `argument-hint` to match. **Because every + Participant artifact's bytes change, the registry advances to revision 6, so a + previously installed artifact is reported as outdated until `crew setup` is re-run** — + that regeneration is in place and needs neither `--force` nor a backup. + ### Added - Little Coder 1.11.0 is now a first-class Participant CLI (`little-coder`), bringing @@ -14,8 +28,8 @@ All notable changes to this project are documented in this file. The format foll tmux launcher, and is included in the Ollama / LM Studio local-model recipes. Setup prints the narrow additive `LITTLE_CODER_BASH_ALLOW` opt-in and never enables `accept-all`. Because Little Coder forwards `--version` to bundled Pi, crew reads the - adjacent installed package metadata for its 1.11.0 version floor. The registry advances - to revision 5. + adjacent installed package metadata for its 1.11.0 version floor. This addition took the + registry to revision 5. - Two new Participant CLI engines, `pi-cli` (Pi) and `opencode-cli` (opencode), bringing the registry to seven Participant CLIs. Each ships a Markdown customization artifact (`crew.md`) that exposes the `/crew [id]` command: pi as a Prompt Template under diff --git a/docs-site/generated/facts.json b/docs-site/generated/facts.json index 7b5d46b..b5c3205 100644 --- a/docs-site/generated/facts.json +++ b/docs-site/generated/facts.json @@ -25,7 +25,7 @@ ] }, "registry": { - "revision": 5, + "revision": 6, "participants": [ { "id": "claude-code", diff --git a/docs/design/setup-integration.md b/docs/design/setup-integration.md index c25dfe4..6a7ccfe 100644 --- a/docs/design/setup-integration.md +++ b/docs/design/setup-integration.md @@ -84,11 +84,12 @@ they replaced are recorded once in Every generated customization file teaches the tool the same fixed, bounded workflow: -1. Parse ` [id]`; when no id is given, the id defaults to the role. +1. Parse ` [id] [--resume]`; when no id is given, the id defaults to the role. 2. Confirm the current directory is inside a crew Workspace by running `crew doctor`; if it is not, report that the operator must run `crew init` in the intended root. 3. Run `crew join --role --platform ` once, and remember the actual - id it prints — it may carry a suffix. Add `--resume` when the pane is recovering a + id it prints — it may carry a suffix. Add `--resume` when the parsed arguments included + it; that trailing token is the pane's only reliable signal that it is recovering a clean stop. 4. Run `crew receive ` once. 5. For a Task, use `task start`, do the work, then `task submit`; only the Inspector @@ -110,13 +111,13 @@ Every Participant template embeds the block below, word for word, where its generators are reproducible. ```text -Parse {{ROLE_ARGS}} as ` [id]`; if no id is given, the id defaults to the role. +Parse {{ROLE_ARGS}} as ` [id] [--resume]`; if no id is given, the id defaults to the role. 1. Confirm this is a crew Workspace: run `crew doctor`. If it is not, tell the operator to run `crew init` in the intended repository root, then stop. 2. Join once: `crew join --role --platform `. Retain the actual id it - prints; it may carry a `-2`..`-99` suffix after a collision. If the pane is recovering - a clean stop, add `--resume` to the join command. + prints; it may carry a `-2`..`-99` suffix after a collision. If the arguments included + `--resume` (this pane is recovering a clean stop), add `--resume` to the join command. 3. Read your inbox once: `crew receive `. 4. Act only within your Role: - Worker: `crew task start `, do the work, then @@ -163,10 +164,10 @@ name: crew description: Join and coordinate through the local crew inbox and reviewed task workflow. disable-model-invocation: true allowed-tools: Bash(crew *) -argument-hint: [agent-id] +argument-hint: [agent-id] [--resume] --- - + Use the finite crew workflow below for `$ARGUMENTS`. [shared workflow rendered here] @@ -191,7 +192,7 @@ name: crew description: Join and coordinate through the local crew inbox and reviewed task workflow. Use when the user asks to start or act as a crew role. --- - + Use the finite crew workflow below for the role and optional id supplied by the user. [shared workflow rendered here] @@ -224,7 +225,7 @@ Gemini custom commands are TOML files under `.gemini/commands`; a project file w a user file with the same name. crew generates: ```toml -# generated-by: crew setup; registry-revision: 5 +# generated-by: crew setup; registry-revision: 6 description = "Join and coordinate through the local crew inbox and reviewed task workflow" prompt = """ Role and optional id: {{args}} @@ -254,7 +255,7 @@ tools: - execute --- - + [shared finite workflow rendered here] ``` @@ -301,10 +302,10 @@ operator's ` [id]` with `$ARGUMENTS`. crew generates: ```markdown --- description: Join and coordinate through the local crew inbox and reviewed task workflow. -argument-hint: [agent-id] +argument-hint: [agent-id] [--resume] --- - + [shared finite workflow rendered here] ``` @@ -361,7 +362,7 @@ operator's ` [id]`. crew generates: description: Join and coordinate through the local crew inbox and reviewed task workflow. --- - + [shared finite workflow rendered here] ``` @@ -476,8 +477,8 @@ file's own format, with three fields separated by semicolons: generated-by: crew setup; registry-revision: ; content-hash: sha256:<64-hex> ``` -- Markdown / `SKILL.md` / `*.agent.md`: `` -- TOML (`crew.toml`): `# generated-by: crew setup; registry-revision: 5; content-hash: sha256:…` +- Markdown / `SKILL.md` / `*.agent.md`: `` +- TOML (`crew.toml`): `# generated-by: crew setup; registry-revision: 6; content-hash: sha256:…` `content-hash` is the SHA-256 of the **rendered file with the `content-hash:` value replaced by an empty string**, written as lower-case hex, after line endings are @@ -530,9 +531,9 @@ starts as `copilot --agent=crew --prompt …`; nothing is pasted into an already Copilot interface. The Team display and setup keep the guidance about selecting crew via `/agent`. -The registry lives in `src/platforms/` and is currently at **registry-revision 5** -(the revision started at 1; adding Participants and launch facts since then bumped it, -most recently the `little-coder` target). +The registry lives in `src/platforms/` and is currently at **registry-revision 6** +(the revision started at 1; adding Participants, launch facts, and artifact text since +then bumped it, most recently the shared workflow's `[--resume]` parse rule). `registry.ts` looks up targets; `shared.ts` holds the record types, the shared workflow text, the marker and content-hash rules, and the version probe; each target has its own module supplying its facts and rendering (`agent-skills.ts` holds the single renderer diff --git a/src/platforms/claude.ts b/src/platforms/claude.ts index 2d2de4c..ef4a3c8 100644 --- a/src/platforms/claude.ts +++ b/src/platforms/claude.ts @@ -41,7 +41,7 @@ name: crew description: Join and coordinate through the local crew inbox and reviewed task workflow. disable-model-invocation: true allowed-tools: Bash(crew *) -argument-hint: [agent-id] +argument-hint: [agent-id] [--resume] --- ${marker} diff --git a/src/platforms/pi.ts b/src/platforms/pi.ts index f40c936..47fb22f 100644 --- a/src/platforms/pi.ts +++ b/src/platforms/pi.ts @@ -14,7 +14,7 @@ export function renderPiPromptArtifact(): string { 'markdown', (marker) => `--- description: Join and coordinate through the local crew inbox and reviewed task workflow. -argument-hint: [agent-id] +argument-hint: [agent-id] [--resume] --- ${marker} diff --git a/src/platforms/shared.ts b/src/platforms/shared.ts index 7be06d2..a3ac277 100644 --- a/src/platforms/shared.ts +++ b/src/platforms/shared.ts @@ -6,7 +6,9 @@ * (ADR-0006). `setup`, `doctor`, Team display, and the Launcher read these * records; they never keep parallel path/invocation tables. Official platform * facts and sources live in docs/design/setup-integration.md, which this module - * mirrors verbatim — a change there is a registry-revision bump here. + * mirrors verbatim; REGISTRY_REVISION is bumped whenever a change alters a + * generated artifact's rendered bytes (setup-integration.md §7) — a doc-only + * edit that changes no rendered byte must not bump it. */ import { createHash } from 'node:crypto'; import { closeSync, constants, fstatSync, openSync, readSync, realpathSync } from 'node:fs'; @@ -16,7 +18,7 @@ import type { ParticipantId } from '../participants.js'; import { resolveExecutableOnPath } from '../which.js'; /** Integer revision of the platform registry record set; bumped on any artifact change. */ -export const REGISTRY_REVISION = 5; +export const REGISTRY_REVISION = 6; /** Date the documented paths/invocations were last re-verified (setup-integration.md). */ export const VERIFIED_ON = '2026-06-29'; @@ -118,13 +120,13 @@ export type SetupTarget = ParticipantTarget | BackendTarget; * ``, and `` runtime placeholders the model fills — is stable * so generator snapshots are reproducible. */ -const SHARED_WORKFLOW = `Parse {{ROLE_ARGS}} as \` [id]\`; if no id is given, the id defaults to the role. +const SHARED_WORKFLOW = `Parse {{ROLE_ARGS}} as \` [id] [--resume]\`; if no id is given, the id defaults to the role. 1. Confirm this is a crew Workspace: run \`crew doctor\`. If it is not, tell the operator to run \`crew init\` in the intended repository root, then stop. 2. Join once: \`crew join --role --platform \`. Retain the actual id it - prints; it may carry a \`-2\`..\`-99\` suffix after a collision. If the pane is recovering - a clean stop, add \`--resume\` to the join command. + prints; it may carry a \`-2\`..\`-99\` suffix after a collision. If the arguments included + \`--resume\` (this pane is recovering a clean stop), add \`--resume\` to the join command. 3. Read your inbox once: \`crew receive \`. 4. Act only within your Role: - Worker: \`crew task start \`, do the work, then diff --git a/tests/integration/commands/setup.test.ts b/tests/integration/commands/setup.test.ts index 8ee9585..542c610 100644 --- a/tests/integration/commands/setup.test.ts +++ b/tests/integration/commands/setup.test.ts @@ -126,7 +126,7 @@ describe('crew setup (install, FR-G04/G05)', () => { expect(rec.action).toBe('written'); expect(rec.scope).toBe('global'); expect(rec.path).toBe('~/.claude/skills/crew/SKILL.md'); - expect(rec.registry_revision).toBe(5); + expect(rec.registry_revision).toBe(6); const body = readFileSync(join(home, '.claude/skills/crew/SKILL.md'), 'utf8'); expect(classifyArtifact(body)).toBe('managed-current'); }); diff --git a/tests/unit/platforms.test.ts b/tests/unit/platforms.test.ts index e498b9c..9126c6c 100644 --- a/tests/unit/platforms.test.ts +++ b/tests/unit/platforms.test.ts @@ -139,6 +139,33 @@ describe('participant artifact rendering', () => { expect(inv('opencode-cli')).toBe('/crew worker worker-2 --resume'); }); + it('teaches every artifact to read the resume signal off its own arguments (FR-U47)', () => { + // A Participant CLI cannot see tmux session history, so the appended `--resume` + // token is the pane's only reliable evidence that it is recovering a clean stop. + // The workflow's parse rule must therefore admit that token, and the join step + // must key off it rather than off a state the pane cannot reliably observe. + for (const t of PARTICIPANT_TARGETS) { + // Mirror paneLaunch (src/launcher/session.ts): launchArgs wins when it is + // defined, so Copilot's resumed pane never goes through `invocation` at all. + // Checking only `invocation` here would exempt the one target whose token + // travels the other path. + const resuming = { resume: true }; + const launched = + t.launchArgs?.('worker', 'worker-2', resuming)?.join(' ') ?? + t.invocation('worker', 'worker-2', resuming); + expect(launched.split(' ').at(-1), `${t.id} pane is never handed --resume`).toBe('--resume'); + + const body = t.render(); + expect(body, `${t.id} parse rule hides the appended --resume`).toContain( + 'as ` [id] [--resume]`', + ); + expect(body, `${t.id} join step ignores the --resume token`).toContain( + 'If the arguments included', + ); + expect(body).toContain('add `--resume` to the join command.'); + } + }); + it('keeps Copilot interactive guidance separate from its startup command', () => { const copilot = PARTICIPANT_TARGETS.find((target) => target.id === 'copilot-cli')!; expect(copilot.launchArgs?.('worker', 'worker-2')).toEqual([ @@ -232,16 +259,16 @@ describe('participant artifact rendering', () => { // bytes, the digest changes and this fails, forcing both an update here AND a // REGISTRY_REVISION bump so previously-installed artifacts read as managed-outdated. const expected: Record = { - 'claude-code': '836139943f352f9666e8f4a571800f25d1b5a13074bd138b9e6ed364ccba9846', - 'codex-cli': '23d654db4c2f3a9154a9dfdb73ede7205eee16b6613fd2e42adf77eb4e597163', - 'gemini-cli': '94ff656f119473d85dfc90858584d88e7555b262b72edd7bb65552a205abdf29', - 'copilot-cli': '7ae9e5276e6894dd5f18a8f0a6e8ba8ee767b947370acb8e3eec7a9f84d8a779', - 'antigravity-cli': '23d654db4c2f3a9154a9dfdb73ede7205eee16b6613fd2e42adf77eb4e597163', - 'pi-cli': '26853d7cfaafcc613c568bfa5ca07edee3914040745ab2d0e6cb8c9cef5cdd20', - 'little-coder': '26853d7cfaafcc613c568bfa5ca07edee3914040745ab2d0e6cb8c9cef5cdd20', - 'opencode-cli': 'dbe88df8e72e1fb719cc6d3b7667cf0fb0361a81ac0efdbb61cfbbfa4fd3541e', + 'claude-code': '22001355596469c2e0d8484f8254496a829d8fa83e77de2b28ad5ca97d05bccc', + 'codex-cli': '01d9c942ba8f095d6a7853d41a91c719e45e0c558526869f4c89670f15de5e88', + 'gemini-cli': 'e86bd4ae9a7dec4932b136105affeafedf446e07d13d5dacc89813924b099682', + 'copilot-cli': '1ee19562275914d4ff059bfe0d1cd67696b53d4ccec07f6c4eb1ea03d8e90e87', + 'antigravity-cli': '01d9c942ba8f095d6a7853d41a91c719e45e0c558526869f4c89670f15de5e88', + 'pi-cli': '359ff24755556eccb155d5ec2b858d41ebbe98f9ecb10f9fc09862c98e9ae3d6', + 'little-coder': '359ff24755556eccb155d5ec2b858d41ebbe98f9ecb10f9fc09862c98e9ae3d6', + 'opencode-cli': 'b3be65a516d2ac14f90f120e23c0e86e96d1171e7bf852efe6b9693eef3366c0', }; - expect(REGISTRY_REVISION).toBe(5); // bump together with the digests above + expect(REGISTRY_REVISION).toBe(6); // bump together with the digests above for (const t of PARTICIPANT_TARGETS) { const hash = /content-hash: sha256:([0-9a-f]{64})/.exec(t.render())![1]; expect(hash, `${t.id} artifact bytes changed`).toBe(expected[t.id]);