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,157 @@
1
+ /**
2
+ * wait-status-cache.ts — R10-3: mtime+size-gated load cache for task.waitStatus.
3
+ *
4
+ * Problem (docs/refactor-plan.review.md §Round 10, R10-3): the broker's
5
+ * task.waitStatus poll loop ticks every 200ms per waiter and each tick calls
6
+ * loadRunManifestById() — stat manifest + stat tasks + (on the state-store
7
+ * manifest-cache miss, which every write invalidates via generation bump) a
8
+ * full parse of BOTH manifest.json and tasks.json. With N concurrent waiters
9
+ * on a busy run that is ~5-10 syscalls × 5 ticks/sec × N.
10
+ *
11
+ * Fix: each BROKER INSTANCE holds one WaitStatusCache (Map keyed by runId —
12
+ * the broker serves a single cwd). Per tick it stats only manifest.json and
13
+ * tasks.json (via the exact same path derivation loadRunManifestById uses
14
+ * internally: createRunPaths → scopeBaseRoot(cwd)); the expensive
15
+ * loadRunManifestById() is invoked only when mtimeMs or size changed. All
16
+ * waiters on the broker share the entry, so a never-changing run costs ONE
17
+ * parse total (initial) instead of N×polls.
18
+ *
19
+ * Semantics vs the uncached path:
20
+ * - Observable behavior is identical: waiters still poll on the same 200ms
21
+ * schedule; they just avoid the parse when nothing changed. A status
22
+ * change on disk always lands via an atomic rename (new mtime), so the
23
+ * next tick re-parses and wakes the waiter within one poll (mtimeMs on
24
+ * Linux/macOS has sub-millisecond precision; on FAT-derived filesystems
25
+ * with coarse 1-2s mtime granularity staleness is bounded by that
26
+ * granularity — the same tradeoff the state-store manifestCache makes).
27
+ * - The actual parse is delegated to loadRunManifestById itself, so all of
28
+ * its correctness semantics (Windows stat retry, sentinel retry loop,
29
+ * STATE-3 quarantine, validateRunManifestPaths containment/symlink
30
+ * checks) still run at every reload boundary. Between reloads we serve
31
+ * the cached parse, exactly like the in-store manifestCache does.
32
+ * - A run whose files are missing gets a 0/0 stamp; while they stay
33
+ * missing, ticks return the cached undefined ("no-manifest" error),
34
+ * matching the uncached behavior of re-resolving to undefined.
35
+ *
36
+ * Memory: entries are evicted when a run's last broker connection closes
37
+ * (see crew-broker.ts cleanup) and by a hard FIFO cap, so long-lived
38
+ * brokers cannot grow the Map without bound across runs.
39
+ */
40
+
41
+ import * as fs from "node:fs";
42
+ import { createRunPaths, loadRunManifestById } from "../../state/stores/state-store.ts";
43
+
44
+ /** Shape returned by loadRunManifestById (derived — no duplicate type imports). */
45
+ type LoadedRun = ReturnType<typeof loadRunManifestById>;
46
+
47
+ /** Loader seam: defaults to the real loadRunManifestById; tests inject a spy. */
48
+ export type WaitStatusLoader = (cwd: string, runId: string) => LoadedRun;
49
+
50
+ interface WaitStatusCacheEntry {
51
+ manifestMtimeMs: number;
52
+ manifestSize: number;
53
+ tasksMtimeMs: number;
54
+ tasksSize: number;
55
+ loaded: LoadedRun;
56
+ }
57
+
58
+ /** Hard cap so a pathological broker (many runs, no close events) stays bounded. */
59
+ const WAIT_STATUS_CACHE_MAX = 128;
60
+
61
+ interface StatStamp {
62
+ manifestMtimeMs: number;
63
+ manifestSize: number;
64
+ tasksMtimeMs: number;
65
+ tasksSize: number;
66
+ }
67
+
68
+ /** Stat manifest+tasks; a missing/unstattable file is represented as 0/0. */
69
+ function statStamp(manifestPath: string, tasksPath: string): StatStamp {
70
+ let manifestMtimeMs = 0;
71
+ let manifestSize = 0;
72
+ let tasksMtimeMs = 0;
73
+ let tasksSize = 0;
74
+ try {
75
+ const st = fs.statSync(manifestPath);
76
+ manifestMtimeMs = st.mtimeMs;
77
+ manifestSize = st.size;
78
+ } catch {
79
+ /* missing or transient — treat as 0/0 */
80
+ }
81
+ try {
82
+ const st = fs.statSync(tasksPath);
83
+ tasksMtimeMs = st.mtimeMs;
84
+ tasksSize = st.size;
85
+ } catch {
86
+ /* missing or transient — treat as 0/0 */
87
+ }
88
+ return { manifestMtimeMs, manifestSize, tasksMtimeMs, tasksSize };
89
+ }
90
+
91
+ function sameStamp(a: StatStamp, b: StatStamp): boolean {
92
+ return (
93
+ a.manifestMtimeMs === b.manifestMtimeMs &&
94
+ a.manifestSize === b.manifestSize &&
95
+ a.tasksMtimeMs === b.tasksMtimeMs &&
96
+ a.tasksSize === b.tasksSize
97
+ );
98
+ }
99
+
100
+ export class WaitStatusCache {
101
+ private readonly entries = new Map<string, WaitStatusCacheEntry>();
102
+ private readonly loader: WaitStatusLoader;
103
+ private reloadsValue = 0;
104
+
105
+ constructor(options: { loader?: WaitStatusLoader } = {}) {
106
+ this.loader = options.loader ?? loadRunManifestById;
107
+ }
108
+
109
+ /** Diagnostic: number of times the (expensive) loader has been invoked. */
110
+ get reloads(): number {
111
+ return this.reloadsValue;
112
+ }
113
+
114
+ /** Diagnostic: number of cached runs. */
115
+ get size(): number {
116
+ return this.entries.size;
117
+ }
118
+
119
+ /**
120
+ * Stat-gated load. Fast path (stamp unchanged): 2 statSync calls, zero
121
+ * parses. Slow path (first sight or changed mtime/size): delegate to the
122
+ * loader, then record the POST-load stamp so a concurrent writer that
123
+ * landed between our pre-load stat and the parse forces the next tick to
124
+ * reload instead of serving a stale entry.
125
+ */
126
+ load(cwd: string, runId: string): LoadedRun {
127
+ // Same path derivation as resolveRunStateRoot inside
128
+ // loadRunManifestById (scopeBaseRoot(cwd) + runs subdir + contained
129
+ // runId) — createRunPaths exposes exactly that derivation publicly.
130
+ const paths = createRunPaths(cwd, runId);
131
+ const before = statStamp(paths.manifestPath, paths.tasksPath);
132
+ const cached = this.entries.get(runId);
133
+ if (cached && sameStamp(cached, before)) {
134
+ return cached.loaded;
135
+ }
136
+ const loaded = this.loader(cwd, runId);
137
+ this.reloadsValue += 1;
138
+ const after = statStamp(paths.manifestPath, paths.tasksPath);
139
+ if (this.entries.size >= WAIT_STATUS_CACHE_MAX && !this.entries.has(runId)) {
140
+ // Map preserves insertion order — evict the oldest entry.
141
+ const oldest = this.entries.keys().next().value;
142
+ if (oldest !== undefined) this.entries.delete(oldest);
143
+ }
144
+ this.entries.set(runId, { ...after, loaded });
145
+ return loaded;
146
+ }
147
+
148
+ /** Evict one run's entry (called when its last broker connection closes). */
149
+ delete(runId: string): void {
150
+ this.entries.delete(runId);
151
+ }
152
+
153
+ /** Drop everything (tests / broker stop). */
154
+ clear(): void {
155
+ this.entries.clear();
156
+ }
157
+ }
@@ -0,0 +1,281 @@
1
+ /**
2
+ * Per-task run budget enforcement for the team-run scheduler loop.
3
+ *
4
+ * Extracted from team-runner.ts (2026-08, Phase 2.6 maintainability split —
5
+ * CORE-4 extraction 7 + the RT-NEW-2 terminaliseRunWithDrain/drainPendingUnits
6
+ * cluster it depends on). Pure code motion: checkPerTaskBudget /
7
+ * PerTaskBudgetCheckResult / DrainOutcome / drainPendingUnits /
8
+ * terminaliseRunWithDrain / enforceRunBudget moved verbatim.
9
+ *
10
+ * drainPendingUnits + terminaliseRunWithDrain land here (not a separate
11
+ * shared file) so budget-enforcement.ts stays self-contained and the
12
+ * team-runner.ts ↔ budget-enforcement.ts import direction stays one-way
13
+ * (team-runner imports from here; this module never imports from
14
+ * team-runner.ts — checkPerTaskBudget was moved here for exactly that
15
+ * reason: enforceRunBudget calls it, and importing it back from
16
+ * team-runner.ts would create a cycle).
17
+ */
18
+ import { flushPendingAtomicWrites } from "../state/atomic-write.ts";
19
+ import { withRunLock } from "../state/coordination/locks.ts";
20
+ import { appendEventAsync } from "../state/event-log/event-log.ts";
21
+ import { loadRunManifestById, saveRunManifestAsync, saveRunTasksAsync, updateRunStatus } from "../state/stores/state-store.ts";
22
+ import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
23
+ import { aggregateUsage, formatTokens } from "../state/usage.ts";
24
+ import { logInternalError } from "../utils/internal-error.ts";
25
+ import { saveCrewAgents } from "./crew-agent-records.ts";
26
+ import { cancelNonTerminalTasks, markBlocked } from "./dispatch-batch.ts";
27
+ import { isNonTerminalTaskStatus, mergeTaskUpdatesPreservingTerminal } from "./merge-gate.ts";
28
+ import type { SchedulerContext, SchedulerDecision } from "./scheduler-context.ts";
29
+ import { recordsForMaterializedTasks } from "./task-display.ts";
30
+ import { mergeArtifacts } from "./team-runner-artifacts.ts";
31
+
32
+ /**
33
+ * Result of a per-task budget check against cumulative run usage.
34
+ */
35
+ export interface PerTaskBudgetCheckResult {
36
+ /** Whether the abort threshold was exceeded. */
37
+ abort: boolean;
38
+ /** Whether the warning threshold was exceeded. */
39
+ warning: boolean;
40
+ /** IDs of tasks that exceeded their fair share (>50% of remaining budget). */
41
+ fairShareViolators: string[];
42
+ /** Total tokens used so far. */
43
+ totalUsed: number;
44
+ }
45
+
46
+ /**
47
+ * Check cumulative token usage against per-task budget thresholds.
48
+ * Returns a structured result — callers decide how to act (warn vs abort).
49
+ *
50
+ * Exported for unit testing.
51
+ */
52
+ export function checkPerTaskBudget(
53
+ tasks: TeamTaskState[],
54
+ budgetTotal: number,
55
+ budgetWarning: number,
56
+ budgetAbort: number,
57
+ fairShareFraction = 0.5,
58
+ ): PerTaskBudgetCheckResult {
59
+ const usage = aggregateUsage(tasks);
60
+ const totalUsed = (usage?.input ?? 0) + (usage?.output ?? 0) + (usage?.cacheWrite ?? 0);
61
+ const abort = totalUsed >= budgetAbort * budgetTotal;
62
+ const warning = !abort && totalUsed >= budgetWarning * budgetTotal;
63
+ // Fair share threshold based on TOTAL budget, not remaining budget.
64
+ // This ensures a task that consumed 60% of total budget is flagged even
65
+ // if only 40% remains (40% * 50% = 20% threshold would miss the 60% usage).
66
+ const fairShareThreshold = budgetTotal * fairShareFraction;
67
+ const fairShareViolators: string[] = [];
68
+ for (const task of tasks) {
69
+ if (!task.usage) continue;
70
+ const taskTotal = (task.usage.input ?? 0) + (task.usage.output ?? 0) + (task.usage.cacheWrite ?? 0);
71
+ // Only flag tasks that individually consumed a significant portion of the
72
+ // budget (>10% of total) AND exceeded the fair share threshold.
73
+ if (fairShareThreshold > 0 && taskTotal > fairShareThreshold && taskTotal > budgetTotal * 0.1) {
74
+ fairShareViolators.push(task.id);
75
+ }
76
+ }
77
+ return { abort, warning, fairShareViolators, totalUsed };
78
+ }
79
+
80
+ /**
81
+ * Drain in-flight dispatch units (pendingUnits) by aborting the run-scoped
82
+ * controller and awaiting all settled promises before clearing the map.
83
+ *
84
+ * CORE-1 fix: without this, every early-return path inside the main while
85
+ * loop would abandon pendingUnits — leaving zombie child processes running
86
+ * with no one listening for their results.
87
+ *
88
+ * Exported so unit tests can exercise it directly.
89
+ */
90
+ /** Settled outcome of a single in-flight dispatch unit promise (returned by drainPendingUnits). */
91
+ export type DrainOutcome = PromiseSettledResult<{ manifest: TeamRunManifest; tasks: TeamTaskState[] }>;
92
+
93
+ export async function drainPendingUnits<
94
+ T extends { taskIds: string[]; promise: Promise<{ manifest: TeamRunManifest; tasks: TeamTaskState[] }> },
95
+ >(pendingUnits: Map<string, T>, controller?: AbortController): Promise<DrainOutcome[]> {
96
+ if (pendingUnits.size === 0) return [];
97
+ controller?.abort();
98
+ const outcomes = await Promise.allSettled([...pendingUnits.values()].map((p) => p.promise));
99
+ pendingUnits.clear();
100
+ return outcomes;
101
+ }
102
+
103
+ /**
104
+ * RT-NEW-2: terminalise a run as "failed" while draining in-flight dispatch
105
+ * units FIRST and merging their settled results under the run lock.
106
+ *
107
+ * Extracted verbatim from handleFailedTask (the FIXED reference) so every
108
+ * abort path behaves identically: drain pendingUnits (abort controller +
109
+ * await allSettled + clear), merge fulfilled outcomes into manifest/tasks
110
+ * under withRunLock (flushPendingAtomicWrites + loadRunManifestById +
111
+ * mergeArtifacts + mergeTaskUpdatesPreservingTerminal + save both), cancel
112
+ * (not skip) non-settled in-flight tasks so team resume can re-queue them,
113
+ * then markBlocked the remaining never-dispatched queued tasks.
114
+ *
115
+ * Previously enforceRunBudget skipped the drain+merge and called
116
+ * markBlocked directly — in-flight tasks (still "queued" in ctx.tasks since
117
+ * streaming dispatch never sets "running") were clobbered to "skipped",
118
+ * which team resume never re-queues → permanent work loss.
119
+ */
120
+ export async function terminaliseRunWithDrain(
121
+ ctx: SchedulerContext,
122
+ opts: { cancelMessage: string; blockedMessage: string; failedReason: string },
123
+ ): Promise<{ manifest: TeamRunManifest; tasks: TeamTaskState[] }> {
124
+ // Ever-dispatched tasks (monotonic set populated at dispatch). Using this
125
+ // instead of a pendingUnits snapshot closes the RT-NEW-2 race where a task
126
+ // whose unit settled + left pendingUnits before the abort — but whose task
127
+ // status isn't terminal yet — would otherwise fall through to markBlocked
128
+ // and be clobbered to "skipped" (observed CI flake: 02_b skipped on
129
+ // team-runner-budget-abort-inflight across v0.9.59 / cfd68d06 / 12386af2).
130
+ const inflightTaskIds = ctx.dispatchedTaskIds;
131
+ const outcomes = await drainPendingUnits(ctx.pendingUnits, ctx.runController);
132
+ const validResults: { manifest: TeamRunManifest; tasks: TeamTaskState[] }[] = [];
133
+ for (const outcome of outcomes) {
134
+ if (outcome.status === "fulfilled") validResults.push(outcome.value);
135
+ }
136
+ if (validResults.length > 0) {
137
+ // Merge under the run lock — same pattern as mergeUnitResult:
138
+ // flush pending writes, load disk state, merge artifacts + tasks,
139
+ // save atomically.
140
+ const mergeResult = await withRunLock(ctx.manifest, async () => {
141
+ flushPendingAtomicWrites();
142
+ const disk = loadRunManifestById(ctx.manifest.cwd, ctx.manifest.runId);
143
+ const diskManifest = disk?.manifest ?? ctx.manifest;
144
+ const reconciledArtifacts = mergeArtifacts([
145
+ ...diskManifest.artifacts,
146
+ ...validResults.flatMap((item) => item.manifest.artifacts),
147
+ ]);
148
+ const resultManifest = updateRunStatus(
149
+ { ...diskManifest, artifacts: reconciledArtifacts },
150
+ "running",
151
+ "Merged in-flight results during failed-task abort.",
152
+ );
153
+ const resultTasks = mergeTaskUpdatesPreservingTerminal(disk?.tasks ?? ctx.tasks, validResults);
154
+ await saveRunManifestAsync(resultManifest);
155
+ await saveRunTasksAsync(resultManifest, resultTasks);
156
+ return { resultManifest, resultTasks };
157
+ });
158
+ ctx.manifest = mergeResult.resultManifest;
159
+ ctx.tasks = mergeResult.resultTasks;
160
+ }
161
+ // Cancel in-flight tasks that did NOT settle (e.g. rejected promises)
162
+ // so team resume CAN re-queue them. markBlocked maps queued→skipped,
163
+ // which resume never re-queues — work would be lost permanently. Only
164
+ // cancel tasks that are both in-flight AND still non-terminal (settled
165
+ // tasks with a terminal status are preserved).
166
+ ctx.tasks = cancelNonTerminalTasks(
167
+ ctx.tasks,
168
+ "cancelled",
169
+ opts.cancelMessage,
170
+ (task) => inflightTaskIds.has(task.id) && isNonTerminalTaskStatus(task.status),
171
+ );
172
+ // Remaining queued tasks (never dispatched) → skipped (original behavior
173
+ // preserved for downstream tasks not yet in-flight).
174
+ ctx.tasks = markBlocked(ctx.tasks, opts.blockedMessage);
175
+ await saveRunTasksAsync(ctx.manifest, ctx.tasks);
176
+ saveCrewAgents(ctx.manifest, recordsForMaterializedTasks(ctx.manifest, ctx.tasks, ctx.runtimeKind));
177
+ ctx.manifest = updateRunStatus(ctx.manifest, "failed", opts.failedReason);
178
+ return { manifest: ctx.manifest, tasks: ctx.tasks };
179
+ }
180
+
181
+ /**
182
+ * CORE-4 extraction 7: enforce per-task run budget after each batch merge.
183
+ *
184
+ * When `input.budgetTotal` is set (and not unlimited), checks cumulative usage
185
+ * against warn/abort thresholds and a fair-share heuristic. On abort, marks all
186
+ * non-terminal tasks blocked, persists the run as failed, and returns a
187
+ * `{ kind: "return" }` decision so the caller short-circuits the loop.
188
+ * Otherwise emits `run.budget_warning` / `task.budget_fair_share` events and
189
+ * returns null (continue).
190
+ *
191
+ * Reads `ctx.input` (budget config). Mutates `ctx.tasks` / `ctx.manifest` only
192
+ * in the abort path. The caller syncs these locals back after the call.
193
+ *
194
+ * @param ctx The scheduler context.
195
+ * @returns `{ kind: "return", result }` on budget abort; `null` otherwise.
196
+ */
197
+ export async function enforceRunBudget(ctx: SchedulerContext): Promise<SchedulerDecision | null> {
198
+ const input = ctx.input;
199
+ const tasks = ctx.tasks;
200
+ const manifest = ctx.manifest;
201
+ // Per-task budget enforcement: check cumulative usage after each batch merge.
202
+ // This prevents a single task from consuming 100% of the budget before
203
+ // abort triggers (the goal-loop only checks at turn boundaries).
204
+ if (input.budgetTotal !== undefined && input.budgetTotal > 0 && input.budgetUnlimited !== true) {
205
+ const warnThreshold = input.budgetWarning ?? 0.8;
206
+ const abortThreshold = input.budgetAbort ?? 0.95;
207
+ const budgetCheck = checkPerTaskBudget(tasks, input.budgetTotal, warnThreshold, abortThreshold);
208
+
209
+ if (budgetCheck.abort) {
210
+ const message = `Per-task budget abort threshold exceeded: ${formatTokens(budgetCheck.totalUsed)}/${formatTokens(input.budgetTotal)} (${Math.round((budgetCheck.totalUsed / input.budgetTotal) * 100)}%)`;
211
+ // ROUND 12: console → logInternalError with explicit "warn" severity
212
+ // (run-termination event — operator channel; default "debug" is gated).
213
+ logInternalError("team-runner.budget-abort", new Error(message), `runId=${manifest.runId}`, "warn");
214
+ await appendEventAsync(manifest.eventsPath, {
215
+ type: "run.budget_abort",
216
+ runId: manifest.runId,
217
+ message,
218
+ data: {
219
+ budgetTotal: input.budgetTotal,
220
+ budgetUsed: budgetCheck.totalUsed,
221
+ threshold: "abort",
222
+ },
223
+ });
224
+ // RT-NEW-2: drain in-flight units + merge settled results before
225
+ // terminalising, so in-flight tasks become completed/cancelled (not
226
+ // skipped). Same shared helper handleFailedTask uses. Run-failed
227
+ // reason stays the budget message.
228
+ const result = await terminaliseRunWithDrain(ctx, {
229
+ cancelMessage: `Cancelled by budget abort: ${message}`,
230
+ blockedMessage: `Budget abort threshold exceeded: ${message}`,
231
+ failedReason: message,
232
+ });
233
+ return { kind: "return", result };
234
+ }
235
+
236
+ if (budgetCheck.warning) {
237
+ const message = `Per-task budget warning threshold crossed: ${formatTokens(budgetCheck.totalUsed)}/${formatTokens(input.budgetTotal)} (${Math.round((budgetCheck.totalUsed / input.budgetTotal) * 100)}%)`;
238
+ // ROUND 12: console → logInternalError, explicit "warn" severity.
239
+ logInternalError("team-runner.budget-warning", new Error(message), `runId=${manifest.runId}`, "warn");
240
+ await appendEventAsync(manifest.eventsPath, {
241
+ type: "run.budget_warning",
242
+ runId: manifest.runId,
243
+ message,
244
+ data: {
245
+ budgetTotal: input.budgetTotal,
246
+ budgetUsed: budgetCheck.totalUsed,
247
+ threshold: "warning",
248
+ },
249
+ });
250
+ }
251
+
252
+ // Fair-share warning: flag tasks that consumed >50% of remaining budget
253
+ // without killing them mid-execution.
254
+ const fairShareAppends: Promise<void>[] = [];
255
+ for (const violatorId of budgetCheck.fairShareViolators) {
256
+ const violator = tasks.find((t) => t.id === violatorId);
257
+ if (!violator) continue;
258
+ const taskTotal = (violator.usage?.input ?? 0) + (violator.usage?.output ?? 0) + (violator.usage?.cacheWrite ?? 0);
259
+ const message = `Task '${violatorId}' consumed ${formatTokens(taskTotal)} (${Math.round((taskTotal / input.budgetTotal) * 100)}% of total budget) — exceeds fair share`;
260
+ // ROUND 12: console → logInternalError, explicit "warn" severity.
261
+ logInternalError("team-runner.fair-share", new Error(message), `runId=${manifest.runId} taskId=${violatorId}`, "warn");
262
+ fairShareAppends.push(
263
+ appendEventAsync(manifest.eventsPath, {
264
+ type: "task.budget_fair_share",
265
+ runId: manifest.runId,
266
+ taskId: violatorId,
267
+ message,
268
+ data: {
269
+ budgetTotal: input.budgetTotal,
270
+ taskUsage: taskTotal,
271
+ },
272
+ }).then(
273
+ () => undefined,
274
+ (error) => logInternalError("team-runner.fair-share-event", error, `taskId=${violatorId}`),
275
+ ),
276
+ );
277
+ }
278
+ await Promise.all(fairShareAppends);
279
+ }
280
+ return null;
281
+ }
@@ -32,6 +32,14 @@ export const MAX_LINE_BUFFER_BYTES = 1024 * 1024; // 1 MB
32
32
  /** Maximum characters for assistant text fragments in compacted events. */
