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,173 @@
1
+ /**
2
+ * worker-events-channel.ts — bounded worker self-reporting channel
3
+ * (WP-9 / R9, T5).
4
+ *
5
+ * The ONE worker-side primitive for appending self-reported, schema-tagged
6
+ * `worker.*` events to the run's events.jsonl (PI_CREW_EVENTS_PATH —
7
+ * unconditional since WP-9; previously scratchpad-gated, which excluded
8
+ * read-only roles by design and left them with no reporting path).
9
+ *
10
+ * Bounds (a worker must never be able to flood the shared event log):
11
+ * - SCHEMA: event types MUST match /^worker\.[a-z0-9_.-]{1,63}$/ — anything
12
+ * else is dropped (return false). Worker events never impersonate
13
+ * orchestrator namespaces (task.*, run.*, spec.*, …).
14
+ * - RATE LIMIT: sliding window, `maxEventsPerWindow` (default 30) per
15
+ * `windowMs` (default 60s). Dropped events are counted; the next ACCEPTED
16
+ * event carries `droppedSinceLast` so the operator sees suppression.
17
+ * - FIFO BUFFER CAP: failed appends (throwing writer, e.g. transient EBUSY)
18
+ * queue for retry; the queue holds at most `bufferCap` (default 64) —
19
+ * overflow drops the OLDEST pending (FIFO), never grows unboundedly.
20
+ * - CRASH SAFETY: each append is a single O_APPEND write of one JSONL line.
21
+ * A crash mid-write leaves at most a trailing PARTIAL line — the
22
+ * orchestrator-side reader (parseJsonlEvents) already skips corrupt lines,
23
+ * so complete events before/after the partial stay readable (recoverable).
24
+ *
25
+ * Heartbeats remain the liveness corroboration source; worker.* events are
26
+ * informational only — the scheduler never derives liveness from them.
27
+ */
28
+
29
+ import { appendFileSync, readFileSync } from "node:fs";
30
+
31
+ export const WORKER_EVENT_TYPE_PATTERN = /^worker\.[a-z0-9_.-]{1,63}$/;
32
+
33
+ export interface WorkerEventsChannelOptions {
34
+ /** Default: process.env (worker context). */
35
+ env?: NodeJS.ProcessEnv;
36
+ /** Append function — default: appendEventFireAndForget from state. */
37
+ appendEvent?: (eventsPath: string, event: Record<string, unknown>) => void;
38
+ now?: () => number;
39
+ /** Sliding-window rate limit: max accepted events per windowMs. */
40
+ maxEventsPerWindow?: number;
41
+ windowMs?: number;
42
+ /** FIFO cap for queued (failed) appends. */
43
+ bufferCap?: number;
44
+ }
45
+
46
+ export interface WorkerEventsChannel {
47
+ /** Emit a worker.* event. false = dropped (schema/rate/buffer reasons). */
48
+ emit(type: string, data: Record<string, unknown>): boolean;
49
+ /** Retry queued appends (called on the next emit internally). */
50
+ flush(): void;
51
+ /** Test introspection. */
52
+ stats(): { accepted: number; droppedSchema: number; droppedRate: number; droppedBuffer: number; queued: number };
53
+ }
54
+
55
+ export function createWorkerEventsChannel(options: WorkerEventsChannelOptions = {}): WorkerEventsChannel {
56
+ const env = options.env ?? process.env;
57
+ const now = options.now ?? Date.now;
58
+ const appendEvent =
59
+ options.appendEvent ??
60
+ ((eventsPath: string, event: Record<string, unknown>) => {
61
+ // Single O_APPEND write of one JSONL line — the crash-safety contract.
62
+ // A previous crash may have left a trailing PARTIAL line (no newline):
63
+ // appending without a separator would fuse both lines into ONE corrupt
64
+ // line, destroying the complete event too. Guard: if the file is
65
+ // non-empty and does not end with \n, prepend the separator (1-byte
66
+ // read of the tail — the partial line stays quarantined on its own line,
67
+ // which the orchestrator reader skips).
68
+ let prefix = "";
69
+ try {
70
+ const buf = readFileSync(eventsPath);
71
+ if (buf.length > 0 && buf[buf.length - 1] !== 0x0a) prefix = "\n";
72
+ } catch {
73
+ /* absent file — nothing to separate */
74
+ }
75
+ appendFileSync(eventsPath, `${prefix}${JSON.stringify(event)}\n`, "utf-8");
76
+ });
77
+ const maxPerWindow = options.maxEventsPerWindow ?? 30;
78
+ const windowMs = options.windowMs ?? 60_000;
79
+ const bufferCap = options.bufferCap ?? 64;
80
+
81
+ const eventsPath = env.PI_CREW_EVENTS_PATH;
82
+ const runId = env.PI_CREW_BROKER_RUN_ID;
83
+ const taskId = env.PI_CREW_TASK_ID;
84
+
85
+ const windowStarts: number[] = [];
86
+ const pending: { type: string; data: Record<string, unknown>; droppedSinceLast: number }[] = [];
87
+ const counters = { accepted: 0, droppedSchema: 0, droppedRate: 0, droppedBuffer: 0 };
88
+ // Dropped-due-to-rate counter surfaced on the next ACCEPTED event (the
89
+ // operator sees suppression without a flood of drop events).
90
+ let suppressedSinceLast = 0;
91
+
92
+ const write = (item: { type: string; data: Record<string, unknown>; droppedSinceLast: number }): boolean => {
93
+ try {
94
+ appendEvent(eventsPath as string, {
95
+ type: item.type,
96
+ runId,
97
+ taskId,
98
+ data: item.droppedSinceLast > 0 ? { ...item.data, droppedSinceLast: item.droppedSinceLast } : item.data,
99
+ });
100
+ return true;
101
+ } catch {
102
+ return false;
103
+ }
104
+ };
105
+
106
+ const flushInternal = (currentDropped: number): number => {
107
+ let droppedCarry = currentDropped;
108
+ while (pending.length > 0) {
109
+ const item = pending[0]!;
110
+ if (write(item)) {
111
+ pending.shift();
112
+ counters.accepted++;
113
+ droppedCarry = 0;
114
+ } else {
115
+ // Writer still failing — keep the queue (bounded), stop here.
116
+ return droppedCarry;
117
+ }
118
+ }
119
+ return droppedCarry;
120
+ };
121
+
122
+ return {
123
+ emit(type: string, data: Record<string, unknown>): boolean {
124
+ if (!eventsPath || !runId) return false; // non-team context — no-op
125
+ if (!WORKER_EVENT_TYPE_PATTERN.test(type)) {
126
+ counters.droppedSchema++;
127
+ return false;
128
+ }
129
+ // Sliding-window rate limit.
130
+ const t = now();
131
+ while (windowStarts.length > 0 && t - windowStarts[0]! >= windowMs) windowStarts.shift();
132
+ if (windowStarts.length >= maxPerWindow) {
133
+ counters.droppedRate++;
134
+ suppressedSinceLast++;
135
+ return false;
136
+ }
137
+ windowStarts.push(t);
138
+ // Drain any queued items first (FIFO — oldest first), then the new one.
139
+ // The new event carries the suppression count (queue drops are already
140
+ // counted on their own accepted re-write).
141
+ if (pending.length > 0) {
142
+ const carry = flushInternal(0);
143
+ if (carry > 0) {
144
+ // Writer failing: queue the new item under the FIFO cap.
145
+ if (pending.length >= bufferCap) {
146
+ pending.shift();
147
+ counters.droppedBuffer++;
148
+ }
149
+ pending.push({ type, data, droppedSinceLast: 0 });
150
+ return true; // accepted (queued)
151
+ }
152
+ }
153
+ if (write({ type, data, droppedSinceLast: suppressedSinceLast })) {
154
+ counters.accepted++;
155
+ suppressedSinceLast = 0;
156
+ return true;
157
+ }
158
+ if (pending.length >= bufferCap) {
159
+ pending.shift();
160
+ counters.droppedBuffer++;
161
+ }
162
+ pending.push({ type, data, droppedSinceLast: suppressedSinceLast });
163
+ suppressedSinceLast = 0;
164
+ return true;
165
+ },
166
+ flush(): void {
167
+ flushInternal(0);
168
+ },
169
+ stats(): { accepted: number; droppedSchema: number; droppedRate: number; droppedBuffer: number; queued: number } {
170
+ return { ...counters, queued: pending.length };
171
+ },
172
+ };
173
+ }
@@ -10,26 +10,26 @@ clusters are extracted into subdirectories; the remaining files stay at the root
10
10
  | [`child-pi/`](./child-pi/) | B-1 | child Pi worker spawn/lifecycle/steering/transcript | `child-pi.ts`, `child-pi-spawn.ts`, `child-pi-kill.ts`, `child-pi-constants.ts`, `child-pi-steering.ts`, `child-pi-streams.ts`, `child-pi-transcript.ts` |
