pi-crew 0.9.68 → 0.10.2

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.
Files changed (253) hide show
  1. package/CHANGELOG.md +222 -0
  2. package/NOTICE.md +21 -0
  3. package/README.md +44 -2
  4. package/agents/analyst.md +1 -1
  5. package/agents/cold-verifier.md +3 -1
  6. package/agents/critic.md +1 -1
  7. package/agents/executor.md +1 -1
  8. package/agents/explorer.md +1 -1
  9. package/agents/planner.md +1 -1
  10. package/agents/reviewer.md +1 -1
  11. package/agents/security-reviewer.md +1 -1
  12. package/agents/test-engineer.md +1 -1
  13. package/agents/verifier.md +1 -1
  14. package/agents/writer.md +1 -1
  15. package/dist/index.mjs +68113 -60774
  16. package/docs/README.md +2 -0
  17. package/docs/actions-reference.md +31 -0
  18. package/docs/commands-reference.md +17 -6
  19. package/docs/resource-formats.md +13 -0
  20. package/package.json +4 -2
  21. package/schema.json +503 -91
  22. package/scripts/resource-sampler.mjs +36 -2
  23. package/skills/requirements-to-task-packet/SKILL.md +26 -0
  24. package/skills/widget-rendering/SKILL.md +7 -7
  25. package/src/agents/agent-config.ts +2 -1
  26. package/src/agents/discover-agents.ts +23 -14
  27. package/src/config/config-merge.ts +183 -0
  28. package/src/config/config-validation.ts +687 -0
  29. package/src/config/config.ts +22 -864
  30. package/src/config/defaults.ts +43 -2
  31. package/src/config/drift-detector.ts +1 -1
  32. package/src/config/env-vars.ts +691 -0
  33. package/src/config/role-tools.ts +11 -9
  34. package/src/config/sanitize-project-config.ts +172 -0
  35. package/src/config/types.ts +49 -1
  36. package/src/extension/async-notifier.ts +25 -2
  37. package/src/extension/crew-cleanup.ts +13 -0
  38. package/src/extension/crew-vibes/config.ts +2 -1
  39. package/src/extension/crew-vibes/footer.ts +19 -0
  40. package/src/extension/crew-vibes/index.ts +11 -1
  41. package/src/extension/plan-orchestrate.ts +132 -0
  42. package/src/extension/register.ts +8 -0
  43. package/src/extension/registration/command-registration.ts +1 -0
  44. package/src/extension/registration/commands/dashboard.ts +158 -0
  45. package/src/extension/registration/commands/index.ts +35 -0
  46. package/src/extension/registration/commands/manage.ts +303 -0
  47. package/src/extension/registration/commands/run.ts +228 -0
  48. package/src/extension/registration/commands/shared.ts +639 -0
  49. package/src/extension/registration/commands/status.ts +60 -0
  50. package/src/extension/registration/commands.ts +13 -1224
  51. package/src/extension/registration/foreground-run-controller.ts +10 -2
  52. package/src/extension/registration/lifecycle-handlers.ts +178 -17
  53. package/src/extension/registration/runtime-cleanup.ts +23 -5
  54. package/src/extension/registration/subagent-tools.ts +218 -9
  55. package/src/extension/registration/team-tool.ts +5 -1
  56. package/src/extension/registration/ui.ts +5 -4
  57. package/src/extension/rpc-hmac.ts +5 -3
  58. package/src/extension/team-tool/api/heartbeat.ts +47 -10
  59. package/src/extension/team-tool/api/plan-approval.ts +9 -0
  60. package/src/extension/team-tool/api/task-claims.ts +109 -40
  61. package/src/extension/team-tool/cancel.ts +84 -50
  62. package/src/extension/team-tool/dispatch/index.ts +1 -0
  63. package/src/extension/team-tool/dispatch/run.ts +4 -1
  64. package/src/extension/team-tool/doctor.ts +103 -1
  65. package/src/extension/team-tool/orchestrate.ts +66 -1
  66. package/src/extension/team-tool/plans.ts +192 -0
  67. package/src/extension/team-tool/respond.ts +197 -65
  68. package/src/extension/team-tool/run-deadline.ts +35 -3
  69. package/src/extension/team-tool/run-intent.ts +63 -0
  70. package/src/extension/team-tool/run.ts +74 -20
  71. package/src/extension/team-tool/status.ts +84 -26
  72. package/src/extension/team-tool.ts +11 -2
  73. package/src/hooks/registry.ts +1 -6
  74. package/src/i18n.ts +9 -0
  75. package/src/prompt/prompt-runtime.ts +521 -2
  76. package/src/prompt/worker-events-channel.ts +173 -0
  77. package/src/runtime/README.md +8 -8
  78. package/src/runtime/async-runner.ts +7 -3
  79. package/src/runtime/background-runner.ts +42 -14
  80. package/src/runtime/broker/broker-issuer.ts +9 -2
  81. package/src/runtime/broker/crew-broker-tokens.ts +43 -6
  82. package/src/runtime/broker/crew-broker.ts +838 -10
  83. package/src/runtime/broker/wait-status-cache.ts +157 -0
  84. package/src/runtime/budget-enforcement.ts +281 -0
  85. package/src/runtime/child-pi/child-pi-constants.ts +8 -0
  86. package/src/runtime/child-pi/child-pi-spawn.ts +60 -14
  87. package/src/runtime/child-pi/child-pi-streams.ts +21 -1
  88. package/src/runtime/child-pi/child-pi-timers.ts +324 -0
  89. package/src/runtime/child-pi/child-pi.ts +97 -201
  90. package/src/runtime/child-pi/mock-fixtures.ts +16 -2
  91. package/src/runtime/crew-agent-records.ts +259 -14
  92. package/src/runtime/delegate-spawn.ts +148 -0
  93. package/src/runtime/detached-run-results.ts +90 -0
  94. package/src/runtime/deterministic-ast.ts +2 -1
  95. package/src/runtime/dispatch-batch.ts +945 -0
  96. package/src/runtime/finalize-run.ts +557 -0
  97. package/src/runtime/goal-workflow/adaptive-plan.ts +116 -15
  98. package/src/runtime/goal-workflow/dynamic-workflow-runner.ts +8 -3
  99. package/src/runtime/goal-workflow/goal-state-store.ts +1 -1
  100. package/src/runtime/group-join.ts +11 -125
  101. package/src/runtime/live-session/live-session-runtime.ts +26 -1
  102. package/src/runtime/merge-gate.ts +32 -10
  103. package/src/runtime/merge-loop.ts +130 -0
  104. package/src/runtime/model/model-budget-summary.ts +53 -0
  105. package/src/runtime/model/model-fallback.ts +36 -2
  106. package/src/runtime/model/pi-args.ts +10 -0
  107. package/src/runtime/model/provider-extensions.ts +10 -0
  108. package/src/runtime/orphan-worker-registry.ts +1 -1
  109. package/src/runtime/output/output-validator.ts +45 -0
  110. package/src/runtime/parent-guard.ts +3 -1
  111. package/src/runtime/peer-dep.ts +2 -1
  112. package/src/runtime/per-write-validator.ts +0 -5
  113. package/src/runtime/pi-spawn.ts +61 -15
  114. package/src/runtime/plan-approval.ts +125 -0
  115. package/src/runtime/plan-replan.ts +151 -0
  116. package/src/runtime/process-status.ts +16 -1
  117. package/src/runtime/recovery/checkpoint.ts +0 -18
  118. package/src/runtime/recovery/crash-recovery.ts +111 -46
  119. package/src/runtime/run-tracker.ts +77 -10
  120. package/src/runtime/scheduler-context.ts +98 -0
  121. package/src/runtime/scheduling/coalesce-tasks.ts +5 -0
  122. package/src/runtime/scheduling/global-worker-cap.ts +2 -1
  123. package/src/runtime/scheduling/nested-slots.ts +70 -0
  124. package/src/runtime/scheduling/run-coalesced-task-group.ts +64 -13
  125. package/src/runtime/scheduling/task-graph-scheduler.ts +0 -10
  126. package/src/runtime/settings-store.ts +219 -0
  127. package/src/runtime/spawn-policy.ts +217 -0
  128. package/src/runtime/stale-reconciler.ts +87 -6
  129. package/src/runtime/subagent-manager.ts +25 -1
  130. package/src/runtime/task-output-context.ts +230 -9
  131. package/src/runtime/task-packet.ts +23 -1
  132. package/src/runtime/task-runner/child-executor.ts +106 -7
  133. package/src/runtime/task-runner/post-execution.ts +125 -1
  134. package/src/runtime/task-runner/pre-execution.ts +39 -1
  135. package/src/runtime/task-runner/prompt-builder.ts +51 -1
  136. package/src/runtime/task-runner/retrieval-orchestrator.ts +72 -18
  137. package/src/runtime/task-runner/spec-evidence.ts +403 -0
  138. package/src/runtime/task-runner/state-helpers.ts +26 -24
  139. package/src/runtime/task-runner.ts +11 -0
  140. package/src/runtime/team-runner.ts +132 -1673
  141. package/src/runtime/verification/spec-sandbox.ts +255 -0
  142. package/src/runtime/verification/verification-gates.ts +3 -2
  143. package/src/runtime/verification/verification-worktree.ts +2 -1
  144. package/src/runtime/workflow-phase-advance.ts +100 -0
  145. package/src/runtime/workspace-tree.ts +9 -0
  146. package/src/schema/config-schema.ts +66 -26
  147. package/src/schema/sensitive-config-paths.ts +64 -0
  148. package/src/schema/team-tool-schema.ts +13 -3
  149. package/src/state/README.md +4 -10
  150. package/src/state/atomic-write.ts +20 -3
  151. package/src/state/contracts.ts +38 -0
  152. package/src/state/coordination/mailbox.ts +12 -2
  153. package/src/state/event-log/cursor.ts +223 -0
  154. package/src/state/event-log/event-log-rotation.ts +12 -4
  155. package/src/state/event-log/event-log.ts +152 -369
  156. package/src/state/event-log/sequence-cache.ts +373 -0
  157. package/src/state/event-log/worker-atomic-writer.ts +2 -1
  158. package/src/state/stores/active-run-registry.ts +3 -2
  159. package/src/state/stores/manifest-io.ts +237 -0
  160. package/src/state/stores/ownership-map.ts +162 -0
  161. package/src/state/stores/plan-store.ts +241 -0
  162. package/src/state/stores/run-cache.ts +0 -90
  163. package/src/state/stores/spec-store.ts +189 -0
  164. package/src/state/stores/state-store.ts +139 -232
  165. package/src/state/types.ts +199 -0
  166. package/src/ui/dashboard-panes/plan-pane.ts +136 -0
  167. package/src/ui/dashboard-panes/progress-pane.ts +6 -0
  168. package/src/ui/dashboard-panes/transcript-pane.ts +31 -0
  169. package/src/ui/dock-footer.ts +49 -0
  170. package/src/ui/heartbeat-aggregator.ts +9 -1
  171. package/src/ui/inline-panel/agent-pane.ts +375 -0
  172. package/src/ui/inline-panel/agent-transcript.ts +338 -0
  173. package/src/ui/inline-panel/agent-view-overlay.ts +225 -0
  174. package/src/ui/inline-panel/crew-editor.ts +192 -0
  175. package/src/ui/inline-panel/index.ts +290 -0
  176. package/src/ui/inline-panel/panel-rows.ts +37 -0
  177. package/src/ui/inline-panel/panel-selection.ts +157 -0
  178. package/src/ui/inline-panel/panel-store.ts +111 -0
  179. package/src/ui/inline-panel/view-session-store.ts +36 -0
  180. package/src/ui/keybinding-map.ts +54 -13
  181. package/src/ui/pi-ui-compat.ts +9 -0
  182. package/src/ui/powerbar-publisher.ts +52 -1
  183. package/src/ui/run-dashboard.ts +31 -5
  184. package/src/ui/run-snapshot-cache.ts +57 -30
  185. package/src/ui/snapshot-types.ts +6 -1
  186. package/src/ui/widget/index.ts +176 -22
  187. package/src/ui/widget/task-list.ts +198 -0
  188. package/src/ui/widget/widget-formatters.ts +240 -4
  189. package/src/ui/widget/widget-renderer.ts +243 -38
  190. package/src/ui/widget/widget-types.ts +11 -0
  191. package/src/utils/child-process-shield.ts +106 -0
  192. package/src/utils/file-coalescer.ts +0 -4
  193. package/src/utils/fs-errno.ts +66 -0
  194. package/src/utils/fs-watch.ts +1 -1
  195. package/src/utils/internal-error.ts +3 -1
  196. package/src/utils/paths.ts +11 -3
  197. package/src/utils/redaction.ts +7 -0
  198. package/src/utils/safe-abort.ts +45 -0
  199. package/src/utils/task-name-generator.ts +1 -8
  200. package/src/workflows/discover-workflows.ts +20 -2
  201. package/src/workflows/validate-workflow.ts +7 -1
  202. package/src/workflows/workflow-config.ts +17 -0
  203. package/src/workflows/workflow-serializer.ts +3 -0
  204. package/src/worktree/worktree-manager.ts +22 -0
  205. package/workflows/default.workflow.md +36 -26
  206. package/workflows/strict-fast-fix.workflow.md +26 -0
  207. package/src/agents/agent-search.ts +0 -98
  208. package/src/benchmark/benchmark-runner.ts +0 -313
  209. package/src/benchmark/feedback-loop.ts +0 -73
  210. package/src/config/resilient-parser.ts +0 -117
  211. package/src/extension/crew-vibes/cat-frames.ts +0 -18
  212. package/src/extension/result-watcher.ts +0 -139
  213. package/src/observability/exporters/prometheus-exporter.ts +0 -54
  214. package/src/observability/metric-retention.ts +0 -64
  215. package/src/runtime/compaction/compaction-summary.ts +0 -278
  216. package/src/runtime/errors/crew-errors.ts +0 -162
  217. package/src/runtime/live-session/intercom-bridge.ts +0 -187
  218. package/src/runtime/loop-gates.ts +0 -128
  219. package/src/runtime/metric-parser.ts +0 -36
  220. package/src/runtime/output/stream-preview.ts +0 -184
  221. package/src/runtime/output/tool-progress.ts +0 -278
  222. package/src/runtime/phase-tracker.ts +0 -385
  223. package/src/runtime/pipeline-runner.ts +0 -523
  224. package/src/runtime/process/process-lifecycle.ts +0 -491
  225. package/src/runtime/recovery/retry-runner.ts +0 -330
  226. package/src/runtime/run-drift.ts +0 -219
  227. package/src/runtime/task-quality.ts +0 -199
  228. package/src/runtime/task-runner/run-projection.ts +0 -128
  229. package/src/runtime/verification/post-checks.ts +0 -142
  230. package/src/state/coordination/schedule.ts +0 -166
  231. package/src/state/event-log/jsonl-writer.ts +0 -115
  232. package/src/state/hook-instinct-bridge.ts +0 -94
  233. package/src/state/hook-integrations.ts +0 -51
  234. package/src/state/session-state-map.ts +0 -51
  235. package/src/state/stores/blob-store.ts +0 -308
  236. package/src/state/stores/instinct-store.ts +0 -275
  237. package/src/state/stores/observation-store.ts +0 -176
  238. package/src/state/tiered-eval.ts +0 -480
  239. package/src/state/types-eval.ts +0 -58
  240. package/src/tools/safe-bash-extension.ts +0 -54
  241. package/src/tools/safe-bash.ts +0 -505
  242. package/src/ui/agent-management-overlay.ts +0 -160
  243. package/src/ui/crew-footer.ts +0 -102
  244. package/src/ui/crew-select-list.ts +0 -114
  245. package/src/ui/dashboard-panes/capability-pane.ts +0 -77
  246. package/src/ui/transcript-entries.ts +0 -256
  247. package/src/utils/conflict-detect.ts +0 -721
  248. package/src/utils/fingerprint.ts +0 -180
  249. package/src/utils/gh-protocol.ts +0 -556
  250. package/src/utils/project-detector.ts +0 -160
  251. package/src/utils/sse-parser.ts +0 -131
  252. package/src/workflows/cost-estimator.ts +0 -34
  253. package/src/workflows/intermediate-store.ts +0 -166
