Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs-site/generated/facts.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
]
},
"registry": {
"revision": 5,
"revision": 6,
"participants": [
{
"id": "claude-code",
Expand Down
33 changes: 17 additions & 16 deletions docs/design/setup-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,13 @@ they replaced are recorded once in

Every generated customization file teaches the tool the same fixed, bounded workflow:

1. Parse `<role> [id]`; when no id is given, the id defaults to the role.
1. Parse `<role> [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 <id> --role <role> --platform <target>` once, and remember the actual
id it prints — it may carry a suffix. Add `--resume` when the pane is recovering a
clean stop.
id it prints — it may carry a suffix. Add `--resume` when the parsed arguments included
it; that trailing token is the only signal the pane has that it is recovering a clean
stop.
4. Run `crew receive <actual-id>` once.
5. For a Task, use `task start`, do the work, then `task submit`; only the Inspector
uses `task approve` or `task requeue`.
Expand All @@ -110,13 +111,13 @@ Every Participant template embeds the block below, word for word, where its
generators are reproducible.

```text
Parse {{ROLE_ARGS}} as `<role> [id]`; if no id is given, the id defaults to the role.
Parse {{ROLE_ARGS}} as `<role> [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 <id> --role <role> --platform <target>`. 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 <actual-id>`.
4. Act only within your Role:
- Worker: `crew task start <actual-id> <task-id>`, do the work, then
Expand Down Expand Up @@ -166,7 +167,7 @@ allowed-tools: Bash(crew *)
argument-hint: <manager|worker|inspector> [agent-id]
---

<!-- generated-by: crew setup; registry-revision: 5 -->
<!-- generated-by: crew setup; registry-revision: 6 -->

Use the finite crew workflow below for `$ARGUMENTS`.
[shared workflow rendered here]
Expand All @@ -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.
---

<!-- generated-by: crew setup; registry-revision: 5 -->
<!-- generated-by: crew setup; registry-revision: 6 -->

Use the finite crew workflow below for the role and optional id supplied by the user.
[shared workflow rendered here]
Expand Down Expand Up @@ -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}}
Expand Down Expand Up @@ -254,7 +255,7 @@ tools:
- execute
---

<!-- generated-by: crew setup; registry-revision: 5 -->
<!-- generated-by: crew setup; registry-revision: 6 -->

[shared finite workflow rendered here]
```
Expand Down Expand Up @@ -304,7 +305,7 @@ description: Join and coordinate through the local crew inbox and reviewed task
argument-hint: <manager|worker|inspector> [agent-id]
---

<!-- generated-by: crew setup; registry-revision: 5 -->
<!-- generated-by: crew setup; registry-revision: 6 -->

[shared finite workflow rendered here]
```
Expand Down Expand Up @@ -361,7 +362,7 @@ operator's `<role> [id]`. crew generates:
description: Join and coordinate through the local crew inbox and reviewed task workflow.
---

<!-- generated-by: crew setup; registry-revision: 5 -->
<!-- generated-by: crew setup; registry-revision: 6 -->

[shared finite workflow rendered here]
```
Expand Down Expand Up @@ -476,8 +477,8 @@ file's own format, with three fields separated by semicolons:
generated-by: crew setup; registry-revision: <n>; content-hash: sha256:<64-hex>
```

- Markdown / `SKILL.md` / `*.agent.md`: `<!-- generated-by: crew setup; registry-revision: 5; content-hash: sha256:… -->`
- TOML (`crew.toml`): `# generated-by: crew setup; registry-revision: 5; content-hash: sha256:…`
- Markdown / `SKILL.md` / `*.agent.md`: `<!-- generated-by: crew setup; registry-revision: 6; content-hash: sha256:… -->`
- 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
Expand Down Expand Up @@ -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 registry lives in `src/platforms/` and is currently at **registry-revision 6**
(the revision started at 1; adding Participants and launch facts since then bumped it,
most recently the `little-coder` target).
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
Expand Down
12 changes: 7 additions & 5 deletions src/platforms/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -118,13 +120,13 @@ export type SetupTarget = ParticipantTarget | BackendTarget;
* `<target>`, and `<actual-id>` runtime placeholders the model fills — is stable
* so generator snapshots are reproducible.
*/
const SHARED_WORKFLOW = `Parse {{ROLE_ARGS}} as \`<role> [id]\`; if no id is given, the id defaults to the role.
const SHARED_WORKFLOW = `Parse {{ROLE_ARGS}} as \`<role> [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 <id> --role <role> --platform <target>\`. 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 <actual-id>\`.
4. Act only within your Role:
- Worker: \`crew task start <actual-id> <task-id>\`, do the work, then
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/commands/setup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ describe('crew setup <participant> (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');
});
Expand Down
39 changes: 30 additions & 9 deletions tests/unit/platforms.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,27 @@ 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 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 has no way to observe.
const claude = PARTICIPANT_TARGETS.find((t) => t.id === 'claude-code')!;
expect(claude.invocation('worker', 'worker-2', { resume: true }).split(' ').at(-1)).toBe(
'--resume',
);
for (const t of PARTICIPANT_TARGETS) {
const body = t.render();
expect(body, `${t.id} parse rule hides the appended --resume`).toContain(
'as `<role> [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([
Expand Down Expand Up @@ -232,16 +253,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<string, string> = {
'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': '63a59d4a434ccb72948e8dbdef60c9254e942ae7c18e102d04272036193e64e4',
'codex-cli': '01d9c942ba8f095d6a7853d41a91c719e45e0c558526869f4c89670f15de5e88',
'gemini-cli': 'e86bd4ae9a7dec4932b136105affeafedf446e07d13d5dacc89813924b099682',
'copilot-cli': '1ee19562275914d4ff059bfe0d1cd67696b53d4ccec07f6c4eb1ea03d8e90e87',
'antigravity-cli': '01d9c942ba8f095d6a7853d41a91c719e45e0c558526869f4c89670f15de5e88',
'pi-cli': 'daa2e6e6604eeab67b5c7a279ddf088ab92c1837b46da11bc0ffee77c6da65f4',
'little-coder': 'daa2e6e6604eeab67b5c7a279ddf088ab92c1837b46da11bc0ffee77c6da65f4',
'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]);
Expand Down