11
11
  | [`broker/`](./broker/) | B-2 | crew broker server/client/auth (child-pi ↔ parent IPC) | `crew-broker.ts`, `crew-broker-client.ts`, `crew-broker-child.ts`, `crew-broker-tokens.ts`, `broker-issuer.ts` |
12
12
  | [`task-runner/`](./task-runner/) | (pre-existing) | per-task execution (pre-execution, child-executor) | `child-executor.ts`, `pre-execution.ts`, ... |
13
- | [`live-session/`](./live-session/) | 3 | live agent control/manager, session runtime, IRC, health, extension bridge | `live-session-runtime.ts`, `live-agent-manager.ts`, `live-agent-control.ts`, `live-control-realtime.ts`, `live-irc.ts`, `live-session-health.ts`, `live-extension-bridge.ts`, `intercom-bridge.ts` |
14
- | [`recovery/`](./recovery/) | 4 | crash recovery, retry, checkpoint | `crash-recovery.ts`, `crash-classification.ts`, `recovery-recipes.ts`, `overflow-recovery.ts`, `retry-executor.ts`, `retry-runner.ts`, `checkpoint.ts` |
13
+ | [`live-session/`](./live-session/) | 3 | live agent control/manager, session runtime, IRC, health, extension bridge | `live-session-runtime.ts`, `live-agent-manager.ts`, `live-agent-control.ts`, `live-control-realtime.ts`, `live-irc.ts`, `live-session-health.ts`, `live-extension-bridge.ts` |
14
+ | [`recovery/`](./recovery/) | 4 | crash recovery, retry, checkpoint | `crash-recovery.ts`, `crash-classification.ts`, `recovery-recipes.ts`, `overflow-recovery.ts`, `retry-executor.ts`, `checkpoint.ts` |
15
15
  | [`scheduling/`](./scheduling/) | 5 | scheduler, concurrency, task-graph, batch-barrier, parallel utils | `scheduler.ts`, `semaphore.ts`, `concurrency.ts`, `task-graph.ts`, `task-graph-scheduler.ts`, `batch-barrier.ts`, `coalesce-tasks.ts`, `run-coalesced-task-group.ts`, `global-worker-cap.ts`, `parallel-research.ts`, `parallel-utils.ts` |
