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
@@ -0,0 +1,53 @@
1
+ /**
2
+ * model-budget-summary.ts — R8 model-routing transparency (WP-8, T5).
3
+ *
4
+ * One pre-run summary line: the resolved fallback chain + the WORST-CASE
5
+ * spawn budget per task (`attemptModels × (maxAttempts + 1)` — the RT-6
6
+ * formula; always ≥ 1 full attempt above the theoretical max). Cost
7
+ * surprises in multi-model fallback runs were invisible until workers
8
+ * started burning through attempts; this makes the ceiling loud UP FRONT.
9
+ */
10
+
11
+ import { loadConfig } from "../../config/config.ts";
12
+ import { DEFAULT_RETRY_POLICY } from "../recovery/retry-executor.ts";
13
+ import { computeSpawnBudgetMax } from "../task-runner/child-executor.ts";
14
+ import { buildConfiguredModelRouting } from "./model-fallback.ts";
15
+
16
+ export interface ModelBudgetSummary {
17
+ /** Deduped resolved chain (requested first, then fallbacks). */
18
+ chain: string[];
19
+ /** Worst-case child spawns per task: chain.length × (maxAttempts + 1). */
20
+ worstCaseSpawnsPerTask: number;
21
+ /** Configured maxAttempts (or the default when unset/unreadable). */
22
+ maxAttempts: number;
23
+ /** Rendered single-line summary for console output. */
24
+ line: string;
25
+ }
26
+
27
+ /** Compute the pre-run model budget summary. Never throws — degrades to an
28
+ * empty chain + default attempts on any config/catalog read failure. */
29
+ export function summarizeModelBudget(cwd: string): ModelBudgetSummary {
30
+ let maxAttempts = DEFAULT_RETRY_POLICY.maxAttempts;
31
+ let chain: string[] = [];
32
+ try {
33
+ const { config } = loadConfig(cwd);
34
+ // The same routing pipeline tasks use (config catalog + auto tail policy):
35
+ // the summary must describe what would ACTUALLY spawn, not a parallel
36
+ // reconstruction of it.
37
+ const routing = buildConfiguredModelRouting({ cwd, policy: config.runtime?.modelFallback });
38
+ chain = routing.candidates;
39
+ if (config.reliability?.retryPolicy?.maxAttempts !== undefined) {
40
+ maxAttempts = config.reliability.retryPolicy.maxAttempts;
41
+ }
42
+ } catch {
43
+ /* degrade: defaults below */
44
+ }
45
+ const worst = computeSpawnBudgetMax(Math.max(1, chain.length), maxAttempts);
46
+ const chainText = chain.length ? chain.join(" → ") : "(default pi model)";
47
+ return {
48
+ chain,
49
+ worstCaseSpawnsPerTask: worst,
50
+ maxAttempts,
51
+ line: `[team-tool.run] model routing: ${chainText} · worst-case ${worst} spawns/task (chain=${Math.max(1, chain.length)} × maxAttempts+1=${maxAttempts + 1})`,
52
+ };
53
+ }
@@ -273,14 +273,43 @@ export function splitThinkingSuffix(model: string): {
273
273
  };
274
274
  }
275
275
 