33
33
  export const MAX_ASSISTANT_TEXT_CHARS = DEFAULT_CHILD_PI.maxAssistantTextChars;
34
34
 
35
+ /**
36
+ * Maximum characters for reasoning (`thinking`) fragments in compacted events,
37
+ * and the size cap for the thinking parts in the agent view pane / view
38
+ * session. Reasoning is display-only (never folded into results or artifacts),
39
+ * so it gets a tighter cap than full assistant text.
40
+ */
41
+ export const MAX_THINKING_CHARS = 8 * 1024;
42
+
35
43
  /** Maximum characters for tool-result fragments in compacted events. */
36
44
  export const MAX_TOOL_RESULT_CHARS = DEFAULT_CHILD_PI.maxToolResultChars;
37
45
 
@@ -71,8 +71,9 @@ export const BASE_ALLOWLIST: string[] = [
71
71
  * 1. Validate cwd (realpath + isDirectory) — fall back to lexical path on ENOENT.
72
72
  * 2. Filter env vars to the allowlist (model-aware provider key scoping).
73
73
  * 3. Validate NODE_PATH against safe prefixes (/opt, /lib, /usr, /home).
74
- * 4. Add PI_CREW_PARENT_PID for the child-side parent-guard (currently UNUSED —
75
- * see note at the PI_CREW_PARENT_PID assignment below).
74
+ * 4. Add PI_CREW_PARENT_PID for the child-side parent-guard (consumed by the
75
+ * reactive zombie-scanner + background-runner parent-guard; unused only by
76
+ * the pi dist binary — see note at the PI_CREW_PARENT_PID assignment below).
76
77
  */
77
78
  export function buildChildPiSpawnOptions(cwd: string, env: NodeJS.ProcessEnv, model?: string): SpawnOptions {
78
79
  // SECURITY FIX (Issue #1): Validate cwd before passing to spawn.
@@ -137,11 +138,20 @@ export function buildChildPiSpawnOptions(cwd: string, env: NodeJS.ProcessEnv, mo
137
138
  cwd: validatedCwd,
138
139
  env: {
139
140
  ...filteredEnv,
140
- // PI_CREW_PARENT_PID is set so that child pi workers could theoretically
141
- // run a parent-guard (parent-guard.ts). However, as of RT-19/RT-2, this
142
- // env var has ZERO consumers: child workers are the external `pi` binary
143
- // (@earendil-works/pi-coding-agent) which does NOT read
144
- // PI_CREW_PARENT_PID or call startParentGuard (grep of Pi dist = 0 matches).
141
+ // PI_CREW_PARENT_PID is set so child workers can run a parent-guard
142
+ // (parent-guard.ts). Consumers (NOT dead):
143
+ // 1. zombie-scanner.ts:185 — reads PI_CREW_PARENT_PID from
144
+ // /proc/<pid>/environ (readProcEnviron) to detect orphaned/zombie
145
+ // workers whose leader died.
146
+ // 2. background-runner.ts:615 — startParentGuard(parentPid)
147
+ // self-terminates the orchestrator when its own parent dies.
148
+ // 3. scratchpad-lifecycle.ts:50,166 — propagates the leader pid into
149
+ // scratchpad guest env for guest-zombie detection.
150
+ // The var is unused ONLY by the external `pi` binary
151
+ // (@earendil-works/pi-coding-agent): it does NOT read PI_CREW_PARENT_PID
152
+ // or call startParentGuard (grep of Pi dist = 0 matches), so child-pi
153
+ // workers cannot self-terminate on leader death — they rely on the
154
+ // reactive zombie scanner (see docs/decisions/2026-08-14-parent-guard-reactive-scanner.md).
145
155
  // The deeper fix (wiring startParentGuard into the pi worker entry point)
146
156
  // is DEFERRED because workers are an external binary pi-crew doesn't control.
147
157
  //
@@ -158,7 +168,7 @@ export function buildChildPiSpawnOptions(cwd: string, env: NodeJS.ProcessEnv, mo
158
168
  // NOTE: setsid creates a new session; the child process becomes the session leader
159
169
  // and its parent becomes that session leader (still the team-runner in the same
160
170
  // process group). PI_CREW_PARENT_PID is set before spawn using process.pid (team-runner),
161
- // but see the comment above — child pi workers do NOT actually consume it. The
171
+ // but see the comment above — the pi worker binary does NOT actually consume it. The
162
172
  // parent-guard model would check direct parent liveness via process.kill(pid, 0),
163
173
  // but this is only implemented in background-runner.ts, not in the worker binary.
164
174
  windowsHide: true,
@@ -246,6 +256,7 @@ export interface SpawnContext {
246
256
  export function prepareSpawnContext(
247
257
  input: ChildPiRunInput,
248
258
  effectiveTask: string,
259
+ depthEnv?: NodeJS.ProcessEnv,
249
260
  ): { kind: "ready"; ctx: SpawnContext } | { kind: "aborted"; result: ChildPiRunResult } {
250
261
  const built = buildPiWorkerArgs({
251
262
  task: effectiveTask,
@@ -256,6 +267,10 @@ export function prepareSpawnContext(
256
267
  skillPaths: input.skillPaths,
257
268
  role: input.role,
258
269
  thinkingOverride: input.thinkingOverride,
270
+ // ADR-5 §3: depthOverride is pre-encoded into depthEnv by runChildPi
271
+ // (parent's record depth as base-env PI_CREW_DEPTH); forward it so the
272
+ // child env gets parentDepth+1 = the true grandchild depth.
273
+ env: depthEnv,
259
274
  });
260
275
  // Pass steering file path to child for real-time steer injection
261
276
  if (input.steeringFile) built.env.PI_CREW_STEERING_FILE = input.steeringFile;
@@ -265,6 +280,31 @@ export function prepareSpawnContext(
265
280
  // assertOnlyControlEnvKeys.
266
281
  if (input.runId) built.env.PI_CREW_BROKER_RUN_ID = input.runId;
267
282
  if (input.agentId) built.env.PI_CREW_BROKER_TASK_ID = input.agentId;
283
+ // WP-2/R2 (ADR-0 item 2 — P0 env plumbing): UNCONDITIONAL for EVERY role,
284
+ // read-only (reviewer/security-reviewer/explorer/…) included — the
285
+ // worker-side `ask` tool + mailbox poll must be reachable by exactly the
286
+ // roles that cannot write files to unblock themselves. Deliberately NOT
287
+ // scratchpad-gated: the scratchpad gate below excludes read-only roles by
288
+ // design (S-6), which would leave the ask tool dead-on-arrival there.
289
+ // Control-namespace keys → pass assertOnlyControlEnvKeys.
290
+ built.env.PI_CREW_ASK_ENABLED = "1"; // dormant gate (worker conditional registerTool)
291
+ // T3/R5 (ADR-5 §1): the worker-side `delegate` tool is registered ONLY for
292
+ // executor-class roles at depth 1 (read-only roles are spawn-denied
293
+ // server-side anyway — the env gate is UX/dead-weight hygiene, not the
294
+ // security boundary; broker admission re-checks role+depth from the task
295
+ // RECORD). Control-namespace key → assertOnlyControlEnvKeys.
296
+ const childDepth = Number(built.env.PI_CREW_DEPTH ?? "1");
297
+ if (childDepth <= 1 && (input.role === "executor" || input.role === "test-engineer")) {
298
+ built.env.PI_CREW_DELEGATE_ENABLED = "1";
299
+ }
300
+ // stateRoot from the spawn manifest: ChildPiRunInput threads
301
+ // manifest.eventsPath unconditionally (child-executor / background-runner)
302
+ // and the state store pins eventsPath === <stateRoot>/events.jsonl
303
+ // (DEFAULT_PATHS.state.eventsFile; the invariant is validated by
304
+ // state-store.ts + manifest-cache.ts) — dirname(eventsPath) IS the manifest
305
+ // stateRoot. Absent (non-team spawn) → var stays unset and the ask tool
306
+ // fast-fails with a structured notice instead of hanging.
307
+ if (input.eventsPath) built.env.PI_CREW_STATE_ROOT = path.dirname(input.eventsPath);
268
308
  // Phase 0 inter-pi broker: inject socket path + token (control-namespace keys,
269
309
  // safe under assertOnlyControlEnvKeys). Only when the parent broker issued
270
310
  // credentials for this run — i.e. the broker is enabled AND this run is
@@ -275,6 +315,17 @@ export function prepareSpawnContext(
275
315
  built.env.PI_CREW_BROKER_TOKEN = input.brokerSpawn.token;
276
316
  // runId/taskId are threaded unconditionally above (I5); nothing to add here.
277
317
  }
318
+ // WP-9 (R9): the worker self-reporting channel env is UNCONDITIONAL —
319
+ // previously scratchpad-gated, which excluded read-only roles by design
320
+ // (S-6) and left them no reporting path. The bounded channel module
321
+ // (worker-events-channel.ts) rate-limits + schema-tags worker.* appends.
322
+ // Control-namespace keys → assertOnlyControlEnvKeys. PI_CREW_TASK_ID is
323
+ // set here when absent so the channel's events carry their task (the
324
+ // scratchpad block below sets it first when it runs — same value).
325
+ if (input.eventsPath) {
326
+ built.env.PI_CREW_EVENTS_PATH = input.eventsPath;
327
+ if (input.agentId && !built.env.PI_CREW_TASK_ID) built.env.PI_CREW_TASK_ID = input.agentId;
328
+ }
278
329
  // Phase 1 scratchpad: opt in the persistent Bun-free JS evaluator (execute
279
330
  // tool) for this worker. Gated by role/agent (S-6 read-only roles never;
280
331
  // F6 agent.scratchpad===false kills). All keys are PI_CREW_* control vars →
@@ -290,12 +341,7 @@ export function prepareSpawnContext(
290
341
  // artifactsRoot after F4/S-1).
291
342
  built.env.PI_CREW_ARTIFACTS_ROOT = input.artifactsRoot;
292
343
  }
293
- // I5: thread the run's events path so the worker's scratchpad execute
294
- // handler can append fire-and-forget metric events (scratchpad.cell /
295
- // scratchpad.restored). Optional — absent in non-team contexts.
296
- if (input.eventsPath) {
297
- built.env.PI_CREW_EVENTS_PATH = input.eventsPath;
298
- } // F4/S-1: the RAW (unredacted) snapshot must NEVER land in artifactsRoot —
344
+ // F4/S-1: the RAW (unredacted) snapshot must NEVER land in artifactsRoot —
299
345
  // point it at a temp dir; the worker reads it then writeArtifact()
300
346
  // (redact+atomic) is the ONLY writer into artifactsRoot.
301
347
  // R3-1: built.tempDir is only created by buildPiWorkerArgs when the agent
@@ -11,7 +11,13 @@
11
11
  import { logInternalError } from "../../utils/internal-error.ts";
12
12
  import { extractText } from "../output/pi-json-output.ts";
13
13
  import type { ChildPiRunInput } from "./child-pi.ts";
14
- import { MAX_ASSISTANT_TEXT_CHARS, MAX_LINE_BUFFER_BYTES, MAX_TOOL_INPUT_CHARS, MAX_TOOL_RESULT_CHARS } from "./child-pi-constants.ts";
14
+ import {
15
+ MAX_ASSISTANT_TEXT_CHARS,
16
+ MAX_LINE_BUFFER_BYTES,
17
+ MAX_THINKING_CHARS,
18
+ MAX_TOOL_INPUT_CHARS,
19
+ MAX_TOOL_RESULT_CHARS,
20
+ } from "./child-pi-constants.ts";
15
21
  import { appendTranscript, compactString, compactValue, flushPendingTranscriptWrites } from "./child-pi-transcript.ts";
16
22
 
17
23
  function asRecord(value: unknown): Record<string, unknown> | undefined {
@@ -38,6 +44,20 @@ function compactContentPart(part: unknown): unknown | undefined {
38
44
  name: record.name,
39
45
  input: compactValue(typeof record.input === "string" ? compactString(record.input, MAX_TOOL_INPUT_CHARS) : record.input),
40
46
  };
47
+ // Reasoning is display-only, but the agent view pane renders the FULL
48
+ // assistant message (AssistantMessageComponent) — without the thinking
49
+ // parts the pane would look sparse next to a real pi session. Keep them
50
+ // capped so a long-thinking model cannot balloon the event log.
51
+ if (record.type === "thinking")
52
+ return {
53
+ type: "thinking",
54
+ thinking:
55
+ typeof record.thinking === "string"
56
+ ? compactString(record.thinking, MAX_THINKING_CHARS, {
57
+ preserveImportant: false,
58
+ })
59
+ : "",
60
+ };
41
61
  if (record.type === "toolResult")
42
62
  return {
43
63
  type: "toolResult",