pi-crew 0.9.67 → 0.10.1

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 (222) hide show
  1. package/CHANGELOG.md +172 -0
  2. package/agents/analyst.md +1 -1
  3. package/agents/cold-verifier.md +3 -1
  4. package/agents/critic.md +1 -1
  5. package/agents/executor.md +1 -1
  6. package/agents/explorer.md +1 -1
  7. package/agents/planner.md +1 -1
  8. package/agents/reviewer.md +1 -1
  9. package/agents/security-reviewer.md +1 -1
  10. package/agents/test-engineer.md +1 -1
  11. package/agents/verifier.md +1 -1
  12. package/agents/writer.md +1 -1
  13. package/dist/index.mjs +65847 -60331
  14. package/docs/README.md +2 -0
  15. package/docs/actions-reference.md +31 -0
  16. package/docs/commands-reference.md +17 -6
  17. package/docs/resource-formats.md +13 -0
  18. package/package.json +9 -6
  19. package/schema.json +15 -1
  20. package/scripts/resource-sampler.mjs +36 -2
  21. package/skills/requirements-to-task-packet/SKILL.md +26 -0
  22. package/skills/widget-rendering/SKILL.md +7 -7
  23. package/src/agents/agent-config.ts +36 -14
  24. package/src/agents/discover-agents.ts +23 -14
  25. package/src/config/config-merge.ts +183 -0
  26. package/src/config/config-validation.ts +681 -0
  27. package/src/config/config.ts +22 -864
  28. package/src/config/defaults.ts +35 -1
  29. package/src/config/drift-detector.ts +1 -1
  30. package/src/config/env-vars.ts +691 -0
  31. package/src/config/role-tools.ts +8 -8
  32. package/src/config/sanitize-project-config.ts +172 -0
  33. package/src/config/types.ts +30 -0
  34. package/src/extension/async-notifier.ts +25 -2
  35. package/src/extension/crew-vibes/config.ts +2 -1
  36. package/src/extension/plan-orchestrate.ts +132 -0
  37. package/src/extension/registration/commands/dashboard.ts +158 -0
  38. package/src/extension/registration/commands/index.ts +35 -0
  39. package/src/extension/registration/commands/manage.ts +303 -0
  40. package/src/extension/registration/commands/run.ts +214 -0
  41. package/src/extension/registration/commands/shared.ts +631 -0
  42. package/src/extension/registration/commands/status.ts +60 -0
  43. package/src/extension/registration/commands.ts +13 -1224
  44. package/src/extension/registration/lifecycle-handlers.ts +126 -15
  45. package/src/extension/registration/subagent-tools.ts +218 -9
  46. package/src/extension/rpc-hmac.ts +5 -3
  47. package/src/extension/team-tool/api/heartbeat.ts +47 -10
  48. package/src/extension/team-tool/api/plan-approval.ts +9 -0
  49. package/src/extension/team-tool/api/task-claims.ts +109 -40
  50. package/src/extension/team-tool/cancel.ts +84 -50
  51. package/src/extension/team-tool/dispatch/index.ts +1 -0
  52. package/src/extension/team-tool/dispatch/run.ts +4 -1
  53. package/src/extension/team-tool/doctor.ts +103 -1
  54. package/src/extension/team-tool/orchestrate.ts +66 -1
  55. package/src/extension/team-tool/plans.ts +192 -0
  56. package/src/extension/team-tool/respond.ts +197 -65
  57. package/src/extension/team-tool/run-deadline.ts +16 -1
  58. package/src/extension/team-tool/run-intent.ts +63 -0
  59. package/src/extension/team-tool/run.ts +48 -18
  60. package/src/extension/team-tool/status.ts +84 -26
  61. package/src/extension/team-tool.ts +11 -2
  62. package/src/hooks/registry.ts +1 -6
  63. package/src/i18n.ts +9 -0
  64. package/src/prompt/prompt-runtime.ts +521 -2
  65. package/src/prompt/worker-events-channel.ts +173 -0
  66. package/src/runtime/README.md +8 -8
  67. package/src/runtime/async-runner.ts +7 -3
  68. package/src/runtime/background-runner.ts +42 -14
  69. package/src/runtime/broker/broker-issuer.ts +9 -2
  70. package/src/runtime/broker/crew-broker-tokens.ts +43 -6
  71. package/src/runtime/broker/crew-broker.ts +838 -10
  72. package/src/runtime/broker/wait-status-cache.ts +157 -0
  73. package/src/runtime/budget-enforcement.ts +281 -0
  74. package/src/runtime/child-pi/child-pi-spawn.ts +60 -14
  75. package/src/runtime/child-pi/child-pi-timers.ts +324 -0
  76. package/src/runtime/child-pi/child-pi.ts +97 -201
  77. package/src/runtime/child-pi/mock-fixtures.ts +16 -2
  78. package/src/runtime/crew-agent-records.ts +259 -14
  79. package/src/runtime/delegate-spawn.ts +148 -0
  80. package/src/runtime/deterministic-ast.ts +2 -1
  81. package/src/runtime/dispatch-batch.ts +945 -0
  82. package/src/runtime/finalize-run.ts +557 -0
  83. package/src/runtime/goal-workflow/adaptive-plan.ts +87 -13
  84. package/src/runtime/goal-workflow/dynamic-workflow-runner.ts +3 -2
  85. package/src/runtime/goal-workflow/goal-state-store.ts +1 -1
  86. package/src/runtime/group-join.ts +11 -125
  87. package/src/runtime/live-session/live-session-runtime.ts +26 -1
  88. package/src/runtime/merge-gate.ts +7 -1
  89. package/src/runtime/merge-loop.ts +130 -0
  90. package/src/runtime/model/model-budget-summary.ts +53 -0
  91. package/src/runtime/model/model-fallback.ts +32 -2
  92. package/src/runtime/model/pi-args.ts +10 -0
  93. package/src/runtime/model/provider-extensions.ts +10 -0
  94. package/src/runtime/orphan-worker-registry.ts +1 -1
  95. package/src/runtime/output/output-validator.ts +45 -0
  96. package/src/runtime/parent-guard.ts +3 -1
  97. package/src/runtime/peer-dep.ts +2 -1
  98. package/src/runtime/per-write-validator.ts +0 -5
  99. package/src/runtime/pi-spawn.ts +61 -15
  100. package/src/runtime/plan-approval.ts +125 -0
  101. package/src/runtime/plan-replan.ts +151 -0
  102. package/src/runtime/recovery/checkpoint.ts +0 -18
  103. package/src/runtime/recovery/crash-recovery.ts +95 -46
  104. package/src/runtime/scheduler-context.ts +98 -0
  105. package/src/runtime/scheduling/coalesce-tasks.ts +5 -0
  106. package/src/runtime/scheduling/global-worker-cap.ts +2 -1
  107. package/src/runtime/scheduling/nested-slots.ts +70 -0
  108. package/src/runtime/scheduling/run-coalesced-task-group.ts +64 -13
  109. package/src/runtime/scheduling/task-graph-scheduler.ts +0 -10
  110. package/src/runtime/scratchpad/README.md +10 -5
  111. package/src/runtime/scratchpad/guest.ts +103 -2
  112. package/src/runtime/scratchpad/protocol.ts +13 -1
  113. package/src/runtime/scratchpad/transform.ts +206 -12
  114. package/src/runtime/settings-store.ts +219 -0
  115. package/src/runtime/spawn-policy.ts +217 -0
  116. package/src/runtime/stale-reconciler.ts +87 -6
  117. package/src/runtime/subagent-manager.ts +25 -1
  118. package/src/runtime/task-output-context.ts +230 -9
  119. package/src/runtime/task-packet.ts +23 -1
  120. package/src/runtime/task-runner/child-executor.ts +106 -7
  121. package/src/runtime/task-runner/post-execution.ts +125 -1
  122. package/src/runtime/task-runner/pre-execution.ts +39 -1
  123. package/src/runtime/task-runner/prompt-builder.ts +51 -1
  124. package/src/runtime/task-runner/retrieval-orchestrator.ts +72 -18
  125. package/src/runtime/task-runner/spec-evidence.ts +403 -0
  126. package/src/runtime/task-runner/state-helpers.ts +26 -24
  127. package/src/runtime/task-runner.ts +11 -0
  128. package/src/runtime/team-runner.ts +129 -1671
  129. package/src/runtime/verification/spec-sandbox.ts +255 -0
  130. package/src/runtime/verification/verification-gates.ts +3 -2
  131. package/src/runtime/verification/verification-worktree.ts +2 -1
  132. package/src/runtime/workflow-phase-advance.ts +100 -0
  133. package/src/runtime/workspace-tree.ts +9 -0
  134. package/src/schema/config-schema.ts +65 -25
  135. package/src/schema/sensitive-config-paths.ts +64 -0
  136. package/src/schema/team-tool-schema.ts +13 -3
  137. package/src/state/README.md +4 -10
  138. package/src/state/atomic-write.ts +20 -3
  139. package/src/state/contracts.ts +38 -0
  140. package/src/state/coordination/mailbox.ts +12 -2
  141. package/src/state/event-log/cursor.ts +223 -0
  142. package/src/state/event-log/event-log-rotation.ts +12 -4
  143. package/src/state/event-log/event-log.ts +152 -369
  144. package/src/state/event-log/sequence-cache.ts +373 -0
  145. package/src/state/event-log/worker-atomic-writer.ts +2 -1
  146. package/src/state/stores/active-run-registry.ts +3 -2
  147. package/src/state/stores/manifest-io.ts +237 -0
  148. package/src/state/stores/ownership-map.ts +162 -0
  149. package/src/state/stores/plan-store.ts +241 -0
  150. package/src/state/stores/run-cache.ts +0 -90
  151. package/src/state/stores/spec-store.ts +189 -0
  152. package/src/state/stores/state-store.ts +66 -229
  153. package/src/state/types.ts +197 -0
  154. package/src/ui/dashboard-panes/plan-pane.ts +136 -0
  155. package/src/ui/dashboard-panes/progress-pane.ts +6 -0
  156. package/src/ui/dashboard-panes/transcript-pane.ts +31 -0
  157. package/src/ui/heartbeat-aggregator.ts +9 -1
  158. package/src/ui/keybinding-map.ts +54 -13
  159. package/src/ui/powerbar-publisher.ts +52 -1
  160. package/src/ui/run-dashboard.ts +31 -5
  161. package/src/ui/run-snapshot-cache.ts +57 -30
  162. package/src/ui/snapshot-types.ts +6 -1
  163. package/src/ui/widget/widget-renderer.ts +9 -1
  164. package/src/utils/file-coalescer.ts +0 -4
  165. package/src/utils/fs-errno.ts +66 -0
  166. package/src/utils/fs-watch.ts +1 -1
  167. package/src/utils/internal-error.ts +3 -1
  168. package/src/utils/paths.ts +11 -3
  169. package/src/utils/task-name-generator.ts +1 -8
  170. package/src/workflows/discover-workflows.ts +19 -2
  171. package/src/workflows/validate-workflow.ts +7 -1
  172. package/src/workflows/workflow-config.ts +10 -0
  173. package/src/workflows/workflow-serializer.ts +3 -0
  174. package/src/worktree/worktree-manager.ts +22 -0
  175. package/src/agents/agent-search.ts +0 -98
  176. package/src/benchmark/benchmark-runner.ts +0 -313
  177. package/src/benchmark/feedback-loop.ts +0 -73
  178. package/src/config/resilient-parser.ts +0 -117
  179. package/src/extension/crew-vibes/cat-frames.ts +0 -18
  180. package/src/extension/result-watcher.ts +0 -139
  181. package/src/observability/exporters/prometheus-exporter.ts +0 -54
  182. package/src/observability/metric-retention.ts +0 -64
  183. package/src/runtime/compaction/compaction-summary.ts +0 -278
  184. package/src/runtime/errors/crew-errors.ts +0 -162
  185. package/src/runtime/live-session/intercom-bridge.ts +0 -187
  186. package/src/runtime/loop-gates.ts +0 -128
  187. package/src/runtime/metric-parser.ts +0 -36
  188. package/src/runtime/output/stream-preview.ts +0 -184
  189. package/src/runtime/output/tool-progress.ts +0 -278
  190. package/src/runtime/phase-tracker.ts +0 -385
  191. package/src/runtime/pipeline-runner.ts +0 -523
  192. package/src/runtime/process/process-lifecycle.ts +0 -491
  193. package/src/runtime/recovery/retry-runner.ts +0 -330
  194. package/src/runtime/run-drift.ts +0 -219
  195. package/src/runtime/scratchpad/snapshot-hmac.ts +0 -167
  196. package/src/runtime/task-quality.ts +0 -199
  197. package/src/runtime/task-runner/run-projection.ts +0 -128
  198. package/src/runtime/verification/post-checks.ts +0 -142
  199. package/src/state/coordination/schedule.ts +0 -166
  200. package/src/state/event-log/jsonl-writer.ts +0 -115
  201. package/src/state/hook-instinct-bridge.ts +0 -94
  202. package/src/state/hook-integrations.ts +0 -51
  203. package/src/state/session-state-map.ts +0 -51
  204. package/src/state/stores/blob-store.ts +0 -308
  205. package/src/state/stores/instinct-store.ts +0 -275
  206. package/src/state/stores/observation-store.ts +0 -176
  207. package/src/state/tiered-eval.ts +0 -480
  208. package/src/state/types-eval.ts +0 -58
  209. package/src/tools/safe-bash-extension.ts +0 -54
  210. package/src/tools/safe-bash.ts +0 -505
  211. package/src/ui/agent-management-overlay.ts +0 -160
  212. package/src/ui/crew-footer.ts +0 -102
  213. package/src/ui/crew-select-list.ts +0 -114
  214. package/src/ui/dashboard-panes/capability-pane.ts +0 -77
  215. package/src/ui/transcript-entries.ts +0 -256
  216. package/src/utils/conflict-detect.ts +0 -721
  217. package/src/utils/fingerprint.ts +0 -180
  218. package/src/utils/gh-protocol.ts +0 -556
  219. package/src/utils/project-detector.ts +0 -160
  220. package/src/utils/sse-parser.ts +0 -131
  221. package/src/workflows/cost-estimator.ts +0 -34
  222. package/src/workflows/intermediate-store.ts +0 -166