@@ -0,0 +1,45 @@
1
+ /**
2
+ * safe-abort.ts — abort helpers that cannot crash the host process.
3
+ *
4
+ * When a child process was spawned with `options.signal` and the signal fires
5
+ * AFTER that child already exited (fast workers, cleanup-after-completion,
6
+ * session switches mid-run), Node's child_process abort listener throws
7
+ * `AbortError: The operation was aborted` SYNCHRONOUSLY from
8
+ * `AbortController.abort()` (node:child_process abortChildProcess).
9
+ *
10
+ * In pi extension event handlers that throw propagates straight into pi's
11
+ * `uncaughtException` handling and kills the whole terminal session — the
12
+ * crash seen here: session_before_switch → stopSessionBoundSubagents → abort
13
+ * → AbortError → "pi exiting due to uncaughtException".
14
+ *
15
+ * Abort-after-exit is a semantic no-op (the work is already gone), so every
16
+ * controller abort that can be linked to a spawned child MUST go through
17
+ * these helpers: swallow the throw, log it, carry on.
18
+ */
19
+
20
+ import { logInternalError } from "./internal-error.ts";
21
+
22
+ /** Minimal controller shape — anything with an `abort()` method. */
23
+ export interface Abortable {
24
+ abort(): void;
25
+ }
26
+
27
+ /**
28
+ * Abort a controller, tolerating Node's abort-after-child-exit AbortError.
29
+ * Never throws. `scope` names the call site for the internal error log.
30
+ */
31
+ export function safeAbort(controller: Abortable | undefined, scope: string): void {
32
+ if (!controller) return;
33
+ try {
34
+ controller.abort();
35
+ } catch (error) {
36
+ logInternalError(`safe-abort.${scope}`, error);
37
+ }
38
+ }
39
+
40
+ /** Abort every controller in an iterable; a throwing controller does not
41
+ * prevent the remaining ones from being aborted. */
42
+ export function safeAbortAll(controllers: Iterable<Abortable> | undefined, scope: string): void {
43
+ if (!controllers) return;
44
+ for (const controller of controllers) safeAbort(controller, scope);
45
+ }
@@ -295,7 +295,7 @@ function capitalize(s: string): string {
295
295
  return s.charAt(0).toUpperCase() + s.slice(1);
296
296
  }
