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
@@ -37,6 +37,7 @@ export const PiTeamsLimitsConfigSchema = Type.Object(
37
37
  maxRetriesPerTask: Type.Optional(Type.Integer({ minimum: 1 })),
38
38
  maxTasksPerRun: Type.Optional(Type.Integer({ minimum: 1 })),
39
39
  heartbeatStaleMs: Type.Optional(Type.Integer({ minimum: 1 })),
40
+ serializeOnPathOverlap: Type.Optional(Type.Boolean()),
40
41
  },
41
42
  { additionalProperties: false },
42
43
  );
@@ -59,6 +60,17 @@ export const PiTeamsRuntimeConfigSchema = Type.Object(
59
60
  effectivenessGuard: Type.Optional(
60
61
  Type.Union([Type.Literal("off"), Type.Literal("warn"), Type.Literal("block"), Type.Literal("fail")]),
61
62
  ),
63
+ yield: Type.Optional(
64
+ Type.Object(
65
+ {
66
+ enabled: Type.Optional(Type.Boolean()),
67
+ maxReminders: Type.Optional(Type.Integer({ minimum: 0 })),
68
+ reminderPrompt: Type.Optional(Type.String({ maxLength: 1000 })),
69
+ },
70
+ { additionalProperties: false },
71
+ ),
72
+ ),
73
+ excludeContextBash: Type.Optional(Type.Boolean()),
62
74
  isolationPolicy: Type.Optional(
63
75
  Type.Object(
64
76
  {
@@ -182,7 +194,7 @@ export const PiTeamsObservabilityConfigSchema = Type.Object(
182
194
  {
183
195
  enabled: Type.Optional(Type.Boolean()),
184
196
  pollIntervalMs: Type.Optional(Type.Integer({ minimum: 1000, maximum: 60000 })),
185
- metricRetentionDays: Type.Optional(Type.Integer({ minimum: 1, maximum: 365 })),
197
+ metricRetentionDays: Type.Optional(Type.Integer({ minimum: 1, maximum: 90 })),
186
198
  },
187
199
  { additionalProperties: false },
188
200
  );
@@ -205,6 +217,11 @@ export const PiTeamsReliabilityConfigSchema = Type.Object(
205
217
  autoRecover: Type.Optional(Type.Boolean()),
206
218
  deadletterThreshold: Type.Optional(Type.Integer({ minimum: 1 })),
207
219
  cleanupOrphanedTempDirs: Type.Optional(Type.Boolean()),
220
+ autoRepairIntervalMs: Type.Optional(Type.Integer({ minimum: 0 })),
221
+ forcePreflight: Type.Optional(Type.Boolean()),
222
+ ambientStatusInjection: Type.Optional(Type.Boolean()),
223
+ perWriteValidation: Type.Optional(Type.Boolean()),
224
+ scopeModels: Type.Optional(Type.Boolean()),
208
225
  },
209
226
  { additionalProperties: false },
210
227
  );
@@ -235,6 +252,7 @@ export const PiTeamsUiConfigSchema = Type.Object(
235
252
  dashboardLiveRefreshMs: Type.Optional(Type.Integer({ minimum: 250, maximum: 60000 })),
236
253
  autoOpenDashboard: Type.Optional(Type.Boolean()),
237
254
  autoOpenDashboardForForegroundRuns: Type.Optional(Type.Boolean()),
255
+ autoCloseDashboardMs: Type.Optional(Type.Integer({ minimum: 0 })),
238
256
  showModel: Type.Optional(Type.Boolean()),
239
257
  showTokens: Type.Optional(Type.Boolean()),
240
258
  showTools: Type.Optional(Type.Boolean()),
@@ -194,6 +194,8 @@ export const TeamToolParams = Type.Object({
194
194
  description: "Path to a markdown plan document for orchestration.",
195
195
  }),
196
196
  ),
197
+ subAction: Type.Optional(Type.String({ description: "Sub-action for schedule management (remove, disable, enable, update)." })),
198
+ jobId: Type.Optional(Type.String({ description: "Job ID for schedule management actions." })),
197
199
  cron: Type.Optional(
198
200
  Type.String({
199
201
  description: "Cron expression for recurring scheduled runs (e.g., '0 9 * * MON').",
@@ -28,23 +28,34 @@ export interface SkillDescriptor {
28
28
  }
29
29
 
30
30
  /**
31
- * F6 (v0.7.9): discover skills from all five roots (matching pi-subagents'
32
- * skill-loader so users authoring skills under either convention find them).
33
- * Roots, in precedence order (first hit wins):
34
- * 1. <cwd>/.pi/skills (project, Pi standard)
35
- * 2. <cwd>/.agents/skills (project, Agent Skills spec — agentskills.io)
36
- * 3. <cwd>/skills (project, legacy pi-crew convention)
37
- * 4. <getAgentDir>/skills (user, Pi standard)
38
- * 5. <homedir>/.agents/skills (user, Agent Skills spec)
39
- * 6. <homedir>/.pi/skills (user, legacy Pi — pre-standard)
40
- * 7. PACKAGE_SKILLS_DIR (bundled, trusted)
41
- * The `PACKAGE_SKILLS_DIR` (bundled) and the legacy `<cwd>/skills` root are
42
- * kept as separate `source` values to preserve the existing capability
43
- * inventory shape — callers that key on `source === "package"` / `source ===
44
- * "project"` keep working.
31
+ * F6 (v0.7.9): discover skills from all roots (matching pi-subagents' skill-loader
32
+ * so users authoring skills under either convention find them).
33
+ *
34
+ * IMPORTANT — inventory vs injection (SEC-003): this function returns EVERY
35
+ * skill found across roots (NO first-wins dedup) because it feeds the capability
36
+ * INVENTORY, which intentionally surfaces all skills (incl. project shadows) for
37
+ * transparency; `capability-inventory.ts` then marks project/user items that
38
+ * shadow a package/builtin one and re-sorts by id.
39
+ *
40
+ * The actual skill-content INJECTION path is separate: `skill-instructions.ts`
41
+ * `candidateSkillDirs` + `readSkillMarkdown` use first-wins with PACKAGE FIRST
42
+ * (SEC-003 fix, regression-tested in test/unit/skill-instructions.test.ts) so a
43
+ * project skill can NEVER override a trusted package skill's content in worker
44
+ * prompts. The package-first order below mirrors that injection path for
45
+ * consistency/defense-in-depth.
46
+ *
47
+ * Roots:
48
+ * 1. PACKAGE_SKILLS_DIR (bundled, trusted)
49
+ * 2. <cwd>/.pi/skills (project, Pi standard)
50
+ * 3. <cwd>/.agents/skills (project, Agent Skills spec — agentskills.io)
51
+ * 4. <cwd>/skills (project, legacy pi-crew convention)
52
+ * 5. <getAgentDir>/skills (user, Pi standard)
53
+ * 6. <homedir>/.agents/skills (user, Agent Skills spec)
54
+ * 7. <homedir>/.pi/skills (user, legacy Pi — pre-standard)
45
55
  */
46
56
  function listSkillDirs(cwd: string): Array<{ root: string; source: SkillDescriptor["source"] }> {
47
57
  return [
58
+ { root: PACKAGE_SKILLS_DIR, source: "package" },
48
59
  { root: path.resolve(cwd, ".pi", "skills"), source: "project-pi" },
49
60
  {
50
61
  root: path.resolve(cwd, ".agents", "skills"),
@@ -57,7 +68,6 @@ function listSkillDirs(cwd: string): Array<{ root: string; source: SkillDescript
57
68
  source: "user-agents",
58
69
  },
59
70
  { root: path.join(os.homedir(), ".pi", "skills"), source: "user-pi" },
60
- { root: PACKAGE_SKILLS_DIR, source: "package" },
61
71
  ];
62
72
  }
63
73
 
@@ -2,7 +2,6 @@ import * as crypto from "node:crypto";
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
4
  import { DEFAULT_CACHE, DEFAULT_PATHS } from "../config/defaults.ts";
5
- import { logInternalError } from "../utils/internal-error.ts";
6
5
  import { userCrewRoot } from "../utils/paths.ts";
7
6
  import { isSafePathId } from "../utils/safe-paths.ts";
8
7
  import { sharedScanCache } from "../utils/scan-cache.ts";
@@ -315,8 +315,13 @@ function isRetryableLinkError(error: unknown): boolean {
315
315
  * It atomically creates a hard link to the source. We then unlink the source.
316
316
  * This prevents TOCTOU attacks where an attacker plants a symlink at the
317
317
  * destination between the check and the rename operation.
318
+ *
319
+ * ST-8: this is the canonical symlink-safe rename helper used by both
320
+ * `atomicWriteFile` (sync) and `atomicWriteFileAsync` (async) paths. The
321
+ * helper is exported so any future caller can share the same symlink-safe
322
+ * rename semantics; keep this signature stable.
318
323
  */
319
- function renameWithLinkSync(tempPath: string, filePath: string, retries = 8): void {
324
+ export function renameWithLinkSync(tempPath: string, filePath: string, retries = 8): void {
320
325
  let lastError: unknown;
321
326
  for (let attempt = 0; attempt <= retries; attempt++) {
322
327
  try {
@@ -371,7 +376,12 @@ function renameWithLinkSync(tempPath: string, filePath: string, retries = 8): vo
371
376
  throw lastError;
372
377
  }
373
378
 
374
- async function renameWithLinkAsync(tempPath: string, filePath: string, retries = 8): Promise<void> {
379
+ /**
380
+ * ST-8: async twin of `renameWithLinkSync`. Exported so any future caller
381
+ * can share the same symlink-safe rename semantics as the primary
382
+ * `atomicWriteFileAsync` path.
383
+ */
384
+ export async function renameWithLinkAsync(tempPath: string, filePath: string, retries = 8): Promise<void> {
375
385
  let lastError: unknown;
376
386
  for (let attempt = 0; attempt <= retries; attempt++) {
377
387
  try {
@@ -496,21 +506,21 @@ export type WriteDurability = "full" | "best-effort";
496
506
  /** Options accepted by atomicWriteFile (forward-compatible string | object form). */
497
507
  export type AtomicWriteOptions =
498
508
  | string // legacy: expectedHash
499
- | { expectedHash?: string; durability?: WriteDurability };
509
+ | { expectedHash?: string; durability?: WriteDurability; mode?: number };
500
510
 
501
- function normalizeOptions(arg: unknown): { expectedHash?: string; durability: WriteDurability } {
502
- if (typeof arg === "string") return { expectedHash: arg, durability: "full" };
511
+ function normalizeOptions(arg: unknown): { expectedHash?: string; durability: WriteDurability; mode?: number } {
512
+ if (typeof arg === "string") return { expectedHash: arg, durability: "full", mode: undefined };
503
513
  if (arg && typeof arg === "object") {
504
- const o = arg as { expectedHash?: string; durability?: WriteDurability };
514
+ const o = arg as { expectedHash?: string; durability?: WriteDurability; mode?: number };
505
515
  const durability: WriteDurability = o.durability === "best-effort" ? "best-effort" : "full";
506
- return { expectedHash: o.expectedHash, durability };
516
+ return { expectedHash: o.expectedHash, durability, mode: o.mode };
507
517
  }
508
- return { durability: "full" };
518
+ return { durability: "full", mode: undefined };
509
519
  }
510
520
 
511
521
  export function atomicWriteFile(filePath: string, content: string, options?: AtomicWriteOptions): void {
512
522
  cancelPendingCoalescedWrite(filePath);
513
- const { durability } = normalizeOptions(options);
523
+ const { durability, mode } = normalizeOptions(options);
514
524
  if (!isSymlinkSafeDirCached(filePath))
515
525
  throw new Error(`Refusing to write: target is a symlink or inside untrusted directory: ${filePath}`);
516
526
  // On Windows the parent directory may be referenced via a short-name alias
@@ -556,7 +566,7 @@ export function atomicWriteFile(filePath: string, content: string, options?: Ato
556
566
  const O_NOFOLLOW = typeof fs.constants.O_NOFOLLOW === "number" ? fs.constants.O_NOFOLLOW : 0;
557
567
  let fd: number | undefined;
558
568
  try {
559
- fd = fs.openSync(tempPath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | O_NOFOLLOW, 0o600);
569
+ fd = fs.openSync(tempPath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | O_NOFOLLOW, mode ?? 0o600);
560
570
  // Post-open verification: on Windows O_NOFOLLOW is 0, so verify FD is a regular file
561
571
  const openedStat = fs.fstatSync(fd);
562
572
  if (!openedStat.isFile()) {
@@ -747,6 +757,14 @@ let flushInProgress = 0;
747
757
  * Buffer a JSON write and flush it after `coalesceMs` ms (default 50).
748
758
  * Multiple writes to the same path within the window collapse to one disk write.
749
759
  *
760
+ * ST-7 escape hatch: pass `skipCoalesce: true` to bypass the buffer entirely
761
+ * and write synchronously via `atomicWriteJson`. Use for terminal task status
762
+ * transitions where a SIGKILL during the 50ms coalesce window must NOT lose
763
+ * the terminal status update — otherwise crash recovery would see "running"
764
+ * in tasks.json while events.jsonl already shows "completed", causing
765
+ * double-execution / zombie detection. Defaults to `false` (buffered write)
766
+ * so all existing callers preserve their behavior.
767
+ *
750
768
  * @see The coalescing caveat: a `readJsonFile` call between buffer and flush
751
769
  * sees the previous on-disk content. Callers needing read-after-write
752
770
  * must call `flushPendingAtomicWrites()` first or use `atomicWriteJson`.
@@ -754,9 +772,17 @@ let flushInProgress = 0;
754
772
  export function atomicWriteJsonCoalesced<T>(
755
773
  filePath: string,
756
774
  value: T,
757
- coalesceMs = DEFAULT_ATOMIC_COALESCE_MS,
775
+ coalesceMs: number = DEFAULT_ATOMIC_COALESCE_MS,
758
776
  options?: AtomicWriteOptions,
777
+ skipCoalesce: boolean = false,
759
778
  ): void {
779
+ // ST-7: bypass coalescing for callers that need durable synchronous
780
+ // semantics. atomicWriteJson goes through atomicWriteFile which uses
781
+ // renameWithLinkSync + fsync — fully durable, no SIGKILL window.
782
+ if (skipCoalesce) {
783
+ atomicWriteJson(filePath, value, options);
784
+ return;
785
+ }
760
786
  const content = `${JSON.stringify(value, null, 2)}\n`;
761
787
  const previous = pendingAtomicWrites.get(filePath);
762
788
  if (previous) clearTimeout(previous.timer);
@@ -2,7 +2,6 @@ import { createHash, randomUUID } from "node:crypto";
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
4
  import { resolveRealContainedPath } from "../utils/safe-paths.ts";
5
- import { sleepSync } from "../utils/sleep.ts";
6
5
  import { atomicWriteFile, renameWithRetry } from "./atomic-write.ts";
7
6
  import { withFileLockSync } from "./locks.ts";
8
7
 
@@ -152,14 +151,9 @@ export function writeBlob(
152
151
  // FIX: Write blob content FIRST, then metadata. If blob write fails, no orphan metadata.
153
152
  // Previous order was metadata first, then blob - causing orphan metadata on blob failure.
154
153
  let blobContentWritten = false;
155
- try {
156
- // Write blob content first (immutable, content-addressed)
157
- atomicWriteBuffer(blobPath, Buffer.isBuffer(content) ? content : Buffer.from(content, "utf-8"));
158
- blobContentWritten = true;
159
- } catch (error) {
160
- // Blob write failed - no metadata to clean up since we haven't written it yet
161
- throw error;
162
- }
154
+ // Write blob content first (immutable, content-addressed)
155
+ atomicWriteBuffer(blobPath, Buffer.isBuffer(content) ? content : Buffer.from(content, "utf-8"));
156
+ blobContentWritten = true;
163
157
 
164
158
  // Metadata only after blob content is successfully written
165
159
  // Issue 3 fix: Use two-phase commit for metadata - write to temp file first,
@@ -44,7 +44,7 @@ export const TEAM_TASK_STATUS_TRANSITIONS: Readonly<Record<TeamTaskStatus, reado
44
44
  };
45
45
 
46
46
  /** @internal */
47
- const TEAM_EVENT_TYPES = [
47
+ export const TEAM_EVENT_TYPES = [
48
48
  "run.created",
49
49
  "run.queued",
50
50
  "run.planning",
@@ -56,6 +56,10 @@ const TEAM_EVENT_TYPES = [
56
56
  "task.created",
57
57
  "task.started",
58
58
  "task.progress",
59
+ "hook.pre_step_started",
60
+ "hook.pre_step_completed",
61
+ "hook.pre_step_failed",
62
+ "hook.pre_step_optional_failed",
59
63
  "task.blocked",
60
64
  "task.green",
61
65
  "task.red",
@@ -17,6 +17,7 @@
17
17
  */
18
18
  import * as fs from "node:fs";
19
19
  import * as path from "node:path";
20
+ import { atomicWriteFile } from "./atomic-write.ts";
20
21
  import { updateGitignore } from "./gitignore-manager.ts";
21
22
 
22
23
  // Re-export updateGitignore for backwards compatibility with tests.
@@ -238,12 +239,12 @@ export async function ensureCrewDirectory(cwd: string): Promise<void> {
238
239
 
239
240
  for (const placeholder of placeholders) {
240
241
  if (!fs.existsSync(placeholder)) {
241
- fs.writeFileSync(placeholder, "", "utf-8");
242
+ atomicWriteFile(placeholder, "");
242
243
  }
243
244
  }
244
245
 
245
246
  // 3. Write README.md (always overwrite to keep it current)
246
- fs.writeFileSync(safeJoin(crewRoot, "README.md"), buildCrewReadme(), "utf-8");
247
+ atomicWriteFile(safeJoin(crewRoot, "README.md"), buildCrewReadme());
247
248
 
248
249
  // 4. Update .gitignore at project root
249
250
  const repoRoot = findProjectRoot(cwd);
@@ -1,5 +1,5 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
2
- import { dirname, join } from "path";
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
3
  import { projectCrewRoot } from "../utils/paths.ts";
4
4
  import { assertSafePathId } from "../utils/safe-paths.ts";
5
5
  import { atomicWriteFile } from "./atomic-write.ts";
@@ -220,7 +220,7 @@ export function rotateEventLogUnlocked(eventsPath: string): boolean {
220
220
  // copyFileSync + writeFileSync("") ensures eventsPath ALWAYS exists
221
221
  // (no missing-file window for concurrent readers).
222
222
  fs.copyFileSync(eventsPath, archivePath);
223
- fs.writeFileSync(eventsPath, "", "utf-8");
223
+ atomicWriteFile(eventsPath, "");
224
224
  return true;
225
225
  } catch (error) {
226
226
  logInternalError("event-log.rotate", error, `eventsPath=${eventsPath}`);
@@ -93,7 +93,7 @@ export function withEventLogLockSync<T>(eventsPath: string, fn: () => T, options
93
93
  const pidFile = path.join(lockDir, "pid");
94
94
  const start = Date.now();
95
95
  // SECURITY (HIGH #2 fix): Reduced from 120s to 5s to prevent blocking the
96
- // event loop indefinitely. 500 retries × 10ms = 5s max. After timeout, we
96
+ // event loop indefinitely. ~100 retries × 50ms ≈ 5s max. After timeout, we
97
97
  // throw a clear error instead of blocking forever. This ensures AbortSignal
98
98
  // handlers, SIGTERM, and graceful shutdown can fire within seconds.
99
99
  const timeout = options?.timeoutMs ?? 5000;
@@ -109,7 +109,7 @@ export function withEventLogLockSync<T>(eventsPath: string, fn: () => T, options
109
109
  // (reduced from 120s) is appropriate.
110
110
  fs.mkdirSync(lockDir);
111
111
  try {
112
- fs.writeFileSync(pidFile, String(process.pid), "utf-8");
112
+ atomicWriteFile(pidFile, String(process.pid));
113
113
  } catch {
114
114
  /* best-effort */
115
115
  }
@@ -162,7 +162,7 @@ export function withEventLogLockSync<T>(eventsPath: string, fn: () => T, options
162
162
  } catch {
163
163
  /* no pid file — mtime check above already handles it */
164
164
  }
165
- sleepSync(10);
165
+ sleepSync(50);
166
166
  }
167
167
  }
168
168
  try {
@@ -998,6 +998,7 @@ export function appendEventBuffered(eventsPath: string, event: AppendTeamEvent,
998
998
  });
999
999
  }, bufferMs);
1000
1000
  bufferedTimers.set(eventsPath, timer);
1001
+ timer.unref();
1001
1002
  }
1002
1003
  });
1003
1004
  }
@@ -3,6 +3,7 @@
3
3
  * Only adds entries if not already present; preserves existing content.
4
4
  */
5
5
  import * as fs from "node:fs";
6
+ import { atomicWriteFile } from "./atomic-write.ts";
6
7
 
7
8
  /**
8
9
  * Entries to add to .gitignore for .crew directory management.
@@ -25,7 +26,7 @@ const CREW_GITIGNORE_ENTRIES = [
25
26
  */
26
27
  export async function updateGitignore(gitignorePath: string): Promise<void> {
27
28
  if (!fs.existsSync(gitignorePath)) {
28
- fs.writeFileSync(gitignorePath, CREW_GITIGNORE_ENTRIES.join("\n") + "\n", "utf-8");
29
+ atomicWriteFile(gitignorePath, CREW_GITIGNORE_ENTRIES.join("\n") + "\n");
29
30
  return;
30
31
  }
31
32
 
@@ -1,7 +1,8 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import type { ManifestSummary, RunHealth } from "../runtime/task-health.ts";
3
+ import type { ManifestSummary } from "../runtime/task-health.ts";
4
4
  import { computeRunHealth } from "../runtime/task-health.ts";
5
+ import { atomicWriteFile } from "./atomic-write.ts";
5
6
 
6
7
  // Relative to the crew root (`<cwd>/.crew`). BUG A fix (pts/2 hang
7
8
  // investigation 2026-06-16): this was `.crew/state/health`, which double-joined
@@ -41,7 +42,9 @@ export class HealthStore {
41
42
  const dir = this.healthDir();
42
43
  fs.mkdirSync(dir, { recursive: true });
43
44
  const file = path.join(dir, `${manifest.runId}.json`);
44
- fs.writeFileSync(file, JSON.stringify(snapshot, null, 2) + "\n", "utf8");
45
+ // ST-2: atomic (temp + rename + fsync) — a crash mid-writeFileSync would truncate
46
+ // the snapshot and loadAllSnapshots() silently skips unparseable files.
47
+ atomicWriteFile(file, JSON.stringify(snapshot, null, 2) + "\n");
45
48
  return snapshot;
46
49
  }
47
50
 
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
4
  import { assertSafePathId } from "../utils/safe-paths.ts";
5
+ import { atomicWriteFile } from "./atomic-write.ts";
5
6
 
6
7
  /**
7
8
  * Represents a learned instinct that guides agent behavior.
@@ -87,7 +88,13 @@ export class InstinctStore {
87
88
  private appendInstinctToFile(filePath: string, instinct: Instinct): void {
88
89
  const dir = path.dirname(filePath);
89
90
  this.ensureDir(dir);
90
- fs.appendFileSync(filePath, `${JSON.stringify(instinct)}\n`, "utf-8");
91
+ // ST-3: read existing + append + atomic rewrite. appendFileSync has no fsync and
92
+ // can interleave bytes on concurrent writes. Callers should wrap this in
93
+ // withFileLockSync(filePath) to serialize concurrent RMW on the same file.
94
+ const existing = fs.existsSync(filePath) ? this.readInstinctsFromFile(filePath) : [];
95
+ existing.push(instinct);
96
+ const content = existing.map((i) => JSON.stringify(i)).join("\n") + "\n";
97
+ atomicWriteFile(filePath, content);
91
98
  }
92
99
 
93
100
  /**
@@ -97,7 +104,9 @@ export class InstinctStore {
97
104
  const dir = path.dirname(filePath);
98
105
  this.ensureDir(dir);
99
106
  const content = instincts.map((i) => JSON.stringify(i)).join("\n") + "\n";
100
- fs.writeFileSync(filePath, content, "utf-8");
107
+ // ST-3: atomic write (temp + rename + fsync) — crash mid-writeFileSync truncates
108
+ // the JSONL and readInstinctsFromFile silently returns [] (all instincts lost).
109
+ atomicWriteFile(filePath, content);
101
110
  }
102
111
 
103
112
  /**