pi-crew 0.9.41 → 0.9.44

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 (170) hide show
  1. package/CHANGELOG.md +253 -0
  2. package/README.md +14 -0
  3. package/dist/build-meta.json +6450 -12419
  4. package/dist/index.mjs +57998 -68417
  5. package/dist/index.mjs.map +4 -4
  6. package/docs/migration/atomic-write-v2-migration.md +2 -0
  7. package/docs/superpowers/plans/2026-06-09-fallow-patterns-adoption.md +2 -0
  8. package/package.json +4 -2
  9. package/scripts/README.md +6 -0
  10. package/scripts/build-bundle.mjs +2 -1
  11. package/scripts/check-all-skills.ts +3 -3
  12. package/scripts/dev-runner.mjs +106 -0
  13. package/scripts/postinstall.mjs +6 -2
  14. package/scripts/verify-skill.ts +2 -2
  15. package/src/agents/agent-config.ts +1 -1
  16. package/src/config/config.ts +20 -1
  17. package/src/config/drift-detector.ts +97 -15
  18. package/src/config/resilient-parser.ts +4 -4
  19. package/src/config/types.ts +0 -2
  20. package/src/extension/crew-cleanup.ts +17 -3
  21. package/src/extension/crew-vibes/config.ts +1 -1
  22. package/src/extension/crew-vibes/font-detect.ts +16 -3
  23. package/src/extension/crew-vibes/index.ts +1 -1
  24. package/src/extension/cross-extension-rpc.ts +26 -9
  25. package/src/extension/management.ts +6 -5
  26. package/src/extension/register.ts +58 -1573
  27. package/src/extension/registration/command-registration.ts +57 -0
  28. package/src/extension/registration/commands.ts +7 -2
  29. package/src/extension/registration/context-builder.ts +131 -0
  30. package/src/extension/registration/crash-recovery-cache.ts +54 -0
  31. package/src/extension/registration/foreground-run-controller.ts +238 -0
  32. package/src/extension/registration/hook-registration.ts +119 -0
  33. package/src/extension/registration/lazy-configurers.ts +124 -0
  34. package/src/extension/registration/lifecycle-handlers.ts +731 -0
  35. package/src/extension/registration/observability.ts +33 -4
  36. package/src/extension/registration/registration-types.ts +149 -0
  37. package/src/extension/registration/runtime-cleanup.ts +230 -0
  38. package/src/extension/registration/subagent-manager-setup.ts +211 -0
  39. package/src/extension/registration/tool-registration.ts +50 -0
  40. package/src/extension/registration/wire-cross-extension.ts +37 -0
  41. package/src/extension/rpc-hmac.ts +21 -1
  42. package/src/extension/run-import.ts +3 -3
  43. package/src/extension/team-tool/auto-summarize.ts +4 -4
  44. package/src/extension/team-tool/cancel.ts +5 -5
  45. package/src/extension/team-tool/chain-executor.ts +5 -4
  46. package/src/extension/team-tool/context.ts +11 -1
  47. package/src/extension/team-tool/doctor.ts +2 -1
  48. package/src/extension/team-tool/goal-wrap.ts +2 -2
  49. package/src/extension/team-tool/goal.ts +1 -1
  50. package/src/extension/team-tool/handle-schedule.ts +25 -36
  51. package/src/extension/team-tool/orchestrate.ts +0 -1
  52. package/src/extension/team-tool/parallel-dispatch.ts +5 -0
  53. package/src/extension/team-tool/run.ts +220 -214
  54. package/src/extension/team-tool/workflow-manage.ts +1 -1
  55. package/src/extension/team-tool.ts +69 -16
  56. package/src/hooks/registry.ts +7 -2
  57. package/src/observability/correlation.ts +4 -3
  58. package/src/observability/event-bus.ts +10 -1
  59. package/src/observability/event-to-metric.ts +8 -7
  60. package/src/observability/exporters/adapter.ts +6 -9
  61. package/src/observability/exporters/otlp-exporter.ts +81 -0
  62. package/src/observability/metric-retention.ts +4 -0
  63. package/src/observability/metric-sink.ts +14 -4
  64. package/src/prompt/prompt-runtime.ts +15 -0
  65. package/src/runtime/adaptive-plan.ts +1 -1
  66. package/src/runtime/async-runner.ts +13 -4
  67. package/src/runtime/background-runner.ts +32 -23
  68. package/src/runtime/capability-inventory.ts +1 -1
  69. package/src/runtime/chain-runner.ts +5 -5
  70. package/src/runtime/checkpoint.ts +6 -16
  71. package/src/runtime/child-pi-constants.ts +42 -0
  72. package/src/runtime/child-pi-kill.ts +180 -0
  73. package/src/runtime/child-pi-spawn.ts +234 -0
  74. package/src/runtime/child-pi-steering.ts +128 -0
  75. package/src/runtime/child-pi-streams.ts +296 -0
  76. package/src/runtime/child-pi-transcript.ts +169 -0
  77. package/src/runtime/child-pi.ts +80 -822
  78. package/src/runtime/coalesce-tasks.ts +1 -1
  79. package/src/runtime/compact-stages/tail-capture-stage.ts +1 -1
  80. package/src/runtime/crash-recovery.ts +63 -47
  81. package/src/runtime/delivery-coordinator.ts +1 -1
  82. package/src/runtime/deterministic-ast.ts +2 -2
  83. package/src/runtime/diagnostic-export.ts +2 -1
  84. package/src/runtime/dwf-state-store.ts +5 -0
  85. package/src/runtime/dynamic-workflow-context.ts +78 -18
  86. package/src/runtime/dynamic-workflow-runner.ts +18 -2
  87. package/src/runtime/foreground-control.ts +3 -2
  88. package/src/runtime/goal-evaluator.ts +59 -27
  89. package/src/runtime/goal-loop-runner.ts +46 -13
  90. package/src/runtime/goal-state-store.ts +1 -1
  91. package/src/runtime/handoff-manager.ts +0 -2
  92. package/src/runtime/intercom-bridge.ts +0 -2
  93. package/src/runtime/live-agent-manager.ts +30 -5
  94. package/src/runtime/live-extension-bridge.ts +0 -2
  95. package/src/runtime/live-session-runtime.ts +65 -57
  96. package/src/runtime/manifest-cache.ts +59 -0
  97. package/src/runtime/mcp-proxy.ts +1 -1
  98. package/src/runtime/orphan-worker-registry.ts +3 -3
  99. package/src/runtime/output-validator.ts +1 -1
  100. package/src/runtime/path-overlap.ts +1 -1
  101. package/src/runtime/pi-args.ts +3 -3
  102. package/src/runtime/pi-json-output.ts +4 -3
  103. package/src/runtime/pi-spawn.ts +7 -1
  104. package/src/runtime/pipeline-runner.ts +1 -4
  105. package/src/runtime/post-checks.ts +0 -1
  106. package/src/runtime/retry-runner.ts +1 -1
  107. package/src/runtime/run-coalesced-task-group.ts +18 -23
  108. package/src/runtime/scheduler.ts +2 -0
  109. package/src/runtime/session-resources.ts +1 -1
  110. package/src/runtime/settings-store.ts +31 -1
  111. package/src/runtime/skill-effectiveness.ts +2 -2
  112. package/src/runtime/stale-reconciler.ts +23 -53
  113. package/src/runtime/subagent-manager.ts +19 -3
  114. package/src/runtime/task-output-context.ts +2 -1
  115. package/src/runtime/task-runner/prompt-builder.ts +3 -3
  116. package/src/runtime/task-runner/run-projection.ts +3 -1
  117. package/src/runtime/task-runner/state-helpers.ts +13 -1
  118. package/src/runtime/task-runner.ts +31 -6
  119. package/src/runtime/team-runner.ts +8 -2
  120. package/src/runtime/verification-gates.ts +54 -5
  121. package/src/schema/config-schema.ts +19 -1
  122. package/src/schema/team-tool-schema.ts +2 -0
  123. package/src/skills/discover-skills.ts +25 -15
  124. package/src/state/active-run-registry.ts +0 -1
  125. package/src/state/atomic-write.ts +37 -11
  126. package/src/state/blob-store.ts +3 -9
  127. package/src/state/contracts.ts +5 -1
  128. package/src/state/crew-init.ts +3 -2
  129. package/src/state/decision-ledger.ts +2 -2
  130. package/src/state/event-log-rotation.ts +1 -1
  131. package/src/state/event-log.ts +4 -3
  132. package/src/state/gitignore-manager.ts +2 -1
  133. package/src/state/health-store.ts +5 -2
  134. package/src/state/instinct-store.ts +11 -2
  135. package/src/state/locks.ts +132 -49
  136. package/src/state/mailbox.ts +3 -4
  137. package/src/state/observation-store.ts +1 -13
  138. package/src/state/run-cache.ts +12 -12
  139. package/src/state/run-graph.ts +2 -1
  140. package/src/state/run-metrics.ts +1 -1
  141. package/src/state/schedule.ts +7 -1
  142. package/src/state/state-store.ts +9 -3
  143. package/src/state/tiered-eval.ts +1 -1
  144. package/src/state/worker-atomic-writer.ts +1 -0
  145. package/src/ui/dynamic-border.ts +13 -3
  146. package/src/ui/live-conversation-overlay.ts +7 -27
  147. package/src/ui/live-duration.ts +2 -2
  148. package/src/ui/live-run-sidebar.ts +56 -32
  149. package/src/ui/mascot.ts +19 -13
  150. package/src/ui/powerbar-publisher.ts +244 -178
  151. package/src/ui/render-coalescer.ts +44 -3
  152. package/src/ui/render-diff.ts +2 -2
  153. package/src/ui/run-dashboard.ts +122 -21
  154. package/src/ui/run-event-bus.ts +66 -0
  155. package/src/ui/run-snapshot-cache.ts +84 -81
  156. package/src/ui/settings-overlay.ts +3 -3
  157. package/src/ui/status-colors.ts +24 -0
  158. package/src/ui/tool-progress-formatter.ts +1 -1
  159. package/src/ui/tool-render.ts +3 -3
  160. package/src/ui/transcript-cache.ts +5 -10
  161. package/src/ui/transcript-entries.ts +68 -72
  162. package/src/ui/transcript-viewer.ts +54 -14
  163. package/src/ui/widget/index.ts +26 -15
  164. package/src/ui/widget/widget-model.ts +18 -9
  165. package/src/ui/widget/widget-renderer.ts +2 -5
  166. package/src/utils/fingerprint.ts +1 -1
  167. package/src/utils/paths.ts +1 -1
  168. package/src/utils/project-detector.ts +1 -1
  169. package/src/worktree/worktree-manager.ts +47 -19
  170. package/src/state/atomic-write-v2.ts +0 -108
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Tool registration installer for pi-crew.
3
+ *
4
+ * Single entry point that wires every `pi.registerTool(...)` call site
5
+ * in the extension. Today that means two heavy tools:
6
+ * • `team` — the multi-agent orchestration tool (team-tool.ts).
7
+ * • subagent tools — agent spawn tool + result-join helpers
8
+ * (subagent-tools.ts).
9
+ *
10
+ * The actual tool handlers live in `registration/team-tool.ts` and
11
+ * `registration/subagent-tools.ts` (already extracted); this file is
12
+ * the orchestrator-level wrapper that calls them with the right deps.
13
+ */
14
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
15
+ import type { RegistrationContext } from "./registration-types.ts";
16
+ import { registerSubagentTools } from "./subagent-tools.ts";
17
+ import { registerTeamTool } from "./team-tool.ts";
18
+
19
+ /**
20
+ * Register all pi-crew tools on the ExtensionAPI.
21
+ *
22
+ * Pulls every dependency the underlying tools need off `ctx`. The
23
+ * orchestrator guarantees those fields are populated before this call
24
+ * (see registerPiTeams in ../register.ts).
25
+ */
26
+ export function registerPiTools(pi: ExtensionAPI, ctx: RegistrationContext): void {
27
+ registerTeamTool(pi, {
28
+ foregroundControllers: ctx.foregroundControllers,
29
+ startForegroundRun: ctx.startForegroundRun,
30
+ abortForegroundRun: ctx.abortForegroundRun,
31
+ openLiveSidebar: ctx.openLiveSidebar,
32
+ getManifestCache: ctx.getManifestCache,
33
+ getRunSnapshotCache: ctx.getRunSnapshotCache,
34
+ getMetricRegistry: () => ctx.observabilityState.metricRegistry,
35
+ widgetState: ctx.widgetState,
36
+ onJsonEvent: (taskId, runId, event) => {
37
+ const record = event as Record<string, unknown>;
38
+ const eventType = typeof record.type === "string" ? record.type : undefined;
39
+ if (eventType) ctx.lifecycleState.overflowTracker?.feedEvent(taskId, runId, eventType);
40
+ },
41
+ });
42
+ registerSubagentTools(pi, ctx.subagentManager, {
43
+ ownerSessionGeneration: ctx.captureSessionGeneration,
44
+ // Subagent options only type-check against `unknown` for ctx; cast
45
+ // back to ExtensionContext since the subagent tool re-validates.
46
+ startForegroundRun: (subCtx, runner, runId) =>
47
+ ctx.startForegroundRun(subCtx as Parameters<typeof ctx.startForegroundRun>[0], runner, runId),
48
+ batchBarrier: ctx.batchBarrier,
49
+ });
50
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Cross-extension wiring helper.
3
+ *
4
+ * Two small wires:
5
+ * • wireRpc — the in-process pi-crew RPC handle (other modules in
6
+ * the same extension can subscribe via `pi.events`).
7
+ * • wireGlobalRegistry — the cross-extension crew global registry,
8
+ * lazily loaded from team-tool.ts (heavy module) and installed under
9
+ * `globalThis[Symbol.for("pi-crew:registry")]` so peer extensions
10
+ * (e.g., pi-subagents3) can discover pi-crew at runtime.
11
+ */
12
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
13
+ import { registerPiCrewRpc } from "../cross-extension-rpc.ts";
14
+ import type { RegistrationContext } from "./registration-types.ts";
15
+
16
+ /** Install both wires on the registration context. */
17
+ export function installCrossExtensionWiring(pi: ExtensionAPI, ctx: RegistrationContext): void {
18
+ // Wire the in-process RPC handle.
19
+ const getPiEvents = (): Parameters<typeof registerPiCrewRpc>[0] | undefined => {
20
+ if (pi && typeof pi === "object" && "events" in pi) {
21
+ return (pi as { events?: Parameters<typeof registerPiCrewRpc>[0] }).events;
22
+ }
23
+ return undefined;
24
+ };
25
+ ctx.rpcHandle = registerPiCrewRpc(getPiEvents(), () => ctx.currentCtx);
26
+
27
+ // Install the cross-extension crew global registry. Lazy import keeps
28
+ // team-tool.ts (which pulls in the entire runtime chain) out of the
29
+ // cold-start module graph.
30
+ void import("../team-tool.ts").then(({ installCrewGlobalRegistry }) => {
31
+ const manifestCacheForRegistry = ctx.getManifestCache(ctx.currentCtx?.cwd ?? process.cwd());
32
+ installCrewGlobalRegistry({
33
+ manifestCache: manifestCacheForRegistry,
34
+ cwdProvider: () => ctx.currentCtx?.cwd ?? process.cwd(),
35
+ });
36
+ });
37
+ }
@@ -53,6 +53,20 @@ export interface RpcSignaturePayload {
53
53
 
54
54
  export type RpcSignedPayload = RpcSignaturePayload & { signature: string };
55
55
 
56
+ // Process-latched: emit a soft, non-alarming note once per process when an RPC
57
+ // request is actually processed without HMAC. The previous warning was emitted
58
+ // at extension registration (every pi load) and was considered noisy by users
59
+ // who never wire cross-extension RPC. Opt out with PI_CREW_SUPPRESS_RPC_WARNING=1.
60
+ let _rpcHmacSoftInfoEmitted = false;
61
+ function rpcHmacSoftInfoOnce(): void {
62
+ if (_rpcHmacSoftInfoEmitted) return;
63
+ _rpcHmacSoftInfoEmitted = true;
64
+ console.warn(
65
+ "[pi-crew] PI_CREW_RPC_SECRET not set; cross-extension RPC requests will be accepted without origin authentication. " +
66
+ "Operation allowlists and session checks still apply. Set PI_CREW_RPC_SECRET or PI_CREW_SUPPRESS_RPC_WARNING=1 to silence this message.",
67
+ );
68
+ }
69
+
56
70
  // --- Signing -----------------------------------------------------------------
57
71
 
58
72
  /**
@@ -123,7 +137,13 @@ export function withHmacVerification<P extends { requestId: string }>(
123
137
  ): (params: P) => unknown | Promise<unknown> {
124
138
  return (params: P) => {
125
139
  if (!isHmacEnabled()) {
126
- // No secret configured → backward compat: allow unsigned
140
+ // No secret configured → backward compat: allow unsigned.
141
+ // Emit a soft info note (once per process) only when an actual RPC
142
+ // request is being processed — the prior registration-time warning
143
+ // was too noisy for users who never wire cross-extension RPC.
144
+ if (process.env.PI_CREW_SUPPRESS_RPC_WARNING !== "1") {
145
+ rpcHmacSoftInfoOnce();
146
+ }
127
147
  return handler(params);
128
148
  }
129
149
 
@@ -3,6 +3,7 @@ import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
4
  import { DEFAULT_PATHS } from "../config/defaults.ts";
5
5
  import { type ConflictReport, detectImportConflicts } from "../runtime/delta-conflict.ts";
6
+ import { atomicWriteFile } from "../state/atomic-write.ts";
6
7
  import { projectCrewRoot, userCrewRoot } from "../utils/paths.ts";
7
8
  import { assertSafePathId, resolveContainedRelativePath, resolveRealContainedPath } from "../utils/safe-paths.ts";
8
9
  import { assertRunBundle } from "./run-bundle-schema.ts";
@@ -116,8 +117,8 @@ export function importRunBundle(cwd: string, bundlePath: string, scope: "project
116
117
  for (const target of [targetJson, targetSummary]) {
117
118
  if (fs.existsSync(target) && fs.lstatSync(target).isSymbolicLink()) throw new Error(`Invalid import target: ${target}`);
118
119
  }
119
- fs.writeFileSync(targetJson, `${JSON.stringify({ ...raw, importedAt, importedFrom: resolvedPath }, null, 2)}\n`, "utf-8");
120
- fs.writeFileSync(
120
+ atomicWriteFile(targetJson, `${JSON.stringify({ ...raw, importedAt, importedFrom: resolvedPath }, null, 2)}\n`);
121
+ atomicWriteFile(
121
122
  targetSummary,
122
123
  [
123
124
  `# Imported pi-crew run ${runId}`,
@@ -136,7 +137,6 @@ export function importRunBundle(cwd: string, bundlePath: string, scope: "project
136
137
  ),
137
138
  "",
138
139
  ].join("\n"),
139
- "utf-8",
140
140
  );
141
141
  return {
142
142
  runId,
@@ -25,14 +25,14 @@ export function handleAutoSummarizeOn(params: TeamToolParamsValue, ctx: TeamCont
25
25
  // Apply config updates if provided
26
26
  if (cfg.threshold !== undefined) {
27
27
  const threshold = typeof cfg.threshold === "number" ? cfg.threshold : parseInt(String(cfg.threshold), 10);
28
- if (!isNaN(threshold) && threshold >= 0) {
28
+ if (!Number.isNaN(threshold) && threshold >= 0) {
29
29
  service.setThreshold(threshold);
30
30
  }
31
31
  }
32
32
 
33
33
  if (cfg.minTools !== undefined) {
34
34
  const minTools = typeof cfg.minTools === "number" ? cfg.minTools : parseInt(String(cfg.minTools), 10);
35
- if (!isNaN(minTools) && minTools >= 0) {
35
+ if (!Number.isNaN(minTools) && minTools >= 0) {
36
36
  service.setMinToolsUsed(minTools);
37
37
  }
38
38
  }
@@ -103,14 +103,14 @@ export function handleAutoSummarizeConfig(params: TeamToolParamsValue, ctx: Team
103
103
 
104
104
  if (cfg.threshold !== undefined) {
105
105
  const threshold = typeof cfg.threshold === "number" ? cfg.threshold : parseInt(String(cfg.threshold), 10);
106
- if (!isNaN(threshold) && threshold >= 0) {
106
+ if (!Number.isNaN(threshold) && threshold >= 0) {
107
107
  updates.threshold = threshold;
108
108
  }
109
109
  }
110
110
 
111
111
  if (cfg.minTools !== undefined) {
112
112
  const minTools = typeof cfg.minTools === "number" ? cfg.minTools : parseInt(String(cfg.minTools), 10);
113
- if (!isNaN(minTools) && minTools >= 0) {
113
+ if (!Number.isNaN(minTools) && minTools >= 0) {
114
114
  updates.minTools = minTools;
115
115
  }
116
116
  }
@@ -56,7 +56,7 @@ export function abortOwned(runId: string, taskIds: string[] | undefined, ctx: Te
56
56
  continue;
57
57
  }
58
58
  if (task.status !== "queued" && task.status !== "running" && task.status !== "waiting") continue;
59
- if (foreignRun && !force) {
59
+ if (foreignRun && force !== true) {
60
60
  result.foreignIds.push(id);
61
61
  continue;
62
62
  }
@@ -92,7 +92,7 @@ export async function handleRetry(params: TeamToolParamsValue, ctx: TeamContext,
92
92
 
93
93
  // Pre-lock ownership check: reject foreign-owned runs unless force is set
94
94
  const foreignRun = typeof loaded.manifest.ownerSessionId === "string" && loaded.manifest.ownerSessionId !== ctx.sessionId;
95
- if (foreignRun && !params.force) {
95
+ if (foreignRun && params.force !== true) {
96
96
  return result(
97
97
  `Run ${loaded.manifest.runId} belongs to another session. Use force: true to override.`,
98
98
  { action: "retry", status: "error", runId: loaded.manifest.runId },
@@ -184,7 +184,7 @@ export async function handleCancel(params: TeamToolParamsValue, ctx: TeamContext
184
184
 
185
185
  // Pre-lock ownership check: reject foreign-owned runs unless force is set
186
186
  const preCheck = abortOwned(loaded.manifest.runId, undefined, ctx, params.force);
187
- if (preCheck.abortedIds.length === 0 && preCheck.foreignIds.length > 0 && !params.force) {
187
+ if (preCheck.abortedIds.length === 0 && preCheck.foreignIds.length > 0 && params.force !== true) {
188
188
  return result(
189
189
  `Run ${loaded.manifest.runId} belongs to another session. Use force: true to override.`,
190
190
  {
@@ -230,7 +230,7 @@ export async function handleCancel(params: TeamToolParamsValue, ctx: TeamContext
230
230
  }
231
231
 
232
232
  return withRunLockSync(loaded.manifest, () => {
233
- if ((loaded.manifest.status === "completed" || loaded.manifest.status === "cancelled") && !params.force)
233
+ if ((loaded.manifest.status === "completed" || loaded.manifest.status === "cancelled") && params.force !== true)
234
234
  return result(
235
235
  `Run ${loaded.manifest.runId} is already ${loaded.manifest.status}; nothing to cancel. Use force: true to mark it cancelled anyway.`,
236
236
  {
@@ -243,7 +243,7 @@ export async function handleCancel(params: TeamToolParamsValue, ctx: TeamContext
243
243
 
244
244
  // Classify tasks for foreign-aware cancellation
245
245
  const abortResult = abortOwned(loaded.manifest.runId, undefined, ctx, params.force);
246
- if (abortResult.abortedIds.length === 0 && abortResult.foreignIds.length > 0 && !params.force) {
246
+ if (abortResult.abortedIds.length === 0 && abortResult.foreignIds.length > 0 && params.force !== true) {
247
247
  return result(
248
248
  `Run ${loaded.manifest.runId} belongs to another session. Use force: true to override.`,
249
249
  {
@@ -30,6 +30,7 @@ import type { ChainTaskRunner } from "../../runtime/chain-runner.ts";
30
30
  import type { Decision, TaskPacket, TaskResult } from "../../runtime/handoff-manager.ts";
31
31
  import { readIfSmallWithTee } from "../../runtime/task-output-context.ts";
32
32
  import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
33
+ import { atomicWriteFile } from "../../state/atomic-write.ts";
33
34
  import { createRunPaths, loadRunManifestById } from "../../state/state-store.ts";
34
35
  import type { TeamRunManifest, TeamTaskState } from "../../state/types.ts";
35
36
  import type { PiTeamsToolResult } from "../tool-result.ts";
@@ -361,7 +362,7 @@ export function writeRunFixture(
361
362
  if (opts.resultText) {
362
363
  const resultDir = path.join(paths.artifactsRoot, "results");
363
364
  fs.mkdirSync(resultDir, { recursive: true });
364
- fs.writeFileSync(path.join(resultDir, `${taskId}.txt`), opts.resultText, "utf-8");
365
+ atomicWriteFile(path.join(resultDir, `${taskId}.txt`), opts.resultText);
365
366
  manifest.artifacts.push({
366
367
  kind: "result",
367
368
  path: `results/${taskId}.txt`,
@@ -371,8 +372,8 @@ export function writeRunFixture(
371
372
  });
372
373
  }
373
374
 
374
- fs.writeFileSync(paths.manifestPath, JSON.stringify(manifest), "utf-8");
375
- fs.writeFileSync(paths.tasksPath, JSON.stringify(tasks ?? []), "utf-8");
376
- fs.writeFileSync(paths.eventsPath, "", "utf-8");
375
+ atomicWriteFile(paths.manifestPath, JSON.stringify(manifest));
376
+ atomicWriteFile(paths.tasksPath, JSON.stringify(tasks ?? []));
377
+ atomicWriteFile(paths.eventsPath, "");
377
378
  return manifest;
378
379
  }
@@ -135,5 +135,15 @@ export function buildParentContext(ctx: TeamContext): string | undefined {
135
135
 
136
136
  export function configRecord(config: unknown): Record<string, unknown> {
137
137
  if (!config || typeof config !== "object" || Array.isArray(config)) return {};
138
- return config as Record<string, unknown>;
138
+ // AUDIT-07 defense-in-depth: filter prototype-pollution keys. Callers (api, doctor,
139
+ // lifecycle-actions, intent-policy, team-tool) pass `params.config` from the team-tool
140
+ // invocation which is NOT sanitized by the same loadConfig/sanitizeObject path.
141
+ const obj = config as Record<string, unknown>;
142
+ const DANGEROUS_KEYS = new Set(["__proto__", "constructor", "prototype"]);
143
+ const safe: Record<string, unknown> = {};
144
+ for (const [k, v] of Object.entries(obj)) {
145
+ if (DANGEROUS_KEYS.has(k)) continue;
146
+ safe[k] = v;
147
+ }
148
+ return safe;
139
149
  }
@@ -10,6 +10,7 @@ import { getRuntimeWarmupStatus } from "../../runtime/runtime-warmup.ts";
10
10
  import { formatZombieReport, scanZombieSubagents } from "../../runtime/zombie-scanner.ts";
11
11
  import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
12
12
  import { TeamToolParams } from "../../schema/team-tool-schema.ts";
13
+ import { atomicWriteFile } from "../../state/atomic-write.ts";
13
14
  import { allTeams, discoverTeams } from "../../teams/discover-teams.ts";
14
15
  import { projectCrewRoot, userCrewRoot } from "../../utils/paths.ts";
15
16
  import { allWorkflows, discoverWorkflows } from "../../workflows/discover-workflows.ts";
@@ -98,7 +99,7 @@ function checkWritableDir(dir: string): { ok: boolean; detail: string } {
98
99
  // fs.accessSync(W_OK) is unreliable on Windows; verify by writing a temp file.
99
100
  const probePath = `${dir}/.pi-crew-write-test`;
100
101
  try {
101
- fs.writeFileSync(probePath, "ok", "utf-8");
102
+ atomicWriteFile(probePath, "ok");
102
103
  fs.rmSync(probePath, { force: true });
103
104
  } catch {
104
105
  return {
@@ -152,7 +152,7 @@ export function shouldGoalWrap(
152
152
  message?: string;
153
153
  } {
154
154
  const wc = readGoalWrapConfig(cwd, workflow.name);
155
- if (!wc || wc.enabled !== true) {
155
+ if (wc?.enabled !== true) {
156
156
  return { enabled: false, reason: "config-off" };
157
157
  }
158
158
  const validationError = validateGoalWrapConfig(wc);
@@ -181,7 +181,7 @@ export async function startGoalWrappedRun(
181
181
  ): Promise<ReturnType<typeof result>> {
182
182
  const cwd = ctx.cwd;
183
183
  const wc = readGoalWrapConfig(cwd, workflow.name);
184
- if (!wc || wc.enabled !== true) {
184
+ if (wc?.enabled !== true) {
185
185
  return result(
186
186
  `goal-wrap is not enabled for workflow '${workflow.name}' in .crew/config.json.`,
187
187
  { action: "run", status: "error" },
@@ -16,7 +16,7 @@
16
16
 
17
17
  import { GoalStore } from "../../runtime/goal-state-store.ts";
18
18
  import { snapshotManifests } from "../../runtime/verification-integrity.ts";
19
- import { acquireWorkspaceLock, isWorkspaceBusy, type WorkspaceLockHandle } from "../../runtime/workspace-lock.ts";
19
+ import { isWorkspaceBusy } from "../../runtime/workspace-lock.ts";
20
20
  import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
21
21
  import { appendEvent } from "../../state/event-log.ts";
22
22
  import { createRunPaths, saveRunManifestAsync } from "../../state/state-store.ts";
@@ -1,6 +1,6 @@
1
1
  import * as crypto from "node:crypto";
2
2
  import { humanizeSchedule, nextRunTime, parseSchedule } from "../../runtime/scheduler.ts";
3
- import { loadCrewSettings, saveCrewSettings } from "../../runtime/settings-store.ts";
3
+ import { type CrewSettings, updateCrewSettings } from "../../runtime/settings-store.ts";
4
4
  import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
5
5
  import type { PiTeamsToolResult } from "../tool-result.ts";
6
6
  import { result, type TeamContext } from "./context.ts";
@@ -50,7 +50,14 @@ function buildScheduleSpec(params: ScheduleParams): {
50
50
  scheduleType: "cron" as const,
51
51
  };
52
52
  }
53
- if (params.interval !== undefined && params.interval > 0) {
53
+ if (params.interval !== undefined && (!Number.isFinite(params.interval) || params.interval <= 0)) {
54
+ throw new Error("interval must be a positive finite number");
55
+ }
56
+ if (params.once !== undefined) {
57
+ const ts = typeof params.once === "number" ? params.once : Date.parse(String(params.once));
58
+ if (!Number.isFinite(ts)) throw new Error("once must be a valid timestamp");
59
+ }
60
+ if (params.interval !== undefined) {
54
61
  const specStr = `${params.interval}ms`;
55
62
  const spec = parseSchedule(specStr);
56
63
  if ("error" in spec) throw new Error(spec.error);
@@ -183,21 +190,16 @@ export function handleSchedule(params: TeamToolParamsValue, ctx: TeamContext): P
183
190
  );
184
191
  }
185
192
 
193
+ function scheduledJobsOf(settings: CrewSettings): import("../../runtime/scheduler.ts").ScheduledJob[] {
194
+ return Array.isArray(settings.scheduledJobs) ? (settings.scheduledJobs as import("../../runtime/scheduler.ts").ScheduledJob[]) : [];
195
+ }
196
+
186
197
  function persistScheduledJob(cwd: string, job: import("../../runtime/scheduler.ts").ScheduledJob): void {
187
198
  try {
188
- const settings = loadCrewSettings(cwd);
189
- const existingJobs: import("../../runtime/scheduler.ts").ScheduledJob[] = Array.isArray(
190
- (settings as Record<string, unknown>).scheduledJobs,
191
- )
192
- ? ((settings as Record<string, unknown>).scheduledJobs as import("../../runtime/scheduler.ts").ScheduledJob[])
193
- : [];
194
- saveCrewSettings(
195
- {
196
- ...settings,
197
- scheduledJobs: [...existingJobs, job],
198
- } as Parameters<typeof saveCrewSettings>[0],
199
- cwd,
200
- );
199
+ updateCrewSettings(cwd, (settings) => ({
200
+ ...settings,
201
+ scheduledJobs: [...scheduledJobsOf(settings), job],
202
+ }));
201
203
  } catch {
202
204
  /* best-effort persistence */
203
205
  }
@@ -206,14 +208,10 @@ function persistScheduledJob(cwd: string, job: import("../../runtime/scheduler.t
206
208
  /** Update an existing scheduled job in persistent settings. */
207
209
  export function persistScheduledJobUpdate(cwd: string, job: import("../../runtime/scheduler.ts").ScheduledJob): void {
208
210
  try {
209
- const settings = loadCrewSettings(cwd);
210
- const existingJobs: import("../../runtime/scheduler.ts").ScheduledJob[] = Array.isArray(
211
- (settings as Record<string, unknown>).scheduledJobs,
212
- )
213
- ? ((settings as Record<string, unknown>).scheduledJobs as import("../../runtime/scheduler.ts").ScheduledJob[])
214
- : [];
215
- const updated = existingJobs.map((j) => (j.id === job.id ? job : j));
216
- saveCrewSettings({ ...settings, scheduledJobs: updated } as Parameters<typeof saveCrewSettings>[0], cwd);
211
+ updateCrewSettings(cwd, (settings) => ({
212
+ ...settings,
213
+ scheduledJobs: scheduledJobsOf(settings).map((j) => (j.id === job.id ? job : j)),
214
+ }));
217
215
  } catch {
218
216
  /* best-effort persistence */
219
217
  }
@@ -222,19 +220,10 @@ export function persistScheduledJobUpdate(cwd: string, job: import("../../runtim
222
220
  /** Remove a scheduled job from persistent settings. */
223
221
  function persistScheduledJobRemove(cwd: string, jobId: string): void {
224
222
  try {
225
- const settings = loadCrewSettings(cwd);
226
- const existingJobs: import("../../runtime/scheduler.ts").ScheduledJob[] = Array.isArray(
227
- (settings as Record<string, unknown>).scheduledJobs,
228
- )
229
- ? ((settings as Record<string, unknown>).scheduledJobs as import("../../runtime/scheduler.ts").ScheduledJob[])
230
- : [];
231
- saveCrewSettings(
232
- {
233
- ...settings,
234
- scheduledJobs: existingJobs.filter((j) => j.id !== jobId),
235
- } as Parameters<typeof saveCrewSettings>[0],
236
- cwd,
237
- );
223
+ updateCrewSettings(cwd, (settings) => ({
224
+ ...settings,
225
+ scheduledJobs: scheduledJobsOf(settings).filter((j) => j.id !== jobId),
226
+ }));
238
227
  } catch {
239
228
  /* best-effort persistence */
240
229
  }
@@ -4,7 +4,6 @@
4
4
  * Parses a plan document and outputs agent chain commands.
5
5
  */
6
6
  import * as fs from "node:fs";
7
- import * as path from "node:path";
8
7
  import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
9
8
  import { resolveContainedPath } from "../../utils/safe-paths.ts";
10
9
  import {
@@ -149,6 +149,11 @@ async function spawnSingleTask(
149
149
  team,
150
150
  workflow,
151
151
  goal,
152
+ // EXT F1: mirror handleRun — without ownerSessionId, the foreign-run check in
153
+ // cancel/retry/resume (cancel.ts:50: `typeof ownerSessionId === "string"`) is
154
+ // skipped and any session can cancel/retry parallel-dispatched runs.
155
+ ownerSessionId: ctx.sessionId,
156
+ runKind: "team-run",
152
157
  });
153
158
 
154
159
  appendEvent(created.manifest.eventsPath, {