276
+ // WP-8 (R8): loud passthrough warnings — deduped per model so a chain of
277
+ // tasks resolving the same unvalidated model warns once per process. The
278
+ // delegate surface (spawn-policy admission) validates models against the
279
+ // catalog itself (WP-5) and never routes through resolveModelCandidate.
280
+ const passthroughWarned = new Set<string>();
281
+ function warnUnvalidatedPassthrough(model: string, reason: string): void {
282
+ const key = `${model}|${reason}`;
283
+ if (passthroughWarned.has(key)) return;
284
+ passthroughWarned.add(key);
285
+ console.warn(
286
+ `[model-routing] unvalidated passthrough: '${model}' (${reason}) — not confirmed against the configured model catalog; spawn may fail at the provider.`,
287
+ );
288
+ }
289
+
290
+ /** Test seam: the dedup set is module-global (one warn per model+reason per
291
+ * process); tests reset it for deterministic counting. */
292
+ export function resetPassthroughWarnings(): void {
293
+ passthroughWarned.clear();
294
+ }
295
+
276
296
  export function resolveModelCandidate(
277
297
  model: string | undefined,
278
298
  availableModels: AvailableModelInfo[] | undefined,
279
299
  preferredProvider?: string,
280
300
  ): string | undefined {
281
301
  if (!model) return undefined;
282
- if (model.includes("/")) return model;
283
- if (!availableModels || availableModels.length === 0) return model;
302
+ // Provider-qualified refs ("provider/model") pass through UNVALIDATED —
303
+ // the caller asserted a full id; there is nothing to resolve. Loud (R8):
304
+ // this is the documented trust path, not a silent one.
305
+ if (model.includes("/")) {
306
+ warnUnvalidatedPassthrough(model, "provider-qualified ref, no catalog check");
307
+ return model;
308
+ }
309
+ if (!availableModels || availableModels.length === 0) {
310
+ warnUnvalidatedPassthrough(model, "no model catalog available");
311
+ return model;
312
+ }
284
313
 
285
314
  const { baseModel, thinkingSuffix } = splitThinkingSuffix(model);
286
315
  const matches = availableModels.filter((entry) => entry.id === baseModel);
@@ -294,6 +323,7 @@ export function resolveModelCandidate(
294
323
  // Fuzzy fallback: try to resolve via partial name matching
295
324
  const fuzzy = fuzzyResolveModelId(baseModel, availableModels);
296
325
  if (fuzzy) return `${fuzzy}${thinkingSuffix}`;
326
+ warnUnvalidatedPassthrough(model, "no exact or fuzzy catalog match");
297
327
  return model;
298
328
  }
299
329
  return `${matches[0]!.fullId}${thinkingSuffix}`;
@@ -328,6 +328,16 @@ export function buildPiWorkerArgs(input: BuildPiWorkerArgsInput): BuildPiWorkerA
328
328
  ) {
329
329
  allowed = allowed.filter((ext) => path.resolve(ext) === path.resolve(PROMPT_RUNTIME_EXTENSION_PATH));
330
330
  }
331
+ // ADR-5 §8 (governed nesting, T3/WP-5 step 8): every spawn this builder
332
+ // produces runs at depth > 0 (workers are depth 1, delegate grandchildren
333
+ // are 2+; currentCrewDepth(base)+1 is ALWAYS >= 1) — the extension list
334
+ // is an unconditional ALLOWLIST: only the trusted
335
+ // PROMPT_RUNTIME_EXTENSION_PATH passes, REGARDLESS OF SOURCE. User-sourced
336
+ // agent declarations can no longer inject extensions into sub-agents
337
+ // (SEC-1's project-only strip left user/builtin declarations unfiltered —
338
+ // the audit's untested hole). The denylist + SEC-1 strip above remain as
339
+ // defense-in-depth; the allowlist is authoritative.
340
+ allowed = [];
331
341
  for (const extension of [PROMPT_RUNTIME_EXTENSION_PATH, ...allowed]) args.push("--extension", extension);
332
342
  } else {
333
343
  args.push("--extension", PROMPT_RUNTIME_EXTENSION_PATH);
@@ -44,6 +44,12 @@ export interface DiscoveredProviderExtension {
44
44
  */
45
45
  const cache = new Map<string, { mtimeMs: number; result: DiscoveredProviderExtension[] }>();
46
46
 
47
+ // R5-L3 (Round 5 LOW-3): FIFO cap at insertion (mirrors knowledgeCache in
48
+ // knowledge-injection.ts). Entries are only removed when a re-access sees a
49
+ // changed mtime, so distinct settings paths (e.g. per-test temp roots) would
50
+ // otherwise accumulate for the process lifetime.
51
+ const MAX_PROVIDER_EXTENSION_CACHE = 64;
52
+
47
53
  function cachedResult(settingsPath: string): { mtimeMs: number; result: DiscoveredProviderExtension[] } | undefined {
48
54
  const entry = cache.get(settingsPath);
49
55
  if (!entry) return undefined;
@@ -157,6 +163,10 @@ export function discoverProviderExtensions(settingsPath?: string): DiscoveredPro
157
163
  // Cache the resolved result keyed on the settings.json path + mtime.
158
164
  try {
159
165
  cache.set(settingsFile, { mtimeMs: fs.statSync(settingsFile).mtimeMs, result: out });
166
+ if (cache.size > MAX_PROVIDER_EXTENSION_CACHE) {
167
+ const oldest = cache.keys().next().value;
168
+ if (oldest !== undefined) cache.delete(oldest);
169
+ }
160
170
  } catch {
161
171
  cache.delete(settingsFile);
162
172
  }
@@ -201,7 +201,7 @@ function readRegistry(): OrphanWorkerEntry[] {
201
201
  }
202
202
  // Silent failure is deliberate for robustness (registry read failures
203
203
  // shouldn't crash the process), but log at warning level to aid troubleshooting.
204
- console.warn(`[orphan-worker-registry] readRegistry failed: ${error}`);
204
+ logInternalError("orphan-worker-registry", new Error(`readRegistry failed: ${error}`), undefined, "warn");
205
205
  return [];
206
206
  }
207
207
  }
@@ -115,6 +115,51 @@ export function validateWorkerOutput(role: string, output: string): OutputValida
115
115
  };