16
- | [`verification/`](./verification/) | 6a | verification gates, green-contract, post-checks, completion-guard | `verification-gates.ts`, `verification-integrity.ts`, `verification-worktree.ts`, `green-contract.ts`, `post-checks.ts`, `completion-guard.ts` |
16
+ | [`verification/`](./verification/) | 6a | verification gates, green-contract, completion-guard | `verification-gates.ts`, `verification-integrity.ts`, `verification-worktree.ts`, `green-contract.ts`, `completion-guard.ts` |
17
17
  | [`model/`](./model/) | 6b | model fallback/resolver/scope, pi-args, runtime policy/resolver/warmup | `model-fallback.ts`, `model-resolver.ts`, `model-scope.ts`, `pi-args.ts`, `runtime-policy.ts`, `runtime-resolver.ts`, `runtime-warmup.ts` |
18
- | [`output/`](./output/) | 6c | streaming output, progress tracking, sidechain, result extraction | `streaming-output.ts`, `stream-preview.ts`, `output-validator.ts`, `pi-json-output.ts`, `tool-progress.ts`, `progress-event-coalescer.ts`, `progress-tracker.ts`, `result-extractor.ts`, `sidechain-output.ts` |
18
+ | [`output/`](./output/) | 6c | streaming output, progress tracking, sidechain, result extraction | `streaming-output.ts`, `output-validator.ts`, `pi-json-output.ts`, `progress-event-coalescer.ts`, `progress-tracker.ts`, `result-extractor.ts`, `sidechain-output.ts` |
19
19
  | [`heartbeat/`](./heartbeat/) | 6d | heartbeat gradient/watcher, worker heartbeat/startup | `heartbeat-gradient.ts`, `heartbeat-watcher.ts`, `worker-heartbeat.ts`, `worker-startup.ts` |
