opencode-swarm 7.183.1 → 7.184.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 (123) hide show
  1. package/binaries/win32-arm64/swarm-sandbox-runner.exe +0 -0
  2. package/binaries/win32-x64/swarm-sandbox-runner.exe +0 -0
  3. package/dist/background/delegation-health.d.ts +1 -1
  4. package/dist/background/index.d.ts +1 -0
  5. package/dist/background/pr-event-delivery.d.ts +29 -14
  6. package/dist/background/pr-event-subscribers.d.ts +6 -5
  7. package/dist/background/pr-feedback-event-queue.d.ts +19 -2
  8. package/dist/background/pr-feedback-loop-runtime.d.ts +75 -0
  9. package/dist/background/pr-feedback-loop.d.ts +47 -12
  10. package/dist/cli/{coder-settlement-rserb0hm.js → coder-settlement-yvpewr52.js} +15 -12
  11. package/dist/cli/{config-doctor-r4fmzprn.js → config-doctor-qgwpvrbz.js} +7 -3
  12. package/dist/cli/{core-rzek3e2n.js → core-9qt388hq.js} +4 -4
  13. package/dist/cli/{curation-policy-g0jjwrg5.js → curation-policy-49f8jx48.js} +7 -6
  14. package/dist/cli/{curator-drift-a0r5krfx.js → curator-drift-wf0sm2te.js} +9 -9
  15. package/dist/cli/{curator-v84tfqcr.js → curator-ffz5pxgc.js} +40 -39
  16. package/dist/cli/curator-llm-factory-drs36v1q.js +75 -0
  17. package/dist/cli/{dispatch-p5zjg5e6.js → dispatch-cv89p2y1.js} +1 -1
  18. package/dist/cli/{evidence-summary-service-t4csk176.js → evidence-summary-service-8dfzjnxe.js} +21 -20
  19. package/dist/cli/{gate-evidence-3z04zprn.js → gate-evidence-aa5q30f4.js} +14 -7
  20. package/dist/cli/guardrail-explain-9g5y2gjm.js +76 -0
  21. package/dist/cli/{guardrail-log-cbzdh4qj.js → guardrail-log-0g51p94w.js} +8 -8
  22. package/dist/cli/{guardrail-reset-5e75rz44.js → guardrail-reset-dcbmt2wr.js} +40 -39
  23. package/dist/cli/{hive-promoter-f54nrhqn.js → hive-promoter-2g8nga0y.js} +40 -39
  24. package/dist/cli/{index-31e105jf.js → index-046psajf.js} +80 -2
  25. package/dist/cli/{index-rhw6m7s9.js → index-0874kghs.js} +9 -3
  26. package/dist/cli/{index-6h81yvyc.js → index-131ay7x1.js} +3 -3
  27. package/dist/cli/{index-raws06d3.js → index-2c91cxdv.js} +4 -4
  28. package/dist/cli/{index-9f930sy8.js → index-32vszgv0.js} +3 -3
  29. package/dist/cli/{index-0bpr8vag.js → index-3n5zsp31.js} +2 -2
  30. package/dist/cli/{index-b26hje53.js → index-3skvygca.js} +5 -5
  31. package/dist/cli/{index-w81jmdkt.js → index-456awy1p.js} +2 -2
  32. package/dist/cli/{index-hb1zqz50.js → index-475bkab8.js} +2 -2
  33. package/dist/cli/{index-80qhm8aq.js → index-49ptc9xx.js} +10 -4
  34. package/dist/cli/{index-bsjwchw7.js → index-57swfxyp.js} +8 -8
  35. package/dist/cli/{index-y5hhkx1n.js → index-5ppx86w1.js} +38 -18
  36. package/dist/cli/{index-jb7ywm4w.js → index-6b427dy8.js} +6 -6
  37. package/dist/cli/{index-3qy7v4k1.js → index-6cvdk98y.js} +2 -2
  38. package/dist/cli/{index-hsvsz3mj.js → index-6g664sy8.js} +1 -1
  39. package/dist/cli/{index-8me6m6ew.js → index-76abqm8d.js} +2 -2
  40. package/dist/cli/{index-z425p51d.js → index-7hvy33gc.js} +271 -80
  41. package/dist/cli/{index-4xr4gtwd.js → index-7p4ym4q3.js} +1 -1
  42. package/dist/cli/{index-j8ty457v.js → index-aghd9kcx.js} +2 -2
  43. package/dist/cli/{index-h01p5ygy.js → index-ar2gsn6b.js} +8 -8
  44. package/dist/cli/{index-ak1dd17g.js → index-dbshccrr.js} +3 -3
  45. package/dist/cli/{index-908k7jhw.js → index-depfky1j.js} +158 -27
  46. package/dist/cli/{index-5t08vc6m.js → index-earq3mda.js} +1 -1
  47. package/dist/cli/{index-6dm6ettv.js → index-ejb7ejhd.js} +1 -1
  48. package/dist/cli/{index-3rb5gs4w.js → index-f0nj3d8z.js} +1 -1
  49. package/dist/cli/{index-qjzbs971.js → index-gsgy1psq.js} +4 -4
  50. package/dist/cli/{index-jk05g11c.js → index-jc8pcf0r.js} +7 -7
  51. package/dist/cli/{index-mt0s67hg.js → index-jkxef52n.js} +2 -2
  52. package/dist/cli/{index-wpn7ftw1.js → index-jvk95e6y.js} +2 -2
  53. package/dist/cli/{index-5cve9g2b.js → index-k53a69vb.js} +6 -6
  54. package/dist/cli/{index-1xnse449.js → index-mc72pk74.js} +1 -1
  55. package/dist/cli/{index-w196wb3a.js → index-mz4zz59c.js} +42 -41
  56. package/dist/cli/{index-4hnk9npg.js → index-n107956q.js} +5 -5
  57. package/dist/cli/{index-gaykghrm.js → index-qczxr0wh.js} +12 -10
  58. package/dist/cli/{index-nazzb4em.js → index-s7bseqza.js} +3220 -1893
  59. package/dist/cli/{index-dg3f2sqq.js → index-t7hmqgjy.js} +90 -141
  60. package/dist/cli/{index-08x7gsnf.js → index-w9hxzcz4.js} +2 -2
  61. package/dist/cli/{index-bqphhrwf.js → index-wfdx7yw2.js} +39 -15
  62. package/dist/cli/{index-xtysafzj.js → index-wsb0wbw6.js} +40 -39
  63. package/dist/cli/{index-nmh0s6dg.js → index-wy3zpwjy.js} +1 -1
  64. package/dist/cli/{index-tt705bap.js → index-x1gyqgze.js} +7 -7
  65. package/dist/cli/index-xf69xty9.js +105 -0
  66. package/dist/cli/{index-ydefv5fn.js → index-ya32wfkp.js} +4 -2
  67. package/dist/cli/{index-1hnknn34.js → index-yk0r42nr.js} +4 -4
  68. package/dist/cli/{index-77650pyg.js → index-zzvxj02e.js} +3 -3
  69. package/dist/cli/index.js +41 -40
  70. package/dist/cli/{knowledge-escalator-cs7fjgy0.js → knowledge-escalator-pt21q3eh.js} +14 -13
  71. package/dist/cli/{knowledge-events-262c1jg2.js → knowledge-events-hp96b55k.js} +12 -11
  72. package/dist/cli/{knowledge-link-8kcms7pj.js → knowledge-link-ap2274vm.js} +6 -5
  73. package/dist/cli/{knowledge-store-yjrkfens.js → knowledge-store-rnw1e7zp.js} +7 -6
  74. package/dist/cli/{knowledge-validator-t98cvpt5.js → knowledge-validator-xy7r6eyx.js} +9 -8
  75. package/dist/cli/{mcp-wj6143vj.js → mcp-3nd85kww.js} +15 -1
  76. package/dist/cli/mcp.d.ts +1 -0
  77. package/dist/cli/{model-preflight-tz4ar9ys.js → model-preflight-xe0chjad.js} +40 -39
  78. package/dist/cli/{pending-delegations-vvbkqe24.js → pending-delegations-2pdngdzq.js} +13 -12
  79. package/dist/cli/{pr-subscriptions-maeppcws.js → pr-subscriptions-hxxwszyr.js} +13 -12
  80. package/dist/cli/{pr-workflow-gate-en54gabv.js → pr-workflow-gate-m37ynr6h.js} +40 -39
  81. package/dist/cli/{runner-618229m8.js → runner-yz3ysehf.js} +7 -6
  82. package/dist/cli/{scan-cursor-sxwh1ax8.js → scan-cursor-be9hbq1v.js} +8 -7
  83. package/dist/cli/{schema-fb8vm14k.js → schema-j17hv98z.js} +3 -1
  84. package/dist/cli/{scope-persistence-sc2zhr15.js → scope-persistence-rmt56rma.js} +16 -15
  85. package/dist/cli/{server-7ksq9wr7.js → server-wzrb59mh.js} +40 -39
  86. package/dist/cli/{skill-generator-ccmq47cm.js → skill-generator-vsfya55z.js} +16 -15
  87. package/dist/cli/{snapshot-coordination-init-h8eg88w2.js → snapshot-coordination-init-pc8ykvh8.js} +40 -39
  88. package/dist/cli/{speckit-checkoff-vkqn317p.js → speckit-checkoff-j407j8ew.js} +4 -4
  89. package/dist/cli/{telemetry-vqyy5nha.js → telemetry-39zmx4fp.js} +2 -2
  90. package/dist/cli/{workspace-snapshot-t22m6gbd.js → workspace-snapshot-0q1n1xq5.js} +4 -4
  91. package/dist/cli/{worktree-collision-ownership-8hr7aycc.js → worktree-collision-ownership-je4fb407.js} +14 -13
  92. package/dist/cli/{worktree-isolation-gwq3y9tf.js → worktree-isolation-27rt584e.js} +40 -39
  93. package/dist/commands/doctor.d.ts +5 -0
  94. package/dist/config/schema.d.ts +15 -0
  95. package/dist/context/role-filter.d.ts +2 -0
  96. package/dist/db/qa-gate-session-override.d.ts +12 -12
  97. package/dist/gate-evidence.d.ts +43 -1
  98. package/dist/hooks/final-context-accounting.d.ts +3 -2
  99. package/dist/hooks/guardrails/index.d.ts +13 -7
  100. package/dist/hooks/host-boundary.d.ts +10 -0
  101. package/dist/hooks/system-enhancer.d.ts +9 -2
  102. package/dist/hooks/system-guidance-carrier.d.ts +18 -0
  103. package/dist/index.js +520 -514
  104. package/dist/plan/ledger.d.ts +20 -4
  105. package/dist/plan/manager.d.ts +20 -2
  106. package/dist/services/config-doctor.d.ts +24 -0
  107. package/dist/services/injection-budget.d.ts +56 -8
  108. package/dist/session/hydration-ownership.d.ts +39 -12
  109. package/dist/session/snapshot-coordination-init.d.ts +11 -4
  110. package/dist/session/snapshot-reader.d.ts +9 -4
  111. package/dist/session/snapshot-writer.d.ts +2 -3
  112. package/dist/state.d.ts +35 -5
  113. package/dist/tools/phase-complete.d.ts +1 -0
  114. package/dist/tools/save-plan.d.ts +12 -0
  115. package/dist/tools/update-task-status.d.ts +1 -1
  116. package/dist/utils/project-boundary.d.ts +32 -1
  117. package/dist/workflow/coder-settlement.d.ts +8 -0
  118. package/dist/workflow/task-terminal.d.ts +2 -1
  119. package/dist/workflow/workflow-wal-schema.d.ts +2 -0
  120. package/opencode-swarm.schema.json +8 -0
  121. package/package.json +1 -1
  122. package/dist/cli/curator-llm-factory-xp4hgd8r.js +0 -74
  123. package/dist/cli/guardrail-explain-j2y5sj4d.js +0 -75