116
116
  }
117
117
 
118
+ /**
119
+ * bug-026 sub-issue A: strict log-noise line shapes observed in the corrupted
120
+ * `02_explore-core.txt` result artifact (run team_20260815144514) — extension/
121
+ * MCP session-log stderr that the child-executor result fallback chain leaked
122
+ * into the result artifact when the worker payload was corrupted/empty.
123
+ *
124
+ * A trimmed non-empty line counts as log noise ONLY if it matches one of:
125
+ * 1. a bracket-tag extension log line: `[oc-go] ...`, `[pi-qwen-mm] ...`,
126
+ * `[pi-qwen-mm] [core] [stderr] ...` — the leading tag is a lowercase
127
+ * identifier (deliberately excludes capitalized bracketed prose like
128
+ * `[Note] ...`), optionally followed by known sub-tags
129
+ * (core/mcp/stderr/stdout/warn/info/error/debug);
130
+ * 2. a Python `warnings.warn(` line (deprecation-warning continuation);
131
+ * 3. a timestamped logging line: `2026-08-15 21:49:02,986 WARNING ...`.
132
+ *
133
+ * Anything else — prose, markdown, file:line results, `OK done.` — is NOT
134
+ * noise, so a single such line marks the artifact as usable content.
135
+ *
136
+ * NOTE: a `true` return is NOT by itself failure evidence; the caller
137
+ * (post-execution.ts) applies the two-gate rule (authoritative output
138
+ * sources empty AND artifact log-noise-only) before failing a task.
139
+ */
140
+ const BRACKET_TAG_LOG_LINE = /^\[[a-z0-9][a-z0-9_.-]*\](?:\s*\[(?:core|mcp|stderr|stdout|warn|info|error|debug)\])*(?:\s.*)?$/;
141
+ const PYTHON_WARNING_LINE = /(?:^|\s)warnings\.warn\(/;
142
+ const TIMESTAMPED_LOG_LINE = /^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:[.,]\d{1,6})?(?:Z|[+-]\d{2}:?\d{2})?\s+[A-Za-z]+\b/;
143
+
144
+ /**
145
+ * Detect a "stderr-only" result artifact: every non-empty trimmed line is
146
+ * strict log noise (see the pattern docs above). Empty/whitespace-only input
147
+ * returns false — emptiness is the caller's separate, explicit check.
148
+ * Conservative by design: any prose/markdown line makes it return false.
149
+ */
150
+ export function isStderrOnlyResult(text: string): boolean {
151
+ if (!text?.trim()) return false;
152
+ let noiseLines = 0;
153
+ for (const line of text.split("\n")) {
154
+ const trimmed = line.trim();
155
+ if (!trimmed) continue;
156
+ const isNoise = BRACKET_TAG_LOG_LINE.test(trimmed) || PYTHON_WARNING_LINE.test(trimmed) || TIMESTAMPED_LOG_LINE.test(trimmed);
157
+ if (!isNoise) return false;
158
+ noiseLines++;
159
+ }
160
+ return noiseLines > 0;
161
+ }
162
+
118
163
  /**
119
164
  * Extract structured findings from reviewer output.
120
165
  * Returns array of { file, line, severity, message } objects.
@@ -1,3 +1,5 @@
1
+ import { getCrewEnv } from "../config/env-vars.ts";
2
+
1
3
  /**
2
4
  * Parent liveness guard for pi-crew background-runner process.
3
5
  *
@@ -70,7 +72,7 @@
70
72
  * polling approach for near-instantaneous parent-death detection on Unix
71
73
  * systems, avoiding the polling overhead entirely.
72
74
  */