297
297
 
298
- let usedNames = new Set<string>();
298
+ const usedNames = new Set<string>();
299
299
 
300
300
  /**
301
301
  * Generate a unique two-word identifier (e.g., "SwiftFalcon").
@@ -328,10 +328,3 @@ export function generateTaskName(): string {
328
328
  usedNames.clear();
329
329
  return generateTaskName();
330
330
  }
331
-
332
- /**
333
- * Reset name generator state (for testing).
334
- */
335
- export function resetTaskNames(): void {
336
- usedNames = new Set<string>();
337
- }
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import type { ResourceSource } from "../agents/agent-config.ts";
4
4
  import { parseCsv, parseFrontmatter } from "../utils/frontmatter.ts";
5
+ import { logInternalError } from "../utils/internal-error.ts";
5
6
  import { packageRoot, projectCrewRoot, userPiRoot } from "../utils/paths.ts";
6
7
  import type { WorkflowConfig, WorkflowStep } from "./workflow-config.ts";
7
8
 
@@ -24,6 +25,8 @@ const STEP_CONFIG_KEYS = new Set([
24
25
  "verify",
25
26
  "task",
26
27
  "seedPaths",
28
+ "specRefs",
29
+ "specStrict",
27
30
  "preStepScript",
28
31
  "preStepArgs",
29
32
  "preStepTimeout",
@@ -68,6 +71,11 @@ function parseStepSection(id: string, body: string): WorkflowStep | undefined {
68
71
  worktree: config.worktree === "true" ? true : config.worktree === "false" ? false : undefined,
69
72
  verify: config.verify === "true" ? true : config.verify === "false" ? false : undefined,
70
73
  seedPaths: parseCsv(config.seedPaths) || undefined,
74
+ specRefs: parseCsv(config.specRefs) || undefined,
75
+ // Tri-state (round-1 P1-2): ABSENT = undefined so the workflow-level flag
76
+ // survives the `step.specStrict ?? workflow.specStrict` dispatch merge; an
77
+ // explicit `false` stays false (per-step opt-out of a strict workflow).
78
+ specStrict: config.specStrict === undefined ? undefined : config.specStrict === "true" || config.specStrict === "1",
71
79
  preStepScript: config.preStepScript || undefined,
72
80
  preStepArgs: parseCsv(config.preStepArgs) || undefined,
73
81
  preStepTimeout: parseOptionalInteger(config.preStepTimeout) ?? undefined,
@@ -155,8 +163,16 @@ function parseWorkflowFile(filePath: string, source: ResourceSource): WorkflowCo
155
163
  // runtime, but stripping here prevents the script value from even reaching
156
164
  // downstream consumers (coalesce-tasks, policy-engine, etc.).
157
165
  if (source === "project" && step.preStepScript) {
158
- console.warn(
159
- `[discover-workflows] Stripping preStepScript from project workflow '${name}' step '${step.id}': '${step.preStepScript}' (F-02: project-sourced pre-step scripts are not allowed for RCE prevention)`,
166
+ // F-02 redaction: NEVER log the preStepScript body — project workflows
167
+ // are untrusted input and the script may contain secrets. Log name,
168
+ // step id, and script length only.
169
+ logInternalError(
170
+ "discover-workflows",
171
+ new Error(
172
+ `Stripping preStepScript from project workflow '${name}' step '${step.id}' — script body redacted (${step.preStepScript.length} chars) (F-02: project-sourced pre-step scripts are not allowed for RCE prevention)`,
173
+ ),
174
+ undefined,
175
+ "warn",
160
176
  );
161
177
  step.preStepScript = undefined;
162
178
  step.preStepArgs = undefined;
@@ -174,6 +190,8 @@ function parseWorkflowFile(filePath: string, source: ResourceSource): WorkflowCo
174
190
  maxConcurrency: parseOptionalInteger(frontmatter.maxConcurrency),
175
191
  topology: parseTopology(frontmatter.topology),
176
192
  coalesceMicroTasks: parseOptionalBoolean(frontmatter.coalesceMicroTasks),
193
+ specStrict: parseOptionalBoolean(frontmatter.specStrict),
194
+ adaptive: parseOptionalBoolean(frontmatter.adaptive),
177
195
  steps,
178
196
  };
179
197
  } catch {
@@ -9,7 +9,13 @@ export function validateWorkflowForTeam(workflow: WorkflowConfig, team: TeamConf
9
9
  for (const step of workflow.steps) {
10
10
  if (stepIds.has(step.id)) errors.push(`Duplicate workflow step id '${step.id}'.`);
11
11
  stepIds.add(step.id);
12
- if (!roles.has(step.role)) errors.push(`Step '${step.id}' references unknown team role '${step.role}'.`);
12
+ if (!roles.has(step.role)) {
13
+ // F19-9: the distill adapter needs roles the default team lacks
14
+ // (analyst/critic) — hint the correct invocation instead of a bare
15
+ // unknown-role error.
16
+ const distillHint = workflow.name === "distill" ? " (run with team='implementation' — needs analyst+critic roles)" : "";
17
+ errors.push(`Step '${step.id}' references unknown team role '${step.role}'.${distillHint}`);
18
+ }
13
19
  }
14
20
 
15
21
  for (const step of workflow.steps) {
@@ -22,6 +22,12 @@ export interface WorkflowStep {
22
22
  /** Per-step files to overlay into the worktree (in addition to global worktree.seedPaths).
23
23
  * Useful when only certain steps need access to local drafts or scripts. */
24
24
  seedPaths?: string[];
25
+ /** T4/R6 (ADR-6): workspace SpecRecord ids this step is held to; frozen
26
+ * into the task packet at dispatch (specStrict per workflow frontmatter). */
27
+ specRefs?: string[];
28
+ /** T4/R6 (ADR-6 §7): per-step strict-mode opt-in. Normally inherited from
29
+ * the workflow-level frontmatter flag (merged at dispatch). */
30
+ specStrict?: boolean;
25
31
  /** Path to a deterministic script to run before dispatching the LLM worker.
26
32
  * Script stdout is injected into the worker's prompt as context.
27
33
  * Pattern origin: Understand-Anything deterministic pre-step pattern. */
@@ -65,6 +71,17 @@ export interface WorkflowConfig {
65
71
  * in v0.9.17 unless the workflow author opts in. See
66
72
  * src/runtime/scheduling/coalesce-tasks.ts. */
67
73
  coalesceMicroTasks?: boolean;
74
+ /** T4/R6 (ADR-6 §7): strict spec mode opt-in (frontmatter `specStrict: true`).
75
+ * Requires a verifier-role step (reject-start, §5) and fails the write-gate
76
+ * on coverage gaps or machine-check failures (§4). */
77
+ specStrict?: boolean;
78
+ /** Frontmatter `adaptive: true` — the workflow's `assess` step (planner role)
79
+ * emits an ADAPTIVE_PLAN_JSON block that the runtime parses and injects as
80
+ * concrete tasks, so the plan painted in the task list is derived from the
81
+ * goal, not from fixed phase templates. The `implementation` workflow is
82
+ * adaptive by name (backward compat); this flag extends the mechanism to
83
+ * any workflow. See src/runtime/goal-workflow/adaptive-plan.ts. */
84
+ adaptive?: boolean;
68
85
  }
69
86
 
70
87
  /** A dynamic workflow (runtime === "dynamic"). steps is empty — the script is the source of truth. */
@@ -14,6 +14,8 @@ function serializeStep(step: WorkflowStep): string[] {
14
14
  if (step.progress !== undefined) lines.push(`progress: ${step.progress ? "true" : "false"}`);
15
15
  if (step.worktree !== undefined) lines.push(`worktree: ${step.worktree ? "true" : "false"}`);
16
16
  if (step.verify !== undefined) lines.push(`verify: ${step.verify ? "true" : "false"}`);
17
+ if (step.specRefs?.length) lines.push(`specRefs: ${step.specRefs.join(", ")}`);
18
+ if (step.specStrict !== undefined && step.specStrict !== false) lines.push("specStrict: true");
17
19
  lines.push("", step.task.trim(), "");
18
20
  return lines;
19
21
  }
@@ -24,6 +26,7 @@ export function serializeWorkflow(workflow: WorkflowConfig): string {
24
26
  `name: ${workflow.name}`,
25
27
  `description: ${workflow.description}`,
26
28
  ...(workflow.maxConcurrency !== undefined ? [`maxConcurrency: ${workflow.maxConcurrency}`] : []),
29
+ ...(workflow.specStrict !== undefined ? [`specStrict: ${workflow.specStrict ? "true" : "false"}`] : []),
27
30
  "---",
28
31
  "",
29
32
  ...workflow.steps.flatMap(serializeStep),
@@ -146,6 +146,12 @@ export function assertCleanLeader(repoRoot: string): void {
146
146
 
147
147
  // --- Async versions ---
148
148
 
149
+ // R5-L1 (Round 5 LOW-1): the exported clear*() fns below stay unwired by
150
+ // leader decision (rewiring run-start/lock-release paths is riskier than the
151
+ // leak); the FIFO caps bound these caches instead.
152
+ const MAX_GIT_ROOT_CACHE = 256;
153
+ const MAX_CLEAN_LEADER_CACHE = 256;
154
+
149
155
  /** Cache for findGitRoot results keyed by cwd. Cleared per-run. */
150
156
  const _gitRootCache = new Map<string, string>();
151
157
 
@@ -159,6 +165,10 @@ export async function findGitRootAsync(cwd: string): Promise<string> {
159
165
  if (cached) return cached;
160
166
  const root = await gitAsync(cwd, ["rev-parse", "--show-toplevel"]);
161
167
  _gitRootCache.set(cwd, root);
168
+ if (_gitRootCache.size > MAX_GIT_ROOT_CACHE) {
169
+ const oldest = _gitRootCache.keys().next().value;
170
+ if (oldest !== undefined) _gitRootCache.delete(oldest);
171
+ }
162
172
  return root;
163
173
  }
164
174
 
@@ -179,6 +189,10 @@ export async function assertCleanLeaderAsync(repoRoot: string): Promise<void> {
179
189
  throw new Error("Worktree mode requires a clean leader repository. Commit/stash changes or use workspaceMode: 'single'.");
180
190
  }
181
191
  _cleanLeaderCache.add(repoRoot);
192
+ if (_cleanLeaderCache.size > MAX_CLEAN_LEADER_CACHE) {
193
+ const oldest = _cleanLeaderCache.values().next().value;
194
+ if (oldest !== undefined) _cleanLeaderCache.delete(oldest);
195
+ }
182
196
  }
183
197
 
184
198
  function linkNodeModulesIfPresent(repoRoot: string, worktreePath: string): boolean {
@@ -469,6 +483,10 @@ async function branchExistsAsync(repoRoot: string, branch: string): Promise<{ lo
469
483
  // run no new stale worktrees appear, so one prune suffices).
470
484
  const _prunedRepos = new Set<string>();
471
485
  const _pruneInFlight = new Map<string, Promise<void>>();
486
+ // R5-L2 (Round 5 LOW-2): FIFO cap — dedupe-only Set, never cleared in
487
+ // production; eviction just means a re-prune for that repo later, which is
488
+ // harmless (prune is idempotent and coalesced).
489
+ const MAX_PRUNED_REPOS = 128;
472
490
  async function pruneStaleWorktreesAsync(repoRoot: string): Promise<void> {
473
491
  if (_prunedRepos.has(repoRoot)) return;
474
492
  const inFlight = _pruneInFlight.get(repoRoot);
@@ -483,6 +501,10 @@ async function pruneStaleWorktreesAsync(repoRoot: string): Promise<void> {
483
501
  /* best-effort */
484
502
  }
485
503
  _prunedRepos.add(repoRoot);
504
+ if (_prunedRepos.size > MAX_PRUNED_REPOS) {
505
+ const oldest = _prunedRepos.values().next().value;
506
+ if (oldest !== undefined) _prunedRepos.delete(oldest);
507
+ }
486
508
  _pruneInFlight.delete(repoRoot);
487
509
  })();
488
510
  _pruneInFlight.set(repoRoot, p);
@@ -1,31 +1,41 @@
1
1
  ---
2
2
  name: default
3
- description: Explore, plan, execute, and verify
4
- topology: sequential
3
+ description: Plan the goal into concrete tasks, execute them in parallel phases, verify
4
+ topology: complex-dag
5
+ adaptive: true
5
6
  ---
6
7
 
7
- ## explore
8
- role: explorer
9
-
10
- Explore the codebase for the goal: {goal}
11
-
12
- ## plan
8
+ ## assess
13
9
  role: planner
14
- dependsOn: explore
15
- output: plan.md
16
-
17
- Create a concise implementation plan for: {goal}
18
-
19
- ## execute
20
- role: executor
21
- dependsOn: plan
22
-
23
- Implement the plan for: {goal}
24
-
25
- ## verify
26
- role: verifier
27
- dependsOn: execute
28
- verify: true
29
-
30
- Verify completion for: {goal}
31
- Run FAST checks ONCE (cache output to .crew/cache/): `npm run test:critical && npx tsc --noEmit` (completes in <60s). Do NOT run `npm run test:unit` or `npm test` — too slow (642 files, >4 min). Cross-reference cached output with the changes. Do NOT re-run tests. Give PASS or FAIL with specific test evidence.
10
+ output: adaptive-plan.json
11
+
12
+ Plan the concrete task list for: {goal}
13
+
14
+ You are the orchestration planner. Inspect the repository enough to break the goal into specific, executable tasks; do not use a fixed template. Small tasks may need one executor plus one verifier; broader tasks may need parallel explorers, executors, and a verification phase.
15
+
16
+ Return a concise rationale, then include exactly one JSON block between these markers:
17
+
18
+ ADAPTIVE_PLAN_JSON_START
19
+ {
20
+ "phases": [
21
+ {
22
+ "name": "short-phase-name",
23
+ "tasks": [
24
+ {
25
+ "role": "explorer|executor|verifier",
26
+ "title": "short task title",
27
+ "task": "specific autonomous task prompt for this subagent"
28
+ }
29
+ ]
30
+ }
31
+ ]
32
+ }
33
+ ADAPTIVE_PLAN_JSON_END
34
+
35
+ Rules:
36
+ - **MAXIMIZE PARALLELISM**: Put independent tasks in the SAME phase so they run concurrently. Never create sequential phases when tasks are independent.
37
+ - Task titles name the concrete piece of work — they become the task list the user watches.
38
+ - Choose the smallest effective number of tasks per phase; a simple goal may have 1-2 phases with 1-2 tasks.
39
+ - End implementation plans with a verification phase (`verifier` role): run FAST targeted checks only (e.g. the affected test files), never the full suite.
40
+ - Tasks within the same phase run in parallel; phases run sequentially.
41
+ - Do not include more than 12 tasks per phase; split or summarize oversized plans instead.
@@ -0,0 +1,26 @@
1
+ ---
2
+ name: strict-fast-fix
3
+ description: T4 follow-up — fast-fix variant with strict spec binding (ADR-6 §5). Use this to exercise provenance v2 / freeze-at-dispatch / strict-mode gate end-to-end. Imports the spec via `scripts/spec-import.mjs` first.
4
+ topology: sequential
5
+ ---
6
+
7
+ ## explore
8
+ role: explorer
9
+ specRefs: b5-t4-strict-probe
10
+ specStrict: true
11
+
12
+ Find the likely source of the issue: {goal}. You are held to the SPEC-EVIDENCE contract for the referenced spec — emit a `=== SPEC-EVIDENCE ===` block with `cited: <id>:<id>` lines for each requirement the work touches, then `=== END ===`.
13
+
14
+ ## execute
15
+ role: executor
16
+ dependsOn: explore
17
+
18
+ Make the smallest safe fix. Continue the SPEC-EVIDENCE contract from explore: cite every requirement the implementation satisfies.
19
+
20
+ ## verify
21
+ role: verifier
22
+ dependsOn: execute
23
+ verify: true
24
+
25
+ Verify the fix with available evidence, AND cross-reference the SPEC-EVIDENCE footer from explore+execute against the spec's requirements (machine-check the citations vs the spec id set).
26
+ Run FAST checks ONCE (cache output to .crew/cache/): `npm run test:critical && npx tsc --noEmit` (completes in <60s). Do NOT run `npm run test:unit` or `npm test` — too slow (642 files, >4 min). Cross-reference cached output with the fix. Do NOT re-run tests. Give PASS or FAIL with specific test evidence + spec gate verdict.
@@ -1,98 +0,0 @@
1
- import type { AgentConfig } from "./agent-config.ts";
2
-
3
- // ─── BM25 Agent Search ──────────────────────────────────────────────────────
4
- // Lightweight BM25 search over agent descriptors for task-to-agent matching.
5
- // Based on the same BM25 algorithm used in oh-my-pi's tool-index.ts.
6
-
7
- export interface AgentSearchDocument {
8
- agent: AgentConfig;
9
- termFrequencies: Map<string, number>;
10
- length: number;
11
- }
12
-
13
- export interface AgentSearchIndex {
14
- documents: AgentSearchDocument[];
15
- averageLength: number;
16
- documentFrequencies: Map<string, number>;
17
- }
18
-
19
- export interface AgentSearchResult {
20
- agent: AgentConfig;
21
- score: number;
22
- }
23
-
24
- const BM25_K1 = 1.2;
25
- const BM25_B = 0.75;
26
- const FIELD_WEIGHTS = {
27
- name: 6,
28
- description: 2,
29
- role: 3,
30
- } as const;
31
-
32
- function tokenize(value: string): string[] {
33
- return value
34
- .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
35
- .replace(/[^a-zA-Z0-9]+/g, " ")
36
- .toLowerCase()
37
- .trim()
38
- .split(/\s+/)
39
- .filter((token) => token.length > 0);
40
- }
41
-
42
- function addWeightedTokens(termFrequencies: Map<string, number>, value: string | undefined, weight: number): void {
43
- if (!value) return;
44
- for (const token of tokenize(value)) {
45
- termFrequencies.set(token, (termFrequencies.get(token) ?? 0) + weight);
46
- }
47
- }
48
-
49
- function buildAgentSearchDocument(agent: AgentConfig): AgentSearchDocument {
50
- const termFrequencies = new Map<string, number>();
51
- addWeightedTokens(termFrequencies, agent.name, FIELD_WEIGHTS.name);
52
- addWeightedTokens(termFrequencies, agent.description, FIELD_WEIGHTS.description);
53
- // Role from agent name heuristic
54
- const roleHint = agent.name?.replace(/[-_]/g, " ") ?? "";
55
- addWeightedTokens(termFrequencies, roleHint, FIELD_WEIGHTS.role);
56
- const length = Array.from(termFrequencies.values()).reduce((sum, value) => sum + value, 0);
57
- return { agent, termFrequencies, length };
58
- }
59
-
60
- export function buildAgentSearchIndex(agents: Iterable<AgentConfig>): AgentSearchIndex {
61
- const documents = Array.from(agents, buildAgentSearchDocument);
62
- const averageLength = documents.reduce((sum, document) => sum + document.length, 0) / documents.length || 1;
63
- const documentFrequencies = new Map<string, number>();
64
- for (const document of documents) {
65
- for (const token of new Set(document.termFrequencies.keys())) {
66
- documentFrequencies.set(token, (documentFrequencies.get(token) ?? 0) + 1);
67
- }
68
- }
69
- return { documents, averageLength, documentFrequencies };
70
- }
71
-
72
- export function searchAgents(index: AgentSearchIndex, query: string, limit: number): AgentSearchResult[] {
73
- const queryTokens = tokenize(query);
74
- if (queryTokens.length === 0) return [];
75
- if (index.documents.length === 0) return [];
76
-
77
- const queryTermCounts = new Map<string, number>();
78
- for (const token of queryTokens) {
79
- queryTermCounts.set(token, (queryTermCounts.get(token) ?? 0) + 1);
80
- }
81
-
82
- return index.documents
83
- .map((document) => {
84
- let score = 0;
85
- for (const [token, queryTermCount] of queryTermCounts) {
86
- const termFrequency = document.termFrequencies.get(token) ?? 0;
87
- if (termFrequency === 0) continue;
88
- const documentFrequency = index.documentFrequencies.get(token) ?? 0;
89
- const idf = Math.log(1 + (index.documents.length - documentFrequency + 0.5) / (documentFrequency + 0.5));
90
- const normalization = BM25_K1 * (1 - BM25_B + BM25_B * (document.length / index.averageLength));
91
- score += queryTermCount * idf * ((termFrequency * (BM25_K1 + 1)) / (termFrequency + normalization));
92
- }
93
- return { agent: document.agent, score };
94
- })
95
- .filter((result) => result.score > 0)
96
- .sort((left, right) => right.score - left.score || left.agent.name.localeCompare(right.agent.name))
97
- .slice(0, limit);
98
- }