@@ -16,6 +16,7 @@ import { allAgents, discoverAgents } from "../../agents/discover-agents.ts";
16
16
  import { loadConfig } from "../../config/config.ts";
17
17
  import { sanitizeTaskText } from "../../runtime/task-packet.ts";
18
18
  import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
19
+ import { loadSpecRecord } from "../../state/stores/spec-store.ts";
19
20
  import { allTeams, discoverTeams } from "../../teams/discover-teams.ts";
20
21
  import type { TeamConfig } from "../../teams/team-config.ts";
21
22
  import { errorMessage } from "../../utils/guards.ts";
@@ -23,6 +24,38 @@ import { logInternalError } from "../../utils/internal-error.ts";
23
24
  import { resolveRealContainedPath } from "../../utils/safe-paths.ts";
24
25
  import { allWorkflows, discoverWorkflows } from "../../workflows/discover-workflows.ts";
25
26
  import type { WorkflowConfig } from "../../workflows/workflow-config.ts";
27
+
28
+ /** T4/R6 (ADR-6 §5 + round-1): reject-start reason for strict usage —
29
+ * exported for tests. undefined = start allowed. Covers: workflow-level OR
30
+ * step-level specStrict without a verifier-role step (per-step flags bypass
31
+ * the same no-silent-self-certification rule), DWF (structurally verifier-
32
+ * less in v0.10.x), and — when cwd is provided — declared specRefs that
33
+ * resolve to nothing at start (fail-closed freeze, round-1 P2). */
34
+ export function specStrictRejectReason(workflow: WorkflowConfig, cwd?: string): string | undefined {
35
+ const strict = workflow.specStrict === true || workflow.steps.some((s) => s.specStrict === true);
36
+ if (!strict) return undefined;
37
+ if (workflow.runtime === "dynamic") {
38
+ return `Workflow '${workflow.name}' uses specStrict, but dynamic workflows do not support strict spec mode in v0.10.x (ctx.agent() tasks have no verifier-role gate). Drop specStrict for DWF runs (ADR-6 erratum §11).`;
39
+ }
40
+ if (!workflow.steps.some((s) => s.role === "verifier")) {
41
+ return `Workflow '${workflow.name}' enables specStrict (workflow or step level) but has no verifier-role step — strict mode requires independent verification (ADR-6 §5). Add a verifier step or drop specStrict.`;
42
+ }
43
+ if (cwd) {
44
+ // Round-1 P2: fail-closed at START — a strict step whose specRefs resolve
45
+ // to nothing would otherwise silently degrade to ungated at dispatch.
46
+ const declared = [...(workflow.specStrict === true ? workflow.steps : workflow.steps.filter((s) => s.specStrict === true))]
47
+ .flatMap((s) => s.specRefs ?? [])
48
+ .map((id) => ({ id, resolved: loadSpecRecord(cwd, id) !== undefined }))
49
+ .filter((x) => !x.resolved);
50
+ if (declared.length > 0) {
51
+ return `Workflow '${workflow.name}' is strict but these specRefs resolve to nothing in state/specs (typo, not imported, or corrupted): ${declared
52
+ .map((x) => x.id)
53
+ .join(", ")}. Import them first (scripts/spec-import.mjs) or fix the refs (ADR-6 erratum §11).`;
54
+ }
55
+ }
56
+ return undefined;
57
+ }
58
+
26
59
  import { assertCleanLeaderAsync, findGitRootAsync } from "../../worktree/worktree-manager.ts";