73
- const POLL_INTERVAL_MS = Number(process.env.PI_CREW_PARENT_GUARD_INTERVAL_MS) || 500;
75
+ const POLL_INTERVAL_MS = Number(getCrewEnv("PI_CREW_PARENT_GUARD_INTERVAL_MS")) || 500;
74
76
 
75
77
  const guardIntervals = new Map<number, ReturnType<typeof setInterval>>();
76
78
 
@@ -38,6 +38,7 @@ import * as fs from "node:fs";
38
38
  import * as os from "node:os";
39
39
  import * as path from "node:path";
40
40
  import { fileURLToPath, pathToFileURL } from "node:url";
41
+ import { getCrewEnv } from "../config/env-vars.ts";
41
42
  import { resolveNpmGlobalRoot } from "./pi-spawn.ts";
42
43
 
43
44
  /**
@@ -78,7 +79,7 @@ export function peerDepResolutionBases(): string[] {
78
79
  const bases: string[] = [];
79
80
 
80
81
  // 0. Parent-provided hint (fastest — no probe). Set by async-runner.
81
- const envHint = process.env[PEER_DEP_DIR_ENV]?.trim();
82
+ const envHint = getCrewEnv(PEER_DEP_DIR_ENV)?.trim();
82
83
  if (envHint) bases.push(path.resolve(envHint));
83
84
 
84
85
  // 1. This file's location — works when pi-crew and pi-coding-agent share a
@@ -87,11 +87,6 @@ function rememberSeen(path: string, content: string): void {
87
87
  }
88
88
  }
89
89
 
90
- /** Test seam: reset the dedup cache between tests. */
91
- export function resetPerWriteValidatorCache(): void {
92
- seenContent.clear();
93
- }
94
-
95
90
  /**
96
91
  * Replace the validator registry (test seam). Production uses
97
92
  * DEFAULT_VALIDATORS; tests inject a custom map to exercise specific extensions.
@@ -3,6 +3,8 @@ import * as fs from "node:fs";
3
3
  import * as os from "node:os";
4
4
  import * as path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
+ import { getCrewEnv } from "../config/env-vars.ts";
7
+ import { logInternalError } from "../utils/internal-error.ts";
6
8
 
7
9
  export interface PiSpawnCommand {
8
10
  command: string;
@@ -30,8 +32,11 @@ function isWithinAllowedPrefixes(resolvedPath: string): boolean {
30
32
  const execDir = path.dirname(fs.realpathSync.native(process.execPath));
31
33
  allowedPrefixes.push(execDir.toLowerCase());
32
34
  allowedPrefixes.push(path.join(path.dirname(execDir), "lib", "node_modules").toLowerCase());
33
- } catch {
34
- /* ignore */
35
+ } catch (error) {
36
+ // R17-B3 (LOW): execPath realpath failure is genuinely unexpected (the
37
+ // running binary vanished / permission issue) — surface it so the
38
+ // "cannot find pi" fallback is diagnosable.
39
+ logInternalError("pi-spawn.allowlist-prefixes", error, "execPath realpath failed", "warn");
35
40
  }
36
41
 
37
42
  // npm global bin via APPDATA
