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,64 @@
1
+ import { PiTeamsConfigSchema } from "./config-schema.ts";
2
+
3
+ /**
4
+ * Schema-driven sensitive-config discovery (Phase 5.1, ADR
5
+ * docs/decisions/2026-08-15-schema-driven-sanitize.md).
6
+ *
7
+ * Sensitive fields are marked in `config-schema.ts` via TypeBox Options
8
+ * metadata: `Type.Boolean({ sensitive: true })`. In TypeBox 0.34 the
9
+ * constructor spreads unknown option keys verbatim onto the emitted schema
10
+ * object (`build/cjs/type/create/type.js` CreateType: `options !== undefined
11
+ * ? { ...options, ...schema } : schema`), and `Type.Optional` is a flat
12
+ * spread modifier (`{ ...schema, [OptionalKind]: 'Optional' }`), so the mark
13
+ * lands directly on `properties[key]` and survives Clone/Decode.
14
+ *
15
+ * The walk below collects the dotted paths of every marked property. Marked
16
+ * properties are TERMINAL: Record/Object-valued marked props
17
+ * (`agents.overrides`, `otlp.headers`, `runtime.isolationPolicy`) collapse
18
+ * to a single dotted path instead of being recursed into.
19
+ */
20
+
21
+ type SchemaLike = {
22
+ sensitive?: unknown;
23
+ properties?: Record<string, SchemaLike>;
24
+ anyOf?: SchemaLike[];
25
+ } & Record<string, unknown>;
26
+
27
+ function isMarkedSensitive(property: SchemaLike): boolean {
28
+ if (property.sensitive === true) return true;
29
+ // Defensive: a mark placed on a Union *member* rather than the outermost
30
+ // property schema (the recommended form) still counts. Not used by the
31
+ // current config schema; guards future editors.
32
+ const members = property.anyOf;
33
+ if (Array.isArray(members) && members.some((member) => member.sensitive === true)) return true;
34
+ return false;
35
+ }
36
+
37
+ function walkProperties(schema: SchemaLike, prefix: string, out: string[]): void {
38
+ const properties = schema.properties;
39
+ if (!properties || typeof properties !== "object") return;
40
+ for (const [key, property] of Object.entries(properties)) {
41
+ if (!property || typeof property !== "object") continue;
42
+ const dotted = prefix === "" ? key : `${prefix}.${key}`;
43
+ if (isMarkedSensitive(property)) {
44
+ out.push(dotted);
45
+ continue;
46
+ }
47
+ // `Type.Optional` needs no unwrapping in 0.34 (flat spread — the mark
48
+ // and `.properties` sit on the property schema itself). Union members
49
+ // are visited defensively for nested Object schemas.
50
+ const branches: SchemaLike[] = Array.isArray(property.anyOf) ? property.anyOf : [property];
51
+ for (const branch of branches) {
52
+ if (branch && typeof branch === "object" && branch.properties) walkProperties(branch, dotted, out);
53
+ }
54
+ }
55
+ }
56
+
57
+ /** Dotted paths of every `sensitive: true`-marked field in the config schema
58
+ * (top-level keys unprefixed, nested keys as `section.key`). Order follows
59
+ * schema property declaration order. */
60
+ export function collectSensitiveConfigPaths(schema: SchemaLike = PiTeamsConfigSchema as SchemaLike): string[] {
61
+ const out: string[] = [];
62
+ walkProperties(schema, "", out);
63
+ return out;
64
+ }
@@ -235,7 +235,13 @@ const sharedFields = {
235
235
  description: "Path to a markdown plan document for orchestration.",
236
236
  }),
237
237
  ),