@@ -3,10 +3,10 @@ import {
3
3
  EvaluationRunV1Schema,
4
4
  GateAuditResultV1Schema,
5
5
  getProtectedEvaluationRunIds
6
- } from "./index-ydefv5fn.js";
6
+ } from "./index-ya32wfkp.js";
7
7
  import {
8
8
  readEffectiveSpecSync
9
- } from "./index-4xr4gtwd.js";
9
+ } from "./index-7p4ym4q3.js";
10
10
  import {
11
11
  validateWorkspaceRoot
12
12
  } from "./index-6b295cas.js";
@@ -16,20 +16,23 @@ import {
16
16
  import {
17
17
  GitBinaryMissingError,
18
18
  GitSpawnCwdError,
19
+ TASK_WORKFLOW_SCHEMA_MARKER,
20
+ deriveApplicableGateSet,
19
21
  isGitBinaryMissing,
20
22
  isSpawnCwdMissing,
21
23
  isSpawnCwdUnreadable,
22
24
  isValidTaskId,
25
+ readCurrentTaskDeclaredFiles,
23
26
  readTaskEvidence,
24
27
  readTaskEvidenceRaw,
25
28
  sanitizeTaskId
26
- } from "./index-908k7jhw.js";
29
+ } from "./index-depfky1j.js";
27
30
  import {
28
31
  estimateCharsForTokens,
29
32
  estimateTokens,
30
33
  readSwarmFileAsync,
31
34
  validateSwarmPath
32
- } from "./index-nmh0s6dg.js";
35
+ } from "./index-wy3zpwjy.js";
33
36
  import {
34
37
  bunHash,
35
38
  bunWrite,
@@ -58,10 +61,10 @@ import {
58
61
  replayFromLedgerWithStatus,
59
62
  takeSnapshotEvent,
60
63
  takeSnapshotWithRetry
61
- } from "./index-bqphhrwf.js";
64
+ } from "./index-wfdx7yw2.js";
62
65
  import {
63
66
  appendCoreEventSync
64
- } from "./index-wpn7ftw1.js";
67
+ } from "./index-jvk95e6y.js";
65
68
  import {
66
69
  derivePlanId,
67
70
  derivePlanIdentityHash,
@@ -69,23 +72,25 @@ import {
69
72
  importCoordinationOnce,
70
73
  listCoordinationStates,
71
74
  transitionCoordinationState
72
- } from "./index-3rb5gs4w.js";
75
+ } from "./index-f0nj3d8z.js";
73
76
  import {
74
77
  DURABILITY_CLASSES,
75
78
  getProjectDb,
76
79
  projectDbExists,
77
80
  withDurabilityClass
78
- } from "./index-08x7gsnf.js";
81
+ } from "./index-w9hxzcz4.js";
79
82
  import {
80
- assertProjectRoot,
81
83
  withEvidenceLock
82
- } from "./index-dg3f2sqq.js";
84
+ } from "./index-xf69xty9.js";
85
+ import {
86
+ assertProjectRoot
87
+ } from "./index-t7hmqgjy.js";
83
88
  import {
84
89
  emit
85
- } from "./index-tt705bap.js";
90
+ } from "./index-x1gyqgze.js";
86
91
  import {
87
92
  canonicalRootKeyFresh
88
- } from "./index-1xnse449.js";
93
+ } from "./index-mc72pk74.js";
89
94
  import {
90
95
  tryAcquireLock
91
96
  } from "./index-ejhxgyvm.js";
@@ -93,7 +98,7 @@ import {
93
98
  atomicWriteSwarmFileSync,
94
99
  invalidateCachedArtifact,
95
100
  readCachedParsedFile
96
- } from "./index-5cve9g2b.js";
101
+ } from "./index-k53a69vb.js";
97
102
  import {
98
103
  PlanSchema,
99
104
  normalizeCurrentPhaseInPlace,
@@ -1723,6 +1728,13 @@ class PlanConcurrentModificationError extends Error {
1723
1728
  }
1724
1729
  }
1725
1730
 