@@ -49,24 +54,46 @@ function isWithinAllowedPrefixes(resolvedPath: string): boolean {
49
54
  try {
50
55
  const projectBin = path.resolve("node_modules", ".bin");
51
56
  allowedPrefixes.push(projectBin.toLowerCase());
52
- } catch {
53
- /* ignore */
57
+ } catch (error) {
58
+ // R17-B3: probe failure → prefix simply not allowed; keep the cause
59
+ // visible under PI_TEAMS_DEBUG.
60
+ logInternalError("pi-spawn.allowlist-prefixes.project-bin", error, undefined, "debug");
54
61
  }
55
62
 
56
63
  // User home npm-global
57
64
  try {
58
65
  const homeNpm = path.join(os.homedir(), ".npm-global", "bin");
59
66
  allowedPrefixes.push(homeNpm.toLowerCase());
60
- } catch {
61
- /* ignore */
67
+ } catch (error) {
68
+ // R17-B3: probe failure → prefix simply not allowed; keep the cause
69
+ // visible under PI_TEAMS_DEBUG.
70
+ logInternalError("pi-spawn.allowlist-prefixes.npm-global", error, undefined, "debug");
62
71
  }
63
72
 
64
73
  // User home .local/bin
65
74
  try {
66
75
  const homeLocal = path.join(os.homedir(), ".local", "bin");
67
76
  allowedPrefixes.push(homeLocal.toLowerCase());
68
- } catch {
69
- /* ignore */
77
+ } catch (error) {
78
+ // R17-B3: probe failure → prefix simply not allowed; keep the cause
79
+ // visible under PI_TEAMS_DEBUG.
80
+ logInternalError("pi-spawn.allowlist-prefixes.local-bin", error, undefined, "debug");
81
+ }
82
+
83
+ // Canonicalize prefixes (macOS: /var/folders/... realpath → /private/var/folders/...).
84
+ // validateExplicitBin() compares fs.realpathSync(resolved) against this list —
85
+ // without the canonical forms, any prefix reached through a symlink (macOS
86
+ // tmpdir, ~/.npm-global as symlink) rejects its own realpath'd target
87
+ // (CI incident: run-worker-cap.test.ts on macos-latest). Additive: original
88
+ // forms stay, so Windows \\?\-prefixed realpaths simply never match.
89
+ for (let i = 0; i < allowedPrefixes.length; i++) {
90
+ try {
91
+ const real = fs.realpathSync.native(allowedPrefixes[i]!).toLowerCase();
92
+ if (real !== allowedPrefixes[i] && !allowedPrefixes.includes(real)) allowedPrefixes.push(real);
93
+ } catch {
94
+ // Prefix path doesn't exist (env var points nowhere) — the original form
95
+ // stays; nothing to canonicalize.
96
+ }
70
97
  }
71
98
 
72
99
  return allowedPrefixes.some((prefix) => normalized.startsWith(prefix));
@@ -81,12 +108,20 @@ function resolvePiPackageRoot(): string | undefined {
81
108
  try {
82
109
  const pkg = JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf-8")) as { name?: string };
83
110
  if (pkg.name && PI_PACKAGE_NAMES.includes(pkg.name)) return dir;
84
- } catch {
111
+ } catch (error) {
112
+ // R17-B3: the upward walk EXPECTS ENOENT probes (most dirs have no
113
+ // readable package.json) — debug-gated so the walk stays silent
114
+ // unless PI_TEAMS_DEBUG is set, but EACCES-style causes are visible.
115
+ logInternalError("pi-spawn.resolve-pi-package-root.probe", error, `dir=${dir}`, "debug");
85
116
  // Continue walking upward.
86
117
  }
87
118
  dir = path.dirname(dir);
88
119
  }
89
- } catch {
120
+ } catch (error) {
121
+ // R17-B3: realpath failure on argv[1] is unexpected (not the normal
122
+ // missing-entry case, which is handled by the !entry guard above) —
123
+ // debug-gated visibility for the "cannot find pi" diagnosis.
124
+ logInternalError("pi-spawn.resolve-pi-package-root", error, `argv1=${process.argv[1] ?? ""}`, "debug");
90
125
  return undefined;
91
126
  }
92
127
  return undefined;
@@ -101,7 +136,10 @@ function packageBinScript(packageJsonPath: string): string | undefined {
101
136
  if (!binPath) return undefined;
102
137
  const candidate = path.resolve(path.dirname(packageJsonPath), binPath);
103
138
  return isRunnableNodeScript(candidate) ? candidate : undefined;
104
- } catch {
139
+ } catch (error) {
140
+ // R17-B3 (LOW): a package.json that EXISTS but fails to parse is corrupt —
141
+ // not the expected ENOENT of the upward-walk probes.
142
+ logInternalError("pi-spawn.package-bin-script", error, `packageJsonPath=${packageJsonPath}`, "warn");
105
143
  return undefined;
106
144
  }
107
145
  }
@@ -115,7 +153,10 @@ function findPiPackageJsonFrom(startDir: string): string | undefined {
115
153
  name?: string;
116
154
  };
117
155
  if (pkg.name && PI_PACKAGE_NAMES.includes(pkg.name)) return direct;
118
- } catch {
156
+ } catch (error) {
157
+ // R17-B3: same EXPECTED-ENOENT upward-walk probe as
158
+ // resolvePiPackageRoot — debug-gated, keeps walking either way.
159
+ logInternalError("pi-spawn.find-pi-package-json.probe", error, `dir=${dir}`, "debug");
119
160
  // Continue searching upward and in node_modules.
120
161
  }
121
162
  for (const pkgName of PI_PACKAGE_NAMES) {
@@ -163,7 +204,12 @@ export function resolveNpmGlobalRoot(): string | undefined {
163
204
  windowsHide: true,
164
205
  }).trim();