27
60
  import type { PiTeamsToolResult } from "../tool-result.ts";
28
61
  import type { TeamContext } from "./context.ts";
@@ -274,6 +307,36 @@ export async function validateRunIntent(
274
307
  );
275
308
  }
276
309
 
310
+ // T4/R6 (ADR-6 §5): REJECT-START — a strict-mode workflow without a
311
+ // verifier-role step fails at start (no silent self-certification).
312
+ // Non-strict workflows are unaffected.
313
+ const rejectReason = !directAgent ? specStrictRejectReason(workflow, resolvedCtx.cwd) : undefined;
314
+ if (rejectReason) {
315
+ return {
316
+ kind: "error",
317
+ result: result(rejectReason, { action: "run", status: "error" }, true),
318
+ };
319
+ }
320
+ // Platform honesty (ADR §4): loud warning — NOT best-effort. Where the
321
+ // re-run sandbox is unavailable every strict check fails closed.
322
+ if (!directAgent && workflow.specStrict === true && process.platform !== "linux") {
323
+ console.warn(
324
+ `⚠️ [team-tool.run] specStrict is enabled but this platform (${process.platform}) has no unshare -rn equivalent: every strict machine-check will FAIL CLOSED (ADR-6 §4).`,
325
+ );
326
+ }
327
+
328
+ // WP-8 (R8): model-routing transparency — one pre-run line: resolved
329
+ // chain + worst-case spawns/task (attemptModels × (maxAttempts+1)).
330
+ if (!directAgent) {
331
+ try {
332
+ // LAZY: defer the routing-pipeline import to run start.
333
+ const { summarizeModelBudget } = await import("../../runtime/model/model-budget-summary.ts");
334
+ console.warn(summarizeModelBudget(resolvedCtx.cwd).line);
335
+ } catch {
336
+ /* advisory only — never block a run on the summary */
337
+ }
338
+ }
339
+
277
340
  // PREFLIGHT (advisory only, since v0.9.15) — informational notes per the