1731
+ class PlanRecoverySupersededError extends Error {
1732
+ constructor(message = "plan recovery superseded by a newer generation") {
1733
+ super(message);
1734
+ this.name = "PlanRecoverySupersededError";
1735
+ }
1736
+ }
1737
+
1726
1738
  class PlanTaskRemovalNotAcknowledgedError extends Error {
1727
1739
  missingTasks;
1728
1740
  constructor(missingTasks) {
@@ -1742,6 +1754,7 @@ var _internals5 = {
1742
1754
  readPlanJsonUtf8,
1743
1755
  readPlanFileUtf8,
1744
1756
  verifyWrittenPlanJson,
1757
+ writeRebuildPlanMarkdown,
1745
1758
  ledgerExists,
1746
1759
  replayFromLedger,
1747
1760
  loadLastApprovedPlan,
@@ -1825,7 +1838,8 @@ async function retryCasWithBackoff(directory, eventInput, options) {
1825
1838
  try {
1826
1839
  return await appendLedgerEvent(directory, eventInput, {
1827
1840
  expectedHash: currentExpected,
1828
- planHashAfter: options.planHashAfter
1841
+ planHashAfter: options.planHashAfter,
1842
+ preCommitCheck: options.preCommitCheck
1829
1843
  });
1830
1844
  } catch (error2) {
1831
1845
  if (!(error2 instanceof LedgerStaleWriterError) || attempt >= maxRetries) {
@@ -1882,7 +1896,7 @@ async function getLatestLedgerHash(directory) {
1882
1896
  return "";
1883
1897
  }
1884
1898
  }
1885
- async function surfaceLedgerStaleIfPersisted(directory, plan) {
1899
+ async function surfaceLedgerStaleIfPersisted(directory, plan, options) {
1886
1900
  const resolvedWorkspace = canonicalRootKeyFresh(directory);
1887
1901
  if (!ledgerStaleWorkspaces.has(resolvedWorkspace)) {
1888
1902
  return plan;
@@ -1891,10 +1905,14 @@ async function surfaceLedgerStaleIfPersisted(directory, plan) {
1891
1905
  const planHash = computePlanLedgerHash(plan);
1892
1906
  const ledgerHash = await getLatestLedgerHash(directory);
1893
1907
  if (ledgerHash !== "" && planHash === ledgerHash) {
1908
+ options?.preCommitCheck?.();
1894
1909
  ledgerStaleWorkspaces.delete(resolvedWorkspace);
1895
1910
  return plan;
1896
1911
  }
1897
- } catch {}
1912
+ } catch (error2) {
1913
+ if (error2 instanceof PlanRecoverySupersededError)
1914
+ throw error2;
1915
+ }
1898
1916
  plan._ledgerReplayStale = true;
1899
1917
  if (typeof plan._ledgerReplayStaleReason !== "string" || plan._ledgerReplayStaleReason.length === 0) {
1900
1918
  plan._ledgerReplayStaleReason = "plan.json still hash-mismatches the ledger after a startup ledger-replay failure (replay could not be applied and no critic-approved snapshot was available). Run /swarm reset-session if this persists.";
@@ -1968,7 +1986,7 @@ async function isPlanMdInSync(directory, plan, cache) {
1968
1986
  }
1969
1987
  return false;
1970
1988
  }
1971
- async function regeneratePlanMarkdown(directory, plan) {
1989
+ async function regeneratePlanMarkdown(directory, plan, options) {
1972
1990
  assertProjectRoot(directory);
1973
1991
  const swarmDir = path3.resolve(directory, ".swarm");
1974
1992
  const contentHash = computePlanContentHash(plan);
@@ -1979,6 +1997,7 @@ ${markdown}`;
1979
1997
  const mdTempPath = path3.join(swarmDir, `plan.md.tmp.${Date.now()}.${Math.floor(Math.random() * 1e9)}`);
1980
1998
  try {
1981
1999
  await bunWrite(mdTempPath, markdownWithHash);
2000
+ options?.preCommitCheck?.();
1982
2001
  renameSync2(mdTempPath, mdPath);
1983
2002
  } finally {
1984
2003
  try {
@@ -1987,8 +2006,10 @@ ${markdown}`;
1987
2006
  }
1988
2007
  invalidateCachedArtifact(mdPath);
1989
2008
  }
1990
- async function loadPlan(directory, cache) {
2009
+ async function loadPlan(directory, cache, options) {
2010
+ options?.preCommitCheck?.();
1991
2011
  let planJsonContent = null;
2012
+ let claimedStartupWorkspace = null;
1992
2013
  try {
1993
2014
  planJsonContent = await _internals5.readPlanJsonUtf8(directory);
1994
2015
  } catch (error2) {
@@ -2006,8 +2027,12 @@ async function loadPlan(directory, cache) {
2006
2027
  const inSync = await isPlanMdInSync(directory, validated, cache);
2007
2028
  if (!inSync) {
2008
2029
  try {
2009
- await _internals5.regeneratePlanMarkdown(directory, validated);
2030
+ await _internals5.regeneratePlanMarkdown(directory, validated, {
2031
+ preCommitCheck: options?.preCommitCheck
2032
+ });
2010
2033
  } catch (regenError) {
2034
+ if (regenError instanceof PlanRecoverySupersededError)
2035
+ throw regenError;
2011
2036
  warn(`Failed to regenerate plan.md: ${regenError instanceof Error ? regenError.message : String(regenError)}. Proceeding with plan.json only.`);
2012
2037
  }
2013
2038
  }
@@ -2016,7 +2041,9 @@ async function loadPlan(directory, cache) {
2016
2041
  const ledgerHash = await getLatestLedgerHash(directory);
2017
2042
  const resolvedWorkspace = canonicalRootKeyFresh(directory);
2018
2043
  if (!startupLedgerCheckedWorkspaces.has(resolvedWorkspace)) {
2044
+ options?.preCommitCheck?.();
2019
2045
  startupLedgerCheckedWorkspaces.add(resolvedWorkspace);
2046
+ claimedStartupWorkspace = resolvedWorkspace;
2020
2047
  if (ledgerHash !== "" && planHash !== ledgerHash) {
2021
2048
  const currentPlanId = derivePlanId(validated);
2022
2049
  const ledgerEvents = await readLedgerEvents(directory);
@@ -2026,46 +2053,60 @@ async function loadPlan(directory, cache) {
2026
2053
  } else {
2027
2054
  warn("[loadPlan] plan.json is stale (hash mismatch with ledger) \u2014 rebuilding from ledger. If this recurs, run /swarm reset-session to clear stale session state.");
2028
2055
  try {
2029
- const { plan: rebuilt, truncated } = await replayFromLedgerWithStatus(directory);
2056
+ const { plan: rebuilt, truncated } = await replayFromLedgerWithStatus(directory, {
2057
+ preCommitCheck: options?.preCommitCheck
2058
+ });
2030
2059
  if (truncated) {
2031
2060
  const runtimeStale = validated;
2032
2061
  runtimeStale._ledgerReplayStale = true;
2033
2062
  runtimeStale._ledgerReplayStaleReason = "Ledger truncated: a malformed line stopped replay before the tail, so the ledger could only be reconstructed up to the corruption. plan.json was PRESERVED (not rolled back to the prefix-only projection) and the corrupted suffix was quarantined to .swarm/plan-ledger.quarantine.*. Verify state, then run /swarm reset-session if this persists.";
2063
+ options?.preCommitCheck?.();
2034
2064
  ledgerStaleWorkspaces.add(resolvedWorkspace);
2035
2065
  criticalWarn("[loadPlan] Ledger truncated (poison line detected) \u2014 preserving plan.json instead of rolling back to the prefix-only ledger projection. Durable post-poison events remain in plan.json. Corrupted suffix quarantined to .swarm/plan-ledger.quarantine.*. Run /swarm reset-session after verifying state if this persists.");
2036
2066
  return runtimeStale;
2037
2067
  }
2038
2068
  if (rebuilt) {
2039
2069
  await rebuildPlan(directory, rebuilt, {
2040
- reason: "ledger_hash_mismatch_recovery"
2070
+ reason: "ledger_hash_mismatch_recovery",
2071
+ preCommitCheck: options?.preCommitCheck
2041
2072
  });
2042
2073
  warn("[loadPlan] Rebuilt plan from ledger. Checkpoint available at .swarm/plan-export/SWARM_PLAN.md if it exists.");
2043
2074
  return rebuilt;
2044
2075
  }
2045
2076
  } catch (replayError) {
2077
+ if (replayError instanceof PlanRecoverySupersededError)
2078
+ throw replayError;
2046
2079
  try {
2047
2080
  const approved = await loadLastApprovedPlan(directory, currentPlanId);
2048
2081
  if (approved) {
2049
2082
  await rebuildPlan(directory, approved.plan, {
2050
- reason: "approved_snapshot_fallback"
2083
+ reason: "approved_snapshot_fallback",
2084
+ preCommitCheck: options?.preCommitCheck
2051
2085
  });
2052
2086
  try {
2053
2087
  await takeSnapshotEvent(directory, approved.plan, {
2054
2088
  source: "recovery_from_approved_snapshot",
2055
- approvalMetadata: approved.approval
2089
+ approvalMetadata: approved.approval,
2090
+ preCommitCheck: options?.preCommitCheck
2056
2091
  });
2057
2092
  } catch (healError) {
2093
+ if (healError instanceof PlanRecoverySupersededError)
2094
+ throw healError;
2058
2095
  warn(`[loadPlan] Recovery-heal snapshot append failed: ${healError instanceof Error ? healError.message : String(healError)}. Next loadPlan may re-enter recovery path.`);
2059
2096
  }
2060
2097
  const approvedPhase = approved.approval && typeof approved.approval === "object" && "phase" in approved.approval ? approved.approval.phase : undefined;
2061
2098
  warn(`[loadPlan] Ledger replay failed (${replayError instanceof Error ? replayError.message : String(replayError)}) \u2014 recovered from critic-approved snapshot seq=${approved.seq} (approval phase=${approvedPhase ?? "unknown"}, timestamp=${approved.timestamp}). This may roll the plan back to an earlier phase \u2014 verify before continuing.`);
2062
2099
  return approved.plan;
2063
2100
  }
2064
- } catch {}
2101
+ } catch (recoveryError) {
2102
+ if (recoveryError instanceof PlanRecoverySupersededError)
2103
+ throw recoveryError;
2104
+ }
2065
2105
  {
2066
2106
  const runtimeStale = validated;
2067
2107
  runtimeStale._ledgerReplayStale = true;
2068
2108
  runtimeStale._ledgerReplayStaleReason = `Ledger replay failed during hash-mismatch rebuild and no approved snapshot was available: ${replayError instanceof Error ? replayError.message : String(replayError)}`;
2109
+ options?.preCommitCheck?.();
2069
2110
  ledgerStaleWorkspaces.add(resolvedWorkspace);
2070
2111
  }
2071
2112
  warn(`[loadPlan] Ledger replay failed during hash-mismatch rebuild: ${replayError instanceof Error ? replayError.message : String(replayError)}. Returning stale plan.json. To recover: check .swarm/plan-export/SWARM_PLAN.md for a checkpoint, or run /swarm reset-session.`);
@@ -2090,7 +2131,7 @@ async function loadPlan(directory, cache) {
2090
2131
  assertProjectRoot(directory);
2091
2132
  const diffInfo = computeSpecDiff(directory);
2092
2133
  const specStalenessPath = path3.join(directory, ".swarm", "spec-staleness.json");
2093
- await fsPromises.writeFile(specStalenessPath, JSON.stringify({
2134
+ await commitAsyncPreparedFile(specStalenessPath, JSON.stringify({
2094
2135
  type: "spec_stale_detected",
2095
2136
  timestamp: new Date().toISOString(),
2096
2137
  phase: validated.current_phase ?? 1,
@@ -2100,9 +2141,12 @@ async function loadPlan(directory, cache) {
2100
2141
  planTitle: validated.title,
2101
2142
  diff: diffInfo?.diff ?? null,
2102
2143
  changedSections: diffInfo?.changedSections ?? []
2103
- }, null, 2), "utf-8");
2144
+ }, null, 2), options?.preCommitCheck, "spec-staleness");
2104
2145
  invalidateCachedArtifact(specStalenessPath);
2105
- } catch {}
2146
+ } catch (error2) {
2147
+ if (error2 instanceof PlanRecoverySupersededError)
2148
+ throw error2;
2149
+ }
2106
2150
  try {
2107
2151
  assertProjectRoot(directory);
2108
2152
  const event = {
@@ -2114,22 +2158,35 @@ async function loadPlan(directory, cache) {
2114
2158
  reason: staleResult.reason ?? "unknown",
2115
2159
  planTitle: validated.title
2116
2160
  };
2161
+ options?.preCommitCheck?.();
2117
2162
  appendCoreEventSync(directory, { ...event });
2118
- } catch {}
2163
+ } catch (error2) {
2164
+ if (error2 instanceof PlanRecoverySupersededError)
2165
+ throw error2;
2166
+ }
2119
2167
  }
2120
2168
  }
2121
2169
  }
2122
- return await surfaceLedgerStaleIfPersisted(directory, validated);
2170
+ return await surfaceLedgerStaleIfPersisted(directory, validated, options);
2123
2171
  }
2124
2172
  } catch (error2) {
2173
+ if (error2 instanceof PlanRecoverySupersededError) {
2174
+ if (claimedStartupWorkspace !== null) {
2175
+ startupLedgerCheckedWorkspaces.delete(claimedStartupWorkspace);
2176
+ }
2177
+ throw error2;
2178
+ }
2125
2179
  warn(`[loadPlan] plan.json validation failed: ${error2 instanceof Error ? error2.message : String(error2)}. Attempting rebuild from ledger. If rebuild fails, check .swarm/plan-export/SWARM_PLAN.md for a checkpoint.`);
2126
2180
  let rawPlanId = null;
2181
+ let rawPlanJsonParseFailed = false;
2127
2182
  try {
2128
2183
  const rawParsed = JSON.parse(planJsonContent);
2129
2184
  if (typeof rawParsed?.swarm === "string" && typeof rawParsed?.title === "string") {
2130
2185
  rawPlanId = derivePlanId(rawParsed);
2131
2186
  }
2132
- } catch {}
2187
+ } catch {
2188
+ rawPlanJsonParseFailed = true;
2189
+ }
2133
2190
  if (await _internals5.ledgerExists(directory)) {
2134
2191
  const catchIntegrity = await _internals5.readLedgerEventsWithIntegrity(directory);
2135
2192
  const ledgerEventsForCatch = catchIntegrity.events;
@@ -2137,16 +2194,21 @@ async function loadPlan(directory, cache) {
2137
2194
  const identityMatch = rawPlanId === null || catchFirstEvent === null || catchFirstEvent.plan_id === rawPlanId;
2138
2195
  if (!identityMatch) {
2139
2196
  warn(`[loadPlan] Ledger identity mismatch in validation-failure path (ledger: ${catchFirstEvent?.plan_id}, plan: ${rawPlanId}) \u2014 skipping ledger rebuild (migration detected).`);
2140
- } else if (catchFirstEvent !== null && rawPlanId !== null) {
2197
+ } else if (catchFirstEvent !== null && (rawPlanId !== null || rawPlanJsonParseFailed && !catchIntegrity.truncated)) {
2141
2198
  let rebuilt = null;
2142
2199
  try {
2143
- rebuilt = await _internals5.replayFromLedger(directory);
2200
+ rebuilt = await _internals5.replayFromLedger(directory, {
2201
+ preCommitCheck: options?.preCommitCheck
2202
+ });
2144
2203
  } catch (replayError) {
2204
+ if (replayError instanceof PlanRecoverySupersededError)
2205
+ throw replayError;
2145
2206
  warn(`[loadPlan] Ledger replay threw in validation-failure path: ${replayError instanceof Error ? replayError.message : String(replayError)}. Falling back to critic-approved snapshot before legacy migration.`);
2146
2207
  }
2147
2208
  if (rebuilt) {
2148
2209
  await rebuildPlan(directory, rebuilt, {
2149
- reason: "validation_failure_recovery"
2210
+ reason: "validation_failure_recovery",
2211
+ preCommitCheck: options?.preCommitCheck
2150
2212
  });
2151
2213
  warn("[loadPlan] Rebuilt plan from ledger after validation failure. Projection was stale.");
2152
2214
  return rebuilt;
@@ -2154,7 +2216,7 @@ async function loadPlan(directory, cache) {
2154
2216
  try {
2155
2217
  const approved = await _internals5.loadLastApprovedPlan(directory, catchFirstEvent.plan_id);
2156
2218
  if (approved) {
2157
- const { removedCount } = await savePlanWithAutoAcknowledgedRemovals(directory, approved.plan, "load_plan_recovery_from_approved_snapshot", "restore from critic-approved snapshot");
2219
+ const { removedCount } = await savePlanWithAutoAcknowledgedRemovals(directory, approved.plan, "load_plan_recovery_from_approved_snapshot", "restore from critic-approved snapshot", { preCommitCheck: options?.preCommitCheck });
2158
2220
  if (removedCount > 0) {
2159
2221
  approved.plan._midLoadRemovals = {
2160
2222
  count: removedCount,
@@ -2164,15 +2226,20 @@ async function loadPlan(directory, cache) {
2164
2226
  try {
2165
2227
  await takeSnapshotEvent(directory, approved.plan, {
2166
2228
  source: "recovery_from_approved_snapshot",
2167
- approvalMetadata: approved.approval
2229
+ approvalMetadata: approved.approval,
2230
+ preCommitCheck: options?.preCommitCheck
2168
2231
  });
2169
2232
  } catch (healError) {
2233
+ if (healError instanceof PlanRecoverySupersededError)
2234
+ throw healError;
2170
2235
  warn(`[loadPlan] Recovery-heal snapshot append failed: ${healError instanceof Error ? healError.message : String(healError)}. Next loadPlan may re-enter recovery path.`);
2171
2236
  }
2172
2237
  warn(`[loadPlan] Recovered from critic-approved snapshot seq=${approved.seq} after validation failure with exhausted ledger replay (approved snapshot beats lossy Markdown migration).`);
2173
2238
  return approved.plan;
2174
2239
  }
2175
2240
  } catch (approvedError) {
2241
+ if (approvedError instanceof PlanRecoverySupersededError)
2242
+ throw approvedError;
2176
2243
  warn(`[loadPlan] Approved-snapshot recovery failed in validation-failure path: ${approvedError instanceof Error ? approvedError.message : String(approvedError)}`);
2177
2244
  }
2178
2245
  }
@@ -2180,14 +2247,14 @@ async function loadPlan(directory, cache) {
2180
2247
  const planMdContent2 = await readSwarmFileAsync(directory, "plan.md", cache);
2181
2248
  if (planMdContent2 !== null) {
2182
2249
  const migrated = migrateLegacyPlan(planMdContent2);
2183
- const { removedCount } = await savePlanWithAutoAcknowledgedRemovals(directory, migrated, "load_plan_migration_from_md", "migrate legacy plan.md to plan.json");
2250
+ const { removedCount } = await savePlanWithAutoAcknowledgedRemovals(directory, migrated, "load_plan_migration_from_md", "migrate legacy plan.md to plan.json", { preCommitCheck: options?.preCommitCheck });
2184
2251
  if (removedCount > 0) {
2185
2252
  migrated._midLoadRemovals = {
2186
2253
  count: removedCount,
2187
2254
  source: "load_plan_migration_from_md"
2188
2255
  };
2189
2256
  }
2190
- await appendMigrationProvenanceEvent(directory, migrated);
2257
+ await appendMigrationProvenanceEvent(directory, migrated, options?.preCommitCheck);
2191
2258
  return migrated;
2192
2259
  }
2193
2260
  }
@@ -2210,12 +2277,16 @@ async function loadPlan(directory, cache) {
2210
2277
  try {
2211
2278
  let rebuilt = null;
2212
2279
  try {
2213
- rebuilt = await _internals5.replayFromLedger(directory);
2280
+ rebuilt = await _internals5.replayFromLedger(directory, {
2281
+ preCommitCheck: options?.preCommitCheck
2282
+ });
2214
2283
  } catch (replayError) {
2284
+ if (replayError instanceof PlanRecoverySupersededError)
2285
+ throw replayError;
2215
2286
  warn(`[loadPlan] Ledger replay threw in missing-projection path: ${replayError instanceof Error ? replayError.message : String(replayError)}. Falling back to critic-approved snapshot before legacy migration.`);
2216
2287
  }
2217
2288
  if (rebuilt) {
2218
- const { removedCount } = await savePlanWithAutoAcknowledgedRemovals(directory, rebuilt, "load_plan_rebuild_from_ledger", "rebuild plan from ledger replay");
2289
+ const { removedCount } = await savePlanWithAutoAcknowledgedRemovals(directory, rebuilt, "load_plan_rebuild_from_ledger", "rebuild plan from ledger replay", { preCommitCheck: options?.preCommitCheck });
2219
2290
  if (removedCount > 0) {
2220
2291
  rebuilt._midLoadRemovals = {
2221
2292
  count: removedCount,
@@ -2236,7 +2307,7 @@ async function loadPlan(directory, cache) {
2236
2307
  if (approved) {
2237
2308
  const approvedPhase = approved.approval && typeof approved.approval === "object" && "phase" in approved.approval ? approved.approval.phase : undefined;
2238
2309
  warn(`[loadPlan] Ledger replay returned no plan \u2014 recovered from critic-approved snapshot seq=${approved.seq} timestamp=${approved.timestamp} (approval phase=${approvedPhase ?? "unknown"}). This may roll the plan back to an earlier phase \u2014 verify before continuing.`);
2239
- const { removedCount: snapshotRemovedCount } = await savePlanWithAutoAcknowledgedRemovals(directory, approved.plan, "load_plan_recovery_from_approved_snapshot", "restore from critic-approved snapshot");
2310
+ const { removedCount: snapshotRemovedCount } = await savePlanWithAutoAcknowledgedRemovals(directory, approved.plan, "load_plan_recovery_from_approved_snapshot", "restore from critic-approved snapshot", { preCommitCheck: options?.preCommitCheck });
2240
2311
  if (snapshotRemovedCount > 0) {
2241
2312
  approved.plan._midLoadRemovals = {
2242
2313
  count: snapshotRemovedCount,
@@ -2246,14 +2317,19 @@ async function loadPlan(directory, cache) {
2246
2317
  try {
2247
2318
  await takeSnapshotEvent(directory, approved.plan, {
2248
2319
  source: "recovery_from_approved_snapshot",
2249
- approvalMetadata: approved.approval
2320
+ approvalMetadata: approved.approval,
2321
+ preCommitCheck: options?.preCommitCheck
2250
2322
  });
2251
2323
  } catch (healError) {
2324
+ if (healError instanceof PlanRecoverySupersededError)
2325
+ throw healError;
2252
2326
  warn(`[loadPlan] Recovery-heal snapshot append failed: ${healError instanceof Error ? healError.message : String(healError)}. Next loadPlan may re-enter recovery path.`);
2253
2327
  }
2254
2328
  return approved.plan;
2255
2329
  }
2256
2330
  } catch (recoveryError) {
2331
+ if (recoveryError instanceof PlanRecoverySupersededError)
2332
+ throw recoveryError;
2257
2333
  warn(`[loadPlan] Approved-snapshot recovery failed: ${recoveryError instanceof Error ? recoveryError.message : String(recoveryError)}`);
2258
2334
  }
2259
2335
  } finally {
@@ -2264,14 +2340,14 @@ async function loadPlan(directory, cache) {
2264
2340
  const planMdContent = await readSwarmFileAsync(directory, "plan.md", cache);
2265
2341
  if (planMdContent !== null) {
2266
2342
  const migrated = migrateLegacyPlan(planMdContent);
2267
- const { removedCount } = await savePlanWithAutoAcknowledgedRemovals(directory, migrated, "load_plan_migration_from_md", "migrate legacy plan.md to plan.json");
2343
+ const { removedCount } = await savePlanWithAutoAcknowledgedRemovals(directory, migrated, "load_plan_migration_from_md", "migrate legacy plan.md to plan.json", { preCommitCheck: options?.preCommitCheck });
2268
2344
  if (removedCount > 0) {
2269
2345
  migrated._midLoadRemovals = {
2270
2346
  count: removedCount,
2271
2347
  source: "load_plan_migration_from_md"
2272
2348
  };
2273
2349
  }
2274
- await appendMigrationProvenanceEvent(directory, migrated);
2350
+ await appendMigrationProvenanceEvent(directory, migrated, options?.preCommitCheck);
2275
2351
  return migrated;
2276
2352
  }
2277
2353
  return null;
@@ -2347,8 +2423,9 @@ async function verifyWrittenPlanJson(directory, expected) {
2347
2423
  throw new PlanWriteVerificationError("PLAN_WRITE_VERIFICATION_FAILED: plan.json read-back content differs from the projected plan");
2348
2424
  }
2349
2425
  }
2350
- async function appendMigrationProvenanceEvent(directory, plan) {
2426
+ async function appendMigrationProvenanceEvent(directory, plan, preCommitCheck) {
2351
2427
  try {
2428
+ preCommitCheck?.();
2352
2429
  await appendLedgerEvent(directory, {
2353
2430
  event_type: "plan_rebuilt",
2354
2431
  source: "load_plan_migration_from_md",
@@ -2358,8 +2435,10 @@ async function appendMigrationProvenanceEvent(directory, plan) {
2358
2435
  phases_count: plan.phases.length,
2359
2436
  tasks_count: plan.phases.reduce((sum, phase) => sum + phase.tasks.length, 0)
2360
2437
  }
2361
- });
2438
+ }, { preCommitCheck });
2362
2439
  } catch (error2) {
2440
+ if (error2 instanceof PlanRecoverySupersededError)
2441
+ throw error2;
2363
2442
  warn(`[loadPlan] Markdown-migration provenance event append failed (plan remains migrated): ${error2 instanceof Error ? error2.message : String(error2)}`);
2364
2443
  }
2365
2444
  }
@@ -2420,7 +2499,9 @@ async function savePlan(directory, plan, options) {
2420
2499
  if (!await ledgerExists(directory)) {
2421
2500
  try {
2422
2501
  options?.preCommitCheck?.();
2423
- await initLedger(directory, planId, planHashForInit, validated);
2502
+ await initLedger(directory, planId, planHashForInit, validated, {
2503
+ preCommitCheck: options?.preCommitCheck
2504
+ });
2424
2505
  } catch (initErr) {
2425
2506
  const msg = initErr instanceof Error ? initErr.message : String(initErr);
2426
2507
  if (!/already initialized/i.test(msg)) {
@@ -2431,7 +2512,7 @@ async function savePlan(directory, plan, options) {
2431
2512
  const existingEvents = await readLedgerEvents(directory);
2432
2513
  if (existingEvents.length > 0 && existingEvents[0].plan_id !== planId) {
2433
2514
  options?.preCommitCheck?.();
2434
- await replacePlanLedgerWithRoot(directory, validated, "savePlan_identity_migration");
2515
+ await replacePlanLedgerWithRoot(directory, validated, "savePlan_identity_migration", { preCommitCheck: options?.preCommitCheck });
2435
2516
  warn(`[savePlan] Ledger identity mismatch (was "${existingEvents[0].plan_id}", now "${planId}") \u2014 archived the prior exact history and committed a new root.`);
2436
2517
  }
2437
2518
  }
@@ -2530,6 +2611,7 @@ async function savePlan(directory, plan, options) {
2530
2611
  await retryCasWithBackoff(directory, eventInput, {
2531
2612
  expectedHash: currentHash,
2532
2613
  planHashAfter: hashAfter,
2614
+ preCommitCheck: options?.preCommitCheck,
2533
2615
  verifyValid: async () => {
2534
2616
  const onDisk = await _internals5.loadPlanJsonOnly(directory);
2535
2617
  if (!onDisk)
@@ -2569,6 +2651,7 @@ async function savePlan(directory, plan, options) {
2569
2651
  await retryCasWithBackoff(directory, eventInput, {
2570
2652
  expectedHash: currentHash,
2571
2653
  planHashAfter: hashAfter,
2654
+ preCommitCheck: options?.preCommitCheck,
2572
2655
  verifyValid: async () => {
2573
2656
  const onDisk = await _internals5.loadPlanJsonOnly(directory);
2574
2657
  if (!onDisk)
@@ -2593,13 +2676,16 @@ async function savePlan(directory, plan, options) {
2593
2676
  }
2594
2677
  }
2595
2678
  const ledgerStatusTaskIds = collectLedgerStatusTaskIds(await readLedgerEvents(directory));
2596
- const replayedBeforeProjection = await _internals5.replayFromLedger(directory);
2679
+ const replayedBeforeProjection = await _internals5.replayFromLedger(directory, {
2680
+ preCommitCheck: options?.preCommitCheck
2681
+ });
2597
2682
  const projectionCandidate = replayedBeforeProjection ? mergeStatusesTakingPrecedence(validated, replayedBeforeProjection, ledgerStatusTaskIds) : validated;
2598
2683
  if (replayedBeforeProjection && computePlanLedgerHash(replayedBeforeProjection) !== computePlanLedgerHash(projectionCandidate)) {
2599
2684
  options?.preCommitCheck?.();
2600
2685
  await takeSnapshotEvent(directory, projectionCandidate, {
2601
2686
  planHashAfter: computePlanLedgerHash(projectionCandidate),
2602
- source: "savePlan_structural_projection"
2687
+ source: "savePlan_structural_projection",
2688
+ preCommitCheck: options?.preCommitCheck
2603
2689
  });
2604
2690
  }
2605
2691
  const projectedPlan = projectionCandidate;
@@ -2634,8 +2720,11 @@ async function savePlan(directory, plan, options) {
2634
2720
  tasks_count: projectedPlan.phases.reduce((sum, p) => sum + p.tasks.length, 0),
2635
2721
  in_progress: true
2636
2722
  });
2637
- await bunWrite(markerPath, inProgressMarker);
2638
- } catch {}
2723
+ await commitAsyncPreparedFile(markerPath, inProgressMarker, options?.preCommitCheck, "plan-write-marker");
2724
+ } catch (error2) {
2725
+ if (error2 instanceof PlanRecoverySupersededError)
2726
+ throw error2;
2727
+ }
2639
2728
  let mdWriteError;
2640
2729
  try {
2641
2730
  const contentHash = computePlanContentHash(projectedPlan);
@@ -2646,6 +2735,7 @@ ${markdown}`;
2646
2735
  const mdTempPath = path3.join(swarmDir, `plan.md.tmp.${Date.now()}.${Math.floor(Math.random() * 1e9)}`);
2647
2736
  try {
2648
2737
  await bunWrite(mdTempPath, markdownWithHash);
2738
+ options?.preCommitCheck?.();
2649
2739
  renameSync2(mdTempPath, mdPath);
2650
2740
  } finally {
2651
2741
  try {
@@ -2654,6 +2744,8 @@ ${markdown}`;
2654
2744
  }
2655
2745
  invalidateCachedArtifact(mdPath);
2656
2746
  } catch (mdError) {
2747
+ if (mdError instanceof PlanRecoverySupersededError)
2748
+ throw mdError;
2657
2749
  const message = mdError instanceof Error ? mdError.message : String(mdError);
2658
2750
  mdWriteError = message;
2659
2751
  warn(`[savePlan] plan.md write failed (non-fatal, plan.json is authoritative): ${message}`);
@@ -2675,8 +2767,11 @@ ${markdown}`;
2675
2767
  tasks_count: tasksCount,
2676
2768
  in_progress: false
2677
2769
  });
2678
- await bunWrite(markerPath, marker);
2679
- } catch {}
2770
+ await commitAsyncPreparedFile(markerPath, marker, options?.preCommitCheck, "plan-write-marker");
2771
+ } catch (error2) {
2772
+ if (error2 instanceof PlanRecoverySupersededError)
2773
+ throw error2;
2774
+ }
2680
2775
  if (currentPlan) {
2681
2776
  try {
2682
2777
  const oldIdentityHash = derivePlanIdentityHash(currentPlan);
@@ -2695,16 +2790,20 @@ ${markdown}`;
2695
2790
  for (const [taskId, oldStatus] of oldStatuses) {
2696
2791
  const newStatus = newStatuses.get(taskId);
2697
2792
  if (oldStatus === "completed" && newStatus !== "completed") {
2793
+ options?.preCommitCheck?.();
2698
2794
  advanceTaskCheckpointReceiptGeneration(directory, oldIdentityHash, taskId);
2699
2795
  }
2700
2796
  }
2701
2797
  for (const [taskId, newStatus] of newStatuses) {
2702
2798
  if (newStatus === "completed" && oldStatuses.get(taskId) !== "completed") {
2799
+ options?.preCommitCheck?.();
2703
2800
  repairTaskCheckpointReceiptForCompletion(directory, newIdentityHash, taskId);
2704
2801
  }
2705
2802
  }
2706
2803
  }
2707
2804
  } catch (receiptError) {
2805
+ if (receiptError instanceof PlanRecoverySupersededError)
2806
+ throw receiptError;
2708
2807
  warn(`[savePlan] task checkpoint receipt lifecycle sync failed (plan remains authoritative): ${receiptError instanceof Error ? receiptError.message : String(receiptError)}`);
2709
2808
  }
2710
2809
  }
@@ -2717,27 +2816,53 @@ ${markdown}`;
2717
2816
  }
2718
2817
  return { durability: "complete", degraded_surfaces: [] };
2719
2818
  }
2819
+ async function commitAsyncPreparedFile(targetPath, content, preCommitCheck, tempLabel = "atomic") {
2820
+ const tempPath = `${targetPath}.${tempLabel}.${Date.now()}.${Math.floor(Math.random() * 1e9)}`;
2821
+ try {
2822
+ await bunWrite(tempPath, content);
2823
+ preCommitCheck?.();
2824
+ renameSync2(tempPath, targetPath);
2825
+ } catch (error2) {
2826
+ try {
2827
+ unlinkSync(tempPath);
2828
+ } catch {}
2829
+ throw error2;
2830
+ }
2831
+ }
2832
+ async function writeRebuildPlanMarkdown(tempPath, content) {
2833
+ await bunWrite(tempPath, content);
2834
+ }
2720
2835
  async function rebuildPlan(directory, plan, options) {
2721
2836
  assertProjectRoot(directory);
2722
- const targetPlan = plan ?? await replayFromLedger(directory);
2837
+ const targetPlan = plan ?? await replayFromLedger(directory, {
2838
+ preCommitCheck: options?.preCommitCheck
2839
+ });
2723
2840
  if (!targetPlan)
2724
2841
  return null;
2842
+ options?.preCommitCheck?.();
2725
2843
  const swarmDir = path3.join(directory, ".swarm");
2726
2844
  const planPath = path3.join(swarmDir, "plan.json");
2727
2845
  const mdPath = path3.join(swarmDir, "plan.md");
2728
2846
  mkdirSync2(swarmDir, { recursive: true });
2729
2847
  const tempPlanPath = path3.join(swarmDir, `plan.json.rebuild.${Date.now()}.${Math.floor(Math.random() * 1e9)}`);
2730
- {
2731
- const fd = openSync(tempPlanPath, "w");
2732
- try {
2733
- writeFileSync(fd, JSON.stringify(targetPlan, null, 2), "utf8");
2734
- fsyncSync(fd);
2735
- } finally {
2736
- closeSync(fd);
2848
+ try {
2849
+ {
2850
+ const fd = openSync(tempPlanPath, "w");
2851
+ try {
2852
+ writeFileSync(fd, JSON.stringify(targetPlan, null, 2), "utf8");
2853
+ fsyncSync(fd);
2854
+ } finally {
2855
+ closeSync(fd);
2856
+ }
2737
2857
  }
2858
+ options?.preCommitCheck?.();
2859
+ renameSync2(tempPlanPath, planPath);
2860
+ invalidateCachedArtifact(planPath);
2861
+ } finally {
2862
+ try {
2863
+ unlinkSync(tempPlanPath);
2864
+ } catch {}
2738
2865
  }
2739
- renameSync2(tempPlanPath, planPath);
2740
- invalidateCachedArtifact(planPath);
2741
2866
  try {
2742
2867
  const markerPath = path3.join(swarmDir, ".plan-write-marker");
2743
2868
  const inProgressMarker = JSON.stringify({
@@ -2747,17 +2872,33 @@ async function rebuildPlan(directory, plan, options) {
2747
2872
  tasks_count: targetPlan.phases.reduce((sum, phase) => sum + phase.tasks.length, 0),
2748
2873
  in_progress: true
2749
2874
  });
2750
- await bunWrite(markerPath, inProgressMarker);
2751
- } catch {}
2875
+ await commitAsyncPreparedFile(markerPath, inProgressMarker, options?.preCommitCheck, "rebuild");
2876
+ } catch (error2) {
2877
+ if (error2 instanceof PlanRecoverySupersededError)
2878
+ throw error2;
2879
+ }
2880
+ let markdownWriteFailed = false;
2881
+ let markdownWriteError;
2882
+ let markerSupersededError;
2752
2883
  try {
2753
2884
  const contentHash = computePlanContentHash(targetPlan);
2754
2885
  const markdown = derivePlanMarkdown(targetPlan);
2755
2886
  const markdownWithHash = `<!-- PLAN_HASH: ${contentHash} -->
2756
2887
  ${markdown}`;
2757
2888
  const tempMdPath = path3.join(swarmDir, `plan.md.rebuild.${Date.now()}.${Math.floor(Math.random() * 1e9)}`);
2758
- await bunWrite(tempMdPath, markdownWithHash);
2759
- renameSync2(tempMdPath, mdPath);
2760
- invalidateCachedArtifact(mdPath);
2889
+ try {
2890
+ await _internals5.writeRebuildPlanMarkdown(tempMdPath, markdownWithHash);
2891
+ options?.preCommitCheck?.();
2892
+ renameSync2(tempMdPath, mdPath);
2893
+ invalidateCachedArtifact(mdPath);
2894
+ } finally {
2895
+ try {
2896
+ unlinkSync(tempMdPath);
2897
+ } catch {}
2898
+ }
2899
+ } catch (error2) {
2900
+ markdownWriteFailed = true;
2901
+ markdownWriteError = error2;
2761
2902
  } finally {
2762
2903
  try {
2763
2904
  const markerPath = path3.join(swarmDir, ".plan-write-marker");
@@ -2769,10 +2910,23 @@ ${markdown}`;
2769
2910
  tasks_count: tasksCount,
2770
2911
  in_progress: false
2771
2912
  });
2772
- await bunWrite(markerPath, marker);
2773
- } catch {}
2913
+ await commitAsyncPreparedFile(markerPath, marker, options?.preCommitCheck, "rebuild");
2914
+ } catch (error2) {
2915
+ if (error2 instanceof PlanRecoverySupersededError) {
2916
+ markerSupersededError = error2;
2917
+ }
2918
+ }
2919
+ }
2920
+ if (markerSupersededError)
2921
+ throw markerSupersededError;
2922
+ if (markdownWriteFailed) {
2923
+ if (markdownWriteError instanceof PlanRecoverySupersededError)
2924
+ throw markdownWriteError;
2925
+ const message = markdownWriteError instanceof Error ? markdownWriteError.message : String(markdownWriteError);
2926
+ warn(`[rebuildPlan] plan.md projection write failed (non-fatal; plan.json is authoritative): ${message}`.slice(0, 512));
2774
2927
  }
2775
2928
  try {
2929
+ options?.preCommitCheck?.();
2776
2930
  const planId = derivePlanId(targetPlan);
2777
2931
  const planHashAfter = computePlanLedgerHash(targetPlan);
2778
2932
  await appendLedgerEvent(directory, {
@@ -2784,8 +2938,11 @@ ${markdown}`;
2784
2938
  phases_count: targetPlan.phases.length,
2785
2939
  tasks_count: targetPlan.phases.reduce((sum, p) => sum + p.tasks.length, 0)
2786
2940
  }
2787
- }, { planHashAfter });
2788
- } catch {}
2941
+ }, { planHashAfter, preCommitCheck: options?.preCommitCheck });
2942
+ } catch (error2) {
2943
+ if (error2 instanceof PlanRecoverySupersededError)
2944
+ throw error2;
2945
+ }
2789
2946
  return targetPlan;
2790
2947
  }
2791
2948
  async function closePlanTerminalState(directory, plan, options) {
@@ -2940,7 +3097,7 @@ async function updateTaskStatus(directory, taskId, status, options) {
2940
3097
  if (status === "completed") {
2941
3098
  try {
2942
3099
  const completedTask = updatedPlan.phases.flatMap((phase) => phase.tasks).find((candidate) => candidate.id === taskId);
2943
- const { maybePropagateSpeckitCheckoff } = await import("./speckit-checkoff-vkqn317p.js");
3100
+ const { maybePropagateSpeckitCheckoff } = await import("./speckit-checkoff-j407j8ew.js");
2944
3101
  await maybePropagateSpeckitCheckoff(directory, {
2945
3102
  taskId,
2946
3103
  frRefs: completedTask?.fr_refs ?? [],
@@ -2965,7 +3122,7 @@ async function updateTaskStatus(directory, taskId, status, options) {
2965
3122
  break;
2966
3123
  }
2967
3124
  }
2968
- const { readDeclaredScopeFilesFromBindings } = await import("./scope-persistence-sc2zhr15.js");
3125
+ const { readDeclaredScopeFilesFromBindings } = await import("./scope-persistence-rmt56rma.js");
2969
3126
  const canonicalScope = readDeclaredScopeFilesFromBindings({
2970
3127
  directory,
2971
3128
  taskId,
@@ -4082,7 +4239,7 @@ function getDurableGateEvidenceStatus(evidence) {
4082
4239
  invalid: false
4083
4240
  };
4084
4241
  }
4085
- if (!Array.isArray(evidence.required_gates) || evidence.required_gates.length === 0) {
4242
+ if (!Array.isArray(evidence.required_gates)) {
4086
4243
  return {
4087
4244
  isComplete: false,
4088
4245
  missingGates: ["required_gates"],
@@ -4090,10 +4247,10 @@ function getDurableGateEvidenceStatus(evidence) {
4090
4247
  invalid: false
4091
4248
  };
4092
4249
  }
4093
- const missingGates = evidence.required_gates.filter((gate) => evidence.gates[gate] == null);
4250
+ const derivedGates = deriveApplicableGateSet(evidence);
4094
4251
  return {
4095
- isComplete: missingGates.length === 0,
4096
- missingGates,
4252
+ isComplete: derivedGates.missingGates.length === 0,
4253
+ missingGates: derivedGates.missingGates,
4097
4254
  evidenceExists: true,
4098
4255
  invalid: false
4099
4256
  };
@@ -4108,7 +4265,7 @@ async function getDurableGateEvidenceStatusForTask(directory, taskId) {
4108
4265
  };
4109
4266
  }
4110
4267
  try {
4111
- return getDurableGateEvidenceStatus(readTaskEvidenceRaw(directory, taskId));
4268
+ return getDurableGateEvidenceStatusWithCurrentScope(readTaskEvidenceRaw(directory, taskId), directory, taskId);
4112
4269
  } catch {
4113
4270
  return {
4114
4271
  isComplete: false,
@@ -4118,6 +4275,40 @@ async function getDurableGateEvidenceStatusForTask(directory, taskId) {
4118
4275
  };
4119
4276
  }
4120
4277
  }
4278
+ function getDurableGateEvidenceStatusWithCurrentScope(evidence, directory, taskId) {
4279
+ if (!evidence?.gates || typeof evidence.gates !== "object") {
4280
+ return getDurableGateEvidenceStatus(evidence);
4281
+ }
4282
+ if (!Array.isArray(evidence.required_gates)) {
4283
+ return getDurableGateEvidenceStatus(evidence);
4284
+ }
4285
+ if (!evidence.workflow || evidence.workflow.schema !== TASK_WORKFLOW_SCHEMA_MARKER) {
4286
+ if (evidence.required_gates.length === 0) {
4287
+ return {
4288
+ isComplete: false,
4289
+ missingGates: ["required_gates"],
4290
+ evidenceExists: true,
4291
+ invalid: false
4292
+ };
4293
+ }
4294
+ const missingGates = evidence.required_gates.filter((gate) => evidence.gates[gate] == null);
4295
+ return {
4296
+ isComplete: missingGates.length === 0,
4297
+ missingGates,
4298
+ evidenceExists: true,
4299
+ invalid: false
4300
+ };
4301
+ }
4302
+ const derivedGates = deriveApplicableGateSet(evidence, {
4303
+ currentDeclaredFiles: readCurrentTaskDeclaredFiles(directory, taskId)
4304
+ });
4305
+ return {
4306
+ isComplete: derivedGates.missingGates.length === 0,
4307
+ missingGates: derivedGates.missingGates,
4308
+ evidenceExists: true,
4309
+ invalid: false
4310
+ };
4311
+ }
4121
4312
  function gateEvidenceToEntry(taskId, gate, type, evidence) {
4122
4313
  const gateRecord = evidence.gates[gate];
4123
4314
  if (!gateRecord) {
@@ -4171,4 +4362,4 @@ function mergeDurableGateEntriesFromEvidence(taskId, entries, evidence) {
4171
4362
  return merged;
4172
4363
  }
4173
4364
 
4174
- export { buildTaskCompletionDescriptor, ensureTaskCheckpointReceipt, updateTaskCheckpointReceipt, MAX_TRANSIENT_RETRIES, isTransientSpawnError, transientBackoff, readLaneEnvFileFromDiskSync, getGitRepositoryStatus, isGitRepo, getDefaultBaseBranch, detectDefaultRemoteBranch, resetToRemoteBranch, resetToMainAfterMerge, sanitizeContextText, getRunMemorySummary, isStateUnreadable, loadEpicSessionState, isEpicModeActive, enableEpicMode, disableEpicMode, loadPlanJsonOnly, loadPlan, withPlanLifecycleLock, savePlan, closePlanTerminalState, updateTaskStatus, derivePlanMarkdown, getCurrentTaskId, isValidEvidenceType, isSecretscanEvidence, sanitizeTaskId2 as sanitizeTaskId, validateProjectRoot, saveEvidence, loadEvidence, listEvidenceTaskIds, checkRequirementCoverage, archiveEvidence, _internals7 as _internals, readDurableGateEvidence, getDurableGateEvidenceStatus, getDurableGateEvidenceStatusForTask, mergeDurableGateEntriesFromEvidence };
4365
+ export { buildTaskCompletionDescriptor, ensureTaskCheckpointReceipt, updateTaskCheckpointReceipt, MAX_TRANSIENT_RETRIES, isTransientSpawnError, transientBackoff, readLaneEnvFileFromDiskSync, getGitRepositoryStatus, isGitRepo, getDefaultBaseBranch, detectDefaultRemoteBranch, resetToRemoteBranch, resetToMainAfterMerge, sanitizeContextText, getRunMemorySummary, isStateUnreadable, loadEpicSessionState, isEpicModeActive, enableEpicMode, disableEpicMode, PlanRecoverySupersededError, loadPlanJsonOnly, loadPlan, withPlanLifecycleLock, savePlan, closePlanTerminalState, updateTaskStatus, derivePlanMarkdown, getCurrentTaskId, isValidEvidenceType, isSecretscanEvidence, sanitizeTaskId2 as sanitizeTaskId, validateProjectRoot, saveEvidence, loadEvidence, listEvidenceTaskIds, checkRequirementCoverage, archiveEvidence, _internals7 as _internals, readDurableGateEvidence, getDurableGateEvidenceStatusForTask, mergeDurableGateEntriesFromEvidence };