@clipboard-health/groundcrew 4.47.5 → 4.48.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -159,6 +159,7 @@ There is no `linear` config block. Groundcrew reads `GROUNDCREW_LINEAR_API_KEY`
159
159
  - [Runners](./docs/runners.md): Safehouse, Docker Sandboxes, and the `none` escape hatch.
160
160
  - [Credentials](./docs/credentials.md): Linear API keys, 1Password, build secrets, and `preLaunch`.
161
161
  - [Prepare worktree hooks](./docs/setup-hooks.md): `.groundcrew/config.json` `hooks.prepareWorktree` for per-repo dependency setup.
162
+ - [Operator host setup](./docs/setup-hooks.md#unsandboxedhooks-operator-only-host-setup): `crew.config.ts` `knownRepositories[].unsandboxedHooks.prepareWorktree` runs trusted, per-repo setup on the host outside the sandbox.
162
163
  - [Task sources](./docs/task-sources.md): custom shell/Jira/local-plan adapters.
163
164
  - [Development](./docs/development.md): local source workflow and README/demo asset regeneration.
164
165
  - [Troubleshooting](./docs/troubleshooting.md): common operational pitfalls and fixes.
@@ -1 +1 @@
1
- {"version":3,"file":"openWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/openWorkspace.ts"],"names":[],"mappings":"AAKA,OAAO,EAAiC,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAetF,UAAU,gBAAgB;IACxB,IAAI,EAAE,IAAI,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;IACX,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,UAAU,WAAW;IACnB,IAAI,EAAE,QAAQ,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,gBAAgB,GAAG,WAAW,CAAC;IACtC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0EAA0E;IAC1E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AA6JD,wBAAsB,aAAa,CACjC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,IAAI,CAAC,CA8Gf;AA4ID,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,oBAAoB,CAE3E;AAED,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAIpE"}
1
+ {"version":3,"file":"openWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/openWorkspace.ts"],"names":[],"mappings":"AAKA,OAAO,EAAiC,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAetF,UAAU,gBAAgB;IACxB,IAAI,EAAE,IAAI,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;IACX,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,UAAU,WAAW;IACnB,IAAI,EAAE,QAAQ,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,gBAAgB,GAAG,WAAW,CAAC;IACtC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0EAA0E;IAC1E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AA6JD,wBAAsB,aAAa,CACjC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,IAAI,CAAC,CA0Hf;AA4ID,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,oBAAoB,CAE3E;AAED,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAIpE"}
@@ -156,11 +156,20 @@ export async function openWorkspace(config, options) {
156
156
  });
157
157
  let srtSettingsDir;