20
- | [`process/`](./process/) | 6e | cancellation, process lifecycle, zombie scanner, stdio guard | `cancellation-token.ts`, `cancellation.ts`, `process-lifecycle.ts`, `zombie-scanner.ts`, `post-exit-stdio-guard.ts` |
20
+ | [`process/`](./process/) | 6e | cancellation, zombie scanner, stdio guard | `cancellation-token.ts`, `cancellation.ts`, `zombie-scanner.ts`, `post-exit-stdio-guard.ts` |
21
21
  | [`goal-workflow/`](./goal-workflow/) | 6f | goal achievement/evaluation/loop, dynamic-workflow, adaptive plan | `goal-achievement.ts`, `goal-evaluator.ts`, `goal-loop-runner.ts`, `goal-state-store.ts`, `dynamic-workflow-context.ts`, `dynamic-workflow-runner.ts`, `adaptive-plan.ts`, `plan-templates.ts` |
22
- | [`compaction/`](./compaction/) | 6g | compaction pipeline, tool-output-pruner, important-line-classifier, compact-stages | `compact-pipeline.ts`, `compaction-summary.ts`, `tool-output-pruner.ts`, `important-line-classifier.ts`, [`compact-stages/`](./compaction/compact-stages/) |
22
+ | [`compaction/`](./compaction/) | 6g | compaction pipeline, tool-output-pruner, important-line-classifier, compact-stages | `compact-pipeline.ts`, `tool-output-pruner.ts`, `important-line-classifier.ts`, [`compact-stages/`](./compaction/compact-stages/) |
23
23
  | [`custom-tools/`](./custom-tools/) | (pre-existing) | custom tool definitions | `irc-tool.ts`, `submit-result-tool.ts`, ... |
24
24
  | [`errors/`](./errors/) | (pre-existing) | error types | ... |
25
25
 
26
26
  ## Remaining flat files at root (~77)
27
27
 
28
28
  - **team-runner** — `team-runner.ts` (the orchestrator) + `run-worker.ts`, `run-tracker.ts`
29
- - **chain/pipeline runners** — `chain-parser.ts`, `chain-runner.ts`, `pipeline-runner.ts`, `direct-run.ts`
29
+ - **chain runners** — `chain-parser.ts`, `chain-runner.ts`, `direct-run.ts`
30
30
  - **foreground/background runners** — `foreground-control.ts`, `foreground-watchdog.ts`, `async-runner.ts`, `background-runner.ts`
31
31
  - **crew agent** — `crew-agent-records.ts`, `crew-agent-runtime.ts`, `crew-hooks.ts`, `single-agent-compose.ts`
32
- - **observability** — `effectiveness.ts`, `agent-observability.ts`, `command-trace.ts`, `metric-parser.ts`
32
+ - **observability** — `effectiveness.ts`, `agent-observability.ts`, `command-trace.ts`
33
33
  - **misc** — `stale-reconciler.ts`, `parent-guard.ts`, `manifest-cache.ts`, `event-stream-bridge.ts`, `policy-engine.ts`, `role-permission.ts`, `workspace-lock.ts`, `workspace-tree.ts`, `session-*.ts`, ...