278
341
  // rule in .crew/knowledge.md "pi-crew USAGE THRESHOLD RULE". Never blocks.
279
342
  if (!directAgent) {
@@ -1,8 +1,10 @@
1
1
  import { loadConfig } from "../../config/config.ts";
2
+ import { getCrewEnv } from "../../config/env-vars.ts";
2
3
  // Heavy runtime — lazy-loaded to avoid 1.4s import cost at extension registration.
3
4
  import type { executeTeamRun as ExecuteTeamRunFn } from "../../runtime/team-runner.ts";
4
5
  import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
5
6
  import { atomicWriteJson } from "../../state/atomic-write.ts";
7
+ import { withRunLockSync } from "../../state/coordination/locks.ts";
6
8
  import { registerActiveRun, unregisterActiveRun } from "../../state/stores/active-run-registry.ts";
7
9
  import { writeArtifact } from "../../state/stores/artifact-store.ts";
8
10
  import { createRunManifest, loadRunManifestById, updateRunStatus } from "../../state/stores/state-store.ts";
@@ -10,6 +12,7 @@ import { createRunManifest, loadRunManifestById, updateRunStatus } from "../../s
10
12
  // eslint-disable-next-line @typescript-eslint/no-unused-vars -- type-only import for TS inference
11
13
  const _typeCheck: typeof ExecuteTeamRunFn = null as never as typeof ExecuteTeamRunFn;
12
14
 
15
+ import { fsFailureLabel } from "../../utils/fs-errno.ts";
13
16
  import { errorMessage } from "../../utils/guards.ts";
14
17
  import { logInternalError } from "../../utils/internal-error.ts";
15
18
  import { resolveRealContainedPath } from "../../utils/safe-paths.ts";
@@ -99,7 +102,7 @@ function tailFile(filePath: string, maxBytes = 4096): string | undefined {
99
102
  }
100
103
 
101
104
  function scheduleBackgroundEarlyExitGuard(cwd: string, runId: string, pid: number | undefined, logPath: string): void {
102
- if (process.env.PI_CREW_ASYNC_EARLY_EXIT_GUARD === "0") return;
105
+ if (getCrewEnv("PI_CREW_ASYNC_EARLY_EXIT_GUARD") === "0") return;
103
106
  const timer = setTimeout(() => {
104
107
  const loaded = loadRunManifestById(cwd, runId);
105
108
  if (!loaded || !isActiveRunStatus(loaded.manifest.status)) return;
@@ -112,15 +115,38 @@ function scheduleBackgroundEarlyExitGuard(cwd: string, runId: string, pid: numbe
112
115
  return;
113
116
  const liveness = checkProcessLiveness(pid);
114
117
  if (liveness.alive) return;
115
- const tail = tailFile(logPath);
116
- const message = `Background runner exited within 3s; see background.log${tail ? `\n${tail}` : ""}`;
117
- const failed = updateRunStatus(loaded.manifest, "failed", "Background runner exited within 3s; see background.log");
118
- void appendEventAsync(failed.eventsPath, {
119
- type: "async.failed",
120
- runId: failed.runId,
121
- message,
122
- data: { pid, detail: liveness.detail },
123
- });
118
+ // R14-3 (Round 14): stale-snapshot RMW — the failed-status write used the
119
+ // pre-lock `loaded` snapshot, so a concurrent non-terminal write (e.g.
120
+ // runtime.resolved) could be clobbered. Re-read fresh inside the run lock
121
+ // and re-verify the guard checks so a run that completed/cancelled since
122
+ // the initial load is not flipped to failed.
123
+ try {
124
+ withRunLockSync(loaded.manifest, () => {
125
+ const fresh = loadRunManifestById(cwd, runId);
126
+ if (!fresh) return;
127
+ if (!isActiveRunStatus(fresh.manifest.status)) return;
128
+ if (hasAsyncStartMarker(fresh.manifest)) return;
129
+ if (
130
+ readEventsCursor(fresh.manifest.eventsPath).events.some(
131
+ (event) => event.type === "async.started" || event.type === "async.completed" || event.type === "async.failed",
132
+ )
133
+ )
134
+ return;
135
+ const tail = tailFile(logPath);
136
+ const message = `Background runner exited within 3s; see background.log${tail ? `\n${tail}` : ""}`;
137
+ const failed = updateRunStatus(fresh.manifest, "failed", "Background runner exited within 3s; see background.log");
138
+ void appendEventAsync(failed.eventsPath, {
139
+ type: "async.failed",
140
+ runId: failed.runId,
141
+ message,
142
+ data: { pid, detail: liveness.detail },
143
+ });
144
+ });
145
+ } catch (error) {
146
+ // One-shot unref'd timer — a lock-contention error must not escape and
147
+ // crash the process; the owning session's poll will surface the failure.
148
+ logInternalError("team-tool.run.earlyExitGuard", error instanceof Error ? error : new Error(String(error)), `runId=${runId}`);
149
+ }
124
150
  }, 3000);
125
151
  timer.unref();
126
152
  }
@@ -255,8 +281,12 @@ function formatRunResult(manifest: TeamRunManifest, options: FormatRunResultOpti
255
281
  }
256
282
  const shortResult = resultExcerpt.slice(0, 500);
257
283
  const statusTag = task.status === "completed" ? "✓" : task.status === "failed" ? "✗" : task.status === "cancelled" ? "⊘" : "·";
284
+ // bug-026 sub-issue B: surface the classified fatal-fs cause in the
285
+ // final report — "failed (disk full)" instead of a bare "failed".
286
+ const statusText =
287
+ task.status === "failed" && task.failureCause ? `${task.status} (${fsFailureLabel(task.failureCause)})` : task.status;
258
288
  taskLines.push(
259
- `- ${statusTag} ${task.id} [${task.role}]: ${task.status}${shortResult ? " — " + shortResult : ""}${task.error ? ` | Error: ${task.error.slice(0, 200)}` : ""}`,
289
+ `- ${statusTag} ${task.id} [${task.role}]: ${statusText}${shortResult ? " — " + shortResult : ""}${task.error ? ` | Error: ${task.error.slice(0, 200)}` : ""}`,
260
290
  );
261
291
  if (task.status === "failed" || task.status === "needs_attention") {
262
292
  failedCount++;
@@ -471,7 +501,7 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
471
501
  const runtime = await resolveCrewRuntime(executedConfig);
472
502
  const runtimeResolution = runtimeResolutionState(runtime);
473
503
  // DEBUG: log what we received (gated to avoid stdout pollution in production)
474
- if (process.env.PI_CREW_DEBUG_BUDGET === "1") {
504
+ if (getCrewEnv("PI_CREW_DEBUG_BUDGET") === "1") {
475
505
  console.log(
476
506
  "[DEBUG budget] params keys:",
477
507
  Object.keys(params),
@@ -558,8 +588,8 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
558
588
  diagnostics: {
559
589
  requestedMode: effectiveRuntime.requestedMode,
560
590
  workersDisabled: executedConfig.executeWorkers === false,
561
- envCrew: process.env.PI_CREW_EXECUTE_WORKERS,
562
- envTeams: process.env.PI_TEAMS_EXECUTE_WORKERS,
591
+ envCrew: getCrewEnv("PI_CREW_EXECUTE_WORKERS"),
592
+ envTeams: getCrewEnv("PI_TEAMS_EXECUTE_WORKERS"),
563
593
  },
564
594
  },
565
595
  });
@@ -570,7 +600,7 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
570
600
  `Runtime: ${effectiveRuntime.kind} (requested ${effectiveRuntime.requestedMode})`,
571
601
  `Reason: ${effectiveRuntime.reason ?? "unknown"}`,
572
602
  `Config: executeWorkers=${executedConfig.executeWorkers ?? "<default>"}, runtime.mode=${executedConfig.runtime?.mode ?? "<default>"}`,
573
- `Env: PI_CREW_EXECUTE_WORKERS=${process.env.PI_CREW_EXECUTE_WORKERS ?? "<unset>"}, PI_TEAMS_EXECUTE_WORKERS=${process.env.PI_TEAMS_EXECUTE_WORKERS ?? "<unset>"}`,
603
+ `Env: PI_CREW_EXECUTE_WORKERS=${getCrewEnv("PI_CREW_EXECUTE_WORKERS") ?? "<unset>"}, PI_TEAMS_EXECUTE_WORKERS=${getCrewEnv("PI_TEAMS_EXECUTE_WORKERS") ?? "<unset>"}`,
574
604
  ].join("\n"),
575
605
  {
576
606
  action: "run",
@@ -653,8 +683,8 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
653
683
  diagnostics: {
654
684
  requestedMode: runtime.requestedMode,
655
685
  workersDisabled: executedConfig.executeWorkers === false,
656
- envCrew: process.env.PI_CREW_EXECUTE_WORKERS,
657
- envTeams: process.env.PI_TEAMS_EXECUTE_WORKERS,
686
+ envCrew: getCrewEnv("PI_CREW_EXECUTE_WORKERS"),
687
+ envTeams: getCrewEnv("PI_TEAMS_EXECUTE_WORKERS"),
658
688
  },
659
689
  },
660
690
  });