158
158
  try {
159
+ const repositoryEntry = config.workspace.repositories.find((entry) => entry.name === repository);
159
160
  const prepareWorktreeCommand = resolvePrepareWorktreeCommand({
160
161
  worktreeDir: launchDir,
162
+ // Spread-conditional: exactOptionalPropertyTypes forbids an explicit
163
+ // `undefined` for an optional field, and the lookup yields undefined for
164
+ // repos with no hooks. Mirrors setupWorkspace so `crew open` honors the
165
+ // same per-repo operator hooks as `crew setup`.
166
+ ...(repositoryEntry?.hooks === undefined ? {} : { perRepoHooks: repositoryEntry.hooks }),
161
167
  defaultHooks: config.defaults.hooks,
162
168
  });
163
- const secretsFile = prepareWorktreeCommand === undefined ? undefined : stageBuildSecrets(stagedPrompt.directory);
169
+ const prepareWorktreeUnsandboxedCommand = repositoryEntry?.unsandboxedHooks?.prepareWorktree;
170
+ const secretsFile = prepareWorktreeCommand === undefined && prepareWorktreeUnsandboxedCommand === undefined
171
+ ? undefined
172
+ : stageBuildSecrets(stagedPrompt.directory);
164
173
  seedLaunchWorkspaceTrust({
165
174
  agentCommandName: inferAgentCommandName(definition.cmd),
166
175
  launchDir,
@@ -176,6 +185,7 @@ export async function openWorkspace(config, options) {
176
185
  workingDir: launchDir,
177
186
  secretsFile,
178
187
  prepareWorktreeCommand,
188
+ prepareWorktreeUnsandboxedCommand,
179
189
  sandboxName,
180
190
  workspaceKind,
181
191
  readOnlyDirs: config.local.readOnlyDirs,
@@ -1 +1 @@
1
- {"version":3,"file":"setupWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/setupWorkspace.ts"],"names":[],"mappings":"AAEA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AA0BnE,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,sEAAsE;IACtE,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,6FAA6F;IAC7F,2BAA2B,CAAC,EAAE,OAAO,CAAC;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAuBD,wBAAsB,cAAc,CAClC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,qBAAqB,EAC9B,UAAU,GAAE,wBAA6B,GACxC,OAAO,CAAC,IAAI,CAAC,CAsJf;AA8MD,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GACjC,OAAO,CAAC,IAAI,CAAC,CAkDf"}
1
+ {"version":3,"file":"setupWorkspace.d.ts","sourceRoot":"","sources":["../../src/commands/setupWorkspace.ts"],"names":[],"mappings":"AAEA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AA0BnE,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,sEAAsE;IACtE,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,6FAA6F;IAC7F,2BAA2B,CAAC,EAAE,OAAO,CAAC;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAuBD,wBAAsB,cAAc,CAClC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,qBAAqB,EAC9B,UAAU,GAAE,wBAA6B,GACxC,OAAO,CAAC,IAAI,CAAC,CA2Jf;AA8MD,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GACjC,OAAO,CAAC,IAAI,CAAC,CAkDf"}
@@ -85,7 +85,9 @@ export async function setupWorkspace(config, options, runOptions = {}) {
85
85
  workspaceContinuationInstruction: renderWorkspaceContinuationInstruction(accessHint),
86
86
  });
87
87
  promptDir = stagedPrompt.directory;
88
- const perRepoHooks = config.workspace.repositories.find((entry) => entry.name === repository)?.hooks;
88
+ const repositoryEntry = config.workspace.repositories.find((entry) => entry.name === repository);
89
+ const perRepoHooks = repositoryEntry?.hooks;
90
+ const prepareWorktreeUnsandboxedCommand = repositoryEntry?.unsandboxedHooks?.prepareWorktree;
89
91
  const prepareWorktreeCommand = resolvePrepareWorktreeCommand({
90
92
  worktreeDir: launchDir,
91
93
  // Spread-conditional rather than a direct assignment: under
@@ -94,7 +96,9 @@ export async function setupWorkspace(config, options, runOptions = {}) {
94
96
  ...(perRepoHooks === undefined ? {} : { perRepoHooks }),
95
97
  defaultHooks: config.defaults.hooks,
96
98
  });
97
- const secretsFile = prepareWorktreeCommand === undefined ? undefined : stageBuildSecrets(promptDir);
99
+ const secretsFile = prepareWorktreeCommand === undefined && prepareWorktreeUnsandboxedCommand === undefined
100
+ ? undefined
101
+ : stageBuildSecrets(promptDir);
98
102
  const completionTaskId = options.completionTaskId ?? task;
99
103
  const completionMarkDoneSupported = options.completionMarkDoneSupported ?? true;
100
104
  const taskSourceWritePaths = runner === "safehouse" || runner === "srt"
@@ -118,6 +122,7 @@ export async function setupWorkspace(config, options, runOptions = {}) {
118
122
  workingDir: launchDir,
119
123
  secretsFile,
120
124
  prepareWorktreeCommand,
125
+ prepareWorktreeUnsandboxedCommand,
121
126
  sandboxName,
122
127
  workspaceKind,
123
128
  readOnlyDirs: config.local.readOnlyDirs,
@@ -1 +1 @@
1
- {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/commands/status.ts"],"names":[],"mappings":"AAIA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAenE,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AA4rBD,wBAAsB,MAAM,CAAC,MAAM,EAAE,cAAc,EAAE,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAU/F;AAED,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAI7D"}
1
+ {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/commands/status.ts"],"names":[],"mappings":"AAIA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAenE,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAosBD,wBAAsB,MAAM,CAAC,MAAM,EAAE,cAAc,EAAE,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAU/F;AAED,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAI7D"}
@@ -489,15 +489,20 @@ function statusByWorktreeTask(boardResult) {
489
489
  }
490
490
  return statuses;
491
491
  }
492
- function writeQueueSections(boardResult) {
492
+ function writeQueueSections(boardResult, worktreeTasks) {
493
493
  if (boardResult.kind === "error") {
494
494
  writeSection("Queue");
495
495
  writeOutput(`unavailable: ${errorMessage(boardResult.error)}`);
496
496
  return;
497
497
  }
498
498
  // Only groundcrew-eligible Todos are dispatchable; non-eligible ones lack
499
- // a repo or agent, so `crew run` would skip them.
500
- const todos = boardResult.issues.filter(isTodoSourceIssue).filter(isGroundcrewIssue);
499
+ // a repo or agent, so `crew run` would skip them. Todos whose task already
500
+ // has a local worktree are omitted so a dispatched-but-not-yet-transitioned
501
+ // ticket doesn't show as both provisioning and queued.
502
+ const todos = boardResult.issues
503
+ .filter(isTodoSourceIssue)
504
+ .filter(isGroundcrewIssue)
505
+ .filter((i) => !worktreeTasks.has(naturalIdFromCanonical(i.id).toLowerCase()));
501
506
  const ready = todos.filter((i) => !hasOpenBlocker(i));
502
507
  const blocked = todos.filter(hasOpenBlocker);
503
508
  // Hide the section entirely when nothing's queued and nothing's blocked.
@@ -587,7 +592,7 @@ async function writeInventoryStatus(config) {
587
592
  writeOutput();
588
593
  writeOutput(`slots: ${used}/${config.orchestrator.maximumInProgress} used`);
589
594
  }
590
- writeQueueSections(boardResult);
595
+ writeQueueSections(boardResult, worktreeTasks);
591
596
  }
592
597
  export async function status(config, options = {}) {
593
598
  const task = options.task?.trim();
@@ -18,6 +18,7 @@ export declare function composeAgentLaunch(input: {
18
18
  workingDir: string;
19
19
  secretsFile?: string | undefined;
20
20
  prepareWorktreeCommand?: string | undefined;
21
+ prepareWorktreeUnsandboxedCommand?: string | undefined;
21
22
  sandboxName?: string | undefined;
22
23
  workspaceKind: WorkspaceKind;
23
24
  workerEnvironment?: WorkerEnvironment | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"agentLaunch.d.ts","sourceRoot":"","sources":["../../src/lib/agentLaunch.ts"],"names":[],"mappings":"AAWA,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,oBAAoB,EACzB,KAAK,cAAc,EACpB,MAAM,aAAa,CAAC;AAErB,OAAO,EAIL,KAAK,iBAAiB,EACvB,MAAM,oBAAoB,CAAC;AAM5B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE;IACxC,MAAM,EAAE,WAAW,CAAC;IACpB,aAAa,EAAE,oBAAoB,CAAC;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,eAAe,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,sBAAsB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5C,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,aAAa,EAAE,aAAa,CAAC;IAC7B,iBAAiB,CAAC,EAAE,iBAAiB,GAAG,SAAS,CAAC;IAClD,kBAAkB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACzC,oBAAoB,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;IACrD,uBAAuB,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;IACxD,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;CAC9C,GAAG;IAAE,aAAa,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CA2ChE;AA4DD,UAAU,mBAAmB;IAC3B,MAAM,EAAE,WAAW,CAAC;IACpB,iFAAiF;IACjF,aAAa,EAAE,oBAAoB,CAAC;IACpC,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,aAAa,EAAE,aAAa,CAAC;IAC7B,WAAW,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAClC;AAED,wBAAsB,kBAAkB,CAAC,KAAK,EAAE;IAC9C,MAAM,EAAE,cAAc,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,eAAe,CAAC;IAC5B,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAsD/B;AAwBD,wBAAsB,kBAAkB,CAAC,KAAK,EAAE;IAC9C,MAAM,EAAE,cAAc,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,GAAG,OAAO,CAAC,IAAI,CAAC,CAahB"}
1
+ {"version":3,"file":"agentLaunch.d.ts","sourceRoot":"","sources":["../../src/lib/agentLaunch.ts"],"names":[],"mappings":"AAWA,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,oBAAoB,EACzB,KAAK,cAAc,EACpB,MAAM,aAAa,CAAC;AAErB,OAAO,EAIL,KAAK,iBAAiB,EACvB,MAAM,oBAAoB,CAAC;AAM5B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE;IACxC,MAAM,EAAE,WAAW,CAAC;IACpB,aAAa,EAAE,oBAAoB,CAAC;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,eAAe,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,sBAAsB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5C,iCAAiC,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACvD,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,aAAa,EAAE,aAAa,CAAC;IAC7B,iBAAiB,CAAC,EAAE,iBAAiB,GAAG,SAAS,CAAC;IAClD,kBAAkB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACzC,oBAAoB,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;IACrD,uBAAuB,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;IACxD,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;CAC9C,GAAG;IAAE,aAAa,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CA4ChE;AA4DD,UAAU,mBAAmB;IAC3B,MAAM,EAAE,WAAW,CAAC;IACpB,iFAAiF;IACjF,aAAa,EAAE,oBAAoB,CAAC;IACpC,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,aAAa,EAAE,aAAa,CAAC;IAC7B,WAAW,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAClC;AAED,wBAAsB,kBAAkB,CAAC,KAAK,EAAE;IAC9C,MAAM,EAAE,cAAc,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,eAAe,CAAC;IAC5B,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAsD/B;AAwBD,wBAAsB,kBAAkB,CAAC,KAAK,EAAE;IAC9C,MAAM,EAAE,cAAc,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,GAAG,OAAO,CAAC,IAAI,CAAC,CAahB"}
@@ -38,6 +38,7 @@ export function composeAgentLaunch(input) {
38
38
  workingDir: input.workingDir,
39
39
  secretsFile: input.secretsFile,
40
40
  prepareWorktreeCommand: input.prepareWorktreeCommand,
41
+ prepareWorktreeUnsandboxedCommand: input.prepareWorktreeUnsandboxedCommand,
41
42
  runner: input.runner,
42
43
  networkEgress: input.networkEgress,
43
44
  sandboxName: input.sandboxName,
@@ -25,6 +25,17 @@ export type SourceConfig = LinearAdapterConfig | ShellAdapterConfig | TodoTxtAda
25
25
  export interface HookCommands {
26
26
  prepareWorktree?: string;
27
27
  }
28
+ /**
29
+ * Operator-only hooks that run OUTSIDE the sandbox, on the host shell. Mirrors
30
+ * the `prepareWorktree` phase name of {@link HookCommands} but is a distinct
31
+ * type: the two families follow opposite rules (host hooks are operator-only
32
+ * with no `defaults` cascade), and keeping them separate lets a consumer that
33
+ * takes an `UnsandboxedHookCommands` be self-documenting about isolation and
34
+ * lets the two shapes diverge without leaking into each other.
35
+ */
36
+ export interface UnsandboxedHookCommands {
37
+ prepareWorktree?: string;
38
+ }
28
39
  /**
29
40
  * Reserved agent name. A task labeled `agent-any` resolves at runtime
30
41
  * to the configured agent with the most available session capacity, so
@@ -202,6 +213,18 @@ export interface KnownRepository {
202
213
  * they don't want to (or can't) commit a `.groundcrew/config.json` into.
203
214
  */
204
215
  hooks?: HookCommands;
216
+ /**
217
+ * Operator-only, per-repository hooks run on the HOST shell outside any
218
+ * sandbox. `unsandboxedHooks.prepareWorktree` runs before the sandboxed
219
+ * `hooks.prepareWorktree` and the agent. Honored ONLY from `crew.config.ts`;
220
+ * a `.groundcrew/config.json` that sets `unsandboxedHooks` is a hard config
221
+ * error (see `repositoryHooks.ts`). There is deliberately no `defaults`
222
+ * equivalent — host execution is an explicit per-repo grant, never a global
223
+ * default. Runs with the operator's full host authority against
224
+ * repo-controlled code (lifecycle scripts, the repo's own `bin/setup`), so
225
+ * granting it is an explicit trust decision.
226
+ */
227
+ unsandboxedHooks?: UnsandboxedHookCommands;
205
228
  }
206
229
  export interface Config {
207
230
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"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"}
@@ -258,6 +258,20 @@ function normalizeHookCommands(value, configKey) {
258
258
  }
259
259
  return hooks;
260
260
  }
261
+ // Caller guards `entry.unsandboxedHooks !== undefined`, so unlike
262
+ // `normalizeHookCommands` (also reached via `defaults.hooks`) this never sees
263
+ // `undefined` — there is deliberately no `defaults.unsandboxedHooks`.
264
+ function normalizeUnsandboxedHookCommands(value, configKey) {
265
+ if (!isPlainObject(value)) {
266
+ fail(`${configKey} must be an object`);
267
+ }
268
+ const hooks = {};
269
+ const prepareWorktree = normalizeOptionalString(value["prepareWorktree"], `${configKey}.prepareWorktree`);
270
+ if (prepareWorktree !== undefined) {
271
+ hooks.prepareWorktree = prepareWorktree;
272
+ }
273
+ return hooks;
274
+ }
261
275
  function normalizeDefaults(value) {
262
276
  if (value === undefined) {
263
277
  return { hooks: {} };
@@ -625,6 +639,9 @@ function normalizeKnownRepository(entry, index) {
625
639
  if (entry.hooks !== undefined) {
626
640
  recipe.hooks = normalizeHookCommands(entry.hooks, `${label}.hooks`);
627
641
  }
642
+ if (entry.unsandboxedHooks !== undefined) {
643
+ recipe.unsandboxedHooks = normalizeUnsandboxedHookCommands(entry.unsandboxedHooks, `${label}.unsandboxedHooks`);
644
+ }
628
645
  return recipe;
629
646
  }
630
647
  /**
@@ -90,6 +90,14 @@ interface LaunchCommandArguments {
90
90
  * `defaults.hooks.prepareWorktree` from crew.config.ts.
91
91
  */
92
92
  prepareWorktreeCommand?: string | undefined;
93
+ /**
94
+ * Operator-only, per-repository setup command run on the HOST shell (never a
95
+ * sandbox), before `prepareWorktreeCommand` and the agent. Resolved by the
96
+ * caller from `knownRepositories[].unsandboxedHooks.prepareWorktree` in
97
+ * crew.config.ts. Emitted for the safehouse, srt, and none runners; the sdx
98
+ * runner rejects it (no host to run it on).
99
+ */
100
+ prepareWorktreeUnsandboxedCommand?: string | undefined;
93
101
  /**
94
102
  * Concrete local isolation backend chosen for this launch. Resolved
95
103
  * from `config.local.runner` via `resolveLocalRunner` before this
@@ -1 +1 @@
1
- {"version":3,"file":"launchCommand.d.ts","sourceRoot":"","sources":["../../src/lib/launchCommand.ts"],"names":[],"mappings":"AAIA,OAAO,EAGL,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,oBAAoB,EAC1B,MAAM,aAAa,CAAC;AAIrB,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,GAAG,eAAe,CAE/F;AAED;;;;;;;;;GASG;AACH,wBAAgB,6BAA6B,CAAC,OAAO,GAAE,MAAwB,GAAG,MAAM,CAcvF;AAID;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,MAAwB,GAAG,MAAM,CAgB3E;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GAAG,MAAM,CAMvF;AAkND,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE9D;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CA8B9D;AAMD,MAAM,MAAM,iBAAiB,GAAG,QAAQ,CAAC;IACvC,kBAAkB,EAAE,MAAM,CAAC;IAC3B,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B,CAAC,CAAC;AAEH,wBAAgB,wBAAwB,CAAC,UAAU,EAAE;IACnD,MAAM,EAAE,MAAM,CAAC;IACf,iBAAiB,EAAE,OAAO,CAAC;CAC5B,GAAG,iBAAiB,CAMpB;AAwCD,MAAM,WAAW,yBAAyB;IACxC,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC;;;;OAIG;IACH,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC/B;AAED,UAAU,sBAAsB;IAC9B,UAAU,EAAE,eAAe,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC;;;;OAIG;IACH,sBAAsB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5C;;;;OAIG;IACH,MAAM,EAAE,WAAW,CAAC;IACpB;;;;OAIG;IACH,aAAa,EAAE,oBAAoB,CAAC;IACpC;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC;;;;OAIG;IACH,sBAAsB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5C;;;OAGG;IACH,oBAAoB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1C;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC;;;;;;;OAOG;IACH,oBAAoB,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC;IACnE;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;IACjD;;;OAGG;IACH,qBAAqB,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;IACtD;;;;;;;OAOG;IACH,uBAAuB,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;IACxD;;;;OAIG;IACH,6BAA6B,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;IAC9D;;;;;OAKG;IACH,yBAAyB,CAAC,EAAE,yBAAyB,GAAG,SAAS,CAAC;IAClE;;;OAGG;IACH,iBAAiB,CAAC,EAAE,iBAAiB,GAAG,SAAS,CAAC;IAClD;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CAC1C;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,sBAAsB,GAAG,MAAM,CAkC7E"}
1
+ {"version":3,"file":"launchCommand.d.ts","sourceRoot":"","sources":["../../src/lib/launchCommand.ts"],"names":[],"mappings":"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"}
@@ -98,6 +98,19 @@ function prepareWorktreeWithStatusReporting(prepareWorktreeCommand) {
98
98
  'if [ "$prepare_status" -ne 0 ]; then echo "groundcrew prepareWorktree hook exited with status $prepare_status; continuing to agent." >&2; fi',
99
99
  ].join("; ");
100
100
  }
101
+ /**
102
+ * Host-shell line for the operator-only `unsandboxedHooks.prepareWorktree` command.
103
+ * Reuses the same status-reporting wrapper as the sandboxed hook so a failure is
104
+ * surfaced but non-fatal. `scrubNames` (the agent's `preLaunchEnv`) are `unset`
105
+ * inside the subshell so this trusted host command cannot read agent
106
+ * credentials minted by `preLaunch`. Build secrets are intentionally left in
107
+ * scope so `npm`/`bundle` can authenticate; the caller `unset`s them before the
108
+ * agent wrap.
109
+ */
110
+ function hostPrepareWorktreeLine(command, scrubNames) {
111
+ const scrub = scrubNames.length === 0 ? "" : `${unsetEnvironmentLine(scrubNames)}; `;
112
+ return `{ ${prepareWorktreeWithStatusReporting(`${scrub}${command}`)}; }`;
113
+ }
101
114
  /**
102
115
  * Source a `KEY='value'` file with auto-export so build-time secrets land
103
116
  * in the shell env before prepareWorktree runs. The `-f` guard keeps it a
@@ -328,6 +341,9 @@ export function buildLaunchCommand(arguments_) {
328
341
  if (hasPreLaunchEnv(arguments_.definition)) {
329
342
  throw new Error("preLaunchEnv is not yet supported for runner='sdx'. Set local.runner to 'safehouse' or 'none', or open an issue for sdx support.");
330
343
  }
344
+ if (arguments_.prepareWorktreeUnsandboxedCommand !== undefined) {
345
+ throw new Error("unsandboxedHooks.prepareWorktree is not supported for runner='sdx': the sdx container has no host to run it on. Remove unsandboxedHooks for this repo, or use runner 'safehouse', 'srt', or 'none'.");
346
+ }
331
347
  return buildSdxLaunchCommand(arguments_);
332
348
  }
333
349
  if (shouldWrapWithSafehouse(arguments_)) {
@@ -374,6 +390,9 @@ function buildUnwrappedHostLaunchCommand(arguments_) {
374
390
  ...hostTrapAndCd({ workingDir: arguments_.workingDir, promptDir }),
375
391
  ...hostSourceSecrets(arguments_.secretsFile),
376
392
  ];
393
+ if (arguments_.prepareWorktreeUnsandboxedCommand !== undefined) {
394
+ lines.push(hostPrepareWorktreeLine(arguments_.prepareWorktreeUnsandboxedCommand, arguments_.definition.preLaunchEnv ?? []));
395
+ }
377
396
  if (arguments_.prepareWorktreeCommand !== undefined) {
378
397
  lines.push(prepareWorktreeWithStatusReporting(arguments_.prepareWorktreeCommand));
379
398
  }
@@ -477,6 +496,9 @@ function buildSafehouseLaunchCommand(arguments_) {
477
496
  promptDir,
478
497
  secretsFile: arguments_.secretsFile,
479
498
  }));
499
+ if (arguments_.prepareWorktreeUnsandboxedCommand !== undefined) {
500
+ lines.push(hostPrepareWorktreeLine(arguments_.prepareWorktreeUnsandboxedCommand, arguments_.definition.preLaunchEnv ?? []));
501
+ }
480
502
  if (prepareWorktreeCommand !== undefined) {
481
503
  lines.push(`${safehouseWrapper} ${safehouseAddDirsFlag}${prepareWorktreeEnvPassFlag}sh -c ${shellSingleQuote(prepareWorktreeCommand)}`);
482
504
  }
@@ -604,6 +626,9 @@ function buildSrtLaunchCommand(arguments_) {
604
626
  secretsFile: arguments_.secretsFile,
605
627
  }),
606
628
  ];
629
+ if (arguments_.prepareWorktreeUnsandboxedCommand !== undefined) {
630
+ lines.push(hostPrepareWorktreeLine(arguments_.prepareWorktreeUnsandboxedCommand, arguments_.definition.preLaunchEnv ?? []));
631
+ }
607
632
  if (prepareWorktreeCommand !== undefined) {
608
633
  lines.push(`${prepareWrap} sh -c ${shellSingleQuote(prepareWorktreeCommand)}`);
609
634
  }
@@ -39,10 +39,23 @@ function normalizeRepositoryConfig(value) {
39
39
  if (value["version"] !== 1) {
40
40
  fail("version must be 1");
41
41
  }
42
+ rejectUnsandboxedField(value);
42
43
  return {
43
44
  hooks: normalizeHookCommands(value["hooks"]),
44
45
  };
45
46
  }
47
+ // `unsandboxedHooks` grants host execution, which is operator-only. A
48
+ // repo-committed config must never be able to set it — top-level or nested under
49
+ // `hooks` — so fail closed with guidance to move it to crew.config.ts. Checked
50
+ // before `normalizeHookCommands`, which would otherwise silently drop the nested
51
+ // form.
52
+ function rejectUnsandboxedField(value) {
53
+ const hooks = value["hooks"];
54
+ const nestedHasField = isPlainObject(hooks) && "unsandboxedHooks" in hooks;
55
+ if ("unsandboxedHooks" in value || nestedHasField) {
56
+ fail("unsandboxedHooks is operator-only and cannot be set in a repository config. Move it to crew.config.ts.");
57
+ }
58
+ }
46
59
  function normalizeHookCommands(value) {
47
60
  if (value === undefined) {
48
61
  return {};
@@ -32,7 +32,7 @@ Set them in the shell you run `crew` from. Anything not in this list is ignored.
32
32
 
33
33
  For each task:
34
34
 
35
- 1. If a `prepareWorktree` hook is configured and any recognized var is set and non-empty, groundcrew writes `secrets.env` with mode `0600` into the task's temp prompt dir as `KEY='value'` lines.
35
+ 1. If a `prepareWorktree` or `unsandboxedHooks.prepareWorktree` hook is configured and any recognized var is set and non-empty, groundcrew writes `secrets.env` with mode `0600` into the task's temp prompt dir as `KEY='value'` lines.
36
36
  2. The launch script sources `secrets.env` with `set -a` so the values are exported into the `prepareWorktree` phase only. Under `sdx`, they are forwarded into the sandbox via `-e KEY` flags.
37
37
  3. After `prepareWorktree` completes, the script removes every name in `BUILD_SECRET_NAMES` from the environment and removes the entire prompt dir before executing the agent.
38
38
 
@@ -44,6 +44,13 @@ Net effect: by the time the agent process exists, the values are gone from the e
44
44
 
45
45
  `preLaunch` runs a host-shell snippet outside Safehouse/sdx before the agent starts. Use it when the agent needs a short-lived credential that must be minted from something the sandbox cannot reach, such as an engineer CLI session in Keychain.
46
46
 
47
+ Build secrets are staged whenever either `prepareWorktree` or
48
+ `unsandboxedHooks.prepareWorktree` is configured. When
49
+ `unsandboxedHooks.prepareWorktree` is set, it runs on the host before
50
+ `prepareWorktree` with build secrets sourced; `preLaunchEnv` names are scrubbed
51
+ before it runs so the command cannot read agent credentials. After it completes,
52
+ execution continues with the normal sandboxed `prepareWorktree` phase.
53
+
47
54
  The "preLaunch never sees build secrets" contract is enforced differently per runner:
48
55
 
49
56
  - `runner: "safehouse"`: `preLaunch` runs immediately after `cd`, before `secrets.env` is sourced into the launch shell. `prepareWorktree` then runs inside its own profile-neutral `safehouse-clearance` wrap with `--env-pass=NPM_TOKEN,BUF_TOKEN`; build secrets are unset on the host before the agent's Safehouse wrap is executed.
package/docs/runners.md CHANGED
@@ -97,6 +97,10 @@ sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
97
97
 
98
98
  ## Docker Sandboxes Setup
99
99
 
100
+ `sdx` does not support `unsandboxedHooks`. The sdx container has no
101
+ host to run it on; configuring it for an `sdx`-runner repo is a launch-time
102
+ config error. Use `safehouse`, `srt`, or `none` if you need host-side setup.
103
+
100
104
  Each agent that runs under `sdx` needs a `sandbox: { agent: "<sbx-agent>" }` block in `crew.config.ts`. Groundcrew addresses the sandbox as `groundcrew-<agent>` and reuses one existing sandbox per agent across repos and tasks.
101
105
 
102
106
  First-time setup is manual:
@@ -94,6 +94,45 @@ This reuses the same `hooks` container and `prepareWorktree` contract as the
94
94
  other two layers. It beats `defaults.hooks` but still yields to a committed
95
95
  `.groundcrew/config.json` in that repo.
96
96
 
97
+ ## `unsandboxedHooks` (operator-only host setup)
98
+
99
+ Some setup cannot run in the sandbox — it needs host toolchains, host network
100
+ posture, or writes outside the worktree. For those cases an operator may grant a
101
+ **per-repository** host hook. It mirrors the `hooks` container's
102
+ `prepareWorktree` phase, on the opposite side of the sandbox boundary:
103
+
104
+ ```ts
105
+ // crew.config.ts (operator only)
106
+ knownRepositories: [
107
+ {
108
+ name: "catalog-admin",
109
+ hooks: { prepareWorktree: "npm ci" }, // sandboxed
110
+ unsandboxedHooks: { prepareWorktree: "bin/setup" }, // HOST, explicit opt-in
111
+ },
112
+ ];
113
+ ```
114
+
115
+ When both are set, `unsandboxedHooks.prepareWorktree` runs first on the host,
116
+ then `hooks.prepareWorktree` runs sandboxed, then the agent starts.
117
+
118
+ **The trust granted.** `unsandboxedHooks.prepareWorktree` runs on the host shell
119
+ outside any sandbox, with the operator's full host authority, against
120
+ repo-controlled code (lifecycle scripts, `Gemfile`, the repo's own `bin/setup`).
121
+ Grant it only to repositories you trust to run arbitrary code on the host.
122
+
123
+ **Constraints.**
124
+
125
+ - **Operator-only.** It is honored only from `crew.config.ts`. Setting
126
+ `unsandboxedHooks` in a repo-committed `.groundcrew/config.json`
127
+ (top-level or under `hooks`) is a hard config error.
128
+ - **Per-repository only.** There is no `defaults.unsandboxedHooks`;
129
+ host execution is never a fleet-wide default.
130
+ - **Runner support.** Runs on the host for `safehouse`, `srt`, and `none`. The
131
+ `sdx` runner rejects it at launch — a container has no host to run it on.
132
+ - **Credentials.** Build secrets are available so `npm`/`bundle` can
133
+ authenticate; the agent's `preLaunchEnv` names are scrubbed so the command
134
+ cannot read agent credentials.
135
+
97
136
  ## Examples
98
137
 
99
138
  Python with uv:
@@ -0,0 +1,164 @@
1
+ # E2E scenario catalog (DEVOP-5974)
2
+
3
+ The executable half of the groundcrew v2 spec: the black-box scenarios the acceptance suite must cover, plus the harness shape that makes them hermetic. The suite is written **before v2 code exists** and brings v2 up red→green; it is v2's system-level TDD loop and the language-agnostic escape hatch (any implementation that passes is conformant).
4
+
5
+ **Iteration 1 changes:** the green-on-v1 gate is dropped — v2 breaks too much of v1's surface for v1 scenarios to pay for themselves; that budget moves to migration easing (map fog item). Suite trustworthiness comes from harness self-tests instead (§1.5). The v1 reviewer loop (poll `gh`, advance in-review/done) does not survive into v2: the completion model (§2) is agent-reported, forge-blind, and lingers for human review.
6
+
7
+ ## 1. Suite architecture
8
+
9
+ ### 1.1 Black-box rules
10
+
11
+ - The suite spawns the `crew` binary and observes the world. It never imports groundcrew code, reads groundcrew internals, or mocks in-process.
12
+ - Assertions target the **observation surface** (§1.2), not stdout prose. Stdout/exit codes are asserted only where the output _is_ the behavior (`status`, `doctor`, loud errors), and then loosely (key substrings, not full-text golden files).
13
+
14
+ ### 1.2 Observation surface
15
+
16
+ Everything a scenario may assert on:
17
+
18
+ | Channel | How observed |
19
+ | ---------------------- | ---------------------------------------------------------------------------------------------------------------- |
20
+ | Exit codes | direct |
21
+ | Worktrees & branches | `git worktree list`, `git branch`, `git log`/`status` in the scenario tmpdir |
22
+ | Run/task state on disk | v2's state files (path + schema fixed by the spec; suite parses them read-only) |
23
+ | Terminal sessions | real tmux on an isolated socket (`tmux -L <scenario-id>`) — session exists / alive / exited |
24
+ | Source interactions | the fixture source's **call journal** (§1.4): every command invocation with args + stdin, appended as JSON lines |
25
+ | Structured logs | JSON-line log file under the scenario's state dir |
26
+ | Human-facing output | `status` / `doctor` stdout, loose match |
27
+
28
+ ### 1.3 Command binding
29
+
30
+ Scenarios call abstract operations bound in exactly one harness module. The CLI surface ([DEVOP-5975](https://linear.app/clipboardhealth/issue/DEVOP-5975)) has since landed the concrete names — `start` (merged run+start, `--watch`/`--force`), `pause`/`resume`, `cleanup`, `status`, `doctor`, and in-session `repo add` / `artifact add` / `done` — so the binding is now mechanical:
31
+
32
+ ```text
33
+ configure(fixture) // write config into the scenario tmpdir
34
+ seedSource(tasks[]) // load the fixture source's task store
35
+ tick() // one poll/dispatch cycle
36
+ start(taskId, {force?}) // dispatch that task; force bypasses eligibility, never the repo-on-disk gate
37
+ pause(taskId) / resume(taskId) / cleanup(taskId, {force}) / status(taskId?) / doctor()
38
+ killOrchestrator() / restart() // crash scenarios
39
+ paths: { worktreeFor(repo, task), workspaceFor(task), stateFor(task), logFile }
40
+ expect: { branchFor(task), sessionFor(task) }
41
+ ```
42
+
43
+ ### 1.4 Fakes
44
+
45
+ | Real thing | Stand-in | Notes |
46
+ | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
47
+ | Linear / Jira / any tracker | **Fixture source**: a protocol-1 manifest bundle backed by a JSON task store in the scenario tmpdir. Every invocation appends `{command, args, stdin, timestamp}` to `calls.jsonl` — the writeback assertion point. | First-party sources are conformance tests of the boundary (DEVOP-5973); the fixture source tests the same boundary from the other side. |
48
+ | Agent CLI (claude/codex/…) | **Scripted agent**: an executable that reads a per-scenario script (env-pointed file) and deterministically acts — sleep, write files, `git commit`, invoke in-session `crew` commands, report artifacts, exit 0/1. Agent profiles are declarative config with `cmd`, so this is honest. | Emits a heartbeat file so scenarios synchronize on "agent is running" without sleeps. |
49
+ | GitHub / forge | **Fake `gh` on PATH that exists only to record calls.** v2 core is forge-blind; any core-process invocation is a failure (FLOW-06). Agent-initiated calls are scripted and allowed. | The absence of a real forge fake is itself a spec assertion. |
50
+ | Git remotes | Local bare repos (`file://` remotes) under the scenario tmpdir; `origin/main` seeded with a few commits. | Real git throughout. |
51
+ | tmux | Real tmux, isolated per scenario via `-L <socket>`. | Real seam, cheap, deterministic enough. |
52
+ | Host env | Per-scenario tmpdir owning `HOME`, `XDG_CONFIG_HOME`, `XDG_STATE_HOME`; PATH prepended with the fakes dir; no network in the core lane. | |
53
+
54
+ ### 1.5 Harness self-tests
55
+
56
+ With no working implementation to validate against (green-on-v1 dropped), the harness proves itself directly: the fixture source, scripted agent, journal, and tmux/git observation helpers each get their own tests that run without any `crew` binary. A suite failure during v2 bring-up then points at v2, not at the suite.
57
+
58
+ ### 1.6 Lanes
59
+
60
+ - **Core lane**: hermetic, no sandbox runner, runs anywhere including CI.
61
+ - **Sandbox lane**: real srt (macOS `sandbox-exec` / Linux bubblewrap), platform-gated; asserts denial behavior that can't be faked honestly. v2-only by definition — v1's posture keeps its existing unit-level coverage.
62
+
63
+ ### 1.7 Suite packaging
64
+
65
+ An `e2e/` package in the groundcrew repo, plain TypeScript + vitest, spawning the built binary; fixture source and scripted agent are committed executables.
66
+
67
+ ## 2. Completion model (codified from iteration 1)
68
+
69
+ - **Core never polls a destination for done-ness.** No forge calls, no tracker reads to infer task completion. `status` reports what groundcrew knows: **observed** workspace git facts (branches, commits, dirty state) and **agent-reported** events and artifacts. Whether the tracker moves to done is the source's business (its `update` handler) or tracker automation's — humans see the agent's report and judge for themselves.
70
+ - **`completed{outcome}` frees the dispatch slot immediately** — a finished task never blocks the queue.
71
+ - **The workspace lingers after completion, with two exits.** Worktrees, branches, and the final state record stay on disk until **either** a human runs `cleanup` **or** polling observes the _source_ reporting the task terminal (e.g. the tracker moved to Done after the PR merged) — v1's cleaner, kept as-is because it watches the source, not the forge. The review window holds in practice: the tracker rarely reaches done the instant the agent reports delivered. Auto-reap never removes a dirty worktree; it skips loudly and leaves it for a human.
72
+
73
+ ## 3. The catalog
74
+
75
+ Each entry: **ID · lane · Given** (source store, config, disk, scripts) · **When** (harness ops) · **Then** (observation-surface assertions). IDs are stable; the suite implements one test per ID.
76
+
77
+ ### A. Dispatch & happy paths
78
+
79
+ - **DISPATCH-01** (core) — _Single-repo happy path._ Given one todo task designating repo `alpha` and an agent profile, repo `alpha` cloned under the base dir. When `tick()`. Then: worktree exists at `paths.worktreeFor(alpha, task)` on branch `expect.branchFor(task)` cut from `origin/main`; tmux session alive; state records running; journal shows `update:claimed` acknowledged.
80
+ - **DISPATCH-02** (core) — _Slot limit respected._ Given max-in-progress 1 and two eligible tasks. When `tick()`. Then exactly one provisions; the other stays queued (no worktree, no session, no writeback).
81
+ - **DISPATCH-03** (core) — _Priority ordering._ Given two eligible tasks with different priorities and one slot. Then the higher-priority task is the one provisioned.
82
+ - **DISPATCH-04** (core) — _Blocked task skipped, dispatched when unblocked._ Given a task with an open blocker. When `tick()`: nothing provisions. When the fixture marks the blocker done and `tick()` again: it provisions.
83
+ - **DISPATCH-05** (core) — _Ineligible task ignored._ Given a task with no agent routing. When `tick()`. Then no provisioning, no writeback, task still listed as queued.
84
+ - **DISPATCH-06** (core) — _Designated repo not on disk → bail._ Given a task designating a repo not cloned under the base dir. When `tick()`. Then no worktree/branch/session/state; the task stays queued at the source; the skip reason is visible (log line + status). (DEVOP-5967 §2.)
85
+ - **DISPATCH-07** (core) — _Manual start bypasses eligibility, never the repo gate._ Given an unlabeled/no-agent task. When `start(task)` (`crew start <task> --force`). Then it provisions exactly as DISPATCH-01. Variant: the same forced start on a task designating a repo not on disk still bails per DISPATCH-06 — `--force` cannot override the repo-on-disk gate.
86
+ - **DISPATCH-08** (core) — _Branch reuse._ Given a prior local branch `expect.branchFor(task)` with a commit. When dispatched. Then the worktree re-attaches to that branch (prior commit present), not a fresh one.
87
+ - **DISPATCH-09** (core) — _Recurrence is a source concern._ Given a fixture source that re-lists a completed task per its recurrence rule. When the task completes and `tick()` runs after re-listing. Then a fresh dispatch occurs; core has no recurrence machinery to observe. (DEVOP-5968 §6.)
88
+
89
+ ### B. Completion & writeback
90
+
91
+ - **COMPLETE-01** (core) — _Publishing work is reporting it._ Scripted agent commits and runs `crew artifact add <pr-url> --kind pr`. Then: the reported artifact appears in the run record and rides the `completed` writeback; status shows both layers (commits _observed_, PR _reported_); core made no forge call.
92
+ - **COMPLETE-02** (core) — _Delivered: slot freed, workspace lingers._ Scripted agent ends `completed {outcome: delivered, artifacts}`. Then: journal carries the completed event exactly once with artifacts intact; state is `complete{delivered}`; session ended; **worktree, branch, and state record still on disk**; next `tick()` dispatches the next queued task (slot freed) while the delivered workspace lingers.
93
+ - **COMPLETE-03** (core) — _Launch failure is truthful and rolled back._ Given an agent profile whose `cmd` doesn't exist. When `start(task)`. Then state records `complete{failed, reason: launch}`; worktree and branch rolled back; the failure appears in the journal if the task was already claimed.
94
+ - **COMPLETE-04** (core) — _Agent failure is truth-told._ Scripted agent ends `completed {outcome: failed, message}`. Then journal carries the failure event with the message; state is `complete{failed}`; no artifact records invented; workspace lingers for inspection.
95
+ - **COMPLETE-05** (core) — _Read-only source end-to-end._ Given a fixture manifest with no `update` command. Then dispatch, run, and completion all succeed; journal shows zero update calls; status labels the source read-only. (Also asserted at discovery level in PLUGIN-05.)
96
+ - **COMPLETE-08** (core) — _`crew done` honors the dirty-worktree guard._ Scripted agent leaves uncommitted changes and runs `crew done --outcome delivered`: refused with a nonzero exit naming the dirt; task stays running; no completed event. Rerun with `--allow-dirty`: completes normally.
97
+ - **COMPLETE-06** (core) — _Manual cleanup ends the linger._ Given a delivered task's lingering workspace. When `cleanup(task)`. Then worktree removed, branch deleted, state record gone; the source hears nothing new.
98
+ - **COMPLETE-07** (core) — _Source-terminal auto-reap (v1 cleaner parity)._ Given a delivered task's lingering clean workspace. When the fixture source's store marks the task done and `tick()` runs. Then worktree, branch, and state record reaped automatically; the reap is logged. Variant: if the lingering worktree is dirty, auto-reap skips it with a visible warning and everything stays on disk.
99
+
100
+ ### C. Session lifecycle
101
+
102
+ - **SESSION-01** (core) — _Pause keeps work._ Given a running task. When `pause(task)`. Then session closed, worktree and branch intact, state paused, resumable.
103
+ - **SESSION-02** (core) — _Resume reopens, never recreates._ When `resume(task)` on a paused task: session live again, same worktree, resume count incremented, state running. When `resume` on a task with no worktree: hard error, nothing created.
104
+ - **SESSION-03** (core) — _Status tells the truth about strays._ Given state says running but the tmux session was killed externally. Then `status` flags the disagreement (stray/dead). Given no state but a live session matching a task: `status` flags the stray session.
105
+
106
+ ### D. Crash & recovery
107
+
108
+ - **CRASH-01** (core) — _SIGKILL mid-run, restart reconciles._ Given a running task. When the orchestrator is SIGKILLed and restarted with `tick()`. Then no duplicate worktree/session/dispatch; state agrees with disk; the running task is still running. (Reconcile-on-startup, DEVOP-5972.)
109
+ - **CRASH-02** (core) — _Orphan worktree directory._ Given a directory at the expected worktree path but absent from `git worktree list`. Then `cleanup --force` removes it only when the path exactly matches the expected shape; a non-matching path is refused.
110
+ - **CRASH-03** (core) — _Dirty worktree is a data-loss guard._ Given a worktree with uncommitted changes. When `cleanup(task)`: refused, names the dirt. When `cleanup(task, {force})`: removed.
111
+ - **CRASH-04** (core) — _Stale state, no disk._ Given a state file for a task with no worktree and no session. Then cleanup clears it; if the session probe is unavailable, everything is left intact.
112
+ - **CRASH-05** (core) — _Reconcile GCs the full triple._ After SIGKILL during provisioning (worktree half-created, session never launched), restart reconciles worktrees **and** tmux **and** sandbox state against expected task state; orphans of each kind GC'd; a log line records each GC action.
113
+
114
+ ### E. Multi-repo & workspace (DEVOP-5967)
115
+
116
+ - **MULTI-01** (core) — _Designated multi-repo._ Given `Repos: alpha, beta`, both cloned. Then one workspace directory with two worktrees side by side, the **same** task branch in each, **one** tmux session at the workspace root.
117
+ - **MULTI-02** (core) — _Designated-and-missing → bail._ Given `Repos: alpha, gamma`, `gamma` not cloned. Then nothing provisions (not even `alpha`), visible "repo not found" skip, task stays queued.
118
+ - **MULTI-03** (core) — _Empty-workspace dispatch._ Given a task with no repo designation. Then the session launches in an empty workspace; zero worktrees; state running.
119
+ - **MULTI-04** (core) — _Runtime acquisition, allowed._ Scripted agent runs `crew repo add alpha` in-session (task identity via `$GROUNDCREW_WORKSPACE`). Then a worktree for `alpha` appears under the workspace on the uniform task branch; the addition is recorded in task state; prepare-worktree hook ran. Variants exercise the other two identity paths: explicit `--task`, and env unset with cwd inside the workspace (walk-up to `.groundcrew/task.json`).
120
+ - **MULTI-05** (core) — _Runtime acquisition, gate-rejected._ Agent runs `crew repo add not-cloned`. Then a loud error, **exit 2** (repo not cloned under the base directory), nothing created; task keeps running.
121
+ - **MULTI-08** (core) — _In-session command without task context._ `crew repo add alpha` run outside any workspace, with no `--task` and no `$GROUNDCREW_WORKSPACE`. Then **exit 3** (no task context), nothing created, no journal entry.
122
+ - **MULTI-06** (core) — _Partial completion truth-telling._ Agent commits in `alpha` and `beta`, reports a pr artifact for `alpha` only, completes `{outcome: failed}`. Then per-repo records show exactly that (alpha: artifact reported + commits observed; beta: commits observed, nothing reported); no invented atomicity, no rollback.
123
+ - **MULTI-07** (core) — _Repo-less delivery._ Agent in an empty workspace reports `{kind: document, locator: <url>}` and completes delivered. Then the completed event carries the artifact; no git facts exist or are claimed.
124
+
125
+ ### F. Artifacts & source contract (DEVOP-5968)
126
+
127
+ - **FLOW-01** (core) — _Reported vs observed are separate layers._ Agent commits but reports nothing. Then status shows commits _observed_ and no artifact _reported_ — a missing link, never a lie.
128
+ - **FLOW-02** (core) — _Mixed artifact kinds round-trip._ Agent reports `pr`, `ticket`, and `file` artifacts. Then `completed.artifacts` in the journal carries all three, kinds and locators intact.
129
+ - **FLOW-03** (core) — _Claim rejected._ Given the fixture source answers `claimed` with _rejected_. Then no provisioning, no session; task remains listed; the rejection is visible in log/status. (The remote-runner door.)
130
+ - **FLOW-04** (core, **deferred post-v2.0**) — _Progress events flow._ Requires an agent-facing progress emitter, and v2.0 ships none: `crew progress` is a documented seam, not built (DEVOP-5982 §6). The `progress` event stays in protocol 1; this ID is reserved and the scenario activates when the emitter ships.
131
+ - **FLOW-05** (core) — _Done-ness is not core's business._ After COMPLETE-02, the source's task store still says whatever its own `update` handler chose; core issues no reads to confirm or reconcile it, and status renders the agent's report unchanged.
132
+ - **FLOW-06** (core) — _Core is forge-blind._ Across MULTI/FLOW scenarios, the fake `gh` (and any recorded network attempt in the core lane) shows **zero** invocations by the core process. Agent-initiated calls don't count.
133
+
134
+ ### G. Source packaging & protocol (DEVOP-5973)
135
+
136
+ - **PLUGIN-01** (core) — _User-dir source discovery._ Given a bundle at `~/.config/groundcrew/task-sources/fixture/` (`source.json` + scripts). Then it lists in `source list`, verifies, and serves dispatch.
137
+ - **PLUGIN-02** (core) — _Name collision: user dir wins._ Given a package-shipped bundle and a user-dir bundle with the same name. Then the user's is used; the override is visible in `source list`/doctor.
138
+ - **PLUGIN-03** (core) — _Protocol mismatch is loud._ Given `protocolVersion: 99`. Then discovery/doctor/status emit an explicit, actionable error naming the version and the supported set; the source is not silently skipped; other sources unaffected.
139
+ - **PLUGIN-04** (core) — _Unparseable manifest: skip + warn._ Given malformed `source.json`. Then a warning names the file; everything else proceeds.
140
+ - **PLUGIN-05** (core) — _Capability by omission._ Given a manifest omitting `update`. Then no version sniffing, no error: the source is treated read-only (COMPLETE-05's mechanism, asserted at discovery level).
141
+
142
+ ### H. Sandbox posture (sandbox lane, platform-gated)
143
+
144
+ - **SANDBOX-01** (sandbox) — _Agent contained by default._ Scripted agent writes inside the worktree (succeeds) and outside `HOME`-scoped allowed paths (fails). Both outcomes observed via marker files.
145
+ - **SANDBOX-02** (sandbox) — _Agent network egress allowlisted._ Agent curls an allowlisted host (loopback fixture; succeeds) and a non-allowlisted one (fails).
146
+ - **SANDBOX-03** (sandbox) — _Sources sandboxed by default._ Fixture source's `list` script attempts undeclared egress and an out-of-scope write → both denied; declared scratch dir + install dir (read) work. (Manifest `network` allowlist, DEVOP-5973 §4.)
147
+ - **SANDBOX-04** (sandbox) — _`sandbox: false` opt-out is loud._ Given the fixture source configured with `sandbox: false`. Then it runs unsandboxed (undeclared write succeeds) **and** status/doctor visibly flag the opt-out.
148
+
149
+ ### I. Surface & diagnostics
150
+
151
+ - **SURFACE-01** (core) — _Init → doctor green._ On a healthy fixture host, the init flow produces a config that `doctor` passes and DISPATCH-01 runs against unmodified.
152
+ - **SURFACE-02** (core) — _Doctor catches broken hosts._ Missing agent binary; unreachable/failing source verify; missing base dir — each produces a failing doctor with the cause named, exit 1.
153
+ - **SURFACE-03** (core) — _Status degrades gracefully._ Given the fixture source errors on list. Then `status` still prints local truth (worktrees, sessions, state) and marks the queue unavailable with the reason; exit 0.
154
+ - **SURFACE-04** (core) — _Structured logs are parseable._ After DISPATCH-01 + COMPLETE-02, every line of the log file parses as JSON and carries the task id on task-scoped events. (Deeper schema assertions belong to the observability design — fog.)
155
+ - **SURFACE-05** (core) — _`init --yes` is non-interactive and sufficient._ On a fixture host with an agent CLI on PATH. When `crew init --yes`. Then a `crew.config.jsonc` exists that `doctor` passes and DISPATCH-01 runs against unmodified — no prompts, exit 0.
156
+ - **SURFACE-06** (core) — _v1 config conversion._ Given a v1 `crew.config.ts` (with `knownRepositories`, a `shell` source block, and a safehouse runner setting). When `crew init`. Then a converted `crew.config.jsonc` is written and every dropped or renamed key is named in the output; `doctor` passes afterward.
157
+ - **SURFACE-07** (core) — _`source doctor` live round-trip._ Given the fixture source installed. When `crew source doctor`. Then it exercises the source's verify/read probe end-to-end (journal shows the probe call) and reports healthy; with the fixture's script made to fail, it reports the failure with the source named, exit 1.
158
+
159
+ ## 4. Iteration log
160
+
161
+ - **Iteration 1** (2026-07-16): green-on-v1 gate dropped in favor of harness self-tests plus a migration-easing budget; completion model codified (forge-blind, agent-reported, linger-until-cleanup); suite stack confirmed (`e2e/` package, TS + vitest); sandbox lane confirmed v2-only. Tier A/B structure and the v1 driver removed accordingly.
162
+ - **Iteration 2** (2026-07-16): linger gets two exits — manual `cleanup` or source-terminal auto-reap (v1's cleaner kept: it watches the source, never the forge; v1's merged-PR reviewer path dies). COMPLETE-07 added; dirty worktrees are never auto-reaped.
163
+ - **Iteration 3** (2026-07-16): absorbed the CLI surface resolution's handoff (DEVOP-5975, resolved concurrently): `workspace add` → `repo add`; completion via `crew done`; task-identity resolution order with exit codes 2/3 (MULTI-04/05/08); `--force` never overrides the repo gate (DISPATCH-07); `done` dirty-guard (COMPLETE-08); `init --yes`, v1-config conversion, and `source doctor` round-trip (SURFACE-05/06/07).
164
+ - **Iteration 4** (2026-07-17): review pass on the design-doc assembly PR: "draft prototype" label dropped (the catalog is ratified); harness binding renamed `stop` → `pause` and `start` gains `{force?}` (SESSION-01, DISPATCH-07); progress assertions removed from COMPLETE-01 and FLOW-04 deferred post-v2.0 — v2.0 ships no agent-facing progress emitter (DEVOP-5982 §6).
@@ -0,0 +1,353 @@
1
+ # Groundcrew v2 design
2
+
3
+ The implementation handoff for groundcrew v2, assembled from the [Groundcrew v2 spec wayfinder map](https://linear.app/clipboardhealth/issue/DEVOP-5966) (DEVOP-5966). Every decision below was resolved on that map; the [decision record](#13-decision-record) links each section to its ticket, where the full resolution and its rationale live. Where resolutions conflicted, later amendments win and are marked.
4
+
5
+ This document and the [E2E scenario catalog](./e2e-scenario-catalog.md) together are the spec. **The first implementation task is building the acceptance suite red-first from the catalog — before any v2 code exists.** The suite brings v2 up red-to-green; harness self-tests carry the suite's trustworthiness (the green-on-v1 gate was dropped: v2 breaks too much of v1's surface for v1 scenarios to pay for themselves).
6
+
7
+ ## 1. Destination and locked constraints
8
+
9
+ Groundcrew dispatches task backlogs to local, interactive AI coding agents — one workspace per task, sandboxed by default. v2 is a rewrite of that product with a smaller, sharper surface.
10
+
11
+ Locked constraints, not to be re-litigated during implementation:
12
+
13
+ - **TypeScript.** The language-agnostic escape hatch is the black-box acceptance suite (any implementation that passes is conformant), not a new language.
14
+ - **Free to break v1** — config, CLI, conventions — easing migration opportunistically (section 11).
15
+ - **Sandboxed and secure by default**; interactive sessions are the baseline.
16
+ - **Pluggable agent harnesses** with model and effort-level selection.
17
+ - **Structured logs; minimal CLI surface; sane defaults with minimal required config; install/setup script.**
18
+ - **E2E-first confidence**: the black-box acceptance suite exists red-first before v2 code starts.
19
+
20
+ ## 2. Verdict: rewrite
21
+
22
+ **Rewrite, not evolve.** ([Rewrite vs evolve](https://linear.app/clipboardhealth/issue/DEVOP-5978))
23
+
24
+ - The locked decisions kill or fundamentally reshape most of v1 (~21K source lines, ~38K test lines): `config.ts` dies with the TS-config format, the reviewer PR-polling loop dies, clearance/safehouse/sbx die, several commands die, and the core nouns are redefined. Every external surface breaks: config, CLI, source contract, sandbox posture, completion model.
25
+ - Evolve's safety-net premise was stale: the acceptance suite never runs against v1 (green-on-v1 dropped), so in-place evolution gets zero protection from it. "Evolve" would be the same rewrite executed as a thousand PRs fighting 38K test lines that assert dying behavior.
26
+ - Roughly 15–25% of v1 survives recognizably (srtPolicy/srtLaunch, the cleaner, tmux/git plumbing, doctor pieces, usage, completions). Surviving code is **ported file-by-file with its unit tests**; `git mv` preserves history.
27
+
28
+ **Repo strategy: same repo, isolated `v2/` workspace.** v2 grows on `main` under a top-level `v2/` with its own `package.json`, flat `src/<module>` layout, and its own `CLAUDE.md`/`AGENTS.md`/`CONTEXT.md` carrying the v2 ubiquitous language. v1 and v2 use the same nouns with contradictory meanings ("Workspace" means a terminal pane in v1 and a per-task directory of worktrees in v2), so sharing one `src/` was rejected — it would contaminate agent-driven work. dependency-cruiser forbids cross-tree imports in both directions. **Flip condition:** if v2-building agents keep tripping over v1 despite the boundary, escalate mid-build to a new repo initialized from a full clone, then rename old → `groundcrew-v1`, new → `groundcrew`.
29
+
30
+ **Package strategy.** Keep `@clipboard-health/groundcrew` and the `crew` bin; the release is **5.0.0** (v1 is at 4.47.x). No npm publish until the E2E suite is green — the team dogfoods from the repo. v1 goes **fixes-only** once v2 construction starts. Cutover: cut a `4.x` maintenance branch, flatten `v2/` to the repo root, delete the v1 tree; later v1 patches publish from `4.x` under a `v4` dist-tag.
31
+
32
+ **Release-automation wrinkle (must be handled before v2 commits land):** `main`'s Nx Release automation would treat v2 conventional commits as 4.x releases — it needs a path filter or commit-scope exclusion so v2 commits don't trigger 4.x publishes.
33
+
34
+ ## 3. Flow model
35
+
36
+ ([Flow model](https://linear.app/clipboardhealth/issue/DEVOP-5968), informed by the [prior-art research](https://linear.app/clipboardhealth/issue/DEVOP-5969): symmetric source/sink type systems are where Concourse and Tekton died, so the generalization is asymmetric.)
37
+
38
+ What exists: **sources, agent profiles, tasks, artifact records.** Two things deliberately do not exist:
39
+
40
+ - **Sinks dissolve.** Every task output happens through the agent's own tools (it opens PRs with `gh`, writes docs, files tickets — it has the credentials and the judgment) or the source's writeback. No typed sink stage; "produce a PR" is the default writeback pattern for repo work, not a privileged path.
41
+ - **Flows dissolve.** Config declares sources and agent profiles; each task carries its routing (an agent designation); a source-level default agent covers the common case. No pipeline/flow config object.
42
+
43
+ **Artifact record: plural, kind-tagged, agent-reported — no exceptions.** Per task: `{status, logs, artifacts: []}`, each artifact `{kind, locator, title?, repo?}` with an open kind set (`pr | branch | document | file | ticket | …`). All artifacts are reported by the agent via in-session `crew artifact add`. Groundcrew observes only what it owns — workspace-local git facts (branches, commits, dirty state), readable without credentials. **The core knows nothing about GitHub or any forge.** Status renders both layers ("3 commits _observed_; PR _reported_"); a forgotten report is a missing link, never a lie.
44
+
45
+ **Source contract** — `list`, `get`, `update`, capability by omission:
46
+
47
+ - `list` (poll for ready tasks) and `get` are required; a source without `update` is legal and read-only (writeback no-ops).
48
+ - `update(task, event)` is the single callback verb. Events: `claimed` (ack; may return _rejected_ where the source arbitrates contention — the remote-runner door, open but unbuilt), `progress` (note), `completed{outcome, artifacts, message}`. Errors are `outcome: failed`, not a separate channel.
49
+ - `create` is dropped; tasks are created in the tracker or file directly.
50
+
51
+ **Run states** (core-owned, recorded only for started work; the queue stays derived from source polls):
52
+
53
+ ```text
54
+ provisioning → running ⇄ paused → complete{outcome: delivered | failed | stopped}
55
+ ```
56
+
57
+ v1's `resumed` collapses into `running` (resumeCount is a field), `interrupted` → `paused`, `failed-to-launch` → `complete{failed, reason: launch}`. Bail (designated repo not on disk) never enters the machine — the task stays queued at the source with a visible skip reason. `input-required` is **not** a state: in interactive mode the terminal is the input surface; the extensible event set is the seam if headless needs it later.
58
+
59
+ **Recurring tasks are a source concern.** A source that re-lists a task on a schedule is indistinguishable from a human re-creating it; core ships no recurrence machinery.
60
+
61
+ ## 4. Task model: workspaces of worktrees
62
+
63
+ ([Multi-repo task model](https://linear.app/clipboardhealth/issue/DEVOP-5967))
64
+
65
+ **One agent session per task, over a workspace of worktrees.** Groundcrew provisions worktrees side by side under a single task workspace directory; one agent session launches at the workspace root and coordinates all repos itself. One terminal surface per task, N PRs out. The session can start before any worktree exists — an empty workspace is legal. The capable agent is the coordinator; there are no federated sessions or relay pipelines.
66
+
67
+ **Repo resolution — two modes, one mechanism:**
68
+
69
+ - **Designated:** the task carries a `Repos:` designation. Groundcrew resolves each name against what is actually cloned under the configured base directory and provisions those worktrees up front. Any designated repo not found on disk → **bail**: skip the task with a visible "repo not found" status, provision nothing (not even the repos that do exist).
70
+ - **Not designated:** dispatch anyway, into an empty workspace. The ticket's prose decides what happens: the agent discovers repos and acquires worktrees at runtime, or never touches a repo at all. Repo-less tasks are legal.
71
+ - There is no `Repos: discover` marker, and v1's prose-inference (scanning descriptions for repo mentions) is dropped: explicit designation or nothing.
72
+
73
+ **`knownRepositories` dies.** The repo universe is the disk under the base directory. Groundcrew never clones — provisioning is always worktree-from-local-clone. (The agent may clone repos itself if the sandbox policy allows; that is the sandbox's jurisdiction.) Accepted tradeoff, recorded honestly: the gate widens from "repos listed in config" to "anything cloned under the base directory" — mitigated by sandboxing; an optional allowlist can be layered on later without redesign.
74
+
75
+ **Runtime acquisition:** the agent runs `crew repo add <repo>` in-session. The CLI applies the disk-presence gate, creates the worktree from the local clone on the uniform task branch, runs the repo's prepare-worktree hook, and records the addition in task state. Provisioning stays groundcrew's job with all guarantees intact — only the trigger moves from dispatch-time to agent-runtime.
76
+
77
+ **Branch/PR topology:** a uniform task-derived branch name across every acquired worktree — the correlation key that keeps status/resume/cleanup trivial; one PR per repo as the default writeback, cross-linked to each other and the task; one artifact record per repo. A repo that needs no change simply produces no PR.
78
+
79
+ **Partial failure: groundcrew does nothing but tell the truth.** Cross-repo atomicity is an explicit non-goal. Per-repo artifact records show exactly how far each repo got; merge outcomes are human/CI territory; recovery is a human action. The uniform branch plus cross-linked PRs guarantee the full blast radius is reconstructible from any one artifact.
80
+
81
+ Single-repo tasks stay dead simple as the degenerate case: one designated repo, one worktree, one branch, one PR — exactly v1's shape.
82
+
83
+ ## 5. Completion model
84
+
85
+ (Codified in the [E2E scenario catalog](https://linear.app/clipboardhealth/issue/DEVOP-5974); full statement in [the catalog, section 2](./e2e-scenario-catalog.md#2-completion-model-codified-from-iteration-1).)
86
+
87
+ - **Core never polls a destination for done-ness.** No forge calls, no tracker reads to infer completion. v1's reviewer (merged-PR polling via `gh`) dies.
88
+ - Status = **observed** workspace git facts plus **agent-reported** events and artifacts; humans judge from the agent's report.
89
+ - `completed{outcome}` **frees the dispatch slot immediately** — a finished task never blocks the queue.
90
+ - **The workspace lingers after completion, with two exits:** a human runs `cleanup`, or polling observes the _source_ reporting the task terminal — v1's cleaner survives unchanged because it watches the source, never the forge (this is how Linear cleanup actually worked in v1: Linear's GitHub integration moves the issue; the poll observes it). Dirty worktrees are never auto-reaped — loud skip, left for a human.
91
+
92
+ ## 6. Plugin boundary: task sources
93
+
94
+ ([Plugin packaging and review isolation](https://linear.app/clipboardhealth/issue/DEVOP-5973), informed by the [plugin-distribution research](https://linear.app/clipboardhealth/issue/DEVOP-5971): in-process npm plugins deliver no sandboxing and no real review isolation; the models that work — Terraform providers, MCP — use a versioned process boundary.)
95
+
96
+ **"Plugin" means exactly one thing in v2: a task source.** Everything else that looked pluggable is config or core:
97
+
98
+ - **Agent harnesses are declarative config**, not plugins: `cmd`, resume args, sandbox preferences, plus first-class `model`/`effort` fields. `cmd`-points-at-your-script is the escape hatch for odd agents.
99
+ - **Sandbox runners are core-only**, explicitly not pluggable — a pluggable sandbox is a contradiction under sandboxed-by-default. **v2 ships exactly one runner: srt** (`sandbox-exec` on macOS, bubblewrap on Linux). Safehouse, clearance, and sbx die, including the `crew-clearance-ensure` bin.
100
+ - **No in-process plugin class exists.** The internal `TaskSource` interface survives only as core's private representation of a discovered manifest.
101
+
102
+ **Packaging:** a plugin is a **directory bundle** — `source.json` plus scripts — discovered under `~/.config/groundcrew/task-sources/<name>/` (user) or bundled in the package (first-party). The directory is the plugin: language-agnostic, no runtime loading. Distribution is deliberately not groundcrew's problem (git clone, `curl | tar`, npm postinstall). No registry or catalog in v2.0; provenance tiers collapse to `package | user`. Review isolation is met structurally: third-party code never enters the core repo and never runs in the core process.
103
+
104
+ **Versioning:** `source.json` carries a required integer `protocolVersion: 1` naming the contract generation, bumped only on breaking change — capability-by-omission carries all additive change (no version sniffing: `update` absent means read-only). Parseable-but-unsupported version → explicit actionable error in discovery/doctor/status, never a silent skip; unparseable manifests keep skip-plus-warn. Core declares a supported set; v2.0 ships `{1}`. No v1 command aliases.
105
+
106
+ **Sources are sandboxed by default**, under the same srt machinery as agents: deny-by-default; a source gets its install dir (read), declared secret files, declared env, stdout/stderr, and a per-source scratch dir. The manifest gains a **network egress allowlist** (e.g. `network: ["api.linear.app"]`) alongside `secrets`/`env`/`prerequisites`. Uniform across origins — bundled first-party sources run under the same policy. Per-source opt-out (`sandbox: false`) is loud in status/doctor.
107
+
108
+ **Batteries included, kernel-pure code paths:** `linear`, `jira`, and `todo-txt` ship as manifest bundles crossing the same process boundary as third-party sources — first-party sources are the protocol's permanent conformance tests. The generic `shell` config-block adapter dies: bring-your-own-scripts is just a user-dir manifest.
109
+
110
+ ## 7. CLI surface and config
111
+
112
+ ([CLI surface](https://linear.app/clipboardhealth/issue/DEVOP-5975); prototype with rendered `--help` for every command on branch [`prototype/devop-5975-cli-surface`](https://github.com/ClipboardHealth/groundcrew/tree/prototype/devop-5975-cli-surface), `node --run proto:cli2 -- --tour`. Amended by [Headless extension point](https://linear.app/clipboardhealth/issue/DEVOP-5976): config field `multiplexer` → `presenter`.)
113
+
114
+ ### 7.1 Commands — 14 leaves (v1 had ~20)
115
+
116
+ | Group | Command | Notes |
117
+ | ---------- | ------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
118
+ | Operate | `start [task]` | Merged v1 `run`+`start`. No task = poll and dispatch eligible (`--watch` continuous); with task = dispatch that one (`--force` bypasses eligibility but never the repo-on-disk gate; `--agent` override). |
119
+ | Operate | `status [task]` | Absorbs `task list`/`get`. Renders **observed** git facts vs **reported** agent claims as separate layers; is the "why" affordance (section 10.4). |
120
+ | Operate | `pause` / `resume` | v1 `stop` renamed. `resume` reopens the _same conversation_ via captured `{{sessionId}}` where the harness exposes one (Claude Code and Codex do); falls back to `resumeArguments`; `--fresh` = new session. |
121
+ | Operate | `cleanup [task\|--all]` | An uncompleted task completes as `stopped`. |
122
+ | In-session | `repo add <repo>` | Runtime acquisition (section 4). Renamed from `workspace add` to dodge the cmux "workspace" collision. |
123
+ | In-session | `artifact add <locator> [--kind\|--title\|--repo]` | Agent-reported artifacts (section 3). |
124
+ | In-session | `done [--outcome delivered\|failed\|stopped] [--allow-dirty]` | Replaces `$GROUNDCREW_COMPLETE` — the prompt just says "run `crew done`". Dirty-worktree guard refuses with the dirt named unless `--allow-dirty`. |
125
+ | Sources | `source list` | Discovered source bundles with origin (`package`/`user`), protocol version, and user-dir override visibility (catalog PLUGIN-01/02). |
126
+ | Sources | `source doctor` | Live round-trip probe of each configured source, naming the failing source on exit 1 (catalog SURFACE-07). Absorbs v1 `source verify` and `task validate`. |
127
+ | Setup | `init` | Interactive 5-step: baseDirectory, agent detection, source pick, write config, run doctor. Detects a v1 `crew.config.ts` and converts it, printing every dropped/renamed key. Global by default; `--local`, `--yes`. |
128
+ | Setup | `doctor` / `upgrade` / `completions` | `doctor` is the health-check verb everywhere: `crew doctor`, `crew source doctor`. |
129
+
130
+ In-session commands are ordinary subcommands (no separate namespace). **Task identity resolution:** `--task` flag → `$GROUNDCREW_WORKSPACE` env (injected at launch) → walk up from cwd to `.groundcrew/task.json`. Exit codes: **2** = repo not cloned under `baseDirectory`, **3** = no task context.
131
+
132
+ **Killed:** `open`, `task create`, `task list/get/validate`, `source install`, `interrupt`, `sandbox`, `setup`, the `crew-clearance-ensure` bin (`source verify` is renamed into `source doctor`, not killed). No `progress` command in v2.0 (the protocol event exists; add a CLI if the need shows up). No `crew migrate` — `init`'s conversion is the migration aid.
133
+
134
+ **Verb families locked:** lifecycle `start → pause → resume → done`; health checks are `doctor`.
135
+
136
+ ### 7.2 Config — `crew.config.jsonc`
137
+
138
+ - **JSONC plus a published JSON Schema** (generated from zod, referenced via `$schema`). TS config dies: a global config can't resolve the package import for its types, so it was ceremony without safety. One filename, one location rule: `~/.config/groundcrew/`, or project-local.
139
+ - **Principle 1 — omitted = detected, specified = exactly yours, never merged.** `sources` omitted → `[{ "kind": "todo-txt" }]`; `agents` omitted → presets for CLIs on PATH (claude > codex > cursor); `presenter` omitted → first of cmux/tmux/zellij found. Listing anything replaces the detected set — disabling = not listing.
140
+ - **Principle 2 — no secrets, structurally.** No schema field accepts a token value; manifests declare secret _names_, resolved from the parent environment, a `secrets.env` (doctor warns unless 0600), or `op run`. `GROUNDCREW_LINEAR_API_KEY` dies; the linear bundle declares `LINEAR_API_KEY` like any source. Doctor flags credential-looking strings in config.
141
+ - Minimal legal config: `{ "workspace": { "baseDirectory": "~/dev" } }`. Worktrees default to `<baseDirectory>/.groundcrew/worktrees`. No abbreviations anywhere (`maximumInProgress`, `pollIntervalMilliseconds`, `sessionLimitPercentage`, `readOnlyDirectories`, `workingDirectory`).
142
+ - Agent profiles get first-class `model` / `effort` / `resume` fields; presets map them to CLI flags; `{{model}}` / `{{sessionId}}` placeholders for custom commands. Per-harness session-id capture mechanics are an implementation detail under this shape.
143
+
144
+ ### 7.3 Setup
145
+
146
+ `curl -fsSL …/install.sh | sh` → node ≥ 24 check, global npm install, exec `crew init` (`--yes` for CI/dotfiles). The script is a thin, inspectable wrapper over `npm install -g @clipboard-health/groundcrew`; that command is the direct path for anyone unwilling to pipe a remote script into a shell, and npm's registry integrity checks are the trust anchor either way. Deliberately manual forever: cloning repos, minting API keys, installing agent CLIs.
147
+
148
+ ## 8. Session presenter
149
+
150
+ ([Headless extension point](https://linear.app/clipboardhealth/issue/DEVOP-5976))
151
+
152
+ **The seam is a standalone core interface: the session presenter** — presentation-only, deciding where a local, already-sandbox-wrapped agent process runs and how a human reaches it. Remote execution is permanently outside this seam: a remote runner enters via the source-contract door (`claimed` → _rejected_ arbitration), never as a presenter.
153
+
154
+ **Contract — born serializable.** JSON in/out, nothing in-process, which outlaws v1's real leak (cmux `set-progress` hooks injected through the launch layer) by construction:
155
+
156
+ - `open(spec)` — spec = `{name, displayName?, cwd, command, status?}`; `command` arrives fully composed, sandbox wrap included. Nesting is **presenter → sandbox → agent**; the presenter never knows about sandboxing.
157
+ - `probe()` — live workspace list; "unavailable" is never treated as "empty".
158
+ - `close(name)`.
159
+ - `accessHint(name)` — how a human attaches, or nothing.
160
+ - `setStatus(name, {text, color?, icon?})` — **optional, capability by omission** (cmux implements; tmux/zellij omit). Driven by core's run-state machine, not agent-internal hooks. v1's cmux/Claude in-sandbox hook plumbing dies with no replacement.
161
+
162
+ **Packaging:** cmux/tmux/zellij ship in-core for v2.0; sources remain the only v2.0 plugin kind. Presenter _bundles_ are pre-committed as an additive v2.x kind — same manifest/process-boundary mechanics as sources but an explicitly different trust posture: presenters are inherently unsandboxable (their job is spawning the agent's pane on the host, launch command in hand), so a presenter bundle is **host-trusted like an agent CLI**, never sandbox-defaulted.
163
+
164
+ **Headless = a future "detached" presenter** implementing the same contract (`open` spawns directly, `probe` is process liveness, `accessHint` returns nothing, `setStatus` omitted). v2.0 ships no detached presenter — one of cmux/tmux/zellij on PATH stays required (doctor checks). **Subagent panes: same seam, deferred** — a future presenter verb plus an in-session command routed through core; nothing outside the presenter may ever talk to a multiplexer.
165
+
166
+ The fate of v1's tmux-shaped env toggles (`GROUNDCREW_TMUX_SESSION_PER_TASK`, `GROUNDCREW_KEEP_DEAD_WINDOWS`) is presenter-internal implementation detail.
167
+
168
+ ## 9. Architecture: one context, seven modules
169
+
170
+ ([Bounded-context layout](https://linear.app/clipboardhealth/issue/DEVOP-5977))
171
+
172
+ ### 9.1 Single bounded context
173
+
174
+ One root `CONTEXT.md`, no `CONTEXT-MAP.md`. v2's real boundaries are **process seams** — the source protocol, the presenter contract, the sandbox wrap — and each carries the _same_ language across it; a source bundle translating tracker-speak into protocol-speak is an anticorruption layer living outside the repo. One team, one deployable, one language ⇒ one context.
175
+
176
+ ### 9.2 The core noun triple
177
+
178
+ - **Workspace** — the per-task directory set: the worktrees (possibly zero) a task's agent works over. Filesystem/git facts only. v1's "Workspace = terminal pane" dies.
179
+ - **Run** — the core-owned execution lifecycle of a claimed task: the state machine of section 3, resumeCount. One claim = one run.
180
+ - **Session** — one live occupancy of a run: the harness process in a presenter surface, with its captured harness session-id. A run spans sessions (`pause`/`resume`; `--fresh` = new session, same run).
181
+
182
+ Consequences: the session presenter presents sessions, not runs; `status` renders runs; `resume` reopens a session within a run.
183
+
184
+ ### 9.3 Modules
185
+
186
+ | Module | Responsibility | Owns |
187
+ | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------- |
188
+ | **Acquisition** | Source bundle discovery, the versioned `list`/`get`/`update` process protocol, source sandboxing | the **source-protocol contract seam** |
189
+ | **Dispatch** | Per-tick picker: poll, eligibility, claim, provision, terminal-status sweep; persists per-task skip verdicts (section 10.4) | wiring the Writeback adapter into Run |
190
+ | **Run** | The run record: state machine, **reported layer** (`artifact add`/`done` intake), outcomes, writeback point | the **Writeback port** (consumer-owned, defined in `src/run/`) |
191
+ | **Workspace** | Worktrees, branches, **observed layer** (credential-free git facts), `.groundcrew/task.json` and the task-identity resolver; typed errors that Shell maps to exit codes 2/3 | the workspace marker-file format |
192
+ | **Session** | Harness profiles (declarative), launch composition (presenter → sandbox → agent), pause/resume and session-id capture; cmux/tmux/zellij adapters in-core | the **presenter contract seam** |
193
+ | **Sandbox** | Pure library: `wrap(command, policy) → command`; srt mechanics hidden; no lifecycle ownership | nothing pluggable — core-only by decision |
194
+ | **Shell** | commander wiring, routing (`repo add` → Workspace; `artifact add`/`done` → Run), rendering, error→exit-code mapping; `status` = read model joining Run (reported) and Workspace (observed) | — |
195
+
196
+ The flow model's central invariant gets one owner per layer — **observed = Workspace, reported = Run** — so a lie or missing link is always attributable to exactly one module. Sandbox has two real callers (Session wraps agents, Acquisition wraps sources): a real seam, and the one place that keeps "sandbox runners are not pluggable" enforceable.
197
+
198
+ ### 9.4 Dependency graph
199
+
200
+ ```text
201
+ Shell ──▶ everything (thin)
202
+ Dispatch ──▶ Acquisition · Workspace · Session · Run
203
+ Run ──▶ Writeback port only (Dispatch injects the adapter closed over the source)
204
+ Session ──▶ Sandbox Acquisition ──▶ Sandbox
205
+ Workspace ──▶ git only Sandbox ──▶ nothing
206
+ ```
207
+
208
+ Load-bearing properties: **(a)** Run never sees Acquisition — a read-only source is a no-op handle, and Run's tests never touch discovery; **(b)** each edge module owns exactly one published contract seam; **(c)** acyclic, nothing calls upward — sources are spawned processes that answer; agents talk back only via in-session Shell commands routed downward.
209
+
210
+ ### 9.5 Layout
211
+
212
+ ```text
213
+ CONTEXT.md ← the one glossary
214
+ docs/adr/ ← wayfinder resolutions seed the first ADRs
215
+ src/
216
+ acquisition/ dispatch/ run/ workspace/ session/ sandbox/ shell/
217
+ task-sources/
218
+ linear/ jira/ todo-txt/ ← shipped bundles, OUTSIDE src/ — no import path
219
+ e2e/ ← black-box suite (spawns the built binary only)
220
+ ```
221
+
222
+ Conventions: **a module's interface is its `index.ts`** — the only importable path from outside; flat `src/<module>/` so path = glossary noun; colocated `*.test.ts` are intra-module and free.
223
+
224
+ ### 9.6 Enforcement — the graph as CI rules
225
+
226
+ Extend v1's `config/dependencyCruiser.cjs` (already wired as `architecture:check`), keeping `no-circular`/`no-orphans` and adding:
227
+
228
+ 1. **Entry-point boundary** — imports into `src/<module>/` from outside must target `index.ts`.
229
+ 2. **The section 9.4 graph as an allowlist** — one `forbidden` rule per module encoding exactly the ratified edges; an undeclared edge fails CI naming the violated seam. The spec diagram and lint config cannot drift silently.
230
+ 3. **Process-boundary rules** — `src ↛ task-sources`, `task-sources ↛ src`, `e2e ↛ src` (the suite stays black-box).
231
+
232
+ Validation step: prove the rules bite with one deliberate deep import and one undeclared edge, watch both fail, revert.
233
+
234
+ ### 9.7 Glossary seed for v2 `CONTEXT.md`
235
+
236
+ Task · Source · Bundle · Workspace · Worktree · Run · Session · Presenter · Agent profile · Sandbox · Artifact · Writeback · Observed/Reported · Reconcile · Skip verdict — with the triple from 9.2 as the anchor definitions.
237
+
238
+ ## 10. Observability
239
+
240
+ ([Observability design](https://linear.app/clipboardhealth/issue/DEVOP-5982))
241
+
242
+ **Root fork: logs are diagnostics; the run record is the truth.** "Why did/didn't X happen" is answered from the run record plus observed git facts, never by parsing logs. The run record gains a compact, versioned, append-only event history `{ts, event, detail}` covering state transitions and writeback events. Logs stay freely deletable; no log line is a compatibility surface beyond the line format itself.
243
+
244
+ ### 10.1 Topology
245
+
246
+ One global JSON-lines file (config `logging.file`), size-based rotation (~10 MB × 3). Runs interleave; run-less events (poll ticks, reconcile, claim-rejected, doctor) share the stream. Correlation ids are the filter mechanism — no per-run files, no sink routing.
247
+
248
+ ### 10.2 Line schema
249
+
250
+ ```json
251
+ {
252
+ "ts": "…Z",
253
+ "level": "info",
254
+ "module": "dispatch",
255
+ "event": "task_claimed",
256
+ "msg": "claimed DEVOP-123 from linear",
257
+ "taskId": "DEVOP-123",
258
+ "runId": "r_8f3a",
259
+ "source": "linear"
260
+ }
261
+ ```
262
+
263
+ - `ts` ISO-8601 UTC; `level` ∈ `debug|info|warn|error` (four, no trace/fatal); `module` ∈ the seven ratified modules — all three on every line.
264
+ - `event`: required snake_case name, unique per call site; no dotted module prefix.
265
+ - Correlation ids **flat at top level**, present when known: `taskId`, `runId`, `sessionId`, plus `source`/`repo` where relevant. Extra fields flat too; reserved keys enforced by the logging lib's types. `msg` optional (human lines only).
266
+ - The cross-cutting logging lib (no eighth module) **exports a zod schema for the line format**; the E2E suite validates every emitted line against it.
267
+
268
+ ### 10.3 Console rendering
269
+
270
+ The file sink gets everything, always. Console: `info`+ by default, human-rendered; `--verbose` = debug threshold. Raw JSON never hits the console — the file is the only JSON surface.
271
+
272
+ ### 10.4 The "why" affordance — no 15th command
273
+
274
+ `crew status <task>` is the why affordance. It must answer three situations:
275
+
276
+ 1. **Ran / running** — run-record event history merged chronologically with observed git facts, observed-vs-reported layering explicit, ending with a log pointer (file path plus a ready-made jq filter on the run id).
277
+ 2. **Queued, never starts** — **spec commitment:** Dispatch persists its last-poll verdict per task id (`{skipReason, ts}` — repo missing / slots full / claim rejected) in a small dispatch-owned state map, making the flow model's "visible skip reason" promise renderable. No run record exists for unclaimed tasks, so this is the only home.
278
+ 3. **Finished / lingering** — terminal outcome, delivered vs merely observed, what `cleanup` would do.
279
+
280
+ ### 10.5 Reconcile and reaping
281
+
282
+ **Active sweep yes, TTL no.** srt sandboxes are process-scoped — nothing standing to reap except a live agent process, and the interactive baseline forbids killing those on a timer.
283
+
284
+ - **Reconcile is an idempotent library routine with two callers**: startup (the crash-safety guarantee — on-disk git/tmux/sandbox state is the source of truth, compared against expected task state; `using`/signal handlers are the graceful path only) and the dispatcher tick (every Nth poll cycle).
285
+ - Auto-GC only the provably dead: presenter surfaces whose process exited, stale run records, clean worktrees of source-terminal tasks (the cleaner, section 5).
286
+ - **Never auto-kill a live agent process.** Orphaned-but-running sessions are reported loudly — `warn` line plus a prominent `status` flag — and left to the human.
287
+
288
+ ### 10.6 Live activity finer than run-state
289
+
290
+ Nothing finer ships in v2.0. Run-state drives presenter `setStatus`; the observed layer is the truthful on-demand activity signal for free. The seam is documented, not built: a future in-session `crew progress <note>` routed to Run, fanning out to presenter `setStatus` and the source `progress` event — fully additive.
291
+
292
+ ## 11. Migration
293
+
294
+ ([Migration easing](https://linear.app/clipboardhealth/issue/DEVOP-5981))
295
+
296
+ **Migration easing is deferred to post-v2 handoff work** — designing conversion details against a spec that will shift during implementation is waste. The spec carries exactly two commitments:
297
+
298
+ 1. **5.0.0 release blocker — the `crew upgrade` ambush.** v1's `upgrade` self-updates to `@latest`; the moment 5.0.0 hits the `latest` dist-tag, every v1 user who runs `crew upgrade` gets silently major-bumped. Before publishing 5.0.0, decide a dist-tag strategy or a first-run v1 guard. (No publish happens until the E2E suite is green, so this fires late — it must not be forgotten.)
299
+ 2. **v2 fails loudly on v1-only config.** When v2 finds a v1 config (`crew.config.ts` et al.) and no v2 config, it errors with a migration pointer — never silently falls back to defaults.
300
+
301
+ Everything else — `crew init` conversion detail, killed-command stubs, the migration doc, 5.0.0 release notes — moves to the handoff backlog, gated on a working v2.
302
+
303
+ ## 12. Implementation stack and handoff
304
+
305
+ ### 12.1 Stack
306
+
307
+ **Plain TypeScript** ([Effect vs plain TypeScript](https://linear.app/clipboardhealth/issue/DEVOP-5972), informed by the [Effect research](https://linear.app/clipboardhealth/issue/DEVOP-5970)). Effect was rejected for now: v4-beta churn with the platform/CLI layers least stable, span-based-not-stack-based debugging against "dead simple to know what happened", and function coloring as a contributor/AI filter against pluggability.
308
+
309
+ - **Public plugin/source boundary: result-shaped protocol data on one channel.** Authors emit a structured response; expected failure is a failure variant in it; a process crash or nonzero exit is mapped by the core into the _same_ failure shape.
310
+ - **Internals: plain exceptions plus typed error classes** (native stack traces). The adapter layer is the single seam converting caught exception / nonzero exit → protocol failure. No code anywhere handles both models. neverthrow dies.
311
+ - **Crash safety = reconcile-on-startup** (section 10.5), strictly stronger than in-process finalizers since it survives SIGKILL/OOM/power loss.
312
+
313
+ | Capability | Choice |
314
+ | ---------------------- | --------------------------------------------------------- |
315
+ | Retry / backoff | p-retry |
316
+ | Concurrency and cancel | p-limit plus AbortSignal (cooperative) |
317
+ | Process spawning | execa (deletes most of v1's 325-line `commandRunner.ts`) |
318
+ | Schema / config | zod 4 |
319
+ | CLI parsing | commander plus `@commander-js/extra-typings` |
320
+ | Logs | JSON-lines via the cross-cutting logging lib (section 10) |
321
+
322
+ If Effect is ever reconsidered: gate on v4-stable, follow the opencode pattern (Effect core plus platform, skip `@effect/cli`), mandate `Effect.fn` spans from day one.
323
+
324
+ ### 12.2 Handoff sequence
325
+
326
+ 1. **Build the acceptance suite red-first from the [E2E scenario catalog](./e2e-scenario-catalog.md)** — the `e2e/` package, fixture source, scripted agent, fake `gh`, harness self-tests. This is the entry point; no v2 code before it.
327
+ 2. Fix `main`'s release automation (path/scope filter) so v2 commits don't trigger 4.x publishes (section 2).
328
+ 3. Scaffold the `v2/` workspace: seven-module skeleton, dependency-cruiser rules (section 9.6) proven to bite, `CONTEXT.md` from the glossary seed, ADRs seeded from the wayfinder resolutions.
329
+ 4. Bring the suite green module by module, porting the surviving v1 code (srtPolicy/srtLaunch, cleaner, tmux/git plumbing, doctor pieces) file-by-file with its tests.
330
+ 5. Before publishing 5.0.0: resolve the `crew upgrade` ambush (section 11) and execute the cutover (section 2).
331
+
332
+ Post-v2 handoff backlog (deferred by decision): migration easing detail (section 11); presenter bundles and their trust posture (section 8); `crew progress` (section 10.6); an optional repo allowlist (section 4).
333
+
334
+ ## 13. Decision record
335
+
336
+ Every map decision, in resolution order. Full rationale lives in the linked tickets.
337
+
338
+ | Decision | Outcome | Section |
339
+ | -------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ------- |
340
+ | [Prior-art research](https://linear.app/clipboardhealth/issue/DEVOP-5969) | Symmetric source/sink abstractions die in practice; generalize asymmetrically (branch `research/agent-flow-prior-art`) | 3 |
341
+ | [Plugin-distribution research](https://linear.app/clipboardhealth/issue/DEVOP-5971) | Versioned process boundary, not in-process npm plugins (branch `research/plugin-distribution-models`) | 6 |
342
+ | [Effect research](https://linear.app/clipboardhealth/issue/DEVOP-5970) | Effect's costs cut against the locked constraints (branch `research/effect-for-cli-orchestrator`) | 12.1 |
343
+ | [Multi-repo task model](https://linear.app/clipboardhealth/issue/DEVOP-5967) | One session per task over a workspace of worktrees | 4 |
344
+ | [Flow model](https://linear.app/clipboardhealth/issue/DEVOP-5968) | Sinks and flows dissolve; `list`/`get`/`update` source contract; run-state machine | 3 |
345
+ | [Effect vs plain TypeScript](https://linear.app/clipboardhealth/issue/DEVOP-5972) | Plain TypeScript; exceptions inside, result-shaped protocol at the boundary; reconcile-on-startup | 12.1 |
346
+ | [Plugin packaging and review isolation](https://linear.app/clipboardhealth/issue/DEVOP-5973) | Task sources are the only plugin kind; directory bundles; `protocolVersion`; sandboxed by default; srt only | 6 |
347
+ | [CLI surface](https://linear.app/clipboardhealth/issue/DEVOP-5975) | 14 commands; `crew.config.jsonc`; install.sh → `crew init` | 7 |
348
+ | [E2E scenario catalog](https://linear.app/clipboardhealth/issue/DEVOP-5974) | v2-only red-first suite; harness self-tests; completion model codified (amends the destination) | 5 |
349
+ | [Headless extension point](https://linear.app/clipboardhealth/issue/DEVOP-5976) | Session-presenter seam; headless = future detached presenter (amends CLI config: `multiplexer` → `presenter`) | 8 |
350
+ | [Bounded-context layout](https://linear.app/clipboardhealth/issue/DEVOP-5977) | One context, seven modules, graph enforced by dependency-cruiser | 9 |
351
+ | [Rewrite vs evolve](https://linear.app/clipboardhealth/issue/DEVOP-5978) | Rewrite in `v2/` on `main`; ships as 5.0.0 | 2 |
352
+ | [Migration easing](https://linear.app/clipboardhealth/issue/DEVOP-5981) | Deferred post-v2; two spec commitments carried | 11 |
353
+ | [Observability design](https://linear.app/clipboardhealth/issue/DEVOP-5982) | Run record is the truth; one JSON-lines file; `status` is the why affordance; active sweep, no TTL | 10 |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clipboard-health/groundcrew",
3
- "version": "4.47.5",
3
+ "version": "4.48.0",
4
4
  "description": "Linear-driven orchestrator that launches AI coding agents in git worktrees, with workspace lifecycle and usage tracking.",
5
5
  "keywords": [
6
6
  "agent",
@@ -71,31 +71,31 @@
71
71
  },
72
72
  "dependencies": {
73
73
  "@anthropic-ai/sandbox-runtime": "0.0.65",
74
- "@clipboard-health/clearance": "1.4.4",
75
- "@linear/sdk": "88.0.0",
74
+ "@clipboard-health/clearance": "1.4.7",
75
+ "@linear/sdk": "88.1.0",
76
76
  "agent-trust": "1.0.0",
77
77
  "cosmiconfig": "9.0.2",
78
78
  "tslib": "2.8.1",
79
79
  "zod": "4.4.3"
80
80
  },
81
81
  "devDependencies": {
82
- "@clipboard-health/ai-rules": "2.34.0",
83
- "@clipboard-health/oxlint-config": "1.12.6",
84
- "@nx/js": "22.7.6",
82
+ "@clipboard-health/ai-rules": "2.37.1",
83
+ "@clipboard-health/oxlint-config": "1.12.9",
84
+ "@nx/js": "22.7.7",
85
85
  "@tsconfig/node24": "24.0.4",
86
86
  "@tsconfig/strictest": "2.0.8",
87
87
  "@types/node": "25.9.5",
88
88
  "@typescript/native-preview": "7.0.0-dev.20260707.2",
89
89
  "@vitest/coverage-v8": "4.1.10",
90
90
  "cspell": "10.0.1",
91
- "dependency-cruiser": "18.0.0",
91
+ "dependency-cruiser": "18.1.0",
92
92
  "husky": "9.1.7",
93
93
  "jscpd": "5.0.12",
94
- "knip": "6.24.0",
94
+ "knip": "6.27.0",
95
95
  "lint-staged": "17.0.8",
96
96
  "markdownlint-cli2": "0.23.0",
97
- "nx": "22.7.6",
98
- "oxfmt": "0.58.0",
97
+ "nx": "22.7.7",
98
+ "oxfmt": "0.59.0",
99
99
  "oxlint": "1.73.0",
100
100
  "oxlint-tsgolint": "0.24.0",
101
101
  "syncpack": "15.3.2",