34
34
 
35
35
  See `docs/COVERAGE-ASSESSMENT-2026-08-01.md` § "Source reorganization" for the reorg history + remaining-cluster plan.
@@ -3,6 +3,7 @@ import * as fs from "node:fs";
3
3
  import { createRequire } from "node:module";
4
4
  import * as path from "node:path";
5
5
  import { fileURLToPath, pathToFileURL } from "node:url";
6
+ import { getCrewEnv } from "../config/env-vars.ts";
6
7
  import { appendEventAsync } from "../state/event-log/event-log.ts";
7
8
  import type { TeamRunManifest } from "../state/types.ts";
8
9
  import { WINDOWS_ESSENTIAL_ENV_VARS } from "../utils/env-allowlist.ts";
@@ -54,8 +55,11 @@ export function resolveJitiRegisterPath(packageRoot = packageRootFromRuntime(),
54
55
  path.join(path.dirname(pkgPath), "dist", "register.mjs"),
55
56
  ];
56
57
  for (const c of candidates) if (exists(c)) return c;
57
- } catch {
58
- // Fall through.
58
+ } catch (error) {
59
+ // R17-B4 (LOW): fall-through is benign (undefined → next loader
60
+ // strategy), but keep the require.resolve root cause visible under
61
+ // PI_TEAMS_DEBUG instead of a bare catch.
62
+ logInternalError("async-runner.jiti-resolve", error, "require.resolve('jiti/package.json') fallback failed", "debug");
59
63
  }
60
64
  return undefined;
61
65
  }