@@ -665,7 +695,7 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
665
695
  `Runtime: ${runtime.kind} (requested ${runtime.requestedMode})`,
666
696
  `Reason: ${runtime.reason ?? "unknown"}`,
667
697
  `Config: executeWorkers=${executedConfig.executeWorkers ?? "<default>"}, runtime.mode=${executedConfig.runtime?.mode ?? "<default>"}`,
668
- `Env: PI_CREW_EXECUTE_WORKERS=${process.env.PI_CREW_EXECUTE_WORKERS ?? "<unset>"}, PI_TEAMS_EXECUTE_WORKERS=${process.env.PI_TEAMS_EXECUTE_WORKERS ?? "<unset>"}`,
698
+ `Env: PI_CREW_EXECUTE_WORKERS=${getCrewEnv("PI_CREW_EXECUTE_WORKERS") ?? "<unset>"}, PI_TEAMS_EXECUTE_WORKERS=${getCrewEnv("PI_TEAMS_EXECUTE_WORKERS") ?? "<unset>"}`,
669
699
  "",
670
700
  "To run effective subagents, remove executeWorkers=false / PI_CREW_EXECUTE_WORKERS=0 / PI_TEAMS_EXECUTE_WORKERS=0 or set runtime.mode=child-process.",
671
701
  "Use runtime.mode=scaffold only for explicit dry-run prompt/artifact generation.",