165
206
  resolved = out.length > 0 ? out : undefined;
166
- } catch {
207
+ } catch (error) {
208
+ // R17-B3 (LOW): `npm root -g` failing (npm not on PATH / timeout) means
209
+ // Windows non-APPDATA installs fall back to the static roots and often
210
+ // fail with ENOENT downstream — surface the root cause here. Memoized,
211
+ // so at most one warn per process.
212
+ logInternalError("pi-spawn.npm-root-g", error, "npm root -g probe failed", "warn");
167
213
  resolved = undefined;
168
214
  }
169
215
  cachedNpmGlobalRoot = resolved ?? null;
@@ -240,14 +286,14 @@ function validateExplicitBin(explicit: string): string | undefined {
240
286
  }
241
287
  } catch (e) {
242
288
  if (e instanceof Error && e.message.includes("allowed prefixes")) throw e;
243
- console.error("[pi-spawn] validateExplicitBin: unexpected realpathSync error:", e);
289
+ logInternalError("pi-spawn", e, "validateExplicitBin: unexpected realpathSync error", "error");
244
290
  return undefined;
245
291
  }
246
292
  return resolved;
247
293
  }
248
294
 
249
295
  export function getPiSpawnCommand(args: string[]): PiSpawnCommand {
250
- const explicit = process.env.PI_TEAMS_PI_BIN?.trim();
296
+ const explicit = getCrewEnv("PI_TEAMS_PI_BIN")?.trim();
251
297
  if (explicit) {
252
298
  const validated = validateExplicitBin(explicit);
253
299
  if (validated) {
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Plan-approval gate helpers.
3
+ *
4
+ * Extracted from team-runner.ts (2026-08, Phase 2.6 maintainability split —
5
+ * plan-approval family). Pure code motion: requiresPlanApproval /
6
+ * isPlanApprovalPending / isMutatingTask / ensurePlanApprovalRequested moved
7
+ * verbatim from team-runner.ts. cancelPlanTasks stays in team-runner.ts
8
+ * (RT-14 structural source-pin test requires it defined there).
9
+ */
10
+
11
+ import * as fs from "node:fs";
12
+ import type { CrewRuntimeConfig } from "../config/config.ts";
13
+ import { parsePlannerPlanOutput } from "../extension/plan-orchestrate.ts";
14
+ import { appendEvent } from "../state/event-log/event-log.ts";
15
+ import { appendPlanRevision, getCurrentPlanRecord, setPlanApproval } from "../state/stores/plan-store.ts";
16
+ import { saveRunManifestAsync } from "../state/stores/state-store.ts";
17
+ import type { PlanRecord, TeamRunManifest, TeamTaskState } from "../state/types.ts";
18
+ import type { WorkflowConfig } from "../workflows/workflow-config.ts";
19
+ import { permissionForRole } from "./role-permission.ts";
20
+
21
+ export function requiresPlanApproval(_workflow: WorkflowConfig, runtimeConfig: CrewRuntimeConfig | undefined): boolean {
22
+ // ROADMAP T1.2: plan-level HITL applies to ANY workflow when
23
+ // config.runtime.requirePlanApproval === true (not just 'implementation').
24
+ // The gate fires at the read-only → mutating (plan → execute) boundary.
25
+ return runtimeConfig?.requirePlanApproval === true;
26
+ }
27
+
28
+ /** State-level predicate shared by every render surface (widget badge,
29
+ * progress banner, powerbar segment) so gating is byte-identical to the
30
+ * action side: `required === true && status === "pending"`. A malformed
31
+ * manifest (`pending` without `required`) must NOT light up surfaces while
32
+ * keys/backends refuse — single source of truth (WP-3 review F2). */
33
+ export function isPlanApprovalStatePending(approval: TeamRunManifest["planApproval"]): boolean {
34
+ return approval?.required === true && approval.status === "pending";
35
+ }
36
+
37
+ export function isPlanApprovalPending(manifest: TeamRunManifest): boolean {
38
+ return isPlanApprovalStatePending(manifest.planApproval);
39
+ }
40
+
41
+ /**
42
+ * T2/R4 (ADR-4 §2, reader migration): plan-record-first approval predicate
43
+ * with manifest fallback. The current revision's approval state, when present,
44
+ * is authoritative; otherwise the legacy manifest gate decides. Pre-v2 runs
45
+ * (no plans.json) take exactly the old path — pinned by the migration
46
+ * negative AC in plan-store.test.ts.
47
+ */
48
+ export function isPlanApprovalPendingEffective(manifest: TeamRunManifest): boolean {
49
+ const current = getCurrentPlanRecord(manifest);
50
+ if (current?.approval) return current.approval.status === "pending";
51
+ return isPlanApprovalPending(manifest);
52
+ }
53
+
54
+ /**
55
+ * T2/R4 (ADR-4 §8 vocabulary mapping): a deny is `cancelled` on the manifest
56
+ * side and `rejected` on the record side — either means "decided negative"
57
+ * to the scheduler cancel paths (team-runner.ts readers).
58
+ */
59
+ export function isPlanApprovalDenied(manifest: TeamRunManifest): boolean {
60
+ if (manifest.planApproval?.status === "cancelled") return true;
61
+ return getCurrentPlanRecord(manifest)?.approval?.status === "rejected";
62
+ }
63
+
64
+ export function isMutatingTask(task: TeamTaskState): boolean {
65
+ return permissionForRole(task.role) !== "read_only";
66
+ }
67
+
68
+ export async function ensurePlanApprovalRequested(manifest: TeamRunManifest, tasks: TeamTaskState[]): Promise<TeamRunManifest> {
69
+ if (manifest.planApproval) return manifest;
70
+ const assessTask = tasks.find((task) => task.stepId === "assess" && task.status === "completed");
71
+ // ROADMAP T1.2: for non-adaptive workflows, fall back to the most recent
72
+ // completed read-only (planning) task as the plan reference.
73
+ const planTask = assessTask ?? [...tasks].reverse().find((t) => t.status === "completed" && !isMutatingTask(t));
74
+ const now = new Date().toISOString();
75
+
76
+ // T2/R4 (ADR-4 §6 producer 3 + §8 dual-write): make sure a PlanRecord
77
+ // exists before the gate lights up, so approval names plan id+version.
78
+ // - Record already present (adaptive producer ran, or a re-request after
79
+ // crash between the record append and the manifest save) → reuse it.
80
+ // - No record → try the planner-tagged `<plan>` contract on the plan
81
+ // artifact; if that fails → manifest-only gate (pre-v2 behavior, the
82
+ // dual-read fallback in plan-store keeps readers correct).
83
+ let record = getCurrentPlanRecord(manifest);
84
+ if (!record && planTask?.resultArtifact?.path) {
85
+ try {
86
+ const text = fs.readFileSync(planTask.resultArtifact.path, "utf-8");
87
+ const parsed: PlanRecord | undefined = parsePlannerPlanOutput(text, manifest.runId, planTask.id);
88
+ if (parsed) record = appendPlanRevision(manifest, parsed);
89
+ } catch {
90
+ // Unreadable/unparsable artifact → manifest-only gate (below); the
91
+ // gate must still light up — never a throw here.
92
+ }
93
+ }
94
+ if (record) {
95
+ setPlanApproval(manifest, { status: "pending", planVersion: record.version });
96
+ }
97
+
98
+ const updated: TeamRunManifest = {
99
+ ...manifest,
100
+ updatedAt: now,
101
+ planApproval: {
102
+ required: true,
103
+ status: "pending",
104
+ requestedAt: now,
105
+ updatedAt: now,
106
+ planTaskId: planTask?.id,
107
+ planArtifactPath: planTask?.resultArtifact?.path,
108
+ },
109
+ plan: record ? { id: record.id, version: record.version } : manifest.plan,
110
+ };
111
+ await saveRunManifestAsync(updated);
112
+ appendEvent(updated.eventsPath, {
113
+ type: "plan.approval_required",
114
+ runId: updated.runId,
115
+ taskId: planTask?.id,
116
+ message: "Plan requires explicit approval before mutating tasks run. Use: team api op=approve-plan runId=...",
117
+ data: { planArtifactPath: planTask?.resultArtifact?.path, planId: record?.id, planVersion: record?.version },
118
+ });
119
+ return updated;
120
+ }
121
+
122
+ /** @internal 1.9(b) test export — exercise requiresPlanApproval directly. */
123
+ export const __test__requiresPlanApproval = requiresPlanApproval;
124
+ /** @internal 1.9(b) test export — exercise ensurePlanApprovalRequested directly. */
125
+ export const __test__ensurePlanApprovalRequested = ensurePlanApprovalRequested;