238
- subAction: Type.Optional(Type.String({ description: "Sub-action for schedule management (remove, disable, enable, update)." })),
238
+ subAction: Type.Optional(
239
+ Type.String({ description: "Sub-action. For action='plans': get (default) | list | diff | approve | reject." }),
240
+ ),
241
+ // T2/R4 (ADR-4 §7): plans action params.
242
+ rev: Type.Optional(Type.Number({ description: "plans get: pin a specific plan revision (default: current)." })),
243
+ a: Type.Optional(Type.Number({ description: "plans diff: left revision number." })),
244
+ b: Type.Optional(Type.Number({ description: "plans diff: right revision number." })),
239
245
  jobId: Type.Optional(Type.String({ description: "Job ID for schedule management actions." })),
240
246
  cron: Type.Optional(
241
247
  Type.String({
@@ -378,11 +384,11 @@ const sharedFields = {
378
384
  ),
379
385
  };
380
386
 
381
- // ─── Domain action unions (9+16+7+16+6 = 54 actions) ───────────────────────
387
+ // ─── Domain action unions (10+16+7+16+6 = 55 actions) ──────────────────────
382
388
 
383
389
  const ACTION_DESCRIPTION = "Team action. Defaults to 'list' when omitted.";
384
390
 
385
- const RUN_ACTIONS = ["run", "parallel", "plan", "orchestrate", "resume", "retry", "wait", "steer", "goal"] as const;
391
+ const RUN_ACTIONS = ["run", "parallel", "plan", "plans", "orchestrate", "resume", "retry", "wait", "steer", "goal"] as const;
386
392
  const runActions = Type.Optional(buildStringEnum(RUN_ACTIONS, ACTION_DESCRIPTION));
387
393
 
388
394
  const STATUS_ACTIONS = [
@@ -545,6 +551,10 @@ export interface TeamToolParamsValue {
545
551
  // schedule sub-actions (removal/toggle/update of an existing job)
546
552
  subAction?: string;
547
553
  jobId?: string;
554
+ // T2/R4 (ADR-4 §7): plans action params.
555
+ rev?: number;
556
+ a?: number;
557
+ b?: number;
548
558
  /** Mark certain bash commands as excludeFromContext to reduce context tokens (default: false). */
549
559
  excludeContextBash?: boolean;
550
560
  /** Total token budget for the run. When set, enables budget tracking (minimum 1000). */
@@ -8,15 +8,10 @@ responsibility (Phase 7 reorg).
8
8
  | File | Responsibility |
9
9
  |------|---------------|
10
10
  | `types.ts` | Core state types (`TeamRunManifest`, `TeamTaskState`, schema versions) |
11
- | `types-eval.ts` | Evaluation-related types |
12
11
  | `contracts.ts` | Status transitions, task-status enums |
13
- | `tiered-eval.ts` | Tiered evaluation logic |
14
12
  | `crew-init.ts` | Crew initialization / bootstrap |
15
13
  | `decision-ledger.ts` | Decision recording |
16
14
  | `gitignore-manager.ts` | `.gitignore` management for crew dirs |
17
- | `hook-instinct-bridge.ts` | Bridge between hooks and instinct store |
18
- | `hook-integrations.ts` | Hook integration points |
19
- | `session-state-map.ts` | Session → state mapping |
20
15
  | `usage.ts` | Token/cost usage tracking |
21
16
  | `atomic-write.ts` | Atomic file writes (widely used — kept at root) |
22
17
 
@@ -26,19 +21,18 @@ Event append/read, rotation/compaction, JSONL writing, and worker-thread
26
21
  atomic writes.
27
22
 
28
23
  - `event-log.ts`, `event-log-rotation.ts`, `event-reconstructor.ts`,
29
- `jsonl-writer.ts`, `worker-atomic-writer.ts`
24
+ `worker-atomic-writer.ts`
30
25
 
31
26
  ## `stores/` — Persisted data stores
32
27
 
33
28
  Manifest, artifact, blob, observation, health, instinct, run-graph/metrics
34
29
  stores, and the active-run registry.
35
30
 
36
- - `state-store.ts`, `run-cache.ts`, `artifact-store.ts`, `blob-store.ts`,
37
- `observation-store.ts`, `health-store.ts`, `instinct-store.ts`,
38
- `active-run-registry.ts`, `run-graph.ts`, `run-metrics.ts`
31
+ - `state-store.ts`, `run-cache.ts`, `artifact-store.ts`,
32
+ `health-store.ts`, `active-run-registry.ts`, `run-graph.ts`, `run-metrics.ts`
39
33
 
40
34
  ## `coordination/` — Concurrency & IPC
41
35
 
42
36
  File locks, mailbox messaging, task scheduling, and task claims.
43
37
 
44
- - `locks.ts`, `mailbox.ts`, `schedule.ts`, `task-claims.ts`
38
+ - `locks.ts`, `mailbox.ts`, `task-claims.ts`
@@ -962,12 +962,29 @@ function cancelPendingCoalescedWrite(filePath: string): void {
962
962
  }
963
963
  }
964
964
 
965
- /** Flush every queued coalesced write synchronously. Safe to call any time. */
966
- export function flushPendingAtomicWrites(): void {
965
+ /**
966
+ * Flush every queued coalesced write synchronously. Safe to call any time.
967
+ *
968
+ * R10-2: pass `filePath` to flush ONLY the pending coalesced entry for that
969
+ * exact file. Hot read paths that know which file they are about to read
970
+ * (e.g. readCrewAgents) no longer wait on unrelated coalesced writes for
971
+ * OTHER files — previously this drained the entire process-wide queue on
972
+ * every such read. Omitted → exact previous global-drain behavior
973
+ * (backward compatible: merge-loop / budget-enforcement / finalize-run /
974
+ * state-helpers / process-exit handlers all rely on the global drain).
975
+ * Re-entrancy guard (`flushInProgress`) applies to both modes: a scoped flush
976
+ * triggered while a global flush is running is a no-op (the global flush
977
+ * already covers this file), and vice versa.
978
+ */
979
+ export function flushPendingAtomicWrites(filePath?: string): void {
967
980
  if (flushInProgress > 0) return;
968
981
  flushInProgress++;
969
982
  try {
970
- for (const filePath of [...pendingAtomicWrites.keys()]) flushOnePendingAtomicWrite(filePath);
983
+ if (filePath === undefined) {
984
+ for (const pending of [...pendingAtomicWrites.keys()]) flushOnePendingAtomicWrite(pending);
985
+ } else if (pendingAtomicWrites.has(filePath)) {
986
+ flushOnePendingAtomicWrite(filePath);
987
+ }
971
988
  } finally {
972
989
  flushInProgress--;
973
990
  }
@@ -53,9 +53,16 @@ export const TEAM_EVENT_TYPES = [
53
53
  "run.completed",
54
54
  "run.failed",
55
55
  "run.cancelled",
56
+ "run.terminal_preserved",
56
57
  "task.created",
57
58
  "task.started",
58
59
  "task.progress",
60
+ // T4/R6 (ADR-6 + erratum): spec-system events
61
+ "spec.frozen",
62
+ "spec.freeze_failed",
63
+ "spec.strict_platform_warning",
64
+ "spec.check_failed",
65
+ "task.spec_gate",
59
66
  "hook.pre_step_started",
60
67
  "hook.pre_step_completed",
61
68
  "hook.pre_step_failed",
@@ -88,7 +95,38 @@ export const TEAM_EVENT_TYPES = [
88
95
  "task.waiting",
89
96
  "task.resumed",
90
97
  "task.retried",
98
+ // WP-2/R2 waiting-producer (ADR-0 2026-08-17-waiting-producer-ask item 10):
99
+ // `ask` tool lifecycle — requested on park acceptance, answered on delivery
100
+ // (mailbox or requeue+inject), timedout on deadline expiry (both the
101
+ // alive-in-tool and dead-requeue outcomes).
102
+ "ask.requested",
103
+ "ask.answered",
104
+ "ask.timedout",
91
105
  "supervisor.contact",
106
+ // T2/R4 first-class Plan object (ADR-4 docs/decisions/2026-08-17-plan-object.md §9):
107
+ // plan-store revision/approval mutations. `plan.approved` and `plan.cancelled`
108
+ // formalize emitters that api/plan-approval.ts:66-67,144-145 already wrote
109
+ // unregistered (pre-existing gap closed by the ADR). The scheduler's
110
+ // items[].taskIds linkage writes append NO event (task dispatch logs its own).
111
+ "plan.created",
112
+ "plan.revised",
113
+ "plan.approved",
114
+ "plan.rejected",
115
+ "plan.cancelled",
116
+ "plan.item.dropped",
117
+ // plan-approval.ts ensurePlanApprovalRequested emits this when the gate
118
+ // lights up — pre-existing unregistered emitter formalized with the rest.
119
+ "plan.approval_required",
120
+ // T3/R5 (ADR-5): governed-nesting delegate lifecycle. Emitted by the
121
+ // broker's delegate.request handler (crew-broker.ts) — every rejection
122
+ // leaves a delegate.rejected trace (never silent), usage roll-up lands as
123
+ // delegate.rolled_up on the parent task record.
124
+ "delegate.requested",
125
+ "delegate.admitted",
126
+ "delegate.rejected",
127
+ "delegate.completed",
128
+ "delegate.timed_out",
129
+ "delegate.rolled_up",
92
130
  // Budget tracking events
93
131
  "budget.initialized",
94
132
  "budget.warning",
@@ -67,6 +67,10 @@ export interface MailboxMessage {
67
67
  priority?: MailboxMessagePriority;
68
68
  deliveryMode?: MailboxDeliveryMode;
69
69
  taskId?: string;
70
+ /** WP-2/R2 (ADR-0 item 11/F11): ask-response correlation id (randomUUID).
71
+ * Carried by `kind:"response"` entries; matches task.waiting.questionId —
72
+ * matching must be exact equality (never prefix/substring). */
73
+ questionId?: string;
70
74
  acknowledgedAt?: string;
71
75
  data?: Record<string, unknown>;
72
76
  /** ID of the original message this is a reply to. */
@@ -294,6 +298,7 @@ function parseMailboxMessage(raw: unknown, expectedDirection: MailboxDirection):
294
298
  priority: isPriority(obj.priority) ? obj.priority : undefined,
295
299
  deliveryMode: isDeliveryMode(obj.deliveryMode) ? obj.deliveryMode : undefined,
296
300
  taskId: typeof obj.taskId === "string" ? obj.taskId : undefined,
301
+ questionId: typeof obj.questionId === "string" ? obj.questionId : undefined,
297
302
  acknowledgedAt: typeof obj.acknowledgedAt === "string" ? obj.acknowledgedAt : undefined,
298
303
  data,
299
304
  replyTo: typeof obj.replyTo === "string" ? obj.replyTo : undefined,
@@ -487,8 +492,11 @@ export function readDeliveryState(manifest: TeamRunManifest): MailboxDeliverySta
487
492
  }
488
493
  // NEW-R4: prominent (ungated) error so corrupt-delivery re-delivery risk is
489
494
  // visible even without PI_TEAMS_DEBUG — messages may be re-delivered.
490
- console.error(
491
- `[pi-crew:mailbox.readDeliveryState] corrupt delivery.json quarantined to ${quarantinePath} — delivery state reset to empty; messages may be re-delivered. Error: ${error instanceof Error ? error.message : String(error)}`,
495
+ logInternalError(
496
+ "mailbox.readDeliveryState",
497
+ error,
498
+ `corrupt delivery.json quarantined to ${quarantinePath} — delivery state reset to empty; messages may be re-delivered`,
499
+ "error",
492
500
  );
493
501
  deliveryCache.delete(filePath);
494
502
  return { messages: {}, updatedAt: new Date().toISOString() };
@@ -565,6 +573,7 @@ export function appendMailboxMessage(
565
573
  priority: message.priority,
566
574
  deliveryMode: message.deliveryMode,
567
575
  taskId: message.taskId,
576
+ questionId: message.questionId,
568
577
  data: message.data,
569
578
  replyTo: message.replyTo,
570
579
  replyFrom: message.replyFrom,
@@ -690,6 +699,7 @@ export async function appendMailboxMessageAsync(
690
699
  priority: message.priority,
691
700
  deliveryMode: message.deliveryMode,
692
701
  taskId: message.taskId,
702
+ questionId: message.questionId,
693
703
  data: message.data,
694
704
  replyTo: message.replyTo,
695
705
  replyFrom: message.replyFrom,
@@ -0,0 +1,223 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { type IncrementalReadState, readJsonlSince, readJsonlTail } from "../../utils/incremental-reader.ts";
4
+ import { logInternalError } from "../../utils/internal-error.ts";
5
+ import type { TeamEvent } from "./event-log.ts";
6
+ import { currentGeneration } from "./event-log-rotation.ts";
7
+
8
+ // --- R18 / R16-B1 effect 2 (Phase 3.6): archive-tail readers ----------------
9
+ // Rotation stranding: a sync append that was mid-appendFileSync holding an fd
10
+ // on the RENAMED inode (ST-8 rename+create) lands in the archive file, not the
11
+ // live file. Round 18 measured 5.43% stranded events under max-contention
12
+ // zero-lock repro. Previously readers saw ONLY the live file, so stranded
13
+ // events were invisible — and sweepOldArchives unlinked them after 7 days
14
+ // (gone forever). Fix (Round 18 option (a)): mirror the mailbox
15
+ // safeReadMailboxFile archive-walk — readers also drain archive TAILS,
16
+ // deduped by seq, merged ahead of the live file's events.
17
+
18
+ /** List `<eventsPath>.<ts>.archive.jsonl` siblings (matches rotateEventLogUnlocked's
19
+ * naming), sorted by name (timestamp) so older generations come first. */
20
+ function listEventArchivePaths(eventsPath: string): string[] {
21
+ const dir = path.dirname(eventsPath);
22
+ const base = path.basename(eventsPath);
23
+ try {
24
+ return fs
25
+ .readdirSync(dir)
26
+ .filter((entry) => entry.startsWith(`${base}.`) && entry.endsWith(".archive.jsonl"))
27
+ .sort()
28
+ .map((entry) => path.join(dir, entry));
29
+ } catch {
30
+ // Directory missing — nothing to read.
31
+ return [];
32
+ }
33
+ }
34
+
35
+ /** Parse a JSONL file into TeamEvents, skipping corrupt/blank lines (mirrors
36
+ * the legacy readEvents parser). Returns [] when the file is unreadable. */
37
+ function parseJsonlEvents(filePath: string): TeamEvent[] {
38
+ let raw: string;
39
+ try {
40
+ raw = fs.readFileSync(filePath, "utf-8");
41
+ } catch {
42
+ return [];
43
+ }
44
+ const events: TeamEvent[] = [];
45
+ for (const line of raw.split("\n")) {
46
+ const trimmed = line.trim();
47
+ if (!trimmed) continue;
48
+ try {
49
+ events.push(JSON.parse(trimmed) as TeamEvent);
50
+ } catch {
51
+ /* skip corrupt lines */
52
+ }
53
+ }
54
+ return events;
55
+ }
56
+
57
+ const ARCHIVE_TAIL_BYTES = 4 * 1024 * 1024; // 4 MB — mirrors readEventsCursor's TAIL_BYTES
58
+
59
+ /** Drain archive TAILS: events with seq > sinceSeq from every archive of
60
+ * eventsPath, deduped by seq, sorted by seq. Stranded in-flight-fd appends
61
+ * land at the END of the archive (the renamed pre-rotation inode), so a
62
+ * bounded tail read per archive is sufficient and keeps this O(tail bytes). */
63
+ function readArchiveTailEvents(eventsPath: string, sinceSeq: number): TeamEvent[] {
64
+ const archives = listEventArchivePaths(eventsPath);
65
+ if (archives.length === 0) return [];
66
+ const bySeq = new Map<number, TeamEvent>();
67
+ for (const archivePath of archives) {
68
+ const tail = readJsonlTail<TeamEvent>(archivePath, ARCHIVE_TAIL_BYTES);
69
+ for (const event of tail.items) {
70
+ const seq = event.metadata?.seq;
71
+ if (typeof seq !== "number" || seq <= sinceSeq) continue;
72
+ if (!bySeq.has(seq)) bySeq.set(seq, event);
73
+ }
74
+ }
75
+ return [...bySeq.values()].sort((a, b) => (a.metadata?.seq ?? 0) - (b.metadata?.seq ?? 0));
76
+ }
77
+
78
+ /** Merge drained archive-tail events with live-file events: dedup by seq
79
+ * (live wins on collision — it is the persisted survivor), seq-sorted. */
80
+ function mergeArchiveTailEvents(archiveEvents: TeamEvent[], liveEvents: TeamEvent[]): TeamEvent[] {
81
+ if (archiveEvents.length === 0) return liveEvents;
82
+ const bySeq = new Map<number, TeamEvent>();
83
+ for (const event of archiveEvents) bySeq.set(event.metadata?.seq ?? 0, event);
84
+ for (const event of liveEvents) bySeq.set(event.metadata?.seq ?? 0, event);
85
+ return [...bySeq.values()].sort((a, b) => (a.metadata?.seq ?? 0) - (b.metadata?.seq ?? 0));
86
+ }
87
+
88
+ export function readEvents(eventsPath: string): TeamEvent[] {
89
+ // R18 / R16-B1 effect 2 (Phase 3.6): FULL-HISTORY semantics — merge every
90
+ // archive (pre-rotation generations, including stranded in-flight appends)
91
+ // with the live file, dedup by seq, ordered by seq. Callers needing the
92
+ // full history (team-runner recovery, run-export, inspect, read, and the
93
+ // event-reconstructor, whose only read entry is this function) now see
94
+ // archived events too — this is the intended fix for "no event stranded".
95
+ const archives = listEventArchivePaths(eventsPath);
96
+ const events: TeamEvent[] = [];
97
+ const seenSeqs = new Set<number>();
98
+ const push = (event: TeamEvent): void => {
99
+ const seq = event.metadata?.seq;
100
+ if (typeof seq === "number" && seq > 0) {
101
+ if (seenSeqs.has(seq)) return;
102
+ seenSeqs.add(seq);
103
+ }
104
+ events.push(event);
105
+ };
106
+ for (const archivePath of archives) {
107
+ for (const event of parseJsonlEvents(archivePath)) push(event);
108
+ }
109
+ if (fs.existsSync(eventsPath)) {
110
+ for (const event of parseJsonlEvents(eventsPath)) push(event);
111
+ }
112
+ return events.sort((a, b) => (a.metadata?.seq ?? 0) - (b.metadata?.seq ?? 0));
113
+ }
114
+
115
+ export interface EventCursorOptions {
116
+ sinceSeq?: number;
117
+ limit?: number;
118
+ fromByteOffset?: number;
119
+ /** R-03: generation the caller captured on its previous read. When set, a
120
+ * mismatch with the live generation signals the file was rotated/truncated
121
+ * and the byte offset is stale — the cursor resets to 0 so the new file is
122
+ * re-read from its start instead of missing post-rotation events. */
123
+ generation?: number;
124
+ }
125
+
126
+ export interface EventCursorResult {
127
+ events: TeamEvent[];
128
+ nextSeq: number;
129
+ total: number;
130
+ nextByteOffset?: number;
131
+ /** R-03: live generation of the events file at read time. Callers doing
132
+ * streaming byte-offset reads should echo this back as `generation` on the
133
+ * next call so rotation is detected and the cursor resets. */
134
+ generation?: number;
135
+ }
136
+
137
+ function positiveInteger(value: number | undefined): number | undefined {
138
+ return value !== undefined && Number.isInteger(value) && value >= 0 ? value : undefined;
139
+ }
140
+
141
+ export function readEventsCursor(eventsPath: string, options: EventCursorOptions = {}): EventCursorResult {
142
+ // Incremental byte-offset path: read only new bytes since last known offset
143
+ if (options.fromByteOffset !== undefined) {
144
+ // R-03: detect file rotation/truncation via the generation sidecar BEFORE
145
+ // reusing the byte offset. If the file was rotated since the caller last
146
+ // read, it was truncated to empty (pre-rotation content archived to
147
+ // `<eventsPath>.<ts>.archive.jsonl`) and is growing again from 0 — the
148
+ // caller's offset now points past EOF, so post-rotation events would be
149
+ // silently missed. Reset to offset 0 to re-read the current file from its
150
+ // start.
151
+ // R18 (Phase 3.6): re-reading from 0 re-delivers no previously-returned
152
+ // events FROM THE LIVE FILE — but events STRANDED into the archive by the
153
+ // rotation (in-flight fd appends on the renamed inode) were never
154
+ // delivered and, before this fix, were lost forever once sweepOldArchives
155
+ // unlinked them. On a detected generation bump we therefore FIRST drain
156
+ // the previous generations' archive TAILS (events with seq > sinceSeq,
157
+ // deduped by seq — mailbox safeReadMailboxFile archive-walk pattern)
158
+ // ahead of the fresh live file's events.
159
+ const liveGen = currentGeneration(eventsPath);
160
+ const staleCursor = options.generation !== undefined && options.generation !== liveGen;
161
+ const sinceSeq = positiveInteger(options.sinceSeq) ?? 0;
162
+ const archiveEvents = staleCursor ? readArchiveTailEvents(eventsPath, sinceSeq) : [];
163
+ const byteOffset = staleCursor ? 0 : (positiveInteger(options.fromByteOffset) ?? 0);
164
+ const initialState: IncrementalReadState = { byteOffset, lineCount: 0 };
165
+ const { items, state: newState, eof } = readJsonlSince<TeamEvent>(eventsPath, initialState);
166
+ const filtered = items.filter((event) => (event.metadata?.seq ?? 0) > sinceSeq);
167
+ const merged = mergeArchiveTailEvents(archiveEvents, filtered);
168
+ const limit = positiveInteger(options.limit);
169
+ const events = limit !== undefined ? merged.slice(0, limit) : merged;
170
+ const returnedMaxSeq = events.reduce((max, event) => Math.max(max, event.metadata?.seq ?? 0), sinceSeq);
171
+ return {
172
+ events,
173
+ nextSeq: returnedMaxSeq,
174
+ total: merged.length,
175
+ nextByteOffset: newState.byteOffset,
176
+ generation: liveGen,
177
+ };
178
+ }
179
+
180
+ // FIND-05 default path: byte-level tail read (last 4MB) instead of
181
+ // full-file read. Bounds CPU to O(tail bytes) instead of O(total
182
+ // events). The legacy readEvents() full parse path is preserved for
183
+ // callers that explicitly need the full history (e.g. tests that
184
+ // assert exact contents) and as a small-file fallback.
185
+ //
186
+ // The 5000-event tail cap and the "event-log.cursor-full-read"
187
+ // warning are preserved. A separate cursor-tail-truncated warning is
188
+ // emitted whenever the file exceeds the 4MB tail budget, signalling
189
+ // that a prefix was dropped and callers should pass fromByteOffset for
190
+ // streaming reads.
191
+ const TAIL_BYTES = 4 * 1024 * 1024; // 4 MB
192
+ const TAIL_EVENT_CAP = 5000;
193
+ const sinceSeq = positiveInteger(options.sinceSeq) ?? 0;
194
+ const limit = positiveInteger(options.limit);
195
+
196
+ const tail = readJsonlTail<TeamEvent>(eventsPath, TAIL_BYTES);
197
+ let all = tail.items;
198
+ if (tail.truncated) {
199
+ logInternalError("event-log.cursor-tail-truncated", {
200
+ eventsPath,
201
+ returned: all.length,
202
+ tailBytes: TAIL_BYTES,
203
+ });
204
+ }
205
+ if (all.length > TAIL_EVENT_CAP) {
206
+ logInternalError(
207
+ "event-log.cursor-full-read",
208
+ new Error(`readEventsCursor tail read dropped events from a larger log; pass fromByteOffset for incremental reads`),
209
+ `eventsPath=${eventsPath}`,
210
+ );
211
+ all = all.slice(-TAIL_EVENT_CAP);
212
+ }
213
+ const filtered = all.filter((event) => (event.metadata?.seq ?? 0) > sinceSeq);
214
+ // R18 (Phase 3.6): rotation stranding — prepend archive-tail events (seq >
215
+ // sinceSeq, deduped, seq-sorted) ahead of the live tail slice, so events
216
+ // stranded into an archive by a rotation are still delivered to sinceSeq
217
+ // streaming consumers. No-rotation case: no archives exist → behavior is
218
+ // byte-identical to before (mergeArchiveTailEvents returns liveEvents).
219
+ const merged = mergeArchiveTailEvents(readArchiveTailEvents(eventsPath, sinceSeq), filtered);
220
+ const events = limit !== undefined ? merged.slice(0, limit) : merged;
221
+ const returnedMaxSeq = events.reduce((max, event) => Math.max(max, event.metadata?.seq ?? 0), sinceSeq);
222
+ return { events, nextSeq: returnedMaxSeq, total: merged.length };
223
+ }
@@ -340,8 +340,13 @@ function bumpGenerationUnlocked(eventsPath: string): number {
340
340
  * Rotate an event log file by archiving it with a timestamp.
341
341
  * The current file is renamed to `<eventsPath>.<timestamp>.archive.jsonl`
342
342
  * and a fresh empty file is created in its place.
343
- * Readers using `readEvents` will see the new file; archived files can be
344
- * picked up by snapshot replay if needed.
343
+ * Readers using `readEvents`/`readEventsCursor` see the new file; archive
344
+ * files are read back by the archive-tail readers in event-log.ts
345
+ * (R18 / R16-B1 effect 2, Phase 3.6): on a detected `.gen` generation bump
346
+ * the cursor drains the previous generations' archive TAILS (events with
347
+ * seq > sinceSeq, deduped by seq) ahead of the fresh live file, so events
348
+ * stranded into an archive by an in-flight fd append during the rename are
349
+ * still delivered instead of being swept away after the retention window.
345
350
  */
346
351
  export function rotateEventLog(eventsPath: string): boolean {
347
352
  if (!fs.existsSync(eventsPath)) return false;
@@ -423,7 +428,7 @@ export function rotateEventLogUnlocked(eventsPath: string): boolean {
423
428
  // our rename and here, we leave their data intact (EEXIST → skip).
424
429
  fs.renameSync(eventsPath, archivePath);
425
430
  try {
426
- const fd = fs.openSync(eventsPath, "wx", 0o644);
431
+ const fd = fs.openSync(eventsPath, "wx", 0o600);
427
432
  fs.closeSync(fd);
428
433
  } catch (err) {
429
434
  if ((err as NodeJS.ErrnoException).code !== "EEXIST") throw err;
@@ -438,7 +443,10 @@ export function rotateEventLogUnlocked(eventsPath: string): boolean {
438
443
  sweepOldArchives(eventsPath);
439
444
  return true;
440
445
  } catch (error) {
441
- logInternalError("event-log.rotate", error, `eventsPath=${eventsPath}`);
446
+ // R17-S1 (Phase 3.8): severity "error" (was default "debug", PI_TEAMS_DEBUG-
447
+ // gated) — a failed rotation leaves the file over the size limit and the
448
+ // next non-terminal appends are silently skipped; the chain must signal.
449
+ logInternalError("event-log.rotate", error, `eventsPath=${eventsPath}`, "error");
442
450
  return false;
443
451
  }
444
452
  }