@@ -8,6 +8,7 @@ import { checkProcessLiveness, isActiveRunStatus } from "../../runtime/process-s
8
8
  import { formatTaskGraphLines, waitingReason } from "../../runtime/task-display.ts";
9
9
  import { verifyTaskCompletion } from "../../runtime/verification/completion-guard.ts";
10
10
  import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
11
+ import { withRunLockSync } from "../../state/coordination/locks.ts";
11
12
  import { readDeliveryState, readMailbox } from "../../state/coordination/mailbox.ts";
12
13
  import { appendEvent, readEventsCursor } from "../../state/event-log/event-log.ts";
13
14
  import { loadRunManifestById, saveRunTasks, updateRunStatus } from "../../state/stores/state-store.ts";
@@ -38,6 +39,71 @@ function markStaleAsync(runId: string): boolean {
38
39
  return true;
39
40
  }
40
41
 
42
+ /**
43
+ * R14-1 (Phase 3.4): the ONLY state mutation on the status read path — the
44
+ * stale-async transition — executed under the run lock with a FRESH re-read
45
+ * (respond.ts:43 pattern). handleStatus's `loaded` snapshot is best-effort
46
+ * (no lock): a concurrent writer can commit a terminal status between that
47
+ * load and this lock, and `updateRunStatus`'s transition guard would validate
48
+ * against the STALE status (running→failed legal) and clobber the fresh
49
+ * terminal state. Inside the lock we re-read and short-circuit when the fresh
50
+ * state no longer warrants the transition (run gone, async entry gone, status
51
+ * no longer active, process alive).
52
+ *
53
+ * Returns the transitioned manifest+tasks so the caller's read view reflects
54
+ * the write (same-poll visibility, matching pre-fix behavior), or undefined
55
+ * when the transition was skipped.
56
+ *
57
+ * Exported for unit testing (R14-1 regression: a run that completed on disk
58
+ * between load and lock must NOT be re-flipped to failed).
59
+ */
60
+ export function transitionStaleAsyncUnderLock(
61
+ loaded: NonNullable<ReturnType<typeof loadRunManifestById>>,
62
+ runCwd: string,
63
+ runId: string,
64
+ ):
65
+ | {
66
+ manifest: NonNullable<ReturnType<typeof loadRunManifestById>>["manifest"];
67
+ tasks: NonNullable<ReturnType<typeof loadRunManifestById>>["tasks"];
68
+ }
69
+ | undefined {
70
+ let transitioned:
71
+ | {
72
+ manifest: NonNullable<ReturnType<typeof loadRunManifestById>>["manifest"];
73
+ tasks: NonNullable<ReturnType<typeof loadRunManifestById>>["tasks"];
74
+ }
75
+ | undefined;
76
+ withRunLockSync(loaded.manifest, () => {
77
+ const fresh = loadRunManifestById(runCwd, runId); // NOTE: inside withRunLockSync - consistent read
78
+ if (!fresh?.manifest.async || !isActiveRunStatus(fresh.manifest.status)) return;
79
+ const freshLiveness = checkProcessLiveness(fresh.manifest.async.pid);
80
+ if (freshLiveness.alive) return;
81
+ const failed = updateRunStatus(fresh.manifest, "failed", `Async process stale: ${freshLiveness.detail}`);
82
+ const freshTasks = fresh.tasks.map((task) =>
83
+ task.status === "running"
84
+ ? {
85
+ ...task,
86
+ status: "cancelled" as const,
87
+ finishedAt: new Date().toISOString(),
88
+ error: "Async process died; task was not completed.",
89
+ }
90
+ : task,
91
+ );
92
+ saveRunTasks(failed, freshTasks);
93
+ // async.stale (2026-08-10): sync append (byte-identical to pre-extract
94
+ // api.ts) so callers reading eventsPath immediately after status see
95
+ // the event — async fire-and-forget would race.
96
+ appendEvent(failed.eventsPath, {
97
+ type: "async.stale",
98
+ runId: failed.runId,
99
+ message: freshLiveness.detail,
100
+ data: { pid: fresh.manifest.async.pid },
101
+ });
102
+ transitioned = { manifest: failed, tasks: freshTasks };
103
+ });
104
+ return transitioned;
105
+ }
106
+
41
107
  export function handleStatus(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
42
108
  if (!params.runId)
43
109
  return result(
@@ -60,31 +126,23 @@ export function handleStatus(params: TeamToolParamsValue, ctx: TeamContext): PiT
60
126
  const liveness = checkProcessLiveness(asyncState.pid);
61
127
  asyncLivenessLine = `Async: pid=${asyncState.pid ?? "unknown"} alive=${liveness.alive ? "true" : "false"} detail=${liveness.detail} log=${asyncState.logPath} spawnedAt=${asyncState.spawnedAt}`;
62
128
  if (!liveness.alive && isActiveRunStatus(manifest.status)) {
63
- // H5: only transition once per runId. Repeated status polls of a
64
- // dead async run previously re-ran saveRunTasks + sync appendEvent
65
- // on every call (~30ms blocking each).
66
- if (markStaleAsync(manifest.runId)) {
67
- manifest = updateRunStatus(manifest, "failed", `Async process stale: ${liveness.detail}`);
68
- tasks = tasks.map((task) =>
69
- task.status === "running"
70
- ? {
71
- ...task,
72
- status: "cancelled" as const,
73
- finishedAt: new Date().toISOString(),
74
- error: "Async process died; task was not completed.",
75
- }
76
- : task,
77
- );
78
- saveRunTasks(manifest, tasks);
79
- // async.stale (2026-08-10): sync append (byte-identical to pre-extract
80
- // api.ts) so callers reading eventsPath immediately after status see
81
- // the event — async fire-and-forget would race.
82
- appendEvent(manifest.eventsPath, {
83
- type: "async.stale",
84
- runId: manifest.runId,
85
- message: liveness.detail,
86
- data: { pid: asyncState.pid },
87
- });
129
+ // R14-1 (Phase 3.4): ownership gate — a foreign session must NOT
130
+ // trigger the stale-async transition on a read path; the owning
131
+ // session's poll handles it. Mirrors handleResume (team-tool.ts R1).
132
+ // Backward compat: only applies when ownerSessionId is a non-empty
133
+ // string — legacy runs without an owner are not gated.
134
+ const foreignRun = typeof manifest.ownerSessionId === "string" && manifest.ownerSessionId !== ctx.sessionId;
135
+ if (!foreignRun || params.force) {
136
+ // H5: only transition once per runId. Repeated status polls of a
137
+ // dead async run previously re-ran saveRunTasks + sync appendEvent
138
+ // on every call (~30ms blocking each).
139
+ if (markStaleAsync(manifest.runId)) {
140
+ const transitioned = transitionStaleAsyncUnderLock(loaded, runCwd, params.runId);
141
+ if (transitioned) {
142
+ manifest = transitioned.manifest;
143
+ tasks = transitioned.tasks;
144
+ }
145
+ }
88
146
  }
89
147
  }
90
148
  }
@@ -193,7 +251,7 @@ export function handleStatus(params: TeamToolParamsValue, ctx: TeamContext): PiT
193
251
  `- ${task.id} [${task.status}] ${task.role} -> ${task.agent}${task.taskPacket ? ` scope=${task.taskPacket.scope}` : ""}${task.verification ? ` green=${task.verification.observedGreenLevel}/${task.verification.requiredGreenLevel}` : ""}${task.modelAttempts?.length ? ` attempts=${task.modelAttempts.length}` : ""}${task.modelRouting ? ` modelRouting=${task.modelRouting.requested ? `${task.modelRouting.requested}->` : ""}${task.modelRouting.resolved}${task.modelRouting.usedAttempt ? ` attempt=${task.modelRouting.usedAttempt + 1}` : ""}` : ""}${task.agentProgress?.activityState ? ` activityState=${task.agentProgress.activityState}` : ""}${(() => {
194
252
  const t = extractCommandTrace(task.agentProgress?.recentTools);
195
253
  return t.summary ? ` ${t.summary}` : "";
196
- })()}${attentionByTask.get(task.id)?.data?.reason ? ` attention=${String(attentionByTask.get(task.id)?.data?.reason)}` : ""}${task.jsonEvents !== undefined ? ` jsonEvents=${task.jsonEvents}` : ""}${task.usage ? ` usage=${JSON.stringify(task.usage)}` : ""}${task.resultArtifact ? ` result=${task.resultArtifact.path}` : ""}${task.transcriptArtifact ? ` transcript=${task.transcriptArtifact.path}` : ""}${task.worktree ? ` worktree=${task.worktree.path}` : ""}${task.error ? ` error=${task.error}` : ""}`,
254
+ })()}${attentionByTask.get(task.id)?.data?.reason ? ` attention=${String(attentionByTask.get(task.id)?.data?.reason)}` : ""}${task.jsonEvents !== undefined ? ` jsonEvents=${task.jsonEvents}` : ""}${task.usage ? ` usage=${JSON.stringify(task.usage)}` : ""}${task.resultArtifact ? ` result=${task.resultArtifact.path}` : ""}${task.transcriptArtifact ? ` transcript=${task.transcriptArtifact.path}` : ""}${task.worktree ? ` worktree=${task.worktree.path}` : ""}${task.error ? ` error=${task.error}` : ""}${task.failureCause ? ` failureCause=${task.failureCause}` : ""}`,
197
255
  )
198
256
  : ["- (none)"]),
199
257
  `Task counts: ${[...counts.entries()].map(([status, count]) => `${status}=${count}`).join(", ") || "none"}`,
@@ -3,6 +3,7 @@ import * as path from "node:path";
3
3
  import type { AgentConfig } from "../agents/agent-config.ts";
4
4
  import { allAgents, discoverAgents, listDynamicAgents, registerDynamicAgent, unregisterDynamicAgent } from "../agents/discover-agents.ts";
5
5
  import { loadConfig } from "../config/config.ts";
6
+ import { getCrewEnv } from "../config/env-vars.ts";
6
7
  // Heavy runtime — lazy-loaded to avoid 1.4s import cost at extension registration.
7
8
  // executeTeamRun is only called when a team run actually executes.
8
9
  import type { executeTeamRun as _executeTeamRunFn } from "../runtime/team-runner.ts";
@@ -373,8 +374,8 @@ export async function handleResume(params: TeamToolParamsValue, ctx: TeamContext
373
374
  if (!executedConfig.runtime?.mode && resumeManifest.runtimeResolution?.safety === "explicit_dry_run") {
374
375
  const workersDisabled =
375
376
  executedConfig.executeWorkers === false ||
376
- process.env.PI_CREW_EXECUTE_WORKERS === "0" ||
377
- process.env.PI_TEAMS_EXECUTE_WORKERS === "0";
377
+ getCrewEnv("PI_CREW_EXECUTE_WORKERS") === "0" ||
378
+ getCrewEnv("PI_TEAMS_EXECUTE_WORKERS") === "0";
378
379
  if (!workersDisabled)
379
380
  executedConfig.runtime = {
380
381
  ...executedConfig.runtime,
@@ -387,6 +388,14 @@ export async function handleResume(params: TeamToolParamsValue, ctx: TeamContext
387
388
  ...resumeManifest,
388
389
  runtimeResolution,
389
390
  updatedAt: new Date().toISOString(),
391
+ // B1 battery 2026-08-18 (case b root-cause fix): resume ADOPTS the run —
392
+ // ownerSessionId moves to the resuming session. Pre-fix, a force-resumed
393
+ // run kept its DEAD original owner id, so any THIRD session's startup
394
+ // orphan-scan saw "owner no longer exists" and cancelled the live,
395
+ // freshly-resumed run (observed live: orphan_cancelled 5m into a parked
396
+ // ask that the resume itself had just dispatched). Same-session resume
397
+ // is a no-op (id equality).
398
+ ...(ctx.sessionId ? { ownerSessionId: ctx.sessionId } : {}),
390
399
  };
391
400
  await saveRunManifestAsync(runtimeManifest);
392
401
  await appendEventAsync(runtimeManifest.eventsPath, {
@@ -23,13 +23,8 @@ export function registerHook(definition: HookDefinition): number {
23
23
  return hookId;
24
24
  }
25
25
 
26
- export function clearHooks(): void {
27
- registry.clear();
28
- _piCrewHookIds.clear();
29
- _nextHookId = 1;
30
- }
31
-
32
26
  // Scope-aware hook clearing: only removes hooks registered by pi-crew
27
+ // (keeps hooks registered by other extensions/hosts intact).
33
28
  export function clearHooksScoped(): void {
34
29
  for (const [name, hooks] of registry) {
35
30
  const remaining = hooks.filter((h) => !("_hookId" in h && _piCrewHookIds.has((h as { _hookId?: number })._hookId ?? -1)));
package/src/i18n.ts CHANGED
@@ -122,6 +122,11 @@ const translations: Record<string, Partial<Record<Key, string>>> = {
122
122
  let currentLocale: string | undefined;
123
123
  const warnedMissing = new Set<string>();
124
124
 
125
+ // R5-L5/MISSED-1 (Round 5): warn-once dedupe Set — bounded naturally by the
126
+ // translation-key count, but cap FIFO so a pathological locale/key space
127
+ // cannot grow it unbounded in a long-lived host process.
128
+ const MAX_WARNED_MISSING = 128;
129
+
125
130
  // --- Helpers ---
126
131
 
127
132
  function format(template: string, params: Params = {}): string {
@@ -132,6 +137,10 @@ function warnOnce(key: string): void {
132
137
  const tag = `${currentLocale}:${key}`;
133
138
  if (warnedMissing.has(tag)) return;
134
139
  warnedMissing.add(tag);
140
+ if (warnedMissing.size > MAX_WARNED_MISSING) {
141
+ const oldest = warnedMissing.values().next().value;
142
+ if (oldest !== undefined) warnedMissing.delete(oldest);
143
+ }
135
144
  logInternalError("i18n.missing", new Error(`Missing translation`), `key="${key}" locale="${currentLocale}"`);
136
145
  }
137
146