@@ -124,7 +128,7 @@ export function getBackgroundRunnerCommand(
124
128
  // default precisely so silent runner deaths (like the explore→code-review
125
129
  // transition crash) leave a native stack/heap/environment trace. Users who
126
130
  // don't want report files can opt out with PI_CREW_BG_REPORT_ON_FATAL=0.
127
- const reportOn = !(process.env.PI_CREW_BG_REPORT_ON_FATAL === "0" || process.env.PI_TEAMS_BG_REPORT_ON_FATAL === "0");
131
+ const reportOn = !(getCrewEnv("PI_CREW_BG_REPORT_ON_FATAL") === "0" || getCrewEnv("PI_TEAMS_BG_REPORT_ON_FATAL") === "0");
128
132
  const reportDir = reportDirectory ?? path.dirname(runnerPath);
129
133
  const reportFlags = reportOn ? ["--report-on-fatalerror", "--report-compact", `--report-directory=${reportDir}`] : [];
130
134
  if (loader.kind === "jiti") {
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { allAgents, discoverAgents } from "../agents/discover-agents.ts";
4
4
  import { loadConfig } from "../config/config.ts";
5
+ import { getCrewEnv } from "../config/env-vars.ts";
5
6
  import { atomicWriteFile } from "../state/atomic-write.ts";
6
7
  import { withRunLockSync } from "../state/coordination/locks.ts";
7
8
  import { appendEvent, appendEventFireAndForget } from "../state/event-log/event-log.ts";
@@ -10,6 +11,7 @@ import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
10
11
  import { allTeams, discoverTeams } from "../teams/discover-teams.ts";
11
12
  import { errorMessage } from "../utils/guards.ts";
12
13
  import { projectCrewRoot } from "../utils/paths.ts";
14
+ import { assertSafePathId } from "../utils/safe-paths.ts";
13
15
  import { allWorkflows, discoverWorkflows } from "../workflows/discover-workflows.ts";
14
16
  // Heavy runtime — lazy-loaded to avoid pulling team-runner into background-runner
15
17
  // at module load time. Only needed when a background run actually starts.
@@ -27,7 +29,7 @@ let _cachedExecuteTeamRun: typeof ExecuteTeamRunFn | undefined;
27
29
  * aborts via the shared AbortController, then force-exits after a grace
28
30
  * period in case the abort signal does not propagate to all execution paths. */
29
31
  const MAX_BACKGROUND_RUN_MS = (() => {
30
- const env = Number.parseInt(process.env.PI_CREW_MAX_RUN_MS ?? "", 10);
32
+ const env = Number.parseInt(getCrewEnv("PI_CREW_MAX_RUN_MS") ?? "", 10);
31
33
  return Number.isFinite(env) && env > 0 ? env : 2 * 60 * 60 * 1000;
32
34
  })();
33
35
  async function executeTeamRun(...args: Parameters<typeof ExecuteTeamRunFn>): Promise<Awaited<ReturnType<typeof ExecuteTeamRunFn>>> {
@@ -59,7 +61,7 @@ import { expandParallelResearchWorkflow } from "./scheduling/parallel-research.t
59
61
  * while keeping diagnostics available when explicitly enabled.
60
62
  */
61
63
  function debugLog(message: string): void {
62
- if (process.env.PI_CREW_DEBUG) console.log(message);
64
+ if (getCrewEnv("PI_CREW_DEBUG")) console.log(message);
63
65
  }
64
66
 
65
67
  /**
@@ -213,7 +215,7 @@ export function startInterruptGuard(
213
215
  const controlPath = path.join(manifest.stateRoot, "foreground-control.json");
214
216
  // FIX: Made configurable via PI_CREW_INTERRUPT_GUARD_INTERVAL_MS env var.
215
217
  // Default 250ms balances fast SIGINT response against filesystem overhead.
216
- const interruptGuardInterval = Number(process.env.PI_CREW_INTERRUPT_GUARD_INTERVAL_MS) || 250;
218
+ const interruptGuardInterval = Number(getCrewEnv("PI_CREW_INTERRUPT_GUARD_INTERVAL_MS")) || 250;
217
219
  // RT-4 FIX: Module-local gate so the interrupt body runs only once per
218
220
  // interrupt request. Without this, the guard re-fires every
219
221
  // interruptGuardInterval (250ms) — each tick does a full
@@ -410,6 +412,13 @@ async function main(): Promise<void> {
410
412
  const _cwd = argValue("--cwd");
411
413
  const _runId = argValue("--run-id");
412
414
  if (_cwd && _runId) {
415
+ // R11-2 (LOW, §ROUND 11 security hardening): assert the argv-supplied
416
+ // runId at the argValue boundary BEFORE any path construction — the
417
+ // background.log join would otherwise embed an untrusted runId (future
418
+ // user-supplied --resume) → path traversal outside the run root. Throws
419
+ // at startup for unsafe ids (intended fail-fast; placed OUTSIDE the
420
+ // best-effort try/catch so the hardening is never silently swallowed).
421
+ assertSafePathId("runId", _runId);
413
422
  try {
414
423
  // Use projectCrewRoot() so the background log lives next to the
415
424
  // manifest in either .crew/state/runs/ or .pi/teams/state/runs/
@@ -464,6 +473,10 @@ async function main(): Promise<void> {
464
473
  const cwd = argValue("--cwd");
465
474
  const runId = argValue("--run-id");
466
475
  if (!cwd || !runId) return undefined;
476
+ // R11-2 (LOW, §ROUND 11): same argv boundary hardening as main() — this
477
+ // IIFE runs at MODULE LOAD, so an unsafe runId throws before the runner
478
+ // starts (intended fail-fast, matching run-import.ts:105 pattern).
479
+ assertSafePathId("runId", runId);
467
480
  // Use projectCrewRoot() to honour the .pi/teams/ fallback (issue #29).
468
481
  return path.join(projectCrewRoot(cwd), "state", "runs", runId, "exit-code.txt");
469
482
  })();
@@ -611,7 +624,7 @@ async function main(): Promise<void> {
611
624
  setupUnhandledRejectionGuard(rejectionGuardState, abortController, setExitFlag);
612
625
 
613
626
  // Start parent guard — if parent is already dead, exit immediately
614
- const parentPid = Number(process.env.PI_CREW_PARENT_PID);
627
+ const parentPid = Number(getCrewEnv("PI_CREW_PARENT_PID"));
615
628
  if (parentPid > 0) startParentGuard(parentPid);
616
629
  // NOTE: intentionally no unref() — the guard keeps the event loop alive
617
630
  // to prevent premature worker exit. See parent-guard.ts:86 for rationale.
@@ -880,10 +893,32 @@ async function main(): Promise<void> {
880
893
  if (manifest.status === "failed" || manifest.status === "cancelled" || manifest.status === "blocked") process.exitCode = 1;
881
894
  } catch (error) {
882
895
  // Terminate live agents on failure too — agents are done when the run fails
896
+ const message = errorMessage(error);
883
897
  try {
884
- const loaded = withRunLockSync(manifest, () => loadRunManifestById(cwd, runId), { staleMs: 30_000 }); // Use withRunLockSync to prevent race with concurrent writers (e.g., stale reconciler)
885
- // between the read and the subsequent save.
886
- const manifestToUse = loaded?.manifest ?? manifest;
898
+ // R14-2: re-read INSIDE the lock and derive the failed-status write from the
899
+ // FRESH manifest. The outer `manifest` is a pre-throw snapshot — using it
900
+ // here could flip a concurrently completed/cancelled run back to "failed"
901
+ // (updateRunStatus validates against the fresh on-disk status; an illegal
902
+ // flip throws and is swallowed by the best-effort catch below). Keeping the
903
+ // write inside withRunLockSync prevents race with concurrent writers
904
+ // (e.g., stale reconciler) between the read and the subsequent save.
905
+ const manifestToUse = withRunLockSync(
906
+ manifest,
907
+ () => {
908
+ const loaded = loadRunManifestById(cwd, runId);
909
+ const fresh = loaded?.manifest ?? manifest;
910
+ if (fresh) {
911
+ manifest = updateRunStatus(fresh, "failed", message);
912
+ appendEvent(manifest.eventsPath, {
913
+ type: "async.failed",
914
+ runId: manifest.runId,
915
+ message,
916
+ });
917
+ }
918
+ return fresh;
919
+ },
920
+ { staleMs: 30_000 },
921
+ );
887
922
  if (manifestToUse) {
888
923
  // LAZY: live-agent-manager only needed on failure cleanup path; avoid module load at hot path.
889
924
  const { terminateLiveAgentsForRun } = await import("./live-session/live-agent-manager.ts");
@@ -894,13 +929,6 @@ async function main(): Promise<void> {
894
929
  } catch {
895
930
  /* best-effort */
896
931
  }
897
- const message = errorMessage(error);
898
- manifest = updateRunStatus(manifest, "failed", message);
899
- appendEvent(manifest.eventsPath, {
900
- type: "async.failed",
901
- runId: manifest.runId,
902
- message,
903
- });
904
932
  process.exitCode = 1;
905
933
  console.log(`[background-runner] catch block, error=${errorMessage(error)}`);
906
934
  } finally {
@@ -23,8 +23,15 @@ export interface BrokerSpawnCredentials {
23
23
  /** Issuer signature: given a runId (+optional taskId for per-task tokens),
24
24
  * return credentials or undefined when the broker is disabled / this process
25
25
  * is not the root session. taskId is optional for backward compat — callers
26
- * that still pass only runId receive the legacy per-run token. */
27
- export type BrokerIssuer = (runId: string, taskId?: string) => Promise<BrokerSpawnCredentials | undefined>;
26
+ * that still pass only runId receive the legacy per-run token.
27
+ *
28
+ * ADR-5 §4 (governed nesting): `childDepth` carries the DEPTH-2+ grandchild's
29
+ * depth when the root-side delegate handler spawns it. The issuer mints only
30
+ * for children that may themselves delegate (childDepth < resolved
31
+ * PI_CREW_MAX_DEPTH) — at the default maxDepth=2 a depth-2 grandchild gets NO
32
+ * credentials (env containment AC: no PI_CREW_BROKER_SOCKET/TOKEN at depth 2).
33
+ * Undefined childDepth = legacy worker spawn (depth 1) — unchanged behavior. */
34
+ export type BrokerIssuer = (runId: string, taskId?: string, childDepth?: number) => Promise<BrokerSpawnCredentials | undefined>;
28
35
 
29
36
  let activeIssuer: BrokerIssuer | undefined;
30
37
 
@@ -123,17 +123,54 @@ export class BrokerTokenRegistry {
123
123
  * if the candidate matches the run's orchestrator token, "worker" if it
124
124
  * matches a per-task/per-run token, or null if neither. Orchestrator key
125
125
  * is checked FIRST so an orchestrator token can never be confused with a
126
- * task token (the two are cryptographically distinct by construction). */
126
+ * task token (the two are cryptographically distinct by construction).
127
+ * Semantics unchanged since extraction; now delegates to
128
+ * tokenRoleWithMatchKind() so both share one compare path. */
127
129
  tokenRole(runId: string, taskId: string | undefined, candidate: unknown): "orchestrator" | "worker" | null {
130
+ const resolved = this.tokenRoleWithMatchKind(runId, taskId, candidate);
131
+ return resolved === null ? null : resolved.role;
132
+ }
133
+
134
+ /** Same resolution as tokenRole() but also reports HOW the token matched:
135
+ * `"compound"` (task-scoped `${runId}:${taskId}` key — or the
136
+ * orchestrator key, which is runId-prefixed by construction and can
137
+ * never be reached via the fallback) vs `"runId-fallback"` (legacy
138
+ * bare-runId key matched because no compound key was issued).
139
+ *
140
+ * WP-2/R2 (ADR-0 2026-08-17-waiting-producer-ask item 6): the broker's
141
+ * `wait.*` methods authenticate via task-scoped tokens ONLY — a
142
+ * `"runId-fallback"` match is rejected at the dispatch site with a
143
+ * migrate hint. Legacy methods keep using tokenRole()/matches() and
144
+ * therefore keep accepting the fallback (backward compat preserved).
145
+ *
146
+ * Match semantics mirror tokenRole() exactly: the bare-runId fallback is
147
+ * consulted ONLY when no compound key exists for (runId, taskId) — a
148
+ * candidate matching the bare key while a DIFFERENT compound key exists
149
+ * still returns null (no accidental fallback activation). */
150
+ tokenRoleWithMatchKind(
151
+ runId: string,
152
+ taskId: string | undefined,
153
+ candidate: unknown,
154
+ ): { role: "orchestrator" | "worker"; matchKind: "compound" | "runId-fallback" } | null {
128
155
  if (BrokerTokenRegistry.equalConstTime(this.map.get(this.orchestratorKey(runId)), candidate)) {
129
- return "orchestrator";
156
+ return { role: "orchestrator", matchKind: "compound" };
157
+ }
158
+ // No taskId → the key degenerates to bare runId; there is no task
159
+ // scoping at all, so an honest matchKind is "runId-fallback".
160
+ if (taskId === undefined) {
161
+ return BrokerTokenRegistry.equalConstTime(this.map.get(runId), candidate)
162
+ ? { role: "worker", matchKind: "runId-fallback" }
163
+ : null;
164
+ }
165
+ const compound = this.map.get(this.key(runId, taskId));
166
+ if (compound !== undefined) {
167
+ return BrokerTokenRegistry.equalConstTime(compound, candidate) ? { role: "worker", matchKind: "compound" } : null;
130
168
  }
131
- let expected = this.map.get(this.key(runId, taskId));
132
169
  // Backward-compat: per-run token issued without taskId.
133
- if (expected === undefined && taskId) {
134
- expected = this.map.get(runId);
170
+ if (BrokerTokenRegistry.equalConstTime(this.map.get(runId), candidate)) {
171
+ return { role: "worker", matchKind: "runId-fallback" };
135
172
  }
136
- return BrokerTokenRegistry.equalConstTime(expected, candidate) ? "worker" : null;
173
+ return null;
137
174
  }
138
175
 
139
176
  /** Remove the token for `runId` (+optional `taskId`).