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
@@ -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,8 +12,10 @@ 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";
18
+ import { safeAbort } from "../../utils/safe-abort.ts";
15
19
  import { resolveRealContainedPath } from "../../utils/safe-paths.ts";
16
20
 
17
21
  let _cachedExecuteTeamRun: typeof ExecuteTeamRunFn | undefined;
@@ -99,7 +103,7 @@ function tailFile(filePath: string, maxBytes = 4096): string | undefined {
99
103
  }
100
104
 
101
105
  function scheduleBackgroundEarlyExitGuard(cwd: string, runId: string, pid: number | undefined, logPath: string): void {
102
- if (process.env.PI_CREW_ASYNC_EARLY_EXIT_GUARD === "0") return;
106
+ if (getCrewEnv("PI_CREW_ASYNC_EARLY_EXIT_GUARD") === "0") return;
103
107
  const timer = setTimeout(() => {
104
108
  const loaded = loadRunManifestById(cwd, runId);
105
109
  if (!loaded || !isActiveRunStatus(loaded.manifest.status)) return;
@@ -112,15 +116,38 @@ function scheduleBackgroundEarlyExitGuard(cwd: string, runId: string, pid: numbe
112
116
  return;
113
117
  const liveness = checkProcessLiveness(pid);
114
118
  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
- });
119
+ // R14-3 (Round 14): stale-snapshot RMW — the failed-status write used the
120
+ // pre-lock `loaded` snapshot, so a concurrent non-terminal write (e.g.
121
+ // runtime.resolved) could be clobbered. Re-read fresh inside the run lock
122
+ // and re-verify the guard checks so a run that completed/cancelled since
123
+ // the initial load is not flipped to failed.
124
+ try {
125
+ withRunLockSync(loaded.manifest, () => {
126
+ const fresh = loadRunManifestById(cwd, runId);
127
+ if (!fresh) return;
128
+ if (!isActiveRunStatus(fresh.manifest.status)) return;
129
+ if (hasAsyncStartMarker(fresh.manifest)) return;
130
+ if (
131
+ readEventsCursor(fresh.manifest.eventsPath).events.some(
132
+ (event) => event.type === "async.started" || event.type === "async.completed" || event.type === "async.failed",
133
+ )
134
+ )
135
+ return;
136
+ const tail = tailFile(logPath);
137
+ const message = `Background runner exited within 3s; see background.log${tail ? `\n${tail}` : ""}`;
138
+ const failed = updateRunStatus(fresh.manifest, "failed", "Background runner exited within 3s; see background.log");
139
+ void appendEventAsync(failed.eventsPath, {
140
+ type: "async.failed",
141
+ runId: failed.runId,
142
+ message,
143
+ data: { pid, detail: liveness.detail },
144
+ });
145
+ });
146
+ } catch (error) {
147
+ // One-shot unref'd timer — a lock-contention error must not escape and
148
+ // crash the process; the owning session's poll will surface the failure.
149
+ logInternalError("team-tool.run.earlyExitGuard", error instanceof Error ? error : new Error(String(error)), `runId=${runId}`);
150
+ }
124
151
  }, 3000);
125
152
  timer.unref();
126
153
  }
@@ -255,8 +282,12 @@ function formatRunResult(manifest: TeamRunManifest, options: FormatRunResultOpti
255
282
  }
256
283
  const shortResult = resultExcerpt.slice(0, 500);
257
284
  const statusTag = task.status === "completed" ? "✓" : task.status === "failed" ? "✗" : task.status === "cancelled" ? "⊘" : "·";
285
+ // bug-026 sub-issue B: surface the classified fatal-fs cause in the
286
+ // final report — "failed (disk full)" instead of a bare "failed".
287
+ const statusText =
288
+ task.status === "failed" && task.failureCause ? `${task.status} (${fsFailureLabel(task.failureCause)})` : task.status;
258
289
  taskLines.push(
259
- `- ${statusTag} ${task.id} [${task.role}]: ${task.status}${shortResult ? " — " + shortResult : ""}${task.error ? ` | Error: ${task.error.slice(0, 200)}` : ""}`,
290
+ `- ${statusTag} ${task.id} [${task.role}]: ${statusText}${shortResult ? " — " + shortResult : ""}${task.error ? ` | Error: ${task.error.slice(0, 200)}` : ""}`,
260
291
  );
261
292
  if (task.status === "failed" || task.status === "needs_attention") {
262
293
  failedCount++;
@@ -471,7 +502,7 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
471
502
  const runtime = await resolveCrewRuntime(executedConfig);
472
503
  const runtimeResolution = runtimeResolutionState(runtime);
473
504
  // DEBUG: log what we received (gated to avoid stdout pollution in production)
474
- if (process.env.PI_CREW_DEBUG_BUDGET === "1") {
505
+ if (getCrewEnv("PI_CREW_DEBUG_BUDGET") === "1") {
475
506
  console.log(
476
507
  "[DEBUG budget] params keys:",
477
508
  Object.keys(params),
@@ -558,8 +589,8 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
558
589
  diagnostics: {
559
590
  requestedMode: effectiveRuntime.requestedMode,
560
591
  workersDisabled: executedConfig.executeWorkers === false,
561
- envCrew: process.env.PI_CREW_EXECUTE_WORKERS,
562
- envTeams: process.env.PI_TEAMS_EXECUTE_WORKERS,
592
+ envCrew: getCrewEnv("PI_CREW_EXECUTE_WORKERS"),
593
+ envTeams: getCrewEnv("PI_TEAMS_EXECUTE_WORKERS"),
563
594
  },
564
595
  },
565
596
  });
@@ -570,7 +601,7 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
570
601
  `Runtime: ${effectiveRuntime.kind} (requested ${effectiveRuntime.requestedMode})`,
571
602
  `Reason: ${effectiveRuntime.reason ?? "unknown"}`,
572
603
  `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>"}`,
604
+ `Env: PI_CREW_EXECUTE_WORKERS=${getCrewEnv("PI_CREW_EXECUTE_WORKERS") ?? "<unset>"}, PI_TEAMS_EXECUTE_WORKERS=${getCrewEnv("PI_TEAMS_EXECUTE_WORKERS") ?? "<unset>"}`,
574
605
  ].join("\n"),
575
606
  {
576
607
  action: "run",
@@ -653,8 +684,8 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
653
684
  diagnostics: {
654
685
  requestedMode: runtime.requestedMode,
655
686
  workersDisabled: executedConfig.executeWorkers === false,
656
- envCrew: process.env.PI_CREW_EXECUTE_WORKERS,
657
- envTeams: process.env.PI_TEAMS_EXECUTE_WORKERS,
687
+ envCrew: getCrewEnv("PI_CREW_EXECUTE_WORKERS"),
688
+ envTeams: getCrewEnv("PI_TEAMS_EXECUTE_WORKERS"),
658
689
  },
659
690
  },
660
691
  });
@@ -665,7 +696,7 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
665
696
  `Runtime: ${runtime.kind} (requested ${runtime.requestedMode})`,
666
697
  `Reason: ${runtime.reason ?? "unknown"}`,
667
698
  `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>"}`,
699
+ `Env: PI_CREW_EXECUTE_WORKERS=${getCrewEnv("PI_CREW_EXECUTE_WORKERS") ?? "<unset>"}, PI_TEAMS_EXECUTE_WORKERS=${getCrewEnv("PI_TEAMS_EXECUTE_WORKERS") ?? "<unset>"}`,
669
700
  "",
670
701
  "To run effective subagents, remove executeWorkers=false / PI_CREW_EXECUTE_WORKERS=0 / PI_TEAMS_EXECUTE_WORKERS=0 or set runtime.mode=child-process.",
671
702
  "Use runtime.mode=scaffold only for explicit dry-run prompt/artifact generation.",
@@ -692,12 +723,14 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
692
723
  // so cancel-via-abortForegroundRun propagates to executeTeamRun.
693
724
  fgCallbackSignal = signal;
694
725
  if (signal && signal !== fgSignal) {
695
- if (signal.aborted) fgDeadline.controller.abort();
726
+ if (signal.aborted) safeAbort(fgDeadline.controller, "fg-run.caller-preaborted");
696
727
  else {
697
728
  // RC-03: keep the ref so we can removeEventListener on completion (the
698
729
  // {once:true} alone leaks on the success path — the listener stays
699
730
  // attached to the long-lived callback signal if the deadline never fires).
700
- fgAbortListener = () => fgDeadline.controller.abort();
731
+ // safe-abort: the deadline signal may be linked to spawned children;
732
+ // aborting after they exited throws AbortError from Node's listener.
733
+ fgAbortListener = () => safeAbort(fgDeadline.controller, "fg-run.caller-abort");
701
734
  signal.addEventListener("abort", fgAbortListener, { once: true });
702
735
  }
703
736
  }
@@ -741,6 +774,27 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
741
774
  // Wait for the foreground run to complete and return actual results.
742
775
  try {
743
776
  const completed = await waitForRun(updatedManifest.runId, resolvedCtx.cwd, { timeoutMs: fgDeadline.deadlineMs });
777
+ if (completed.detached) {
778
+ // The waiter was released so this turn can settle (agent view
779
+ // switch); the run keeps executing and reports on completion.
780
+ return result(
781
+ [
782
+ `pi-crew run detached to background: ${updatedManifest.runId}`,
783
+ `Team: ${team.name}`,
784
+ `Workflow: ${workflow.name}`,
785
+ `Goal: ${goal}`,
786
+ "",
787
+ "The run keeps executing — you will be notified when it finishes.",
788
+ `Check status with: team status runId=${updatedManifest.runId}`,
789
+ ].join("\n"),
790
+ {
791
+ action: "run",
792
+ status: "ok",
793
+ runId: updatedManifest.runId,
794
+ artifactsRoot: updatedManifest.artifactsRoot,
795
+ },
796
+ );
797
+ }
744
798
  return formatRunResult(completed.manifest, {
745
799
  tasks: completed.tasks,
746
800
  metrics: collectRunMetrics(resolvedCtx.cwd, completed.manifest.runId),
@@ -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