@ornexus/neocortex 4.60.72 → 4.60.73

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 (243) hide show
  1. package/dist/release-source.json +1 -1
  2. package/dist/sbom.cdx.json +24 -24
  3. package/install.ps1 +1 -1
  4. package/install.sh +1 -1
  5. package/package.json +2 -2
  6. package/packages/client/dist/adapters/adapter-registry.js +108 -1
  7. package/packages/client/dist/adapters/antigravity-adapter.js +77 -2
  8. package/packages/client/dist/adapters/claude-code-adapter.js +79 -3
  9. package/packages/client/dist/adapters/codex-adapter.js +80 -2
  10. package/packages/client/dist/adapters/cursor-adapter.js +115 -4
  11. package/packages/client/dist/adapters/gemini-adapter.js +71 -2
  12. package/packages/client/dist/adapters/grok-adapter.js +42 -2
  13. package/packages/client/dist/adapters/index.js +22 -1
  14. package/packages/client/dist/adapters/platform-detector.js +212 -1
  15. package/packages/client/dist/adapters/target-adapter.js +12 -0
  16. package/packages/client/dist/adapters/vscode-adapter.js +72 -2
  17. package/packages/client/dist/agent/refresh-stubs.js +417 -2
  18. package/packages/client/dist/agent/update-agent-yaml.js +102 -1
  19. package/packages/client/dist/agent/update-description.js +251 -1
  20. package/packages/client/dist/cache/crypto-utils.js +78 -1
  21. package/packages/client/dist/cache/encrypted-cache.js +108 -1
  22. package/packages/client/dist/cache/in-memory-asset-cache.js +73 -1
  23. package/packages/client/dist/cache/index.js +13 -1
  24. package/packages/client/dist/cache/protected-pi-boundary.js +44 -1
  25. package/packages/client/dist/checkpoint/checkpoint-client-reader.js +127 -2
  26. package/packages/client/dist/checkpoint/index.js +11 -1
  27. package/packages/client/dist/checkpoint/shared-checkpoint-types.js +32 -1
  28. package/packages/client/dist/cli.js +358 -5
  29. package/packages/client/dist/commands/activate.js +394 -8
  30. package/packages/client/dist/commands/architecture-answers.js +274 -7
  31. package/packages/client/dist/commands/architecture-memory-terminal.js +253 -4
  32. package/packages/client/dist/commands/architecture-non-ui-continuation.js +100 -1
  33. package/packages/client/dist/commands/architecture-root-binding.js +51 -1
  34. package/packages/client/dist/commands/cache-status.js +112 -2
  35. package/packages/client/dist/commands/config.js +88 -1
  36. package/packages/client/dist/commands/doctor.js +635 -2
  37. package/packages/client/dist/commands/invoke.js +7200 -143
  38. package/packages/client/dist/commands/legacy-epic-terminal.js +71 -1
  39. package/packages/client/dist/commands/legacy-story-branch.js +96 -1
  40. package/packages/client/dist/commands/legacy-story-pr.js +276 -1
  41. package/packages/client/dist/commands/mutation-qa-checkpoint.js +285 -5
  42. package/packages/client/dist/commands/mutation-qa-gate.js +54 -1
  43. package/packages/client/dist/commands/mutation-qa-git.js +41 -1
  44. package/packages/client/dist/commands/mutation-qa-policy.js +102 -1
  45. package/packages/client/dist/commands/mutation-qa-run.js +219 -3
  46. package/packages/client/dist/commands/mutation-qa-verdict.js +149 -1
  47. package/packages/client/dist/commands/mutation-qa.js +220 -1
  48. package/packages/client/dist/commands/native-agent.js +136 -2
  49. package/packages/client/dist/commands/planning-diagnosis.js +43 -1
  50. package/packages/client/dist/commands/planning-generation.js +53 -1
  51. package/packages/client/dist/commands/planning-memory.js +40 -1
  52. package/packages/client/dist/commands/planning-research.js +42 -1
  53. package/packages/client/dist/commands/private-session.js +75 -1
  54. package/packages/client/dist/commands/property-qa.js +139 -1
  55. package/packages/client/dist/commands/qa-control.js +16 -1
  56. package/packages/client/dist/commands/qa-scan-browser-launch.js +55 -1
  57. package/packages/client/dist/commands/qa-scan-browser-route.js +28 -1
  58. package/packages/client/dist/commands/qa-scan-capture.js +663 -5
  59. package/packages/client/dist/commands/qa-scan-discovery.js +376 -3
  60. package/packages/client/dist/commands/qa-scan-review.js +826 -5
  61. package/packages/client/dist/commands/qa-synthetic-backend-capture.js +266 -1
  62. package/packages/client/dist/commands/qa-synthetic-backend-review.js +297 -1
  63. package/packages/client/dist/commands/qa-synthetic-backend.js +340 -2
  64. package/packages/client/dist/commands/qa-synthetic-broker-port.js +85 -1
  65. package/packages/client/dist/commands/qa-synthetic-browser.js +39 -1
  66. package/packages/client/dist/commands/refactor-characterization.js +671 -21
  67. package/packages/client/dist/commands/refactor-commit.js +399 -13
  68. package/packages/client/dist/commands/refactor-control.js +142 -3
  69. package/packages/client/dist/commands/refactor-cpp-assertions.js +93 -3
  70. package/packages/client/dist/commands/refactor-review.js +283 -5
  71. package/packages/client/dist/commands/refactor-root.js +588 -7
  72. package/packages/client/dist/commands/refresh-memory.js +156 -3
  73. package/packages/client/dist/commands/shared-architecture-non-ui.js +24 -1
  74. package/packages/client/dist/commands/story-checkpoint.js +171 -1
  75. package/packages/client/dist/config/profile-directory.js +13 -1
  76. package/packages/client/dist/config/resolver-selection.js +278 -1
  77. package/packages/client/dist/config/secure-config.js +281 -12
  78. package/packages/client/dist/config.js +223 -2
  79. package/packages/client/dist/constants.js +25 -1
  80. package/packages/client/dist/context/context-collector.js +222 -2
  81. package/packages/client/dist/context/context-sanitizer.js +145 -1
  82. package/packages/client/dist/continuity/autonomous-governance.js +285 -1
  83. package/packages/client/dist/continuity/continuity-client-state-store.js +1587 -1
  84. package/packages/client/dist/continuity/invoke-hooks.js +129 -1
  85. package/packages/client/dist/continuity/migrations/001-initial-schema.js +12 -2
  86. package/packages/client/dist/continuity/migrations/002-processing-output-summary-indexes.js +12 -2
  87. package/packages/client/dist/continuity/migrations/003-headless-execution-attempts.js +12 -2
  88. package/packages/client/dist/continuity/migrations/004-outbox-delivery-state.js +12 -2
  89. package/packages/client/dist/continuity/migrations/005-bounded-headless-queue.js +12 -2
  90. package/packages/client/dist/continuity/migrations/006-native-agent-dispatches.js +10 -2
  91. package/packages/client/dist/continuity/native-agent-store.js +69 -1
  92. package/packages/client/dist/continuity/native-agent-types.js +90 -1
  93. package/packages/client/dist/continuity/outbox.js +117 -1
  94. package/packages/client/dist/continuity/runner-bootstrap-policy.js +172 -1
  95. package/packages/client/dist/continuity/sqlite-store.d.ts +22 -1
  96. package/packages/client/dist/continuity/sqlite-store.js +2612 -81
  97. package/packages/client/dist/effects/invoke-effect-gateways.js +62 -1
  98. package/packages/client/dist/errors/error-messages.js +228 -2
  99. package/packages/client/dist/errors/frontmatter-diagnostics.js +125 -1
  100. package/packages/client/dist/evidence/collector.js +347 -1
  101. package/packages/client/dist/evidence/index.js +1 -1
  102. package/packages/client/dist/evidence/types.js +2 -0
  103. package/packages/client/dist/fs/atomic-state-replace.js +152 -1
  104. package/packages/client/dist/generated/command-registry.generated.js +4076 -1
  105. package/packages/client/dist/generated/public-projections.generated.js +149 -1
  106. package/packages/client/dist/git/direct-yolo-c01-retry.js +138 -4
  107. package/packages/client/dist/git/direct-yolo-checkpoint.js +1234 -4
  108. package/packages/client/dist/git/epic-operational-state.d.ts +198 -0
  109. package/packages/client/dist/git/epic-operational-state.js +672 -0
  110. package/packages/client/dist/git/git-lifecycle-step-ids.js +14 -1
  111. package/packages/client/dist/git/git-lifecycle-store.d.ts +9 -1
  112. package/packages/client/dist/git/git-lifecycle-store.js +1981 -9
  113. package/packages/client/dist/git/legacy-story-branch-checkpoint.js +259 -9
  114. package/packages/client/dist/git/legacy-story-branch-setup.js +348 -4
  115. package/packages/client/dist/git/legacy-story-pr.js +3538 -70
  116. package/packages/client/dist/git/secure-filesystem-boundary.js +182 -5
  117. package/packages/client/dist/graph-retrieval/pre-command-hook.js +284 -1
  118. package/packages/client/dist/graph-retrieval/shared-graph-retrieval-contract.js +237 -1
  119. package/packages/client/dist/i18n/first-run.js +60 -2
  120. package/packages/client/dist/index.js +47 -1
  121. package/packages/client/dist/invocation/invoke-input.js +24 -1
  122. package/packages/client/dist/license/index.js +5 -1
  123. package/packages/client/dist/license/license-client.js +304 -1
  124. package/packages/client/dist/machine/fingerprint.js +163 -2
  125. package/packages/client/dist/machine/index.js +5 -1
  126. package/packages/client/dist/memory/project-memory-path-scope.js +62 -1
  127. package/packages/client/dist/memory/project-memory-writer.js +1007 -40
  128. package/packages/client/dist/memory/shared-project-memory-types.js +253 -2
  129. package/packages/client/dist/migrations/epic-lifecycle-backfill.d.ts +97 -0
  130. package/packages/client/dist/migrations/epic-lifecycle-backfill.js +1060 -0
  131. package/packages/client/dist/migrations/project-identity-capability.js +50 -1
  132. package/packages/client/dist/migrations/project-identity.js +493 -7
  133. package/packages/client/dist/migrations/state-json.d.ts +22 -0
  134. package/packages/client/dist/migrations/state-json.js +393 -4
  135. package/packages/client/dist/policy/architecture-policy.js +184 -2
  136. package/packages/client/dist/policy/index.js +7 -1
  137. package/packages/client/dist/policy/shared-policy-types.js +38 -0
  138. package/packages/client/dist/policy/subagent-approval-policy.js +55 -1
  139. package/packages/client/dist/private-execution/clean-baseline.js +279 -2
  140. package/packages/client/dist/private-execution/codex-executor.js +574 -4
  141. package/packages/client/dist/private-execution/codex-host-runtime.js +137 -1
  142. package/packages/client/dist/private-execution/codex-lifecycle-guard.js +97 -1
  143. package/packages/client/dist/private-execution/codex-scoped-permissions.js +128 -1
  144. package/packages/client/dist/private-execution/delivery.js +410 -13
  145. package/packages/client/dist/private-execution/diagnosis-root-review.js +294 -2
  146. package/packages/client/dist/private-execution/frozen-handoff.js +19 -6
  147. package/packages/client/dist/private-execution/implementation-validation.js +106 -2
  148. package/packages/client/dist/private-execution/isolated-admission.js +1432 -23
  149. package/packages/client/dist/private-execution/isolated-session.js +725 -5
  150. package/packages/client/dist/private-execution/isolated-workspace.js +122 -1
  151. package/packages/client/dist/private-execution/pact-consumer-runtime.js +409 -3
  152. package/packages/client/dist/private-execution/pact-test-source.js +258 -1
  153. package/packages/client/dist/private-execution/pact-toolchain.js +290 -3
  154. package/packages/client/dist/private-execution/pact-write-set.js +172 -1
  155. package/packages/client/dist/private-execution/planning-diagnosis-checkpoint.js +406 -5
  156. package/packages/client/dist/private-execution/planning-generation-checkpoint.js +462 -5
  157. package/packages/client/dist/private-execution/planning-memory-checkpoint.js +139 -3
  158. package/packages/client/dist/private-execution/planning-research-checkpoint.js +325 -4
  159. package/packages/client/dist/private-execution/private-report-store.js +42 -1
  160. package/packages/client/dist/private-execution/property-test-candidate.js +103 -1
  161. package/packages/client/dist/private-execution/property-test-choices.js +147 -3
  162. package/packages/client/dist/private-execution/property-test-plan.js +271 -7
  163. package/packages/client/dist/private-execution/property-test-review.js +258 -4
  164. package/packages/client/dist/private-execution/property-test-runtime.js +372 -9
  165. package/packages/client/dist/private-execution/property-test-source.js +139 -1
  166. package/packages/client/dist/private-execution/root-checkpoint.js +80 -1
  167. package/packages/client/dist/private-execution/root-memory-git-evidence.js +135 -3
  168. package/packages/client/dist/private-execution/root-memory-recovery.js +93 -1
  169. package/packages/client/dist/private-execution/root-memory-request.js +29 -1
  170. package/packages/client/dist/private-execution/root-memory-scope.js +178 -1
  171. package/packages/client/dist/private-execution/root-memory-writer.js +102 -1
  172. package/packages/client/dist/private-execution/session-lease.js +63 -1
  173. package/packages/client/dist/private-execution/shared-private-admission-types.js +50 -1
  174. package/packages/client/dist/private-execution/shared-project-validation.js +66 -1
  175. package/packages/client/dist/private-execution/shared-root-memory-types.js +61 -1
  176. package/packages/client/dist/private-execution/ux-source-validation.js +372 -1
  177. package/packages/client/dist/private-execution/ux-write-set.js +191 -2
  178. package/packages/client/dist/registry/registry-reconciliation.js +234 -1
  179. package/packages/client/dist/resilience/circuit-breaker.js +170 -1
  180. package/packages/client/dist/resilience/degradation-manager.js +164 -1
  181. package/packages/client/dist/resilience/freshness-indicator.js +100 -1
  182. package/packages/client/dist/resilience/index.js +8 -1
  183. package/packages/client/dist/resilience/recovery-detector.js +74 -1
  184. package/packages/client/dist/resolvers/asset-resolver.js +13 -0
  185. package/packages/client/dist/resolvers/local-resolver.js +255 -8
  186. package/packages/client/dist/resolvers/remote-resolver.js +233 -1
  187. package/packages/client/dist/runner/binary.js +404 -1
  188. package/packages/client/dist/runner/cli.js +1571 -17
  189. package/packages/client/dist/runner/headless/adapters.js +49 -1
  190. package/packages/client/dist/runner/headless/claude-code-adapter.js +79 -1
  191. package/packages/client/dist/runner/headless/continuous-story-workspace.js +1628 -11
  192. package/packages/client/dist/runner/headless/epic-git-reconciliation-inventory.js +69 -1
  193. package/packages/client/dist/runner/headless/epic-merge-candidate-qa.js +110 -2
  194. package/packages/client/dist/runner/headless/epic-terminal-git-cli-adapter.js +1236 -8
  195. package/packages/client/dist/runner/headless/epic-terminal-git-runner.js +318 -2
  196. package/packages/client/dist/runner/headless/fake-adapter.js +62 -1
  197. package/packages/client/dist/runner/headless/managed-profile.js +132 -1
  198. package/packages/client/dist/runner/headless/opencode-adapter.js +145 -2
  199. package/packages/client/dist/runner/headless/prepared-story-bundle.js +43 -1
  200. package/packages/client/dist/runner/headless/resolver.js +231 -1
  201. package/packages/client/dist/runner/headless/run-yolo.js +885 -1
  202. package/packages/client/dist/runner/headless/run-yoloop.js +2574 -2
  203. package/packages/client/dist/runner/headless/types.js +574 -2
  204. package/packages/client/dist/runner/local-console.js +396 -13
  205. package/packages/client/dist/runner/portal-sync-worker.js +1487 -1
  206. package/packages/client/dist/runner/project-identity.js +106 -1
  207. package/packages/client/dist/runner/scheduler.js +995 -7
  208. package/packages/client/dist/runner-cli.js +18 -2
  209. package/packages/client/dist/state/invocation-shape.js +13 -1
  210. package/packages/client/dist/state/project-state-snapshot.d.ts +8 -0
  211. package/packages/client/dist/state/project-state-snapshot.js +1731 -1
  212. package/packages/client/dist/state/state-json-repair.js +785 -3
  213. package/packages/client/dist/state/structural-dag-projection.js +118 -1
  214. package/packages/client/dist/telemetry/index.js +5 -1
  215. package/packages/client/dist/telemetry/offline-queue.js +131 -1
  216. package/packages/client/dist/tier/index.js +5 -1
  217. package/packages/client/dist/tier/tier-aware-client.js +405 -1
  218. package/packages/client/dist/types/index.js +38 -1
  219. package/packages/client/dist/version.js +27 -1
  220. package/packages/client/dist/yolo/change-evidence-collector.js +126 -1
  221. package/packages/client/dist/yolo/invoke-envelope-capability.js +62 -1
  222. package/packages/client/dist/yolo/spec-envelope-collector.js +184 -5
  223. package/packages/client/dist/yoloop/discovery-hook.js +300 -2
  224. package/packages/client/dist/yoloop/index.js +8 -1
  225. package/packages/client/dist/yoloop/invoke-hooks.js +669 -12
  226. package/packages/client/dist/yoloop/shared-discover-epics.js +1007 -1
  227. package/packages/client/dist/yoloop/shared-yoloop-types.d.ts +65 -1
  228. package/packages/client/dist/yoloop/shared-yoloop-types.js +33 -1
  229. package/packages/client/dist/yoloop/target-normalization.js +180 -1
  230. package/packages/client/dist/yoloop/yoloop-client-state-store.d.ts +8 -0
  231. package/packages/client/dist/yoloop/yoloop-client-state-store.js +1920 -1
  232. package/packages/shared/dist/yolo/spec-envelope.js +3 -6
  233. package/targets-stubs/antigravity/gemini.md +1 -1
  234. package/targets-stubs/antigravity/skill/SKILL.md +1 -1
  235. package/targets-stubs/claude-code/neocortex-root.agent.yaml +1 -1
  236. package/targets-stubs/claude-code/neocortex-root.md +2 -2
  237. package/targets-stubs/claude-code/neocortex.agent.yaml +1 -1
  238. package/targets-stubs/claude-code/neocortex.md +2 -2
  239. package/targets-stubs/codex/AGENTS.md +1 -1
  240. package/targets-stubs/cursor/agent.md +2 -2
  241. package/targets-stubs/gemini-cli/agent.md +2 -2
  242. package/targets-stubs/opencode/neocortex-root.md +1 -1
  243. package/targets-stubs/vscode/neocortex.agent.md +2 -2
@@ -1,70 +1,3538 @@
1
- import{execFileSync as Q}from"node:child_process";import{createHash as Qe,randomBytes as re}from"node:crypto";import{closeSync as gt,constants as Ye,existsSync as H,fstatSync as mt,lstatSync as W,mkdirSync as St,mkdtempSync as wt,openSync as bt,readFileSync as vt,realpathSync as D,rmdirSync as It,unlinkSync as At,writeFileSync as Nt}from"node:fs";import{tmpdir as $t}from"node:os";import{isAbsolute as Pe,join as A}from"node:path";import{parseDocument as ae}from"yaml";import{projectC12ScopedCiFollowUpDebt as _e,summarizeC12RemoteRollup as fe}from"../../../shared/dist/merge-readiness/c12-remote-observation.js";import{projectC12ScopedCiRegistryUpdate as Re}from"../../../shared/dist/merge-readiness/c12-registry-projection.js";import{resolveBranchUnit as qe}from"../../../shared/dist/yoloop/branch-unit.js";import{acquireGitLifecyclePhysicalIntegrationLock as se,assertGitLifecyclePrivateDirectory as je,assertGitLifecyclePrivateFile as P,ensureGitLifecyclePrivateChildDirectory as T,gitLifecycleRuntimeDirectory as G,releaseGitLifecyclePhysicalIntegrationLock as oe}from"./git-lifecycle-store.js";import{anchoredAtomicWriteFile as L,anchoredCompareAndSwapFile as Y}from"./secure-filesystem-boundary.js";import{GIT_LIFECYCLE_CREATE_PR_STEP_ID as ie,GIT_LIFECYCLE_MERGE_PR_STEP_ID as ye,GIT_LIFECYCLE_REVIEW_PR_STEP_ID as ne,GIT_LIFECYCLE_COMMIT_PUSH_STEP_ID as U,GIT_LIFECYCLE_SYNC_MAIN_STEP_ID as Z,GIT_LIFECYCLE_UPDATE_PROJECT_MEMORY_STEP_ID as ge}from"./git-lifecycle-step-ids.js";import{executeP205TransientAuthority as ze,projectP205TransientExecutionEvidence as Le}from"./git-lifecycle-store.js";import{readCompletedLegacyStoryBranchPlan as Ot}from"./legacy-story-branch-setup.js";function n(t){return{ok:!1,reasonCode:t}}function S(t){return t&&typeof t=="object"&&!Array.isArray(t)?t:null}function g(t,...r){return Q("git",r,{cwd:t,encoding:"utf8",timeout:15e3,stdio:["ignore","pipe","pipe"],maxBuffer:2*1024*1024}).trim()}function v(t,...r){return Q("git",r,{cwd:t,encoding:"utf8",timeout:15e3,stdio:["ignore","pipe","pipe"],maxBuffer:2*1024*1024})}function E(t,r){const a=bt(t,Ye.O_RDONLY|(Ye.O_NOFOLLOW??0));try{const e=mt(a);if(!e.isFile()||e.size>r)throw new Error("legacy-story-pr-file-unsafe");return vt(a,"utf8")}finally{gt(a)}}function z(t,r){const a=g(t,"ls-remote","--heads","origin",`refs/heads/${r}`).split(`
2
- `).filter(Boolean);if(a.length!==1)throw new Error("legacy-story-pr-remote-ref-missing");const e=a[0].match(/^([a-f0-9]{40})\s+refs\/heads\/(.+)$/);if(!e||e[2]!==r)throw new Error("legacy-story-pr-remote-ref-invalid");return e[1]}function X(t){const a=g(t,"config","--get","remote.origin.url").match(/^(?:https:\/\/github\.com\/|git@github\.com:)([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+?)(?:\.git)?$/);if(!a||a[1].includes("..")||a[2].includes(".."))throw new Error("legacy-story-pr-repository-invalid");return`${a[1]}/${a[2]}`}function me(t){const r=g(t,"rev-parse","--git-common-dir");return D(Pe(r)?r:A(t,r))}function Se(t,r){if(!Array.isArray(r)||r.length>=100)throw new Error("legacy-story-pr-list-incomplete");const a=r.filter(s=>s.headRefName===t.branch);if(a.length>1||a.some(s=>s.state!=="OPEN"||s.baseRefName!==t.baseBranch||s.headRefOid!==t.headSha))throw new Error("legacy-story-pr-provider-conflict");const e=a[0];if(!e)return null;if(!Number.isSafeInteger(e.number)||e.number<=0||e.url.toLowerCase()!==`https://github.com/${t.repository}/pull/${e.number}`.toLowerCase())throw new Error("legacy-story-pr-provider-invalid");return e}async function kt(t,r){const a=await T(await G(t),"legacy-story-pr-intents"),e=`${Qe("sha256").update(r.storyId).digest("hex")}.json`,s=A(a,e),o={schemaVersion:1,kind:"legacy-story-pr-create-attempt",storyId:r.storyId,epicId:r.epicId,repository:r.repository,branch:r.branch,baseBranch:r.baseBranch,headSha:r.headSha};if(H(s)){if(await P(s,a),E(s,4096)!==`${JSON.stringify(o)}
3
- `)throw new Error("legacy-story-pr-attempt-conflict");return!1}return await L(a,e,`${JSON.stringify(o)}
4
- `),await P(s,a),!0}function Ct(t){if(!/^[A-Za-z0-9][A-Za-z0-9_-]*\.[A-Za-z0-9._-]+$/.test(t.storyId))return n("legacy-story-commit-target-invalid");try{const r=D(t.projectRoot);if(D(g(r,"rev-parse","--show-toplevel"))!==r||v(r,"diff","--name-only","-z")!==""||v(r,"ls-files","--others","--exclude-standard","-z")!=="")return n("legacy-story-commit-checkout-invalid");const a=v(r,"diff","--cached","--name-only","-z").split("\0").filter(Boolean);if(a.length===0||new Set(a).size!==a.length||a.some(C=>C.length>1024||C.startsWith("/")||C.includes("\\")||C.includes("\0")||C.split("/").includes("..")||C===".git"||C.startsWith(".git/")))return n("legacy-story-commit-write-set-invalid");if(v(r,"ls-files","-u","-z")!==""||a.some(C=>{const V=v(r,"ls-files","-s","-z","--",C).split("\0").filter(Boolean);return V.length>1||V.some(te=>{const K=te.match(/^(100644|100755) [a-f0-9]{40} 0\t(.+)$/);return!K||K[2]!==C})}))return n("legacy-story-commit-index-unsafe");g(r,"diff","--cached","--check");const e=v(r,"show",":.neocortex/state.json"),s=`docs/stories/${t.storyId}.story.md`,o=v(r,"show",`:${s}`);if(E(A(r,".neocortex/state.json"),2*1024*1024)!==e||E(A(r,s),2*1024*1024)!==o)return n("legacy-story-commit-preimage-drift");const l=S(JSON.parse(e)),p=S(l?.stories),y=S(p?.[t.storyId]),i=y?.epic_id,d=typeof i=="string"?S(S(l?.epics)?.[i]):null,c=d?.stories;if(!y||y.id!==t.storyId||typeof i!="string"||!d||d.id!==i||d.branch_mode==="epic"||y.status!=="memory-updated"||y.last_step!==ge||!Array.isArray(y.steps_completed)||!y.steps_completed.includes(ge)||y.steps_completed.includes(U)||!Number.isSafeInteger(y.issue_number)||y.issue_number<=0||!Array.isArray(c)||c.length===0||new Set(c).size!==c.length||!c.includes(t.storyId)||c.some(C=>typeof C!="string"||S(p?.[C])?.epic_id!==i)||Object.entries(p??{}).some(([C,V])=>S(V)?.epic_id===i&&!c.includes(C)))return n("legacy-story-commit-state-invalid");const b=qe(d,c.map(C=>({...S(p?.[C]),id:C}))),w=`story/${i}/${t.storyId}`,f=S(l?.config)?.default_branch;if(b.mode!=="story"||y.branch_name!==w||g(r,"branch","--show-current")!==w||typeof f!="string"||f===w||g(r,"check-ref-format","--branch",f)!==f)return n("legacy-story-commit-branch-invalid");const m=S(JSON.parse(v(r,"show","HEAD:.neocortex/state.json"))),u=S(S(m?.stories)?.[t.storyId]),I=Array.isArray(u?.files_to_modify)?u.files_to_modify:null;if(!I||I.length===0||new Set(I).size!==I.length||u?.epic_id!==i||JSON.stringify(y.files_to_modify)!==JSON.stringify(I)||I.some(C=>typeof C!="string"||C.length>1024||C.startsWith("/")||C.includes("\\")||C.split("/").includes("..")||C===".git"||C.startsWith(".git/"))||!a.every(C=>I.includes(C))||!a.includes(".neocortex/state.json")||!a.includes(s)||!I.includes(".neocortex/state.json")||!I.includes(s))return n("legacy-story-commit-write-set-unauthorized");const N=o.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);if(!N)return n("legacy-story-commit-frontmatter-invalid");const O=ae(N[1],{strict:!0,uniqueKeys:!0}),_=S(O.toJS()),$=["stepsCompleted","steps_completed"].filter(C=>O.has(C)),k=["lastStep","last_step"].filter(C=>O.has(C)),j=["branchName","branch_name"].filter(C=>O.has(C)),B=["issueNumber","issue_number"].filter(C=>O.has(C));if(O.errors.length>0||O.get("id")!==t.storyId||(O.get("epic_id")??O.get("epic"))!==i||O.get("status")!=="memory-updated"||O.has("currentStatus")&&O.get("currentStatus")!=="memory-updated"||$.length!==1||k.length!==1||j.length!==1||B.length!==1||JSON.stringify(_?.[$[0]])!==JSON.stringify(y.steps_completed)||_?.[k[0]]!==ge||_?.[j[0]]!==w||_?.[B[0]]!==y.issue_number)return n("legacy-story-commit-frontmatter-invalid");const x=g(r,"rev-parse","HEAD"),J=g(r,"ls-remote","--heads","origin",`refs/heads/${w}`).split(`
5
- `).filter(Boolean),R=z(r,f);return!/^[a-f0-9]{40}$/.test(x)||J.length!==0||g(r,"rev-parse",`refs/remotes/origin/${f}`)!==R?n("legacy-story-commit-remote-drift"):{ok:!0,value:Object.freeze({schemaVersion:1,kind:"legacy-story-c08-commit-plan",storyId:t.storyId,epicId:i,repository:X(r),branch:w,baseBranch:f,headSha:x,remoteHeadSha:null,baseSha:R,indexDigestSha256:h(v(r,"ls-files","-s","-z")),stagedPatchSha256:h(v(r,"diff","--cached","--binary","--full-index")),changedPaths:Object.freeze(a),stateSha256:h(e),storySha256:h(o),stateIndexBlobSha:g(r,"rev-parse",":.neocortex/state.json"),storyIndexBlobSha:g(r,"rev-parse",`:${s}`),physicalCommit:!1,checkpointAccepted:!1,localOnly:!0})}}catch{return n("legacy-story-commit-physical-unavailable")}}function Ue(t,r){const a=v(t,"show",r.stateIndexBlobSha),e=v(t,"show",r.storyIndexBlobSha);if(g(t,"cat-file","-t",r.stateIndexBlobSha)!=="blob"||g(t,"cat-file","-t",r.storyIndexBlobSha)!=="blob"||h(a)!==r.stateSha256||h(e)!==r.storySha256)throw new Error("legacy-story-commit-preimage-invalid");const s=S(JSON.parse(a)),o=S(S(s?.stories)?.[r.storyId]);if(!o||o.epic_id!==r.epicId||o.status!=="memory-updated"||o.last_step!==ge||!Array.isArray(o.steps_completed)||!o.steps_completed.includes(ge)||o.steps_completed.includes(U))throw new Error("legacy-story-commit-projection-state-invalid");const l=o.steps_completed;o.status="committed",o.last_step=U,o.steps_completed=[...l,U];const p=`${JSON.stringify(s,null,2)}
6
- `,y=e.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);if(!y)throw new Error("legacy-story-commit-projection-frontmatter-invalid");const i=ae(y[1],{strict:!0,uniqueKeys:!0}),d=S(i.toJS()),c=["stepsCompleted","steps_completed"].filter(f=>i.has(f)),b=["lastStep","last_step"].filter(f=>i.has(f));if(i.errors.length>0||i.get("id")!==r.storyId||(i.get("epic_id")??i.get("epic"))!==r.epicId||i.get("status")!=="memory-updated"||c.length!==1||b.length!==1||JSON.stringify(d?.[c[0]])!==JSON.stringify(l)||d?.[b[0]]!==ge)throw new Error("legacy-story-commit-projection-frontmatter-invalid");i.set("status","committed"),i.has("currentStatus")&&i.set("currentStatus","committed"),i.set(c[0],o.steps_completed),i.set(b[0],U);const w=`---
7
- ${i.toString({lineWidth:0})}---
8
- ${e.slice(y[0].length)}`;return{stateBefore:a,storyBefore:e,stateAfter:p,storyAfter:w}}async function Xe(t,r,a){if(a)return a();const e=await Ot({projectRoot:t,storyId:r.storyId});if(!e.ok||e.value.epicId!==r.epicId||e.value.branch!==r.branch||e.value.baseBranch!==r.baseBranch||e.value.worktreePath!==t)return!1;try{g(t,"merge-base","--is-ancestor",e.value.baseHeadSha,r.headSha)}catch{return!1}const s=await T(await G(t),"legacy-story-branch-intents"),o=A(s,`${h(r.storyId)}.c01.accepted.json`);if(!H(o))return!1;await P(o,s);const l=S(JSON.parse(E(o,4096))),p=S(JSON.parse(v(t,"show",r.stateIndexBlobSha))),y=S(S(p?.stories)?.[r.storyId]);return!!l&&l.schemaVersion===1&&l.kind==="legacy-story-c01-acceptance-intent"&&l.storyId===r.storyId&&l.planDigest===h(JSON.stringify(e.value))&&l.repository===r.repository&&l.issueNumber===y?.issue_number}function Ve(t,r){return r.changedPaths.filter(a=>a!==".neocortex/state.json"&&a!==`docs/stories/${r.storyId}.story.md`).map(a=>{const e=v(t,"ls-files","-s","-z","--",a).split("\0").filter(Boolean);if(e.length>1)throw new Error("legacy-story-commit-index-invalid");return{path:a,entry:e[0]??null}})}async function er(t){if(!t.authorize)return n("legacy-story-commit-authorization-required");if(!/^[A-Za-z0-9][A-Za-z0-9_-]*\.[A-Za-z0-9._-]+$/.test(t.storyId))return n("legacy-story-commit-target-invalid");const r=`legacy-c08-${re(16).toString("hex")}`,a=await se(t.projectRoot,{kind:"direct",storyId:t.storyId,sessionId:r,idempotencyKey:r,expiresAt:new Date(Date.now()+60*6e4).toISOString(),persistent:!1});if(!a.ok)return n(a.reasonCode);try{const e=D(t.projectRoot);if(D(g(e,"rev-parse","--show-toplevel"))!==e)return n("legacy-story-commit-checkout-invalid");const s=await T(await G(e),"legacy-story-pr-intents"),o=h(t.storyId),l=`${o}.c08.commit.json`,p=`${o}.c08.commit.accepted.json`;let y=await $e(s,l,64*1024);if(!y){const $=Ct(t);if(!$.ok)return $;const k=$.value;if(!await Xe(e,k,t.verifyC01))return n("legacy-story-commit-c01-unverified");const j=Ue(e,k),B={schemaVersion:1,kind:"legacy-story-c08-commit-intent",plan:k,stateAfterSha256:h(j.stateAfter),storyAfterSha256:h(j.storyAfter),otherIndex:Ve(e,k)};await L(s,l,`${JSON.stringify(B)}
9
- `),await P(A(s,l),s),y=B}const i=S(y.plan);if(y.schemaVersion!==1||y.kind!=="legacy-story-c08-commit-intent"||!i||i.schemaVersion!==1||i.kind!=="legacy-story-c08-commit-plan"||i.storyId!==t.storyId||typeof i.epicId!="string"||i.branch!==`story/${i.epicId}/${t.storyId}`||typeof i.repository!="string"||typeof i.baseBranch!="string"||typeof i.headSha!="string"||!/^[a-f0-9]{40}$/.test(i.headSha)||typeof i.stateIndexBlobSha!="string"||!/^[a-f0-9]{40}$/.test(i.stateIndexBlobSha)||typeof i.storyIndexBlobSha!="string"||!/^[a-f0-9]{40}$/.test(i.storyIndexBlobSha)||!Array.isArray(i.changedPaths)||!i.changedPaths.includes(".neocortex/state.json")||!i.changedPaths.includes(`docs/stories/${t.storyId}.story.md`)||i.remoteHeadSha!==null||i.physicalCommit!==!1||i.checkpointAccepted!==!1||typeof y.stateAfterSha256!="string"||!/^[a-f0-9]{64}$/.test(y.stateAfterSha256)||typeof y.storyAfterSha256!="string"||!/^[a-f0-9]{64}$/.test(y.storyAfterSha256)||!Array.isArray(y.otherIndex)||y.otherIndex.length!==i.changedPaths.length-2||y.otherIndex.some($=>{const k=S($);return!k||typeof k.path!="string"||!i.changedPaths.includes(k.path)||k.path===".neocortex/state.json"||k.path===`docs/stories/${t.storyId}.story.md`||k.entry!==null&&(typeof k.entry!="string"||!/^(100644|100755) [a-f0-9]{40} 0\t/.test(k.entry)||!k.entry.endsWith(` ${k.path}`))})||!/^[a-f0-9]{64}$/.test(String(i.indexDigestSha256))||!/^[a-f0-9]{64}$/.test(String(i.stagedPatchSha256))||!/^[a-f0-9]{64}$/.test(String(i.stateSha256))||!/^[a-f0-9]{64}$/.test(String(i.storySha256))||new Set(i.changedPaths).size!==i.changedPaths.length||i.changedPaths.some($=>typeof $!="string"||$.length>1024||$.startsWith("/")||$.includes("\\")||$.split("/").includes("..")||$===".git"||$.startsWith(".git/"))||X(e)!==i.repository||g(e,"branch","--show-current")!==i.branch)return n("legacy-story-commit-intent-invalid");const d=i;if(!await Xe(e,d,t.verifyC01))return n("legacy-story-commit-c01-unverified");const c=Ue(e,d);if(h(c.stateAfter)!==y.stateAfterSha256||h(c.storyAfter)!==y.storyAfterSha256||JSON.stringify(Ve(e,d))!==JSON.stringify(y.otherIndex))return n("legacy-story-commit-intent-drift");const b=h(JSON.stringify(y)),f=await $e(s,p,4096);if(f&&(f.schemaVersion!==1||f.kind!=="legacy-story-c08-commit-accepted"||f.intentSha256!==b||!/^[a-f0-9]{40}$/.test(f.checkpointHeadSha)||!/^[a-f0-9]{64}$/.test(f.qaSha256)))return n("legacy-story-commit-accepted-invalid");let m=g(e,"rev-parse","HEAD");if(m===d.headSha){if(f)return n("legacy-story-commit-accepted-physical-drift");const $=v(e,"diff","--cached","--name-only","-z").split("\0").filter(Boolean),k=v(e,"diff","--name-only","-z").split("\0").filter(Boolean);if(JSON.stringify($)!==JSON.stringify(d.changedPaths)||k.some(R=>R!==".neocortex/state.json"&&R!==`docs/stories/${d.storyId}.story.md`)||v(e,"ls-files","--others","--exclude-standard","-z")!==""||g(e,"ls-remote","--heads","origin",`refs/heads/${d.branch}`)!==""||z(e,d.baseBranch)!==d.baseSha)return n("legacy-story-commit-precommit-drift");const j=E(A(e,".neocortex/state.json"),2*1024*1024),B=E(A(e,`docs/stories/${d.storyId}.story.md`),2*1024*1024),x=v(e,"show",":.neocortex/state.json"),J=v(e,"show",`:docs/stories/${d.storyId}.story.md`);if(![c.stateBefore,c.stateAfter].includes(j)||![c.storyBefore,c.storyAfter].includes(B)||![c.stateBefore,c.stateAfter].includes(x)||![c.storyBefore,c.storyAfter].includes(J)||x===c.stateAfter&&j!==c.stateAfter||J===c.storyAfter&&B!==c.storyAfter)return n("legacy-story-commit-preimage-drift");if(j===c.stateBefore&&(await Y(A(e,".neocortex"),"state.json",c.stateAfter,h(c.stateBefore)),await t.afterStateWrite?.()),B===c.storyBefore&&await Y(A(e,"docs/stories"),`${d.storyId}.story.md`,c.storyAfter,h(c.storyBefore)),E(A(e,".neocortex/state.json"),2*1024*1024)!==c.stateAfter||E(A(e,`docs/stories/${d.storyId}.story.md`),2*1024*1024)!==c.storyAfter)return n("legacy-story-commit-projection-drift");if(g(e,"add","--",".neocortex/state.json",`docs/stories/${d.storyId}.story.md`),g(e,"diff","--cached","--check"),v(e,"diff","--name-only","-z")!==""||v(e,"ls-files","--others","--exclude-standard","-z")!==""||JSON.stringify(Ve(e,d))!==JSON.stringify(y.otherIndex)||h(v(e,"show",":.neocortex/state.json"))!==y.stateAfterSha256||h(v(e,"show",`:docs/stories/${d.storyId}.story.md`))!==y.storyAfterSha256)return n("legacy-story-commit-index-drift");g(e,"commit","-m",`chore(story): checkpoint ${d.storyId} C08`,"-m",`Neocortex-Legacy-Commit: ${d.storyId}`),m=g(e,"rev-parse","HEAD"),await t.afterCommit?.()}const u=g(e,"rev-list","--parents","-n","1",m).split(" "),I=v(e,"diff-tree","--no-commit-id","--name-only","-r","-z",m).split("\0").filter(Boolean);if(JSON.stringify(u)!==JSON.stringify([m,d.headSha])||JSON.stringify(I)!==JSON.stringify(d.changedPaths)||y.otherIndex.some(({path:$,entry:k})=>{const j=v(e,"ls-tree","-z",m,"--",$).split("\0").filter(Boolean);return k===null?j.length!==0:j.length!==1||j[0]!==k.replace(/^(100644|100755) ([a-f0-9]{40}) 0\t/,"$1 blob $2 ")})||h(v(e,"show",`${m}:.neocortex/state.json`))!==y.stateAfterSha256||h(v(e,"show",`${m}:docs/stories/${d.storyId}.story.md`))!==y.storyAfterSha256||!v(e,"log","-1","--format=%B",m).split(`
10
- `).map($=>$.trim()).includes(`Neocortex-Legacy-Commit: ${d.storyId}`)||v(e,"status","--porcelain=v1","-uall")!=="")return n("legacy-story-commit-checkpoint-invalid");const N=g(e,"ls-remote","--heads","origin",`refs/heads/${d.branch}`);if(N!==""&&N!==`${m} refs/heads/${d.branch}`)return n("legacy-story-commit-remote-drift");let O=f?.qaSha256;if(!O){try{O=t.verifyQa?await t.verifyQa(m):await(async()=>{const $=await ze(e,{headSha:m,graphRevision:h(`legacy-story-c08\0${b}`)}),k=Le($);if(!k||k.executionHeadSha!==m)throw new Error("qa-invalid");return k.executionEvidenceDigest})()}catch{return n("legacy-story-commit-qa-failed")}if(!/^[a-f0-9]{64}$/.test(O))return n("legacy-story-commit-qa-failed")}if(g(e,"rev-parse","HEAD")!==m||v(e,"status","--porcelain=v1","-uall")!=="")return n("legacy-story-commit-final-drift");if(N===""&&g(e,"push","--porcelain","origin",`${m}:refs/heads/${d.branch}`),g(e,"ls-remote","--heads","origin",`refs/heads/${d.branch}`)!==`${m} refs/heads/${d.branch}`)return n("legacy-story-commit-push-unverified");await t.afterPush?.();const _={schemaVersion:1,kind:"legacy-story-c08-commit-accepted",intentSha256:b,checkpointHeadSha:m,qaSha256:O};return f&&JSON.stringify(f)!==JSON.stringify(_)?n("legacy-story-commit-accepted-conflict"):(f||(await L(s,p,`${JSON.stringify(_)}
11
- `),await P(A(s,p),s)),{ok:!0,value:Object.freeze({schemaVersion:1,kind:"legacy-story-c08-commit-accepted",storyId:d.storyId,epicId:d.epicId,branch:d.branch,checkpointHeadSha:m,qaSha256:O,reused:!!f,physicalCommit:!0,checkpointAccepted:!0,publicSafe:!0})})}catch(e){const s=e instanceof Error?e.message:"";return n(s.startsWith("legacy-story-commit-")?s:"legacy-story-commit-effect-uncertain")}finally{await oe(a.path,a.owner).catch(()=>{})}}function _t(t){if(!/^[A-Za-z0-9][A-Za-z0-9_-]*\.[A-Za-z0-9._-]+$/.test(t.storyId))return n("legacy-story-sync-target-invalid");try{const r=D(t.projectRoot);if(D(g(r,"rev-parse","--show-toplevel"))!==r||v(r,"status","--porcelain=v1","-uall")!=="")return n("legacy-story-sync-checkout-invalid");const a=v(r,"show","HEAD:.neocortex/state.json"),e=`docs/stories/${t.storyId}.story.md`,s=v(r,"show",`HEAD:${e}`);if(E(A(r,".neocortex/state.json"),2*1024*1024)!==a||E(A(r,e),2*1024*1024)!==s)return n("legacy-story-sync-committed-content-drift");const o=S(JSON.parse(a)),l=S(o?.stories),p=S(o?.epics),y=S(l?.[t.storyId]),i=y?.epic_id,d=typeof i=="string"?S(p?.[i]):null;if(!y||y.id!==t.storyId||typeof i!="string"||!d||d.id!==i||d.branch_mode==="epic"||y.status!=="committed"||y.last_step!==U||!Array.isArray(y.steps_completed)||!y.steps_completed.includes(U)||y.steps_completed.includes(Z)||!Number.isSafeInteger(y.issue_number)||y.issue_number<=0)return n("legacy-story-sync-state-invalid");const c=Array.isArray(d.stories)?d.stories:null;if(!c||c.length===0||new Set(c).size!==c.length||!c.includes(t.storyId)||c.some(x=>typeof x!="string"||S(l?.[x])?.epic_id!==i)||Object.entries(l??{}).some(([x,J])=>S(J)?.epic_id===i&&!c.includes(x)))return n("legacy-story-sync-topology-invalid");const b=qe(d,c.map(x=>({...S(l?.[x]),id:x}))),w=`story/${i}/${t.storyId}`,m=S(o?.config)?.default_branch;if(b.mode!=="story"||y.branch_name!==w||g(r,"branch","--show-current")!==w||typeof m!="string"||m===w||g(r,"check-ref-format","--branch",m)!==m)return n("legacy-story-sync-branch-invalid");const u=s.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);if(!u)return n("legacy-story-sync-frontmatter-invalid");const I=ae(u[1],{strict:!0,uniqueKeys:!0}),N=S(I.toJS()),O=["stepsCompleted","steps_completed"].filter(x=>I.has(x)),_=["lastStep","last_step"].filter(x=>I.has(x)),$=["branchName","branch_name"].filter(x=>I.has(x));if(I.errors.length>0||I.get("id")!==t.storyId||(I.get("epic_id")??I.get("epic"))!==i||I.get("status")!=="committed"||I.has("currentStatus")&&I.get("currentStatus")!=="committed"||O.length>1||_.length>1||$.length>1||O.length===1&&JSON.stringify(N?.[O[0]])!==JSON.stringify(y.steps_completed)||_.length===1&&N?.[_[0]]!==U||$.length===1&&N?.[$[0]]!==w)return n("legacy-story-sync-frontmatter-invalid");const k=g(r,"rev-parse","HEAD"),j=z(r,m);if(!/^[a-f0-9]{40}$/.test(k)||z(r,w)!==k||g(r,"rev-parse",`refs/remotes/origin/${m}`)!==j)return n("legacy-story-sync-remote-drift");const B=g(r,"merge-base",k,j);return/^[a-f0-9]{40}$/.test(B)?{ok:!0,value:Object.freeze({schemaVersion:1,kind:"legacy-story-c09-sync-plan",storyId:t.storyId,epicId:i,repository:X(r),branch:w,baseBranch:m,headSha:k,baseSha:j,mergeBaseSha:B,stateSha256:h(a),storySha256:h(s),baseAlreadyAncestor:B===j,physicalSync:!1,checkpointAccepted:!1,localOnly:!0})}:n("legacy-story-sync-ancestry-invalid")}catch{return n("legacy-story-sync-physical-unavailable")}}function jt(t,r){const a=E(A(t,".neocortex/state.json"),2097152),e=`docs/stories/${r.storyId}.story.md`,s=E(A(t,e),2*1024*1024),o=v(t,"show",`${r.headSha}:.neocortex/state.json`),l=v(t,"show",`${r.headSha}:${e}`);if(h(o)!==r.stateSha256||h(l)!==r.storySha256||s!==l)throw new Error("legacy-story-sync-preimage-drift");const p=S(JSON.parse(a)),y=S(JSON.parse(o)),i=S(p?.stories),d=S(S(y?.stories)?.[r.storyId]),c=S(i?.[r.storyId]),b=S(S(p?.epics)?.[r.epicId]),w=b?.stories;if(!c||!d||JSON.stringify(c)!==JSON.stringify(d)||c.status!=="committed"||c.last_step!==U||c.branch_name!==r.branch||!Array.isArray(c.steps_completed)||!c.steps_completed.includes(U)||c.steps_completed.includes(Z)||!b||b.id!==r.epicId||b.branch_mode==="epic"||!Array.isArray(w)||!w.includes(r.storyId)||new Set(w).size!==w.length||w.some($=>typeof $!="string"||S(i?.[$])?.epic_id!==r.epicId)||S(p?.config)?.default_branch!==r.baseBranch)throw new Error("legacy-story-sync-merged-state-invalid");c.status="synced",c.last_step=Z,c.steps_completed=[...c.steps_completed,Z],c.sync_base_sha=r.baseSha;const f=`${JSON.stringify(p,null,2)}
12
- `,m=s.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);if(!m)throw new Error("legacy-story-sync-frontmatter-invalid");const u=ae(m[1],{strict:!0,uniqueKeys:!0}),I=S(u.toJS()),N=["stepsCompleted","steps_completed"].filter($=>u.has($)),O=["lastStep","last_step"].filter($=>u.has($));if(u.errors.length>0||u.get("id")!==r.storyId||(u.get("epic_id")??u.get("epic"))!==r.epicId||u.get("status")!=="committed"||N.length!==1||O.length!==1||JSON.stringify(I?.[N[0]])!==JSON.stringify(d.steps_completed)||I?.[O[0]]!==U)throw new Error("legacy-story-sync-frontmatter-invalid");u.set("status","synced"),u.has("currentStatus")&&u.set("currentStatus","synced"),u.set(N[0],[...d.steps_completed,Z]),u.set(O[0],Z),u.set("syncBaseSha",r.baseSha);const _=`---
13
- ${u.toString({lineWidth:0})}---
14
- ${s.slice(m[0].length)}`;return{stateBefore:a,stateAfter:f,storyBefore:s,storyAfter:_}}async function $e(t,r,a){const e=A(t,r);if(!H(e))return null;await P(e,t);const s=S(JSON.parse(E(e,a)));if(!s)throw new Error("legacy-story-sync-journal-invalid");return s}async function tr(t){if(!t.authorize)return n("legacy-story-sync-authorization-required");if(!/^[A-Za-z0-9][A-Za-z0-9_-]*\.[A-Za-z0-9._-]+$/.test(t.storyId))return n("legacy-story-sync-target-invalid");const r=`legacy-c09-sync-${re(16).toString("hex")}`,a=await se(t.projectRoot,{kind:"direct",storyId:t.storyId,sessionId:r,idempotencyKey:r,expiresAt:new Date(Date.now()+60*6e4).toISOString(),persistent:!1});if(!a.ok)return n(a.reasonCode);try{const e=D(t.projectRoot);if(D(g(e,"rev-parse","--show-toplevel"))!==e)return n("legacy-story-sync-checkout-invalid");const s=await T(await G(e),"legacy-story-pr-intents"),o=h(t.storyId),l=`${o}.c09.sync.json`,p=`${o}.c09.sync.prepared.json`,y=`${o}.c09.sync.accepted.json`;let i=await $e(s,l,8192);if(!i){const C=_t(t);if(!C.ok)return C;const V={schemaVersion:1,kind:"legacy-story-c09-sync-intent",plan:C.value};await L(s,l,`${JSON.stringify(V)}
15
- `),i=V}const d=S(i.plan);if(i.schemaVersion!==1||i.kind!=="legacy-story-c09-sync-intent"||!d||d.schemaVersion!==1||d.kind!=="legacy-story-c09-sync-plan"||d.storyId!==t.storyId||typeof d.epicId!="string"||d.branch!==`story/${d.epicId}/${t.storyId}`||typeof d.baseBranch!="string"||typeof d.repository!="string"||typeof d.headSha!="string"||!/^[a-f0-9]{40}$/.test(d.headSha)||typeof d.baseSha!="string"||!/^[a-f0-9]{40}$/.test(d.baseSha)||typeof d.stateSha256!="string"||!/^[a-f0-9]{64}$/.test(d.stateSha256)||typeof d.storySha256!="string"||!/^[a-f0-9]{64}$/.test(d.storySha256)||typeof d.baseAlreadyAncestor!="boolean"||d.physicalSync!==!1||d.checkpointAccepted!==!1||g(e,"branch","--show-current")!==d.branch||X(e)!==d.repository)return n("legacy-story-sync-intent-invalid");const c=d,b=h(JSON.stringify(i));let f=await $e(s,p,4096);if(f&&(f.schemaVersion!==1||f.kind!=="legacy-story-c09-sync-prepared"||f.intentSha256!==b||!/^[a-f0-9]{40}$/.test(f.treeSha)||!/^[a-f0-9]{64}$/.test(f.stateSha256)||!/^[a-f0-9]{64}$/.test(f.storySha256)))return n("legacy-story-sync-prepared-invalid");const u=await $e(s,y,4096);if(u&&(u.schemaVersion!==1||u.kind!=="legacy-story-c09-sync-accepted"||u.intentSha256!==b||!/^[a-f0-9]{40}$/.test(u.checkpointHeadSha)||!/^[a-f0-9]{64}$/.test(u.qaSha256)))return n("legacy-story-sync-accepted-invalid");const I=g(e,"rev-parse","HEAD");if(v(e,"status","--porcelain=v1","-uall")!==""||z(e,c.baseBranch)!==c.baseSha||h(v(e,"show",`${c.headSha}:.neocortex/state.json`))!==c.stateSha256||h(v(e,"show",`${c.headSha}:docs/stories/${c.storyId}.story.md`))!==c.storySha256)return n("legacy-story-sync-physical-drift");const N=A(s,`${o}.c09.sync.checkout`);if(H(N)||Q("git",["worktree","add","--detach",N,u?.checkpointHeadSha??c.headSha],{cwd:e,encoding:"utf8",timeout:6e4,stdio:["ignore","pipe","pipe"],maxBuffer:32*1024}),!W(N).isDirectory()||D(N)!==N||D(g(N,"rev-parse","--show-toplevel"))!==N||me(N)!==me(e)||g(N,"branch","--show-current")!=="")return n("legacy-story-sync-candidate-invalid");let O=g(N,"rev-parse","HEAD");if(O===c.headSha){if(f)return n("legacy-story-sync-prepared-uncommitted");if(I!==c.headSha||z(e,c.branch)!==c.headSha||v(N,"status","--porcelain=v1","-uall")!=="")return n("legacy-story-sync-preparation-drift");if(!c.baseAlreadyAncestor){try{g(N,"merge","--no-ff","--no-commit",c.baseSha)}catch{return n("legacy-story-sync-merge-conflict")}if(v(N,"ls-files","-u")!=="")return n("legacy-story-sync-merge-conflict")}const C=jt(N,c);await Y(A(N,".neocortex"),"state.json",C.stateAfter,h(C.stateBefore)),await Y(A(N,"docs/stories"),`${c.storyId}.story.md`,C.storyAfter,h(C.storyBefore)),g(N,"add","--",".neocortex/state.json",`docs/stories/${c.storyId}.story.md`),g(N,"diff","--cached","--check"),f={schemaVersion:1,kind:"legacy-story-c09-sync-prepared",intentSha256:b,treeSha:g(N,"write-tree"),stateSha256:h(C.stateAfter),storySha256:h(C.storyAfter)},await L(s,p,`${JSON.stringify(f)}
16
- `),await P(A(s,p),s),g(N,"commit","-m",`chore(story): sync ${c.storyId} with ${c.baseBranch}`,"-m",`Neocortex-Legacy-Sync: ${c.storyId}`),O=g(N,"rev-parse","HEAD"),await t.afterCommit?.()}const _=g(N,"rev-list","--parents","-n","1",O).split(" "),$=c.baseAlreadyAncestor?[O,c.headSha]:[O,c.headSha,c.baseSha],k=S(JSON.parse(v(N,"show",`${O}:.neocortex/state.json`))),j=S(S(k?.stories)?.[c.storyId]),B=v(N,"show",`${O}:docs/stories/${c.storyId}.story.md`);if(!f||g(N,"rev-parse",`${O}^{tree}`)!==f.treeSha||h(v(N,"show",`${O}:.neocortex/state.json`))!==f.stateSha256||h(B)!==f.storySha256||JSON.stringify(_)!==JSON.stringify($)||!j||j.status!=="synced"||j.last_step!==Z||j.sync_base_sha!==c.baseSha||!Array.isArray(j.steps_completed)||!j.steps_completed.includes(Z)||!B.startsWith(`---
17
- `)||!v(N,"log","-1","--format=%B",O).split(`
18
- `).map(C=>C.trim()).includes(`Neocortex-Legacy-Sync: ${c.storyId}`)||v(N,"status","--porcelain=v1","-uall")!=="")return n("legacy-story-sync-checkpoint-invalid");if(u&&u.checkpointHeadSha!==O)return n("legacy-story-sync-accepted-conflict");const x=z(e,c.branch);if(![c.headSha,O].includes(I)||![c.headSha,O].includes(x)||u&&(I!==O||x!==O))return n("legacy-story-sync-remote-drift");let J=u?.qaSha256;if(!J){if(!t.verifyQa)try{et(N)}catch{return n("legacy-story-sync-dependencies-failed")}const C={schemaVersion:1,kind:"legacy-story-pr-plan",storyId:c.storyId,epicId:c.epicId,repository:c.repository,branch:c.branch,baseBranch:c.baseBranch,headSha:O,baseSha:c.baseSha,qaAuthority:"root-review-required",localOnly:!0};try{J=await de(C,N,t.verifyQa)}catch{return n("legacy-story-sync-qa-failed")}}if(g(N,"rev-parse","HEAD")!==O||v(N,"status","--porcelain=v1","-uall")!==""||g(e,"rev-parse","HEAD")!==I||v(e,"status","--porcelain=v1","-uall")!==""||z(e,c.baseBranch)!==c.baseSha)return n("legacy-story-sync-final-drift");if(x===c.headSha&&g(e,"push","--porcelain","origin",`${O}:refs/heads/${c.branch}`),z(e,c.branch)!==O)return n("legacy-story-sync-push-unverified");if(await t.afterPush?.(),g(e,"rev-parse","HEAD")===c.headSha&&g(e,"merge","--ff-only",O),g(e,"rev-parse","HEAD")!==O||v(e,"status","--porcelain=v1","-uall")!==""||E(A(e,".neocortex/state.json"),2*1024*1024)!==v(e,"show",`${O}:.neocortex/state.json`)||E(A(e,`docs/stories/${c.storyId}.story.md`),2*1024*1024)!==B)return n("legacy-story-sync-local-readback-invalid");const R={schemaVersion:1,kind:"legacy-story-c09-sync-accepted",intentSha256:b,checkpointHeadSha:O,qaSha256:J};return u&&JSON.stringify(u)!==JSON.stringify(R)?n("legacy-story-sync-accepted-conflict"):(u||(await L(s,y,`${JSON.stringify(R)}
19
- `),await P(A(s,y),s)),{ok:!0,value:Object.freeze({schemaVersion:1,kind:"legacy-story-c09-sync-accepted",storyId:c.storyId,epicId:c.epicId,branch:c.branch,baseSha:c.baseSha,checkpointHeadSha:O,qaSha256:J,reused:!!u,physicalSync:!0,checkpointAccepted:!0,publicSafe:!0})})}catch(e){const s=e instanceof Error?e.message:"";return n(s.startsWith("legacy-story-sync-")?s:"legacy-story-sync-effect-uncertain")}finally{await oe(a.path,a.owner).catch(()=>{})}}function we(t,r){try{const a=D(t.projectRoot);if(!/^[A-Za-z0-9][A-Za-z0-9_-]*\.[A-Za-z0-9._-]+$/.test(t.storyId)||D(g(a,"rev-parse","--show-toplevel"))!==a||!r&&g(a,"status","--porcelain=v1","-uall")!=="")return n("legacy-story-pr-checkout-invalid");const e=v(a,"show","HEAD:.neocortex/state.json");if(!r&&e!==E(A(a,".neocortex/state.json"),2*1024*1024))return n("legacy-story-pr-state-not-committed");const s=S(JSON.parse(e)),o=S(s?.stories),l=S(s?.epics),p=S(o?.[t.storyId]),y=p?.epic_id,i=typeof y=="string"?S(l?.[y]):null;if(!p||p.id!==t.storyId||typeof y!="string"||!i||i.id!==y||i.branch_mode==="epic"||p.status!=="synced"||p.last_step!==Z||!Array.isArray(p.steps_completed)||!p.steps_completed.includes(Z)||p.steps_completed.includes(ie))return n("legacy-story-pr-state-invalid");const d=Array.isArray(i.stories)?i.stories:null;if(!d||d.length===0||new Set(d).size!==d.length||!d.includes(t.storyId)||d.some(_=>typeof _!="string"||S(o?.[_])?.epic_id!==y)||Object.entries(o??{}).some(([_,$])=>S($)?.epic_id===y&&!d.includes(_)))return n("legacy-story-pr-topology-invalid");const c=qe(i,d.map(_=>({...S(o?.[_]),id:_}))),b=`story/${y}/${t.storyId}`,f=S(s?.config)?.default_branch;if(c.mode!=="story"||p.branch_name!==b||g(a,"branch","--show-current")!==b||typeof f!="string"||f===b||g(a,"check-ref-format","--branch",f)!==f)return n("legacy-story-pr-branch-invalid");const u=g(a,"show",`HEAD:docs/stories/${t.storyId}.story.md`).match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);if(!u)return n("legacy-story-pr-frontmatter-invalid");const I=ae(u[1],{strict:!0,uniqueKeys:!0});if(I.errors.length>0||I.get("id")!==t.storyId||(I.get("epic_id")??I.get("epic"))!==y||I.get("status")!=="synced")return n("legacy-story-pr-frontmatter-invalid");const N=g(a,"rev-parse","HEAD"),O=z(a,f);return!/^[a-f0-9]{40}$/.test(N)||z(a,b)!==N||g(a,"rev-parse",`refs/remotes/origin/${f}`)!==O?n("legacy-story-pr-remote-drift"):(g(a,"merge-base","--is-ancestor",O,N),{ok:!0,value:Object.freeze({schemaVersion:1,kind:"legacy-story-pr-plan",storyId:t.storyId,epicId:y,repository:X(a),branch:b,baseBranch:f,headSha:N,baseSha:O,qaAuthority:"root-review-required",localOnly:!0})})}catch{return n("legacy-story-pr-physical-unavailable")}}function be(t){return we(t,!1)}async function de(t,r,a){const e=a?await a(t):await(async()=>{const s=await ze(r,{headSha:t.headSha,graphRevision:h(`legacy-story-pr-c10\0${JSON.stringify(t)}`)}),o=Le(s);if(!o||o.executionHeadSha!==t.headSha)throw new Error("legacy-story-pr-checkpoint-qa-invalid");return o.executionEvidenceDigest})();if(!/^[a-f0-9]{64}$/.test(e))throw new Error("legacy-story-pr-checkpoint-qa-invalid");return e}function et(t){for(const r of[".","packages/server","portal","admin","landing"]){const a=A(t,r),e=A(a,"package.json"),s=A(a,"package-lock.json");if(!H(a)||!W(a).isDirectory()||D(a)!==a||!H(e)||!W(e).isFile()||!H(s)||!W(s).isFile())throw new Error("legacy-story-pr-candidate-dependencies-missing");try{Q("npm",["ci","--include=dev","--no-audit","--no-fund"],{cwd:a,encoding:"utf8",timeout:10*6e4,stdio:["ignore","pipe","pipe"],maxBuffer:1024*1024})}catch{throw new Error("legacy-story-pr-candidate-dependencies-failed")}}}const he={async list(t,r,a){const e=Q("gh",["pr","list","--repo",t,"--state","all","--head",r,"--json","number,state,headRefName,headRefOid,baseRefName,url","--limit","100"],{encoding:"utf8",timeout:15e3,stdio:["ignore","pipe","pipe"],maxBuffer:262144});return JSON.parse(e)},async create(t,r,a,e,s){Q("gh",["pr","create","--repo",t,"--head",r,"--base",a,"--title",e,"--body-file",s],{encoding:"utf8",timeout:3e4,stdio:["ignore","pipe","pipe"],maxBuffer:16*1024})}},F={async view(t,r){const a=Q("gh",["pr","view",String(r),"--repo",t,"--json","number,state,headRefName,headRefOid,baseRefName,baseRefOid,isDraft,mergeable,mergeCommit,statusCheckRollup,url"],{encoding:"utf8",timeout:15e3,stdio:["ignore","pipe","pipe"],maxBuffer:262144});return JSON.parse(a)},async merge(t,r,a){Q("gh",["api","--method","PUT",`repos/${t}/pulls/${r}/merge`,"-f",`sha=${a}`,"-f","merge_method=merge"],{encoding:"utf8",timeout:3e4,stdio:["ignore","pipe","pipe"],maxBuffer:16*1024})}};async function rr(t){if(!t.authorize)return n("legacy-story-pr-authorization-required");let r;try{if(W(t.bodyFile).isSymbolicLink())return n("legacy-story-pr-body-unsafe");const l=E(t.bodyFile,32*1024);if(!l.trim()||l.includes("\0"))return n("legacy-story-pr-body-unsafe");r=l}catch{return n("legacy-story-pr-body-unsafe")}const a=be(t);if(!a.ok)return a;try{await de(a.value,t.projectRoot,t.verifyQa)}catch{return n("legacy-story-pr-checkpoint-qa-failed")}const e=t.provider??he,s=`legacy-c10-${re(16).toString("hex")}`,o=await se(t.projectRoot,{kind:"direct",storyId:t.storyId,sessionId:s,idempotencyKey:s,expiresAt:new Date(Date.now()+5*6e4).toISOString(),persistent:!1});if(!o.ok)return n(o.reasonCode);try{const l=be(t);if(!l.ok||JSON.stringify(l.value)!==JSON.stringify(a.value))return n("legacy-story-pr-pre-effect-race");const p=await e.list(a.value.repository,a.value.branch,a.value.baseBranch);let y=Se(a.value,p);const i=y!==null;if(!y){const c=be(t);if(!c.ok||JSON.stringify(c.value)!==JSON.stringify(a.value))return n("legacy-story-pr-pre-effect-race");if(!await kt(t.projectRoot,a.value))return n("legacy-story-pr-create-attempt-unresolved");const b=wt(A($t(),"legacy-story-pr-body-")),w=A(b,"body.md");try{Nt(w,r,{flag:"wx",mode:384}),await e.create(a.value.repository,a.value.branch,a.value.baseBranch,`[YOLO] Story ${a.value.storyId}`,w)}finally{try{At(w)}catch{}It(b)}const f=be(t);if(!f.ok||JSON.stringify(f.value)!==JSON.stringify(a.value))return n("legacy-story-pr-post-effect-race");if(y=Se(a.value,await e.list(a.value.repository,a.value.branch,a.value.baseBranch)),!y)return n("legacy-story-pr-readback-unavailable")}const d=be(t);return!d.ok||JSON.stringify(d.value)!==JSON.stringify(a.value)?n("legacy-story-pr-post-effect-race"):{ok:!0,value:Object.freeze({schemaVersion:1,kind:"legacy-story-pr-provider-receipt",storyId:a.value.storyId,epicId:a.value.epicId,repository:a.value.repository,branch:a.value.branch,baseBranch:a.value.baseBranch,headSha:a.value.headSha,number:y.number,url:y.url,reused:i,checkpointAccepted:!1,publicSafe:!0})}}catch(l){const p=l instanceof Error?l.message:"";return n(p.startsWith("legacy-story-pr-")?p:"legacy-story-pr-provider-unavailable")}finally{await oe(o.path,o.owner).catch(()=>{})}}function h(t){return Qe("sha256").update(t).digest("hex")}function Me(t,r,a){const e=v(t,"show","HEAD:.neocortex/state.json"),s=S(JSON.parse(e)),o=S(S(s?.stories)?.[r.storyId]);if(!o||o.status!=="synced"||o.last_step!==Z||!Array.isArray(o.steps_completed)||!o.steps_completed.includes(Z)||o.steps_completed.includes(ie)||o.pr_number!=null)throw new Error("legacy-story-pr-checkpoint-state-conflict");const l=o.steps_completed;o.status="pr-created",o.last_step=ie,o.steps_completed=[...l,ie],o.pr_number=a;const p=`${JSON.stringify(s,null,2)}
20
- `,y=`docs/stories/${r.storyId}.story.md`,i=v(t,"show",`HEAD:${y}`),d=i.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);if(!d)throw new Error("legacy-story-pr-checkpoint-frontmatter-invalid");const c=ae(d[1],{strict:!0,uniqueKeys:!0}),b=S(c.toJS()),w=["stepsCompleted","steps_completed"].filter(N=>c.has(N)),f=["lastStep","last_step"].filter(N=>c.has(N)),m=["prNumber","pr_number"].filter(N=>c.has(N)),u=["branchName","branch_name"].filter(N=>c.has(N));if(c.errors.length>0||c.get("id")!==r.storyId||(c.get("epic_id")??c.get("epic"))!==r.epicId||c.get("status")!=="synced"||w.length>1||f.length>1||m.length>1||u.length>1||c.has("currentStatus")&&c.get("currentStatus")!=="synced"||w.length===1&&(!Array.isArray(b?.[w[0]])||JSON.stringify(b?.[w[0]])!==JSON.stringify(l))||f.length===1&&b?.[f[0]]!==Z||u.length===1&&b?.[u[0]]!==r.branch||m.length===1&&b?.[m[0]]!=null)throw new Error("legacy-story-pr-checkpoint-frontmatter-conflict");c.set("status","pr-created"),c.has("currentStatus")&&c.set("currentStatus","pr-created"),c.set(w[0]??"stepsCompleted",[...w.length===1?b[w[0]]:l,ie]),c.set(f[0]??"lastStep",ie),c.set(m[0]??"prNumber",a);const I=`---
21
- ${c.toString({lineWidth:0})}---
22
- ${i.slice(d[0].length)}`;return{stateBefore:e,stateAfter:p,storyBefore:i,storyAfter:I,storyPath:y}}function Oe(t,r,a){const e=we({projectRoot:t,storyId:r.storyId},!0);if(!e.ok||JSON.stringify(e.value)!==JSON.stringify(r))throw new Error("legacy-story-pr-checkpoint-physical-drift");const s=E(A(t,".neocortex/state.json"),2*1024*1024),o=E(A(t,a.storyPath),2*1024*1024);if(![a.stateBefore,a.stateAfter].includes(s)||![a.storyBefore,a.storyAfter].includes(o))throw new Error("legacy-story-pr-checkpoint-preimage-conflict");if(v(t,"status","--porcelain=v1","-uall").split(`
23
- `).filter(Boolean).some(p=>!p.startsWith(" M ")||![".neocortex/state.json",a.storyPath].includes(p.slice(3))))throw new Error("legacy-story-pr-checkpoint-worktree-dirty");return{stateCurrent:s,storyCurrent:o}}async function ve(t,r){const a=A(t,r);if(!H(a))return null;await P(a,t);const e=E(a,4096),s=S(JSON.parse(e));if(!s||s.schemaVersion!==1||s.kind!=="legacy-story-c10-acceptance-intent"||typeof s.qaDigest!="string"||!/^[a-f0-9]{64}$/.test(s.qaDigest))throw new Error("legacy-story-pr-checkpoint-journal-invalid");return s}async function tt(t,r,a){await L(t,r,`${JSON.stringify(a)}
24
- `),await P(A(t,r),t)}async function ar(t){if(!Number.isSafeInteger(t.prNumber)||t.prNumber<=0)return n("legacy-story-pr-checkpoint-number-invalid");const r=be(t);let a=null;if(r.ok)try{a=await de(r.value,t.projectRoot,t.verifyQa)}catch{return n("legacy-story-pr-checkpoint-qa-failed")}const e=`legacy-c10-accept-${re(16).toString("hex")}`,s=await se(t.projectRoot,{kind:"direct",storyId:t.storyId,sessionId:e,idempotencyKey:e,expiresAt:new Date(Date.now()+5*6e4).toISOString(),persistent:!1});if(!s.ok)return n(s.reasonCode);try{const o=D(t.projectRoot),l=we(t,!0);if(!l.ok||r.ok&&JSON.stringify(l.value)!==JSON.stringify(r.value))return n("legacy-story-pr-checkpoint-plan-drift");const p=l.value,y=Me(o,p,t.prNumber),i=Oe(o,p,y),d=await T(await G(o),"legacy-story-pr-intents"),c=h(p.storyId),b=`${c}.c10.json`,w=`${c}.c10.accepted.json`,f=await ve(d,b),m=await ve(d,w);if(!f&&(!r.ok||!a||i.stateCurrent!==y.stateBefore||i.storyCurrent!==y.storyBefore))return n("legacy-story-pr-checkpoint-unjournaled-drift");if(m&&(!f||JSON.stringify(m)!==JSON.stringify(f)))return n("legacy-story-pr-checkpoint-accepted-conflict");const u=Se(p,await(t.provider??he).list(p.repository,p.branch,p.baseBranch));if(!u||u.number!==t.prNumber)return n("legacy-story-pr-checkpoint-provider-mismatch");const I={schemaVersion:1,kind:"legacy-story-c10-acceptance-intent",storyId:p.storyId,planDigest:h(JSON.stringify(p)),repository:p.repository,number:u.number,url:u.url,qaDigest:f?.qaDigest??a,stateBefore:h(y.stateBefore),stateAfter:h(y.stateAfter),storyBefore:h(y.storyBefore),storyAfter:h(y.storyAfter)};if(f&&JSON.stringify(f)!==JSON.stringify(I))return n("legacy-story-pr-checkpoint-journal-conflict");f||await tt(d,b,I);const N=Oe(o,p,y);if(m&&(N.stateCurrent!==y.stateAfter||N.storyCurrent!==y.storyAfter))return n("legacy-story-pr-checkpoint-accepted-physical-mismatch");N.stateCurrent===y.stateBefore&&(await Y(A(o,".neocortex"),"state.json",y.stateAfter,I.stateBefore),await t.afterStatePersist?.()),Oe(o,p,y).storyCurrent===y.storyBefore&&(await Y(A(o,"docs/stories"),`${p.storyId}.story.md`,y.storyAfter,I.storyBefore),await t.afterStoryPersist?.());const _=Oe(o,p,y);return _.stateCurrent!==y.stateAfter||_.storyCurrent!==y.storyAfter?n("legacy-story-pr-checkpoint-post-write-mismatch"):(m||await tt(d,w,I),{ok:!0,value:Object.freeze({schemaVersion:1,kind:"legacy-story-c10-checkpoint",storyId:p.storyId,epicId:p.epicId,branch:p.branch,repository:p.repository,number:u.number,url:u.url,headSha:p.headSha,stateSha256:I.stateAfter,storySha256:I.storyAfter,reused:!!m,checkpointAccepted:!0,localOnly:!0})})}catch(o){const l=o instanceof Error?o.message:"";return n(l.startsWith("legacy-story-pr-")?l:l.includes("secure-filesystem-cas-conflict")?"legacy-story-pr-checkpoint-write-raced":"legacy-story-pr-checkpoint-failed")}finally{await oe(s.path,s.owner).catch(()=>{})}}async function sr(t){if(!Number.isSafeInteger(t.prNumber)||t.prNumber<=0)return n("legacy-story-pr-checkpoint-number-invalid");try{const r=D(t.projectRoot),a=we(t,!0);if(!a.ok)return a;const e=a.value,s=Me(r,e,t.prNumber),o=Oe(r,e,s);if(o.stateCurrent!==s.stateAfter||o.storyCurrent!==s.storyAfter)return n("legacy-story-pr-checkpoint-not-accepted");const l=await T(await G(r),"legacy-story-pr-intents"),p=h(e.storyId),y=await ve(l,`${p}.c10.json`),i=await ve(l,`${p}.c10.accepted.json`);if(!y||!i||JSON.stringify(y)!==JSON.stringify(i))return n("legacy-story-pr-checkpoint-not-accepted");const d=Se(e,await(t.provider??he).list(e.repository,e.branch,e.baseBranch));if(!d||d.number!==t.prNumber)return n("legacy-story-pr-checkpoint-provider-mismatch");const c={schemaVersion:1,kind:"legacy-story-c10-acceptance-intent",storyId:e.storyId,planDigest:h(JSON.stringify(e)),repository:e.repository,number:d.number,url:d.url,qaDigest:i.qaDigest,stateBefore:h(s.stateBefore),stateAfter:h(s.stateAfter),storyBefore:h(s.storyBefore),storyAfter:h(s.storyAfter)};return JSON.stringify(i)!==JSON.stringify(c)?n("legacy-story-pr-checkpoint-journal-conflict"):{ok:!0,value:Object.freeze({plan:e,receipt:Object.freeze({schemaVersion:1,kind:"legacy-story-c10-checkpoint",storyId:e.storyId,epicId:e.epicId,branch:e.branch,repository:e.repository,number:d.number,url:d.url,headSha:e.headSha,stateSha256:i.stateAfter,storySha256:i.storyAfter,reused:!0,checkpointAccepted:!0,localOnly:!0})})}}catch(r){const a=r instanceof Error?r.message:"";return n(a.startsWith("legacy-story-pr-")?a:"legacy-story-pr-checkpoint-read-failed")}}function Et(t,r){const a=v(t,"diff","--raw","-z",r.baseSha,r.headSha),e=v(t,"diff","--name-only","-z",r.baseSha,r.headSha).split("\0").filter(Boolean);if(e.length===0||e.length>256||e.some(s=>!rt(s)))throw new Error("legacy-story-pr-review-diff-invalid");return{diffSha256:h(a),changedPaths:Object.freeze(e)}}function rt(t){return t.length>0&&t.length<=240&&/^[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)*$/.test(t)&&t.split("/").every(r=>r!=="."&&r!=="..")}function ke(t,r,a,e){const s=S(JSON.parse(r.stateAfter)),o=S(S(s?.stories)?.[t.storyId]);if(!o||o.status!=="pr-created"||o.last_step!==ie||o.pr_number!==a||o.review_request!=null||!Array.isArray(o.steps_completed)||!o.steps_completed.includes(ie)||o.steps_completed.includes(ne))throw new Error("legacy-story-pr-review-state-conflict");o.status="pr-reviewed",o.last_step=ne,o.steps_completed=[...o.steps_completed,ne],o.review_request={schemaVersion:1,kind:"git-pull-request",gitPullRequest:!0,prNumber:a,headSha:t.headSha,reviewEvidenceDigest:e};const l=`${JSON.stringify(s,null,2)}
25
- `,p=r.storyAfter.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);if(!p)throw new Error("legacy-story-pr-review-frontmatter-invalid");const y=ae(p[1],{strict:!0,uniqueKeys:!0}),i=S(y.toJS()),d=["stepsCompleted","steps_completed"].filter(f=>y.has(f)),c=["lastStep","last_step"].filter(f=>y.has(f)),b=i?.[d[0]];if(y.errors.length>0||y.get("id")!==t.storyId||(y.get("epic_id")??y.get("epic"))!==t.epicId||y.get("status")!=="pr-created"||d.length!==1||c.length!==1||y.has("currentStatus")&&y.get("currentStatus")!=="pr-created"||i?.[c[0]]!==ie||!Array.isArray(b)||!b.includes(ie))throw new Error("legacy-story-pr-review-frontmatter-conflict");y.set("status","pr-reviewed"),y.has("currentStatus")&&y.set("currentStatus","pr-reviewed"),y.set(d[0],[...b,ne]),y.set(c[0],ne);const w=`---
26
- ${y.toString({lineWidth:0})}---
27
- ${r.storyAfter.slice(p[0].length)}`;return{stateBefore:r.stateAfter,stateAfter:l,storyBefore:r.storyAfter,storyAfter:w,storyPath:r.storyPath}}async function ue(t){const r=D(t.projectRoot),a=we(t,!0);if(!a.ok)throw new Error(a.reasonCode);const e=a.value;if(!Number.isSafeInteger(t.prNumber)||t.prNumber<=0)throw new Error("legacy-story-pr-review-number-invalid");const s=Me(r,e,t.prNumber),o=await T(await G(r),"legacy-story-pr-intents"),l=h(e.storyId),p=await ve(o,`${l}.c10.json`),y=await ve(o,`${l}.c10.accepted.json`);if(!p||!y||JSON.stringify(p)!==JSON.stringify(y))throw new Error("legacy-story-pr-review-c10-unaccepted");const i=Se(e,await(t.provider??he).list(e.repository,e.branch,e.baseBranch));if(!i||i.number!==t.prNumber)throw new Error("legacy-story-pr-review-provider-mismatch");const d={schemaVersion:1,kind:"legacy-story-c10-acceptance-intent",storyId:e.storyId,planDigest:h(JSON.stringify(e)),repository:e.repository,number:i.number,url:i.url,qaDigest:y.qaDigest,stateBefore:h(s.stateBefore),stateAfter:h(s.stateAfter),storyBefore:h(s.storyBefore),storyAfter:h(s.storyAfter)};if(JSON.stringify(y)!==JSON.stringify(d))throw new Error("legacy-story-pr-review-c10-journal-conflict");const c=Et(r,e),b=Object.freeze({schemaVersion:1,kind:"legacy-story-pr-review-plan",storyId:e.storyId,epicId:e.epicId,repository:e.repository,branch:e.branch,baseBranch:e.baseBranch,baseSha:e.baseSha,headSha:e.headSha,prNumber:t.prNumber,diffSha256:c.diffSha256,changedPaths:c.changedPaths,checkpointAccepted:!1,localOnly:!0});return{cwd:r,plan:e,c10:s,directory:o,reviewPlan:b}}function le(t,r,a){const e=we({projectRoot:t,storyId:r.storyId},!0);if(!e.ok||JSON.stringify(e.value)!==JSON.stringify(r))throw new Error("legacy-story-pr-review-physical-drift");const s=E(A(t,".neocortex/state.json"),2*1024*1024),o=E(A(t,a.storyPath),2*1024*1024);if(![a.stateBefore,a.stateAfter].includes(s)||![a.storyBefore,a.storyAfter].includes(o))throw new Error("legacy-story-pr-review-preimage-conflict");if(v(t,"status","--porcelain=v1","-uall").split(`
28
- `).filter(Boolean).some(p=>!p.startsWith(" M ")||![".neocortex/state.json",a.storyPath].includes(p.slice(3))))throw new Error("legacy-story-pr-review-worktree-dirty");return{stateCurrent:s,storyCurrent:o}}function at(t,r,a){if(!Array.isArray(a)||a.length<1||a.length>64)return!1;const e=new Set;return a.every(s=>{const o=S(s);if(!o||Object.keys(o).sort().join(",")!=="disposition,id,path,sha256"||typeof o.id!="string"||!/^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/.test(o.id)||e.has(o.id)||typeof o.path!="string"||!rt(o.path)||typeof o.sha256!="string"||!/^[a-f0-9]{64}$/.test(o.sha256)||o.disposition!=="passed"&&o.disposition!=="not-applicable")return!1;e.add(o.id);try{return h(v(t,"show",`${r}:${o.path}`))===o.sha256}catch{return!1}})}function Bt(t,r){if(!Array.isArray(r)||r.length>16)return!1;const a=new Map([["documentation-gap","update-docs"],["test-gap","add-regression-test"],["minor-maintainability","simplify-code"]]),e=new Set;return r.every(s=>{const o=S(s);if(!o||Object.keys(o).sort().join(",")!=="actionCode,id,path,reasonCode,severity,sha256"||typeof o.id!="string"||!/^F[1-9][0-9]{0,2}$/.test(o.id)||e.has(o.id)||o.severity!=="P3"||typeof o.reasonCode!="string"||o.actionCode!==a.get(o.reasonCode)||typeof o.path!="string"||!t.reviewPlan.changedPaths.includes(o.path)||typeof o.sha256!="string"||!/^[a-f0-9]{64}$/.test(o.sha256))return!1;e.add(o.id);try{return h(v(t.cwd,"show",`${t.reviewPlan.headSha}:${o.path}`))===o.sha256}catch{return!1}})}function Ee(t,r){if(Buffer.byteLength(t)>32*1024)throw new Error("legacy-story-pr-review-report-unsafe");const a=S(JSON.parse(t)),e=r.reviewPlan;if(!a||Object.keys(a).sort().join(",")!==["acceptanceEvidence","baseSha","blockingFindings","diffSha256","epicId","headSha","kind","nonBlockingFindings","prNumber","remoteObservations","schemaVersion","sddEvidence","storyId","verdict"].sort().join(",")||a.schemaVersion!==1||a.kind!=="legacy-story-pr-review"||a.storyId!==e.storyId||a.epicId!==e.epicId||a.prNumber!==e.prNumber||a.headSha!==e.headSha||a.baseSha!==e.baseSha||a.diffSha256!==e.diffSha256||a.verdict!=="approved"||!Array.isArray(a.blockingFindings)||a.blockingFindings.length!==0||!Bt(r,a.nonBlockingFindings)||!Array.isArray(a.remoteObservations)||a.remoteObservations.length>1||a.remoteObservations.some(s=>{const o=S(s);return!o||!Array.isArray(o.reasonCodes)||o.reasonCodes.length===0||_e(s,e.storyId,e.prNumber)===null})||!at(r.cwd,e.headSha,a.acceptanceEvidence)||!at(r.cwd,e.headSha,a.sddEvidence))throw new Error("legacy-story-pr-review-report-invalid");return{report:a,raw:t,sha256:h(t)}}function st(t,r){if(W(t).isSymbolicLink())throw new Error("legacy-story-pr-review-report-unsafe");return Ee(E(t,32*1024),r)}async function ot(t,r){const a=await r.view(t.plan.repository,t.reviewPlan.prNumber),e=t.reviewPlan;if(a.number!==e.prNumber||a.state!=="OPEN"||a.headRefName!==e.branch||a.headRefOid!==e.headSha||a.baseRefName!==e.baseBranch||a.baseRefOid!==e.baseSha||a.url.toLowerCase()!==`https://github.com/${e.repository}/pull/${e.prNumber}`.toLowerCase())throw new Error("legacy-story-pr-review-remote-provider-mismatch");const s=fe(a.statusCheckRollup);if(!s.ok)throw new Error("legacy-story-pr-review-remote-observation-unclassified");return s.value}async function it(t,r,a){if(r.remoteObservations.length===0)return;const e=await ot(t,a);if(JSON.stringify(e)!==JSON.stringify(r.remoteObservations[0]))throw new Error("legacy-story-pr-review-remote-observation-drift")}async function or(t){try{const r=await ue(t),a=le(r.cwd,r.plan,ke(r.plan,r.c10,t.prNumber,"0".repeat(64)));if(a.stateCurrent!==r.c10.stateAfter||a.storyCurrent!==r.c10.storyAfter)return n("legacy-story-pr-review-already-transitioned");const e=await ot(r,t.mergeProvider??F);return{ok:!0,value:Object.freeze({schemaVersion:1,kind:"legacy-story-c11-remote-observation",storyId:r.plan.storyId,prNumber:t.prNumber,observation:e,checkpointAccepted:!1,mergeAuthorized:!1,publicSafe:!0})}}catch(r){const a=r instanceof Error?r.message:"";return n(a.startsWith("legacy-story-pr-")?a:"legacy-story-pr-review-remote-observe-failed")}}async function ce(t,r){const a=A(t,r);if(!H(a))return null;await P(a,t);const e=S(JSON.parse(E(a,64*1024)));if(!e||e.schemaVersion!==1||e.kind!=="legacy-story-c11-acceptance-intent"||typeof e.reviewSha256!="string"||!/^[a-f0-9]{64}$/.test(e.reviewSha256)||typeof e.qaSha256!="string"||!/^[a-f0-9]{64}$/.test(e.qaSha256)||typeof e.reviewRaw!="string"||h(e.reviewRaw)!==e.reviewSha256)throw new Error("legacy-story-pr-review-journal-invalid");return e}async function nt(t,r,a){await L(t,r,`${JSON.stringify(a)}
29
- `),await P(A(t,r),t)}async function ir(t){try{const r=await ue(t),a=le(r.cwd,r.plan,ke(r.plan,r.c10,t.prNumber,"0".repeat(64)));return a.stateCurrent!==r.c10.stateAfter||a.storyCurrent!==r.c10.storyAfter?n("legacy-story-pr-review-already-transitioned"):{ok:!0,value:r.reviewPlan}}catch(r){const a=r instanceof Error?r.message:"";return n(a.startsWith("legacy-story-pr-")?a:"legacy-story-pr-review-plan-failed")}}async function nr(t){if(!t.authorize)return n("legacy-story-pr-review-authorization-required");let r,a,e=null;try{r=await ue(t),a=st(t.reportFile,r),await it(r,a.report,t.mergeProvider??F);const l=ke(r.plan,r.c10,t.prNumber,a.sha256),p=le(r.cwd,r.plan,l);if(p.stateCurrent===r.c10.stateAfter&&p.storyCurrent===r.c10.storyAfter)try{e=await de(r.plan,r.cwd,t.verifyQa)}catch{throw new Error("legacy-story-pr-review-qa-failed")}}catch(l){const p=l instanceof Error?l.message:"";return n(p.startsWith("legacy-story-pr-")?p:"legacy-story-pr-review-preflight-failed")}const s=`legacy-c11-accept-${re(16).toString("hex")}`,o=await se(t.projectRoot,{kind:"direct",storyId:t.storyId,sessionId:s,idempotencyKey:s,expiresAt:new Date(Date.now()+5*6e4).toISOString(),persistent:!1});if(!o.ok)return n(o.reasonCode);try{const l=await ue(t);if(JSON.stringify(l.reviewPlan)!==JSON.stringify(r.reviewPlan))return n("legacy-story-pr-review-plan-drift");const p=st(t.reportFile,l);if(p.sha256!==a.sha256)return n("legacy-story-pr-review-report-drift");await it(l,p.report,t.mergeProvider??F);const y=ke(l.plan,l.c10,t.prNumber,p.sha256),i=le(l.cwd,l.plan,y),d=h(l.plan.storyId),c=`${d}.c11.json`,b=`${d}.c11.accepted.json`,w=await ce(l.directory,c),f=await ce(l.directory,b);if(!w&&(!e||i.stateCurrent!==y.stateBefore||i.storyCurrent!==y.storyBefore))return n("legacy-story-pr-review-unjournaled-drift");if(f&&(!w||JSON.stringify(f)!==JSON.stringify(w)))return n("legacy-story-pr-review-accepted-conflict");const m={schemaVersion:1,kind:"legacy-story-c11-acceptance-intent",storyId:l.plan.storyId,planDigest:h(JSON.stringify(l.reviewPlan)),prNumber:t.prNumber,reviewSha256:p.sha256,qaSha256:w?.qaSha256??e,reviewRaw:p.raw,stateBefore:h(y.stateBefore),stateAfter:h(y.stateAfter),storyBefore:h(y.storyBefore),storyAfter:h(y.storyAfter)};if(w&&JSON.stringify(w)!==JSON.stringify(m))return n("legacy-story-pr-review-journal-conflict");w||await nt(l.directory,c,m);const u=le(l.cwd,l.plan,y);if(f&&(u.stateCurrent!==y.stateAfter||u.storyCurrent!==y.storyAfter))return n("legacy-story-pr-review-accepted-physical-mismatch");u.stateCurrent===y.stateBefore&&(await Y(A(l.cwd,".neocortex"),"state.json",y.stateAfter,m.stateBefore),await t.afterStatePersist?.()),le(l.cwd,l.plan,y).storyCurrent===y.storyBefore&&(await Y(A(l.cwd,"docs/stories"),`${l.plan.storyId}.story.md`,y.storyAfter,m.storyBefore),await t.afterStoryPersist?.());const N=le(l.cwd,l.plan,y);return N.stateCurrent!==y.stateAfter||N.storyCurrent!==y.storyAfter?n("legacy-story-pr-review-post-write-mismatch"):(f||await nt(l.directory,b,m),{ok:!0,value:Object.freeze({schemaVersion:1,kind:"legacy-story-c11-checkpoint",storyId:l.plan.storyId,epicId:l.plan.epicId,branch:l.plan.branch,prNumber:t.prNumber,headSha:l.plan.headSha,reviewSha256:m.reviewSha256,qaSha256:m.qaSha256,stateSha256:m.stateAfter,storySha256:m.storyAfter,reused:!!f,checkpointAccepted:!0,localOnly:!0})})}catch(l){const p=l instanceof Error?l.message:"";return n(p.startsWith("legacy-story-pr-")?p:p.includes("secure-filesystem-cas-conflict")?"legacy-story-pr-review-write-raced":"legacy-story-pr-review-accept-failed")}finally{await oe(o.path,o.owner).catch(()=>{})}}async function xt(t){try{const r=await ue(t),a=h(r.plan.storyId),e=await ce(r.directory,`${a}.c11.json`),s=await ce(r.directory,`${a}.c11.accepted.json`);if(!e||!s||JSON.stringify(e)!==JSON.stringify(s))return n("legacy-story-pr-review-not-accepted");const o=Ee(s.reviewRaw,r),l=ke(r.plan,r.c10,t.prNumber,o.sha256),p={schemaVersion:1,kind:"legacy-story-c11-acceptance-intent",storyId:r.plan.storyId,planDigest:h(JSON.stringify(r.reviewPlan)),prNumber:t.prNumber,reviewSha256:o.sha256,qaSha256:s.qaSha256,reviewRaw:o.raw,stateBefore:h(l.stateBefore),stateAfter:h(l.stateAfter),storyBefore:h(l.storyBefore),storyAfter:h(l.storyAfter)};if(JSON.stringify(s)!==JSON.stringify(p))return n("legacy-story-pr-review-journal-conflict");const y=le(r.cwd,r.plan,l);return y.stateCurrent!==l.stateAfter||y.storyCurrent!==l.storyAfter?n("legacy-story-pr-review-physical-not-accepted"):{ok:!0,value:Object.freeze({plan:r.reviewPlan,receipt:Object.freeze({schemaVersion:1,kind:"legacy-story-c11-checkpoint",storyId:r.plan.storyId,epicId:r.plan.epicId,branch:r.plan.branch,prNumber:t.prNumber,headSha:r.plan.headSha,reviewSha256:s.reviewSha256,qaSha256:s.qaSha256,stateSha256:s.stateAfter,storySha256:s.storyAfter,reused:!0,checkpointAccepted:!0,localOnly:!0})})}}catch(r){const a=r instanceof Error?r.message:"";return n(a.startsWith("legacy-story-pr-")?a:"legacy-story-pr-review-read-failed")}}function ct(t,r,a){const e=o=>o.map(l=>`- ${l.id}: ${l.disposition}; ${l.path}; sha256:${l.sha256}`).join(`
30
- `),s=t.nonBlockingFindings.length===0?"None.":t.nonBlockingFindings.map(o=>`- ${o.id}: ${o.severity}; ${o.reasonCode}; ${o.actionCode}; ${o.path}; sha256:${o.sha256}`).join(`
31
- `);return`# ${t.storyId} \u2014 PR review
32
-
33
- Verdict: approved
34
-
35
- PR: ${t.prNumber}
36
- Reviewed HEAD: ${t.headSha}
37
- Base HEAD: ${t.baseSha}
38
- Diff SHA-256: ${t.diffSha256}
39
- Root review SHA-256: ${r}
40
- Local QA SHA-256: ${a}
41
-
42
- ## Acceptance evidence
43
-
44
- ${e(t.acceptanceEvidence)}
45
-
46
- ## SDD evidence
47
-
48
- ${e(t.sddEvidence)}
49
-
50
- ## Non-blocking review debt
51
-
52
- ${s}
53
- `}async function yt(t){try{const r=await xt(t);if(!r.ok)return r;const a=await ue(t),e=h(a.plan.storyId),s=await ce(a.directory,`${e}.c11.accepted.json`);if(!s||s.reviewSha256!==r.value.receipt.reviewSha256||s.qaSha256!==r.value.receipt.qaSha256)return n("legacy-story-pr-merge-review-drift");const o=Ee(s.reviewRaw,a),l=`.neocortex/stories/${a.plan.storyId}/review.md`;if(H(A(a.cwd,l)))return n("legacy-story-pr-merge-review-artifact-conflict");try{return g(a.cwd,"cat-file","-e",`${a.plan.headSha}:${l}`),n("legacy-story-pr-merge-review-artifact-conflict")}catch{}const p=ct(o.report,s.reviewSha256,s.qaSha256);return{ok:!0,value:Object.freeze({schemaVersion:1,kind:"legacy-story-pr-merge-plan",storyId:a.plan.storyId,epicId:a.plan.epicId,repository:a.plan.repository,branch:a.plan.branch,baseBranch:a.plan.baseBranch,baseSha:a.plan.baseSha,originalHeadSha:a.plan.headSha,prNumber:t.prNumber,reviewArtifactPath:l,reviewArtifactSha256:h(p),reviewSha256:s.reviewSha256,qaSha256:s.qaSha256,stateSha256:s.stateAfter,storySha256:s.storyAfter,nextAuthority:"root-review-commit-and-new-head-qa-required",physicalMerge:!1,localOnly:!0})}}catch(r){const a=r instanceof Error?r.message:"";return n(a.startsWith("legacy-story-pr-")?a:"legacy-story-pr-merge-plan-failed")}}async function Ie(t,r){const a=A(t,r);if(!H(a))return null;await P(a,t);const e=S(JSON.parse(E(a,64*1024))),s=S(e?.plan);if(!e||e.schemaVersion!==1||e.kind!=="legacy-story-c12-review-commit-intent"||!s||s.kind!=="legacy-story-pr-merge-plan"||typeof s.reviewArtifactSha256!="string"||typeof e.reviewArtifact!="string"||h(e.reviewArtifact)!==s.reviewArtifactSha256)throw new Error("legacy-story-pr-merge-intent-invalid");if(e.remoteDebt!==void 0){const o=S(e.remoteDebt),l=S(o?.observation);if(!o||Object.keys(o).sort().join(",")!==["added","itemIds","observation","observedAt","registryAfterSha256","registryBeforeSha256","registryExisted"].sort().join(",")||!l||o.registryExisted!==!0||typeof o.observedAt!="string"||!Number.isFinite(Date.parse(o.observedAt))||new Date(o.observedAt).toISOString()!==o.observedAt||typeof o.registryBeforeSha256!="string"||!/^[a-f0-9]{64}$/.test(o.registryBeforeSha256)||typeof o.registryAfterSha256!="string"||!/^[a-f0-9]{64}$/.test(o.registryAfterSha256)||!Number.isSafeInteger(o.added)||o.added<0||!Array.isArray(o.itemIds)||o.itemIds.some(p=>typeof p!="string"))throw new Error("legacy-story-pr-merge-intent-invalid")}return e}async function Ae(t,r){const a=A(t,r);if(!H(a))return null;await P(a,t);const e=S(JSON.parse(E(a,4096)));if(!e||e.schemaVersion!==1||e.kind!=="legacy-story-c12-prepared"||typeof e.intentSha256!="string"||!/^[a-f0-9]{64}$/.test(e.intentSha256)||typeof e.candidateHeadSha!="string"||!/^[a-f0-9]{40}$/.test(e.candidateHeadSha)||typeof e.qaSha256!="string"||!/^[a-f0-9]{64}$/.test(e.qaSha256))throw new Error("legacy-story-pr-merge-prepared-journal-invalid");return e}function Dt(t,r){const a=[A(t,".neocortex"),A(t,".neocortex/stories"),A(t,".neocortex/stories",r)];for(const e of a)if(H(e)||St(e,{mode:448}),!W(e).isDirectory()||D(e)!==e)throw new Error("legacy-story-pr-merge-review-directory-unsafe");return a[2]}const pe=".neocortex/tech-debt-registry.json",Jt='{"schema_version":2,"items":[]}';function We(t,r){const a=v(t,"ls-tree",r,"--",pe).trim();if(!a)return{existed:!1,raw:Jt};if(!/^100644 blob [a-f0-9]{40}\t\.neocortex\/tech-debt-registry\.json$/.test(a))throw new Error("legacy-story-pr-merge-remote-debt-registry-unsafe");return{existed:!0,raw:v(t,"show",`${r}:${pe}`)}}function Be(t,r){const a=r.remoteDebt;if(!a)return null;const e=We(t,r.plan.originalHeadSha);if(!e.existed||h(e.raw)!==a.registryBeforeSha256)throw new Error("legacy-story-pr-merge-remote-debt-registry-drift");const s=Re({registryRaw:e.raw,observation:a.observation,storyId:r.plan.storyId,prNumber:r.plan.prNumber,nowIso:a.observedAt});if(!s||s.registryAfterSha256!==a.registryAfterSha256||s.added!==a.added||JSON.stringify(s.itemIds)!==JSON.stringify(a.itemIds)||a.observation.reasonCodes.length===0)throw new Error("legacy-story-pr-merge-remote-debt-intent-invalid");return s}function xe(t){const r=t.plan;return[".neocortex/state.json",`docs/stories/${r.storyId}.story.md`,r.reviewArtifactPath,...t.remoteDebt?.added?[pe]:[]].sort()}function De(t,r,a){const e=r.plan;if(g(t,"rev-parse",`${a}^`)!==e.originalHeadSha||g(t,"rev-list","--count",`${e.originalHeadSha}..${a}`)!=="1")throw new Error("legacy-story-pr-merge-candidate-parent-invalid");const s=v(t,"diff-tree","--no-commit-id","--name-only","-r","-z",a).split("\0").filter(Boolean).sort(),o=Be(t,r);if(JSON.stringify(s)!==JSON.stringify(xe(r))||h(v(t,"show",`${a}:.neocortex/state.json`))!==e.stateSha256||h(v(t,"show",`${a}:docs/stories/${e.storyId}.story.md`))!==e.storySha256||h(v(t,"show",`${a}:${e.reviewArtifactPath}`))!==e.reviewArtifactSha256||o&&h(v(t,"show",`${a}:${pe}`))!==o.registryAfterSha256||!v(t,"log","-1","--format=%B",a).split(`
54
- `).map(l=>l.trim()).includes(`Neocortex-Legacy-Review: ${e.storyId}`))throw new Error("legacy-story-pr-merge-candidate-invalid")}function Je(t,r,a,e){const s=r.plan;if(X(t)!==s.repository||g(t,"branch","--show-current")!==s.branch||z(t,s.baseBranch)!==s.baseSha||a.storyId!==s.storyId||a.reviewSha256!==s.reviewSha256||a.qaSha256!==s.qaSha256||a.stateAfter!==s.stateSha256||a.storyAfter!==s.storySha256)throw new Error("legacy-story-pr-merge-intent-drift");const o=g(t,"rev-parse","HEAD"),l=z(t,s.branch);if(o!==s.originalHeadSha&&De(t,r,o),l!==s.originalHeadSha&&l!==o)throw new Error("legacy-story-pr-merge-remote-drift");const p={schemaVersion:1,kind:"legacy-story-pr-plan",storyId:s.storyId,epicId:s.epicId,repository:s.repository,branch:s.branch,baseBranch:s.baseBranch,baseSha:s.baseSha,headSha:l,qaAuthority:"root-review-required",localOnly:!0},y=Se(p,e);if(!y||y.number!==s.prNumber)throw new Error("legacy-story-pr-merge-provider-drift");const i=E(A(t,".neocortex/state.json"),2*1024*1024),d=E(A(t,`docs/stories/${s.storyId}.story.md`),2*1024*1024);if(h(i)!==s.stateSha256||h(d)!==s.storySha256)throw new Error("legacy-story-pr-merge-checkpoint-drift");const c=Be(t,r);if(c){const w=A(t,pe),m=H(w)?h(E(w,2*1024*1024)):null;if(o===s.originalHeadSha?m!==c.registryBeforeSha256&&m!==c.registryAfterSha256:m!==c.registryAfterSha256)throw new Error("legacy-story-pr-merge-remote-debt-registry-drift")}const b=v(t,"status","--porcelain=v1","-uall").split(`
55
- `).filter(Boolean);if(o===s.originalHeadSha){const w=new Set(xe(r));if(b.some(f=>!w.has(f.slice(3))||![" M","M ","MM","??","A "].includes(f.slice(0,2))))throw new Error("legacy-story-pr-merge-worktree-dirty")}else if(b.length>0)throw new Error("legacy-story-pr-merge-worktree-dirty");return{headSha:o,remoteSha:l}}async function cr(t){if(!t.authorize)return n("legacy-story-pr-merge-authorization-required");if(!Number.isSafeInteger(t.prNumber)||t.prNumber<=0||!/^[A-Za-z0-9][A-Za-z0-9_-]*\.[A-Za-z0-9._-]+$/.test(t.storyId))return n("legacy-story-pr-merge-target-invalid");const r=`legacy-c12-prepare-${re(16).toString("hex")}`,a=await se(t.projectRoot,{kind:"direct",storyId:t.storyId,sessionId:r,idempotencyKey:r,expiresAt:new Date(Date.now()+60*6e4).toISOString(),persistent:!1});if(!a.ok)return n(a.reasonCode);try{const e=D(t.projectRoot),s=await T(await G(e),"legacy-story-pr-intents"),o=h(t.storyId),l=await ce(s,`${o}.c11.json`),p=await ce(s,`${o}.c11.accepted.json`);if(!l||!p||JSON.stringify(l)!==JSON.stringify(p))return n("legacy-story-pr-merge-review-unaccepted");const y=`${o}.c12.json`,i=`${o}.c12.prepared.json`;let d=await Ie(s,y);if(!d){const _=await yt(t);if(!_.ok)return _;const $=await ue(t),k=Ee(p.reviewRaw,$),j=ct(k.report,p.reviewSha256,p.qaSha256);let B;if(t.recordRemoteDebt){const x=_.value,J=await(t.mergeProvider??F).view(x.repository,x.prNumber);ee(x,x.originalHeadSha,J,"OPEN");const R=fe(J.statusCheckRollup);if(!R.ok)return n("legacy-story-pr-merge-remote-observation-unclassified");if(R.value.reasonCodes.length===0)return n("legacy-story-pr-merge-remote-debt-not-required");const C=We(e,x.originalHeadSha);if(!C.existed)return n("legacy-story-pr-merge-remote-debt-registry-missing");const V=A(e,pe);if((H(V)?E(V,2*1024*1024):null)!==C.raw)return n("legacy-story-pr-merge-remote-debt-registry-drift");const K=new Date().toISOString(),M=Re({registryRaw:C.raw,observation:R.value,storyId:x.storyId,prNumber:x.prNumber,nowIso:K});if(!M)return n("legacy-story-pr-merge-remote-debt-registry-invalid");B={observation:R.value,observedAt:K,registryExisted:C.existed,registryBeforeSha256:M.registryBeforeSha256,registryAfterSha256:M.registryAfterSha256,added:M.added,itemIds:M.itemIds}}d={schemaVersion:1,kind:"legacy-story-c12-review-commit-intent",plan:_.value,reviewArtifact:j,...B?{remoteDebt:B}:{}},await L(s,y,`${JSON.stringify(d)}
56
- `),await P(A(s,y),s)}const c=d.plan;if(c.storyId!==t.storyId||c.prNumber!==t.prNumber||h(d.reviewArtifact)!==c.reviewArtifactSha256||!!d.remoteDebt!=!!t.recordRemoteDebt)return n("legacy-story-pr-merge-intent-conflict");const b=t.provider??he,w=async()=>Je(e,d,p,await b.list(c.repository,c.branch,c.baseBranch));let f=await w();const m=h(JSON.stringify(d)),u=await Ae(s,i);if(u)return u.intentSha256!==m||u.candidateHeadSha!==f.headSha||f.remoteSha!==f.headSha?n("legacy-story-pr-merge-prepared-conflict"):{ok:!0,value:Object.freeze({schemaVersion:1,kind:"legacy-story-c12-prepared",storyId:c.storyId,epicId:c.epicId,branch:c.branch,prNumber:c.prNumber,originalHeadSha:c.originalHeadSha,candidateHeadSha:u.candidateHeadSha,qaSha256:u.qaSha256,registryRecorded:!!d.remoteDebt,reused:!0,physicalMerge:!1,localOnly:!0})};if(f.headSha===c.originalHeadSha){if(f.remoteSha!==c.originalHeadSha)return n("legacy-story-pr-merge-remote-drift");const _=Be(e,d);if(_){const B=await(t.mergeProvider??F).view(c.repository,c.prNumber);ee(c,c.originalHeadSha,B,"OPEN");const x=fe(B.statusCheckRollup);if(!x.ok||x.value.digest!==d.remoteDebt.observation.digest||JSON.stringify(x.value)!==JSON.stringify(d.remoteDebt.observation))return n("legacy-story-pr-merge-remote-debt-observation-drift");if(_.added>0){const J=A(e,pe),R=H(J)?E(J,2*1024*1024):null;if(R!==_.registryAfterRaw)if(R===We(e,c.originalHeadSha).raw){const C=A(e,".neocortex");if(!W(C).isDirectory()||D(C)!==C)return n("legacy-story-pr-merge-remote-debt-registry-unsafe");await Y(C,"tech-debt-registry.json",_.registryAfterRaw,_.registryBeforeSha256)}else return n("legacy-story-pr-merge-remote-debt-registry-drift")}}const $=Dt(e,c.storyId),k=A($,"review.md");if(H(k)){if(W(k).isSymbolicLink()||E(k,32*1024)!==d.reviewArtifact)return n("legacy-story-pr-merge-review-artifact-conflict")}else await L($,"review.md",d.reviewArtifact);f=await w(),g(e,"add","--",...xe(d));const j=v(e,"diff","--cached","--name-only","-z").split("\0").filter(Boolean).sort();if(JSON.stringify(j)!==JSON.stringify(xe(d))||h(v(e,"show",":.neocortex/state.json"))!==c.stateSha256||h(v(e,"show",`:docs/stories/${c.storyId}.story.md`))!==c.storySha256||h(v(e,"show",`:${c.reviewArtifactPath}`))!==c.reviewArtifactSha256||_&&h(v(e,"show",`:${pe}`))!==_.registryAfterSha256||v(e,"diff","--name-only","-z")!=="")return n("legacy-story-pr-merge-staged-scope-invalid");g(e,"diff","--cached","--check"),g(e,"commit","-m",`chore(story): record reviewed PR ${c.storyId}`,"-m",`Neocortex-Legacy-Review: ${c.storyId}`),f=await w(),await t.afterCommit?.()}if(f.headSha===c.originalHeadSha)return n("legacy-story-pr-merge-commit-missing");const I={schemaVersion:1,kind:"legacy-story-pr-plan",storyId:c.storyId,epicId:c.epicId,repository:c.repository,branch:c.branch,baseBranch:c.baseBranch,headSha:f.headSha,baseSha:c.baseSha,qaAuthority:"root-review-required",localOnly:!0};let N;try{N=await de(I,e,t.verifyQa)}catch{return n("legacy-story-pr-merge-new-head-qa-failed")}if(f=await w(),f.headSha===c.originalHeadSha)return n("legacy-story-pr-merge-candidate-raced");if(f.remoteSha===c.originalHeadSha&&g(e,"push","--porcelain","origin",`${f.headSha}:refs/heads/${c.branch}`),f=await w(),f.remoteSha!==f.headSha)return n("legacy-story-pr-merge-push-unverified");await t.afterPush?.();const O={schemaVersion:1,kind:"legacy-story-c12-prepared",intentSha256:m,candidateHeadSha:f.headSha,qaSha256:N};return await L(s,i,`${JSON.stringify(O)}
57
- `),await P(A(s,i),s),{ok:!0,value:Object.freeze({schemaVersion:1,kind:"legacy-story-c12-prepared",storyId:c.storyId,epicId:c.epicId,branch:c.branch,prNumber:c.prNumber,originalHeadSha:c.originalHeadSha,candidateHeadSha:f.headSha,qaSha256:N,registryRecorded:!!d.remoteDebt,reused:!1,physicalMerge:!1,localOnly:!0})}}catch(e){const s=e instanceof Error?e.message:"";return n(s.startsWith("legacy-story-pr-")?s:"legacy-story-pr-merge-prepare-failed")}finally{await oe(a.path,a.owner).catch(()=>{})}}async function yr(t){if(!Number.isSafeInteger(t.prNumber)||t.prNumber<=0)return n("legacy-story-pr-merge-target-invalid");const r=await yt(t);if(!r.ok)return r;try{const a=r.value,e=await(t.mergeProvider??F).view(a.repository,a.prNumber);ee(a,a.originalHeadSha,e,"OPEN");const s=fe(e.statusCheckRollup);if(!s.ok)return n("legacy-story-pr-merge-remote-observation-unclassified");const o=_e(s.value,a.storyId,a.prNumber);if(!o)return n("legacy-story-pr-merge-remote-debt-projection-invalid");if(o.length===0)return n("legacy-story-pr-merge-remote-debt-not-required");const l=A(D(t.projectRoot),".neocortex/tech-debt-registry.json"),p=H(l),y=p?E(l,2*1024*1024):'{"schema_version":2,"items":[]}',i=Re({registryRaw:y,observation:s.value,storyId:a.storyId,prNumber:a.prNumber,nowIso:new Date().toISOString()});return i?{ok:!0,value:Object.freeze({schemaVersion:1,kind:"legacy-story-c12-pre-prepare-debt-plan",storyId:a.storyId,epicId:a.epicId,prNumber:a.prNumber,originalHeadSha:a.originalHeadSha,baseSha:a.baseSha,remoteObservationDigest:s.value.digest,reasonCodes:s.value.reasonCodes,followUpDebt:o,registryExists:p,registryBeforeSha256:i.registryBeforeSha256,registryProjectedItemIds:i.itemIds,registryProjectedAdded:i.added,registryProjectedExisting:i.existing,registryMutationAuthorized:!1,canMerge:!1,publicSafe:!0})}:n("legacy-story-pr-merge-remote-debt-registry-invalid")}catch(a){const e=a instanceof Error?a.message:"";return n(e.startsWith("legacy-story-pr-")?e:"legacy-story-pr-merge-pre-prepare-debt-uncertain")}}function dt(t,r,a,e,s){const o=_e(a,t.storyId,t.prNumber);return!o||o.length===0?null:Object.freeze({schemaVersion:1,kind:"legacy-story-c12-root-merge-decision",storyId:t.storyId,epicId:t.epicId,prNumber:t.prNumber,candidateHeadSha:r,baseSha:t.baseSha,reviewSha256:t.reviewSha256,remoteObservationDigest:a.digest,canMerge:!0,blockingChecks:[],nonBlockingChecks:Object.freeze(o.map(l=>Object.freeze({checkName:l.checkName,bucket:"nonBlockingChecks",reason:l.reason,origin:"remote_ci_cd",safeToContinueYoloop:!0}))),followUpDebt:o,registrySha256:e,registryItemIds:s,publicSafe:!0})}function Ht(t,r,a){const e=r.plan,s=S(t);if(s?.registrySha256!==void 0){const o=r.remoteDebt,l=o&&dt(e,a,o.observation,o.registryAfterSha256,o.itemIds);return!!l&&JSON.stringify(s)===JSON.stringify(l)}return!s||JSON.stringify(Object.keys(s).sort())!==JSON.stringify(["baseSha","blockingChecks","canMerge","candidateHeadSha","epicId","followUpDebt","kind","nonBlockingChecks","prNumber","remoteObservationDigest","reviewSha256","schemaVersion","storyId"].sort())?!1:s.schemaVersion===1&&s.kind==="legacy-story-c12-root-merge-decision"&&s.storyId===e.storyId&&s.epicId===e.epicId&&s.prNumber===e.prNumber&&s.candidateHeadSha===a&&s.baseSha===e.baseSha&&s.reviewSha256===e.reviewSha256&&typeof s.remoteObservationDigest=="string"&&/^[a-f0-9]{64}$/.test(s.remoteObservationDigest)&&s.canMerge===!0&&Array.isArray(s.blockingChecks)&&s.blockingChecks.length===0&&Array.isArray(s.nonBlockingChecks)&&s.nonBlockingChecks.length===0&&Array.isArray(s.followUpDebt)&&s.followUpDebt.length===0}async function Ce(t,r){const a=A(t,r);if(!H(a))return null;await P(a,t);const e=S(JSON.parse(E(a,4096)));if(!e||e.schemaVersion!==1||e.kind!=="legacy-story-c12-merge-attempt"||typeof e.intentSha256!="string"||!/^[a-f0-9]{64}$/.test(e.intentSha256)||typeof e.candidateHeadSha!="string"||!/^[a-f0-9]{40}$/.test(e.candidateHeadSha)||typeof e.qaSha256!="string"||!/^[a-f0-9]{64}$/.test(e.qaSha256)||typeof e.remoteObservationDigest!="string"||!/^[a-f0-9]{64}$/.test(e.remoteObservationDigest))throw new Error("legacy-story-pr-merge-attempt-invalid");return e}async function He(t,r){const a=A(t,r);if(!H(a))return null;await P(a,t);const e=S(JSON.parse(E(a,4096)));if(!e||e.schemaVersion!==1||e.kind!=="legacy-story-c12-merged"||typeof e.intentSha256!="string"||!/^[a-f0-9]{64}$/.test(e.intentSha256)||typeof e.candidateHeadSha!="string"||!/^[a-f0-9]{40}$/.test(e.candidateHeadSha)||typeof e.qaSha256!="string"||!/^[a-f0-9]{64}$/.test(e.qaSha256)||typeof e.remoteObservationDigest!="string"||!/^[a-f0-9]{64}$/.test(e.remoteObservationDigest)||typeof e.mergeCommitSha!="string"||!/^[a-f0-9]{40}$/.test(e.mergeCommitSha)||typeof e.baseHeadSha!="string"||!/^[a-f0-9]{40}$/.test(e.baseHeadSha))throw new Error("legacy-story-pr-merge-journal-invalid");return e}function ee(t,r,a,e){if(a.number!==t.prNumber||a.state!==e||a.headRefName!==t.branch||a.headRefOid!==r||a.baseRefName!==t.baseBranch||a.url.toLowerCase()!==`https://github.com/${t.repository}/pull/${t.prNumber}`.toLowerCase())throw new Error("legacy-story-pr-merge-provider-drift");if(e==="OPEN"&&(a.baseRefOid!==t.baseSha||a.isDraft!==!1||a.mergeable!=="MERGEABLE"))throw new Error("legacy-story-pr-merge-provider-not-ready")}function lt(t,r,a){const e=fe(t.statusCheckRollup);if(!e.ok)throw new Error("legacy-story-pr-merge-remote-observation-unclassified");if(!("registrySha256"in r)&&e.value.reasonCodes.length>0)throw new Error("legacy-story-pr-merge-remote-observation-unclassified");if(e.value.digest!==r.remoteObservationDigest)throw new Error("legacy-story-pr-merge-remote-observation-drift");if("registrySha256"in r&&(!a.remoteDebt||JSON.stringify(e.value)!==JSON.stringify(a.remoteDebt.observation)))throw new Error("legacy-story-pr-merge-remote-debt-observation-drift");return e.value}function Pt(t,r,a,e){const s=r.plan;ee(s,a.candidateHeadSha,e,"MERGED");const o=e.mergeCommit?.oid;if(!o||!/^[a-f0-9]{40}$/.test(o)||g(t,"rev-parse","HEAD")!==a.candidateHeadSha||v(t,"status","--porcelain=v1","-uall")!=="")throw new Error("legacy-story-pr-merge-physical-invalid");De(t,r,a.candidateHeadSha),g(t,"fetch","--no-tags","origin",`refs/heads/${s.baseBranch}`);const l=g(t,"rev-parse","FETCH_HEAD");if(z(t,s.baseBranch)!==l||!/^[a-f0-9]{40}$/.test(l))throw new Error("legacy-story-pr-merge-base-readback-drift");const p=g(t,"rev-list","--parents","-n","1",o).split(" ");if(p.length!==3||p[0]!==o||p[1]!==s.baseSha||p[2]!==a.candidateHeadSha)throw new Error("legacy-story-pr-merge-commit-parent-invalid");return g(t,"merge-base","--is-ancestor",o,l),{mergeCommitSha:o,baseHeadSha:l}}async function pt(t){if(!Number.isSafeInteger(t.prNumber)||t.prNumber<=0||!/^[A-Za-z0-9][A-Za-z0-9_-]*\.[A-Za-z0-9._-]+$/.test(t.storyId))return n("legacy-story-pr-merge-target-invalid");try{const r=D(t.projectRoot),a=g(r,"rev-parse","--git-common-dir"),e=D(Pe(a)?a:A(r,a)),s=A(e,"neocortex-yoloop"),o=A(s,"legacy-story-pr-intents");if(!H(s)||!H(o))return n("legacy-story-pr-merge-not-prepared");if(!W(s).isDirectory()||D(s)!==s)return n("legacy-story-pr-merge-runtime-unsafe");await je(o,s);const l=h(t.storyId),p=await Ie(o,`${l}.c12.json`),y=await Ae(o,`${l}.c12.prepared.json`),i=await ce(o,`${l}.c11.accepted.json`);if(!p||!y||!i)return n("legacy-story-pr-merge-not-prepared");const d=p.plan;if(d.storyId!==t.storyId||d.prNumber!==t.prNumber||y.intentSha256!==h(JSON.stringify(p))||i.reviewSha256!==d.reviewSha256||i.qaSha256!==d.qaSha256)return n("legacy-story-pr-merge-prepared-drift");const c=Je(r,p,i,await(t.provider??he).list(d.repository,d.branch,d.baseBranch));if(c.headSha!==y.candidateHeadSha||c.remoteSha!==y.candidateHeadSha)return n("legacy-story-pr-merge-remote-drift");De(r,p,y.candidateHeadSha);const b=await(t.mergeProvider??F).view(d.repository,d.prNumber);ee(d,y.candidateHeadSha,b,"OPEN");const w=fe(b.statusCheckRollup);if(!w.ok)return n("legacy-story-pr-merge-remote-observation-unclassified");const f=w.value;if(f.reasonCodes.length>0){const m=p.remoteDebt;if(m&&JSON.stringify(f)===JSON.stringify(m.observation)){const u=Be(r,p);if(!u)return n("legacy-story-pr-merge-remote-debt-registry-invalid");const I=dt(d,y.candidateHeadSha,f,u.registryAfterSha256,u.itemIds);return I?{ok:!0,value:I}:n("legacy-story-pr-merge-remote-debt-projection-invalid")}return{ok:!0,value:Object.freeze({schemaVersion:1,kind:"legacy-story-c12-remote-debt-required",storyId:d.storyId,epicId:d.epicId,prNumber:d.prNumber,candidateHeadSha:y.candidateHeadSha,baseSha:d.baseSha,reviewSha256:d.reviewSha256,remoteObservationDigest:f.digest,reasonCodes:f.reasonCodes,reasonCounts:f.reasonCounts,canMerge:!1,publicSafe:!0})}}return{ok:!0,value:Object.freeze({schemaVersion:1,kind:"legacy-story-c12-root-merge-decision",storyId:d.storyId,epicId:d.epicId,prNumber:d.prNumber,candidateHeadSha:y.candidateHeadSha,baseSha:d.baseSha,reviewSha256:d.reviewSha256,remoteObservationDigest:f.digest,canMerge:!0,blockingChecks:[],nonBlockingChecks:[],followUpDebt:[]})}}catch(r){const a=r instanceof Error?r.message:"";return n(a.startsWith("legacy-story-pr-")?a:"legacy-story-pr-merge-decision-uncertain")}}async function dr(t){if(!Number.isSafeInteger(t.prNumber)||t.prNumber<=0||!/^[A-Za-z0-9][A-Za-z0-9_-]*\.[A-Za-z0-9._-]+$/.test(t.storyId))return n("legacy-story-pr-merge-target-invalid");const r=await pt(t);if(!r.ok)return n(r.reasonCode);if(r.value.canMerge)return n("legacy-story-pr-merge-remote-debt-not-required");const a=`legacy-c12-remote-debt-${re(16).toString("hex")}`,e=await se(t.projectRoot,{kind:"direct",storyId:t.storyId,sessionId:a,idempotencyKey:a,expiresAt:new Date(Date.now()+5*6e4).toISOString(),persistent:!1});if(!e.ok)return n(e.reasonCode);try{const s=await pt(t);if(!s.ok)return n(s.reasonCode);if(s.value.canMerge)return n("legacy-story-pr-merge-remote-debt-not-required");if(JSON.stringify(s.value)!==JSON.stringify(r.value))return n("legacy-story-pr-merge-remote-debt-observation-drift");const o=s.value,l=D(t.projectRoot),p=X(l),y=await(t.mergeProvider??F).view(p,o.prNumber);if(y.number!==o.prNumber||y.state!=="OPEN"||y.headRefOid!==o.candidateHeadSha||y.baseRefOid!==o.baseSha||y.headRefName!==`story/${o.epicId}/${o.storyId}`||y.isDraft!==!1||y.mergeable!=="MERGEABLE"||y.url.toLowerCase()!==`https://github.com/${p}/pull/${o.prNumber}`.toLowerCase())return n("legacy-story-pr-merge-remote-debt-observation-drift");const i=fe(y.statusCheckRollup);if(!i.ok||i.value.digest!==o.remoteObservationDigest)return n("legacy-story-pr-merge-remote-debt-observation-drift");const d=_e(i.value,o.storyId,o.prNumber);if(!d||d.length===0)return n("legacy-story-pr-merge-remote-debt-projection-invalid");const c=await T(await G(l),"legacy-story-pr-intents"),b={schemaVersion:1,kind:"legacy-story-c12-remote-debt-observation",planSha256:h(JSON.stringify(o)),storyId:o.storyId,epicId:o.epicId,prNumber:o.prNumber,candidateHeadSha:o.candidateHeadSha,remoteObservationDigest:o.remoteObservationDigest,reasonCodes:o.reasonCodes,reasonCounts:o.reasonCounts},w=`${h(o.storyId)}.${o.candidateHeadSha}.${o.remoteObservationDigest}.c12.remote-debt.json`,f=A(c,w),m=H(f);if(m){await P(f,c);const u=S(JSON.parse(E(f,4096)));if(!u||JSON.stringify(u)!==JSON.stringify(b))return n("legacy-story-pr-merge-remote-debt-journal-conflict")}else await L(c,w,`${JSON.stringify(b)}
58
- `),await P(f,c);return{ok:!0,value:Object.freeze({schemaVersion:1,kind:"legacy-story-c12-remote-debt-journaled",storyId:o.storyId,epicId:o.epicId,prNumber:o.prNumber,candidateHeadSha:o.candidateHeadSha,remoteObservationDigest:o.remoteObservationDigest,reasonCodes:o.reasonCodes,reasonCounts:o.reasonCounts,followUpDebt:d,reused:m,durableObservation:!0,registryRecorded:!1,canMerge:!1,publicSafe:!0})}}catch(s){const o=s instanceof Error?s.message:"";return n(o.startsWith("legacy-story-pr-")?o:"legacy-story-pr-merge-remote-debt-uncertain")}finally{await oe(e.path,e.owner).catch(()=>{})}}async function lr(t){if(!t.authorize)return n("legacy-story-pr-merge-authorization-required");if(!Number.isSafeInteger(t.prNumber)||t.prNumber<=0||!/^[A-Za-z0-9][A-Za-z0-9_-]*\.[A-Za-z0-9._-]+$/.test(t.storyId))return n("legacy-story-pr-merge-target-invalid");const r=`legacy-c12-merge-${re(16).toString("hex")}`,a=await se(t.projectRoot,{kind:"direct",storyId:t.storyId,sessionId:r,idempotencyKey:r,expiresAt:new Date(Date.now()+60*6e4).toISOString(),persistent:!1});if(!a.ok)return n(a.reasonCode);try{const e=D(t.projectRoot),s=await T(await G(e),"legacy-story-pr-intents"),o=h(t.storyId),l=await Ie(s,`${o}.c12.json`),p=await Ae(s,`${o}.c12.prepared.json`),y=await ce(s,`${o}.c11.accepted.json`);if(!l||!p||!y)return n("legacy-story-pr-merge-not-prepared");const i=l.plan,d=h(JSON.stringify(l));if(i.storyId!==t.storyId||i.prNumber!==t.prNumber||p.intentSha256!==d||X(e)!==i.repository||g(e,"branch","--show-current")!==i.branch||g(e,"rev-parse","HEAD")!==p.candidateHeadSha||y.reviewSha256!==i.reviewSha256||y.qaSha256!==i.qaSha256||y.stateAfter!==i.stateSha256||y.storyAfter!==i.storySha256||h(E(A(e,".neocortex/state.json"),2*1024*1024))!==i.stateSha256||h(E(A(e,`docs/stories/${i.storyId}.story.md`),2*1024*1024))!==i.storySha256||v(e,"status","--porcelain=v1","-uall")!=="")return n("legacy-story-pr-merge-prepared-drift");De(e,l,p.candidateHeadSha);const c=`${o}.c12.merge-attempt.json`,b=`${o}.c12.merged.json`,w=await Ce(s,c),f=await He(s,b),m=t.mergeProvider??F,u=t.rootDecision;if((!w||u)&&!Ht(u,l,p.candidateHeadSha))return n("legacy-story-pr-merge-root-decision-required");if(w&&(w.intentSha256!==d||w.candidateHeadSha!==p.candidateHeadSha||u&&w.remoteObservationDigest!==u.remoteObservationDigest))return n("legacy-story-pr-merge-attempt-conflict");if(f&&(!w||f.intentSha256!==d||f.candidateHeadSha!==p.candidateHeadSha||f.qaSha256!==w.qaSha256||f.remoteObservationDigest!==w.remoteObservationDigest))return n("legacy-story-pr-merge-journal-conflict");if(!w){if(!u)return n("legacy-story-pr-merge-root-decision-required");const _=t.provider??he,$=Je(e,l,y,await _.list(i.repository,i.branch,i.baseBranch));if($.headSha!==p.candidateHeadSha||$.remoteSha!==p.candidateHeadSha)return n("legacy-story-pr-merge-remote-drift");const k=await m.view(i.repository,i.prNumber);ee(i,p.candidateHeadSha,k,"OPEN"),lt(k,u,l);const j={schemaVersion:1,kind:"legacy-story-pr-plan",storyId:i.storyId,epicId:i.epicId,repository:i.repository,branch:i.branch,baseBranch:i.baseBranch,headSha:p.candidateHeadSha,baseSha:i.baseSha,qaAuthority:"root-review-required",localOnly:!0};let B;try{B=await de(j,e,t.verifyQa)}catch{return n("legacy-story-pr-merge-final-qa-failed")}const x=Je(e,l,y,await _.list(i.repository,i.branch,i.baseBranch));if(x.headSha!==p.candidateHeadSha||x.remoteSha!==p.candidateHeadSha)return n("legacy-story-pr-merge-remote-drift");const J=await m.view(i.repository,i.prNumber);ee(i,p.candidateHeadSha,J,"OPEN");const R=lt(J,u,l),C={schemaVersion:1,kind:"legacy-story-c12-merge-attempt",intentSha256:d,candidateHeadSha:p.candidateHeadSha,qaSha256:B,remoteObservationDigest:R.digest};await L(s,c,`${JSON.stringify(C)}
59
- `),await P(A(s,c),s);try{await m.merge(i.repository,i.prNumber,p.candidateHeadSha)}catch{}await t.afterMerge?.()}const I=w??await Ce(s,c);if(!I)return n("legacy-story-pr-merge-attempt-missing");const N=await m.view(i.repository,i.prNumber);if(N.state!=="MERGED")return n("legacy-story-pr-merge-effect-not-confirmed");const O=Pt(e,l,p,N);if(f){if(f.mergeCommitSha!==O.mergeCommitSha)return n("legacy-story-pr-merge-journal-conflict");g(e,"merge-base","--is-ancestor",f.baseHeadSha,O.baseHeadSha)}else{const _={...I,kind:"legacy-story-c12-merged",mergeCommitSha:O.mergeCommitSha,baseHeadSha:O.baseHeadSha};await L(s,b,`${JSON.stringify(_)}
60
- `),await P(A(s,b),s)}return{ok:!0,value:Object.freeze({schemaVersion:1,kind:"legacy-story-c12-merged",storyId:i.storyId,epicId:i.epicId,prNumber:i.prNumber,branch:i.branch,candidateHeadSha:p.candidateHeadSha,mergeCommitSha:O.mergeCommitSha,baseHeadSha:O.baseHeadSha,qaSha256:I.qaSha256,remoteObservationDigest:I.remoteObservationDigest,reused:!!f,physicalMerge:!0,checkpointAccepted:!1,localOnly:!0})}}catch(e){const s=e instanceof Error?e.message:"";return n(s.startsWith("legacy-story-pr-")?s:"legacy-story-pr-merge-effect-uncertain")}finally{await oe(a.path,a.owner).catch(()=>{})}}const Ke={async view(t,r){const a=Q("gh",["issue","view",String(r),"--repo",t,"--json","number,title,state,url"],{encoding:"utf8",timeout:15e3,stdio:["ignore","pipe","pipe"],maxBuffer:16384});return JSON.parse(a)},async close(t,r){Q("gh",["issue","close",String(r),"--repo",t,"--reason","completed"],{encoding:"utf8",timeout:3e4,stdio:["ignore","pipe","pipe"],maxBuffer:16*1024})}};function ft(t,r,a){const e=v(t,"show",`${r.mergeCommitSha}:.neocortex/state.json`),s=`docs/stories/${r.storyId}.story.md`,o=v(t,"show",`${r.mergeCommitSha}:${s}`);if(h(e)!==r.stateSha256||h(o)!==r.storySha256)throw new Error("legacy-story-pr-post-merge-preimage-drift");const l=S(JSON.parse(e)),p=S(l?.stories),y=S(l?.epics),i=S(p?.[r.storyId]),d=S(y?.[r.epicId]),c=i?.steps_completed,b=d?.stories;if(!i||!d||d.id!==r.epicId||i.id!==r.storyId||i.epic_id!==r.epicId||i.status!=="pr-reviewed"||i.last_step!==ne||i.pr_number!==r.prNumber||i.issue_number!==r.issueNumber||i.branch_name!==`story/${r.epicId}/${r.storyId}`||!Array.isArray(c)||!c.includes(ne)||c.includes(ye)||!Array.isArray(b)||!b.includes(r.storyId)||new Set(b).size!==b.length||b.some(j=>typeof j!="string"||S(p?.[j])?.epic_id!==r.epicId))throw new Error("legacy-story-pr-post-merge-state-conflict");i.status="done",i.last_step=ye,i.steps_completed=[...c,ye],i.merged=!0,i.physicalMergeStatus="merged",i.merge_commit_sha=r.mergeCommitSha,i.issue_state="closed";const w=b.filter(j=>S(p?.[j])?.status==="done").length;if(d.total_stories=b.length,d.completed_stories=w,w===b.length)d.status="done",d.completed_at==null&&(d.completed_at=a);else if(d.status==="done")throw new Error("legacy-story-pr-post-merge-epic-conflict");const f=`${JSON.stringify(l,null,2)}
61
- `,m=o.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);if(!m)throw new Error("legacy-story-pr-post-merge-frontmatter-invalid");const u=ae(m[1],{strict:!0,uniqueKeys:!0}),I=S(u.toJS()),N=["stepsCompleted","steps_completed"].filter(j=>u.has(j)),O=["lastStep","last_step"].filter(j=>u.has(j)),_=["issueNumber","issue_number"].filter(j=>u.has(j)),$=["prNumber","pr_number"].filter(j=>u.has(j));if(u.errors.length>0||u.get("id")!==r.storyId||(u.get("epic_id")??u.get("epic"))!==r.epicId||u.get("status")!=="pr-reviewed"||u.has("currentStatus")&&u.get("currentStatus")!=="pr-reviewed"||N.length!==1||O.length!==1||_.length>1||$.length>1||JSON.stringify(I?.[N[0]])!==JSON.stringify(c)||I?.[O[0]]!==ne||_.length===1&&I?.[_[0]]!==r.issueNumber||$.length===1&&I?.[$[0]]!==r.prNumber)throw new Error("legacy-story-pr-post-merge-frontmatter-conflict");u.set("status","done"),u.has("currentStatus")&&u.set("currentStatus","done"),u.set(N[0],[...c,ye]),u.set(O[0],ye),u.set("mergeCommitSha",r.mergeCommitSha),u.set("issueState","closed");const k=`---
62
- ${u.toString({lineWidth:0})}---
63
- ${o.slice(m[0].length)}`;return{stateBefore:e,stateAfter:f,storyBefore:o,storyAfter:k}}async function Rt(t){if(!Number.isSafeInteger(t.prNumber)||t.prNumber<=0||!/^[A-Za-z0-9][A-Za-z0-9_-]*\.[A-Za-z0-9._-]+$/.test(t.storyId))return n("legacy-story-pr-post-merge-target-invalid");try{const r=D(t.projectRoot);if(D(g(r,"rev-parse","--show-toplevel"))!==r||v(r,"status","--porcelain=v1","-uall")!=="")return n("legacy-story-pr-post-merge-checkout-invalid");const a=g(r,"rev-parse","--git-common-dir"),e=D(Pe(a)?a:A(r,a)),s=A(e,"neocortex-yoloop"),o=A(s,"legacy-story-pr-intents");if(!H(s)||!H(o))return n("legacy-story-pr-post-merge-not-journaled");if(!W(s).isDirectory()||D(s)!==s)return n("legacy-story-pr-post-merge-runtime-unsafe");await je(o,s);const l=h(t.storyId),p=await Ie(o,`${l}.c12.json`),y=await Ae(o,`${l}.c12.prepared.json`),i=await Ce(o,`${l}.c12.merge-attempt.json`),d=await He(o,`${l}.c12.merged.json`);if(!p||!y||!i||!d)return n("legacy-story-pr-post-merge-not-journaled");const c=p.plan;if(c.storyId!==t.storyId||c.prNumber!==t.prNumber||y.intentSha256!==h(JSON.stringify(p))||i.intentSha256!==y.intentSha256||d.intentSha256!==i.intentSha256||i.candidateHeadSha!==y.candidateHeadSha||d.candidateHeadSha!==i.candidateHeadSha||d.qaSha256!==i.qaSha256||d.remoteObservationDigest!==i.remoteObservationDigest||X(r)!==c.repository||g(r,"branch","--show-current")!==c.baseBranch)return n("legacy-story-pr-post-merge-journal-drift");const b=g(r,"rev-parse","HEAD");if(b!==d.mergeCommitSha||z(r,c.baseBranch)!==b)return n("legacy-story-pr-post-merge-base-drift");const w=await(t.mergeProvider??F).view(c.repository,c.prNumber);if(ee(c,y.candidateHeadSha,w,"MERGED"),w.mergeCommit?.oid!==d.mergeCommitSha)return n("legacy-story-pr-post-merge-provider-drift");const f=g(r,"rev-list","--parents","-n","1",b).split(" ");if(f.length!==3||f[0]!==b||f[1]!==c.baseSha||f[2]!==y.candidateHeadSha)return n("legacy-story-pr-post-merge-parents-invalid");const m=v(r,"show","HEAD:.neocortex/state.json"),u=`docs/stories/${c.storyId}.story.md`,I=v(r,"show",`HEAD:${u}`);if(h(m)!==c.stateSha256||h(I)!==c.storySha256||E(A(r,".neocortex/state.json"),2*1024*1024)!==m||E(A(r,u),2*1024*1024)!==I)return n("legacy-story-pr-post-merge-content-drift");const N=S(JSON.parse(m)),O=S(S(N?.stories)?.[c.storyId]),_=O?.issue_number;if(!O||O.id!==c.storyId||O.epic_id!==c.epicId||O.status!=="pr-reviewed"||O.last_step!==ne||O.pr_number!==c.prNumber||O.branch_name!==c.branch||!Array.isArray(O.steps_completed)||!O.steps_completed.includes(ne)||!Number.isSafeInteger(_)||_<=0)return n("legacy-story-pr-post-merge-state-invalid");const $=I.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);if(!$)return n("legacy-story-pr-post-merge-frontmatter-invalid");const k=ae($[1],{strict:!0,uniqueKeys:!0});if(k.errors.length>0||k.get("id")!==c.storyId||(k.get("epic_id")??k.get("epic"))!==c.epicId||k.get("status")!=="pr-reviewed")return n("legacy-story-pr-post-merge-frontmatter-invalid");const j=await(t.issueProvider??Ke).view(c.repository,_);return j.number!==_||j.state!=="OPEN"||j.title!==`[YOLO] Story ${c.storyId} - Workflow Run`||j.url.toLowerCase()!==`https://github.com/${c.repository}/issues/${_}`.toLowerCase()?n("legacy-story-pr-post-merge-issue-invalid"):{ok:!0,value:Object.freeze({schemaVersion:1,kind:"legacy-story-c12-post-merge-plan",storyId:c.storyId,epicId:c.epicId,repository:c.repository,baseBranch:c.baseBranch,prNumber:c.prNumber,issueNumber:_,candidateHeadSha:y.candidateHeadSha,mergeCommitSha:d.mergeCommitSha,stateSha256:c.stateSha256,storySha256:c.storySha256,nextAuthority:"root-state-checkpoint-and-issue-closure-required",checkpointAccepted:!1,localOnly:!0})}}catch(r){const a=r instanceof Error?r.message:"";return n(a.startsWith("legacy-story-pr-")?a:"legacy-story-pr-post-merge-physical-unavailable")}}async function ht(t,r){const a=A(t,r);if(!H(a))return null;await P(a,t);const e=S(JSON.parse(E(a,8192))),s=S(e?.plan);if(!e||e.schemaVersion!==1||e.kind!=="legacy-story-c12-post-merge-intent"||!s||s.schemaVersion!==1||s.kind!=="legacy-story-c12-post-merge-plan"||typeof e.nowIso!="string"||!Number.isFinite(Date.parse(e.nowIso))||typeof e.stateAfterSha256!="string"||!/^[a-f0-9]{64}$/.test(e.stateAfterSha256)||typeof e.storyAfterSha256!="string"||!/^[a-f0-9]{64}$/.test(e.storyAfterSha256))throw new Error("legacy-story-pr-post-merge-intent-invalid");return e}async function ut(t,r){const a=A(t,r);if(!H(a))return null;await P(a,t);const e=S(JSON.parse(E(a,4096)));if(!e||e.schemaVersion!==1||e.kind!=="legacy-story-c12-post-merge-accepted"||typeof e.intentSha256!="string"||!/^[a-f0-9]{64}$/.test(e.intentSha256)||typeof e.checkpointHeadSha!="string"||!/^[a-f0-9]{40}$/.test(e.checkpointHeadSha)||typeof e.qaSha256!="string"||!/^[a-f0-9]{64}$/.test(e.qaSha256))throw new Error("legacy-story-pr-post-merge-accepted-invalid");return e}async function pr(t){if(!t.authorize)return n("legacy-story-pr-post-merge-authorization-required");if(!Number.isSafeInteger(t.prNumber)||t.prNumber<=0||!/^[A-Za-z0-9][A-Za-z0-9_-]*\.[A-Za-z0-9._-]+$/.test(t.storyId))return n("legacy-story-pr-post-merge-target-invalid");const r=`legacy-c12-post-merge-${re(16).toString("hex")}`,a=await se(t.projectRoot,{kind:"direct",storyId:t.storyId,sessionId:r,idempotencyKey:r,expiresAt:new Date(Date.now()+60*6e4).toISOString(),persistent:!1});if(!a.ok)return n(a.reasonCode);try{const e=D(t.projectRoot);if(D(g(e,"rev-parse","--show-toplevel"))!==e)return n("legacy-story-pr-post-merge-checkout-invalid");const s=await T(await G(e),"legacy-story-pr-intents"),o=h(t.storyId),l=`${o}.c12.post-merge.json`,p=`${o}.c12.post-merge.accepted.json`;let y=await ht(s,l);if(!y){const q=await Rt(t);if(!q.ok)return q;const Te=new Date().toISOString(),Ge=ft(e,q.value,Te);y={schemaVersion:1,kind:"legacy-story-c12-post-merge-intent",plan:q.value,nowIso:Te,stateAfterSha256:h(Ge.stateAfter),storyAfterSha256:h(Ge.storyAfter)},await L(s,l,`${JSON.stringify(y)}
64
- `),await P(A(s,l),s)}const i=y.plan,d=await Ie(s,`${o}.c12.json`),c=await Ae(s,`${o}.c12.prepared.json`),b=await Ce(s,`${o}.c12.merge-attempt.json`),w=await He(s,`${o}.c12.merged.json`);if(!d||!c||!b||!w||i.storyId!==t.storyId||i.prNumber!==t.prNumber||i.repository!==d.plan.repository||i.epicId!==d.plan.epicId||i.baseBranch!==d.plan.baseBranch||i.candidateHeadSha!==c.candidateHeadSha||i.mergeCommitSha!==w.mergeCommitSha||i.stateSha256!==d.plan.stateSha256||i.storySha256!==d.plan.storySha256||c.intentSha256!==h(JSON.stringify(d))||b.intentSha256!==c.intentSha256||w.intentSha256!==b.intentSha256||w.qaSha256!==b.qaSha256||w.remoteObservationDigest!==b.remoteObservationDigest||X(e)!==i.repository||g(e,"branch","--show-current")!==i.baseBranch)return n("legacy-story-pr-post-merge-intent-drift");const f=ft(e,i,y.nowIso);if(h(f.stateAfter)!==y.stateAfterSha256||h(f.storyAfter)!==y.storyAfterSha256)return n("legacy-story-pr-post-merge-projection-drift");const m=await(t.mergeProvider??F).view(i.repository,i.prNumber);if(ee(d.plan,c.candidateHeadSha,m,"MERGED"),m.mergeCommit?.oid!==i.mergeCommitSha)return n("legacy-story-pr-post-merge-provider-drift");const u=g(e,"rev-list","--parents","-n","1",i.mergeCommitSha).split(" ");if(u.length!==3||u[0]!==i.mergeCommitSha||u[1]!==d.plan.baseSha||u[2]!==i.candidateHeadSha)return n("legacy-story-pr-post-merge-parents-invalid");const I=await ut(s,p),N=h(JSON.stringify(y));if(I&&I.intentSha256!==N)return n("legacy-story-pr-post-merge-accepted-conflict");const O=g(e,"rev-parse","HEAD");let _=z(e,i.baseBranch);if(v(e,"status","--porcelain=v1","-uall")!=="")return n("legacy-story-pr-post-merge-base-worktree-dirty");const $=A(s,`${o}.c12.post-merge.checkout`);if(H($)||Q("git",["worktree","add","--detach",$,I?.checkpointHeadSha??i.mergeCommitSha],{cwd:e,encoding:"utf8",timeout:6e4,stdio:["ignore","pipe","pipe"],maxBuffer:32*1024}),!W($).isDirectory()||D($)!==$||D(g($,"rev-parse","--show-toplevel"))!==$||me($)!==me(e)||g($,"branch","--show-current")!=="")return n("legacy-story-pr-post-merge-candidate-checkout-invalid");let k=g($,"rev-parse","HEAD");if(k!==i.mergeCommitSha&&(g($,"rev-parse",`${k}^`)!==i.mergeCommitSha||JSON.stringify(v($,"diff-tree","--no-commit-id","--name-only","-r","-z",k).split("\0").filter(Boolean).sort())!==JSON.stringify([".neocortex/state.json",`docs/stories/${i.storyId}.story.md`].sort())||h(v($,"show",`${k}:.neocortex/state.json`))!==y.stateAfterSha256||h(v($,"show",`${k}:docs/stories/${i.storyId}.story.md`))!==y.storyAfterSha256||!v($,"log","-1","--format=%B",k).split(`
65
- `).map(q=>q.trim()).includes(`Neocortex-Legacy-Merge: ${i.storyId}`)))return n("legacy-story-pr-post-merge-checkpoint-commit-invalid");if(![i.mergeCommitSha,k].includes(O)||![i.mergeCommitSha,k].includes(_)||I&&(I.checkpointHeadSha!==k||_!==k))return n("legacy-story-pr-post-merge-base-drift");const j=v($,"status","--porcelain=v1","-uall").split(`
66
- `).filter(Boolean),B=new Set([".neocortex/state.json",`docs/stories/${i.storyId}.story.md`]);if(k!==i.mergeCommitSha&&j.length>0||j.some(q=>!B.has(q.slice(3))||![" M","M ","MM"].includes(q.slice(0,2))))return n("legacy-story-pr-post-merge-worktree-dirty");const x=E(A($,".neocortex/state.json"),2*1024*1024),J=E(A($,`docs/stories/${i.storyId}.story.md`),2*1024*1024);if(![f.stateBefore,f.stateAfter].includes(x)||![f.storyBefore,f.storyAfter].includes(J)||k!==i.mergeCommitSha&&(x!==f.stateAfter||J!==f.storyAfter))return n("legacy-story-pr-post-merge-content-drift");const R=t.issueProvider??Ke,C=async()=>{const q=await R.view(i.repository,i.issueNumber);if(q.number!==i.issueNumber||q.title!==`[YOLO] Story ${i.storyId} - Workflow Run`||q.url.toLowerCase()!==`https://github.com/${i.repository}/issues/${i.issueNumber}`.toLowerCase()||!["OPEN","CLOSED"].includes(q.state))throw new Error("legacy-story-pr-post-merge-issue-invalid");return q.state};if(I&&await C()!=="CLOSED")return n("legacy-story-pr-post-merge-accepted-issue-drift");if(!I&&O===i.mergeCommitSha&&_===i.mergeCommitSha){if(j.length>0)return n("legacy-story-pr-post-merge-pre-issue-drift");const q={schemaVersion:1,kind:"legacy-story-pr-plan",storyId:i.storyId,epicId:i.epicId,repository:i.repository,branch:i.baseBranch,baseBranch:i.baseBranch,headSha:i.mergeCommitSha,baseSha:d.plan.baseSha,qaAuthority:"root-review-required",localOnly:!0};try{await de(q,e,t.verifyQa)}catch{return n("legacy-story-pr-post-merge-pre-issue-qa-failed")}if(g(e,"rev-parse","HEAD")!==i.mergeCommitSha||z(e,i.baseBranch)!==i.mergeCommitSha||v(e,"status","--porcelain=v1","-uall")!=="")return n("legacy-story-pr-post-merge-pre-issue-drift")}if(k===i.mergeCommitSha){if(x===f.stateBefore&&await Y(A($,".neocortex"),"state.json",f.stateAfter,h(f.stateBefore)),J===f.storyBefore&&await Y(A($,"docs/stories"),`${i.storyId}.story.md`,f.storyAfter,h(f.storyBefore)),E(A($,".neocortex/state.json"),2*1024*1024)!==f.stateAfter||E(A($,`docs/stories/${i.storyId}.story.md`),2*1024*1024)!==f.storyAfter)return n("legacy-story-pr-post-merge-write-drift");g($,"add","--",".neocortex/state.json",`docs/stories/${i.storyId}.story.md`);const q=v($,"diff","--cached","--name-only","-z").split("\0").filter(Boolean).sort();if(JSON.stringify(q)!==JSON.stringify([...B].sort())||v($,"diff","--name-only","-z")!=="")return n("legacy-story-pr-post-merge-staged-scope-invalid");g($,"diff","--cached","--check"),g($,"commit","-m",`chore(story): accept merged PR ${i.storyId}`,"-m",`Neocortex-Legacy-Merge: ${i.storyId}`),k=g($,"rev-parse","HEAD"),await t.afterCommit?.()}if(v($,"status","--porcelain=v1","-uall")!=="")return n("legacy-story-pr-post-merge-worktree-dirty");const V={schemaVersion:1,kind:"legacy-story-pr-plan",storyId:i.storyId,epicId:i.epicId,repository:i.repository,branch:i.baseBranch,baseBranch:i.baseBranch,headSha:k,baseSha:i.mergeCommitSha,qaAuthority:"root-review-required",localOnly:!0};let te=I?.qaSha256;if(!te){if(!t.verifyQa)try{et($)}catch(q){return n(q instanceof Error&&q.message.endsWith("-missing")?"legacy-story-pr-post-merge-dependencies-missing":"legacy-story-pr-post-merge-dependencies-failed")}try{te=await de(V,$,t.verifyQa)}catch{return n("legacy-story-pr-post-merge-qa-failed")}}if(g($,"rev-parse","HEAD")!==k||v($,"status","--porcelain=v1","-uall")!==""||g(e,"rev-parse","HEAD")!==O||v(e,"status","--porcelain=v1","-uall")!=="")return n("legacy-story-pr-post-merge-final-drift");if(await C()==="OPEN"){if(z(e,i.baseBranch)!==i.mergeCommitSha)return n("legacy-story-pr-post-merge-pre-issue-drift");await R.close(i.repository,i.issueNumber),await t.afterIssueClose?.()}if(await C()!=="CLOSED")return n("legacy-story-pr-post-merge-issue-not-closed");const K=await(t.mergeProvider??F).view(i.repository,i.prNumber);if(ee(d.plan,c.candidateHeadSha,K,"MERGED"),K.mergeCommit?.oid!==i.mergeCommitSha)return n("legacy-story-pr-post-merge-provider-drift");if(_=z(e,i.baseBranch),_===i.mergeCommitSha&&g(e,"push","--porcelain","origin",`${k}:refs/heads/${i.baseBranch}`),z(e,i.baseBranch)!==k)return n("legacy-story-pr-post-merge-push-unverified");if(await t.afterPush?.(),g(e,"rev-parse","HEAD")===i.mergeCommitSha&&g(e,"merge","--ff-only",k),g(e,"rev-parse","HEAD")!==k||v(e,"status","--porcelain=v1","-uall")!==""||E(A(e,".neocortex/state.json"),2*1024*1024)!==f.stateAfter||E(A(e,`docs/stories/${i.storyId}.story.md`),2*1024*1024)!==f.storyAfter)return n("legacy-story-pr-post-merge-base-readback-invalid");const M={schemaVersion:1,kind:"legacy-story-c12-post-merge-accepted",intentSha256:N,checkpointHeadSha:k,qaSha256:te};return I&&JSON.stringify(I)!==JSON.stringify(M)?n("legacy-story-pr-post-merge-accepted-conflict"):(I||(await L(s,p,`${JSON.stringify(M)}
67
- `),await P(A(s,p),s)),{ok:!0,value:Object.freeze({schemaVersion:1,kind:"legacy-story-c12-post-merge-accepted",storyId:i.storyId,epicId:i.epicId,prNumber:i.prNumber,issueNumber:i.issueNumber,mergeCommitSha:i.mergeCommitSha,checkpointHeadSha:k,qaSha256:te,reused:!!I,physicalMerge:!0,issueClosed:!0,checkpointAccepted:!0,publicSafe:!0})})}catch(e){const s=e instanceof Error?e.message:"";return n(s.startsWith("legacy-story-pr-")?s:s.includes("secure-filesystem-cas-conflict")?"legacy-story-pr-post-merge-write-raced":"legacy-story-pr-post-merge-effect-uncertain")}finally{await oe(a.path,a.owner).catch(()=>{})}}async function qt(t){const{cwd:r,directory:a,storyId:e,epicId:s,repository:o,baseBranch:l}=t,p=h(e),y=await Ie(a,`${p}.c12.json`),i=await Ae(a,`${p}.c12.prepared.json`),d=await Ce(a,`${p}.c12.merge-attempt.json`),c=await He(a,`${p}.c12.merged.json`),b=await ht(a,`${p}.c12.post-merge.json`),w=await ut(a,`${p}.c12.post-merge.accepted.json`);if(!y||!i||!d||!c||!b||!w)return n("legacy-epic-terminal-story-receipt-missing");const f=y.plan,m=b.plan;if(f.storyId!==e||f.epicId!==s||f.repository!==o||f.baseBranch!==l||m.storyId!==e||m.epicId!==s||m.repository!==o||m.baseBranch!==l||m.prNumber!==f.prNumber||m.candidateHeadSha!==i.candidateHeadSha||m.mergeCommitSha!==c.mergeCommitSha||m.stateSha256!==f.stateSha256||m.storySha256!==f.storySha256||i.intentSha256!==h(JSON.stringify(y))||d.intentSha256!==i.intentSha256||c.intentSha256!==d.intentSha256||d.candidateHeadSha!==i.candidateHeadSha||c.candidateHeadSha!==d.candidateHeadSha||c.qaSha256!==d.qaSha256||c.remoteObservationDigest!==d.remoteObservationDigest||w.intentSha256!==h(JSON.stringify(b)))return n("legacy-epic-terminal-story-receipt-drift");const u=w.checkpointHeadSha,I=g(r,"rev-parse",`${u}^`),N=g(r,"rev-list","--parents","-n","1",c.mergeCommitSha).split(" "),O=v(r,"diff-tree","--no-commit-id","--name-only","-r","-z",u).split("\0").filter(Boolean).sort(),_=`docs/stories/${e}.story.md`;if(I!==c.mergeCommitSha||N.length!==3||N[0]!==c.mergeCommitSha||N[1]!==f.baseSha||N[2]!==i.candidateHeadSha||JSON.stringify(O)!==JSON.stringify([".neocortex/state.json",_].sort())||!v(r,"log","-1","--format=%B",u).split(`
68
- `).map(M=>M.trim()).includes(`Neocortex-Legacy-Merge: ${e}`))return n("legacy-epic-terminal-story-git-drift");if(!t.firstParentHeads.has(u))return n("legacy-epic-terminal-story-not-on-base");const $=v(r,"show",`${u}:.neocortex/state.json`),k=v(r,"show",`${u}:${_}`);if(h($)!==b.stateAfterSha256||h(k)!==b.storyAfterSha256)return n("legacy-epic-terminal-story-checkpoint-drift");const j=S(JSON.parse($)),B=S(S(j?.stories)?.[e]),x=k.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/),J=x?ae(x[1],{strict:!0,uniqueKeys:!0}):null,R=J?S(J.toJS()):null,C=["stepsCompleted","steps_completed"].filter(M=>J?.has(M)),V=["lastStep","last_step"].filter(M=>J?.has(M));if(!B||B.id!==e||B.epic_id!==s||B.status!=="done"||B.last_step!==ye||!Array.isArray(B.steps_completed)||!B.steps_completed.includes(ye)||B.pr_number!==f.prNumber||B.issue_number!==m.issueNumber||B.issue_state!=="closed"||B.merged!==!0||B.physicalMergeStatus!=="merged"||B.merge_commit_sha!==c.mergeCommitSha||B.branch_name!==`story/${s}/${e}`||!J||J.errors.length>0||J.get("id")!==e||(J.get("epic_id")??J.get("epic"))!==s||J.get("status")!=="done"||J.has("currentStatus")&&J.get("currentStatus")!=="done"||C.length!==1||V.length!==1||JSON.stringify(R?.[C[0]])!==JSON.stringify(B.steps_completed)||R?.[V[0]]!==ye||R?.mergeCommitSha!==c.mergeCommitSha||R?.issueState!=="closed")return n("legacy-epic-terminal-story-state-drift");const te=await t.mergeProvider.view(o,f.prNumber);if(ee(f,i.candidateHeadSha,te,"MERGED"),te.mergeCommit?.oid!==c.mergeCommitSha)return n("legacy-epic-terminal-story-provider-drift");const K=await t.issueProvider.view(o,m.issueNumber);return K.number!==m.issueNumber||K.state!=="CLOSED"||K.title!==`[YOLO] Story ${e} - Workflow Run`||K.url.toLowerCase()!==`https://github.com/${o}/issues/${m.issueNumber}`.toLowerCase()?n("legacy-epic-terminal-story-issue-drift"):{ok:!0,value:Object.freeze({storyId:e,checkpointHeadSha:u,storyRowSha256:h(JSON.stringify(B)),storyDocumentSha256:h(k),qaSha256:w.qaSha256})}}async function Ne(t){if(!/^P[0-9]+$/.test(t.epicId))return n("legacy-epic-terminal-target-invalid");try{const r=D(t.projectRoot);if(D(g(r,"rev-parse","--show-toplevel"))!==r||v(r,"status","--porcelain=v1","-uall")!=="")return n("legacy-epic-terminal-checkout-invalid");for(const u of[".neocortex","docs","docs/stories"]){const I=A(r,u);if(!H(I)||!W(I).isDirectory()||D(I)!==I)return n("legacy-epic-terminal-checkout-unsafe")}const a=v(r,"show","HEAD:.neocortex/state.json");if(E(A(r,".neocortex/state.json"),2*1024*1024)!==a)return n("legacy-epic-terminal-state-drift");const e=S(JSON.parse(a)),s=S(e?.epics),o=S(e?.stories),l=S(s?.[t.epicId]),p=S(e?.config),y=l?.stories,i=p?.default_branch;if(!l||l.id!==t.epicId||l.branch_mode!=="story"||l.status!=="done"||!Array.isArray(y)||y.length===0||y.length>128||new Set(y).size!==y.length||y.some(u=>typeof u!="string"||!/^P[0-9]+\.[0-9]+(?:\.[0-9]+)*$/.test(u))||!o||Object.entries(o).some(([u,I])=>S(I)?.epic_id===t.epicId&&!y.includes(u))||l.total_stories!==y.length||l.completed_stories!==y.length||typeof i!="string"||!/^[A-Za-z0-9][A-Za-z0-9._/-]*$/.test(i)||g(r,"branch","--show-current")!==i)return n("legacy-epic-terminal-topology-invalid");const d=X(r),c=g(r,"rev-parse","HEAD");if(z(r,i)!==c)return n("legacy-epic-terminal-base-not-pushed");const b=A(me(r),"neocortex-yoloop"),w=A(b,"legacy-story-pr-intents");if(!H(b)||!H(w)||!W(b).isDirectory()||D(b)!==b)return n("legacy-epic-terminal-receipts-missing");await je(w,b);const f=new Set(g(r,"rev-list","--first-parent","--max-count=4096",c).split(`
69
- `).filter(Boolean)),m=[];for(const u of y){const I=await qt({cwd:r,directory:w,storyId:u,epicId:t.epicId,repository:d,baseBranch:i,firstParentHeads:f,mergeProvider:t.mergeProvider??F,issueProvider:t.issueProvider??Ke});if(!I.ok)return I;const N=S(o[u]),O=E(A(r,`docs/stories/${u}.story.md`),2*1024*1024);if(!N||h(JSON.stringify(N))!==I.value.storyRowSha256||h(O)!==I.value.storyDocumentSha256)return n("legacy-epic-terminal-story-current-drift");m.push(I.value)}return!m.some(u=>u.checkpointHeadSha===c)||g(r,"rev-parse","HEAD")!==c||z(r,i)!==c||v(r,"status","--porcelain=v1","-uall")!==""?n("legacy-epic-terminal-base-final-drift"):{ok:!0,value:Object.freeze({schemaVersion:1,kind:"legacy-epic-terminal-plan",epicId:t.epicId,repositorySha256:h(d),baseBranchSha256:h(i),baseHeadSha:c,storyCount:m.length,storyEvidenceSha256:h(JSON.stringify(m)),physicalStoriesVerified:!0,quotaReleaseAuthorized:!1,checkpointAccepted:!1,localOnly:!0,publicSafe:!0})}}catch(r){const a=r instanceof Error?r.message:"";return n(a.startsWith("legacy-epic-terminal-")?a:"legacy-epic-terminal-physical-unavailable")}}async function Ze(t,r){const a=A(t,r);if(!H(a))return null;await P(a,t);const e=S(JSON.parse(E(a,4096)));if(!e||Object.keys(e).sort().join(",")!==["schemaVersion","kind","epicId","planSha256","baseHeadSha","qaEvidenceDigest","acceptedAt"].sort().join(",")||e.schemaVersion!==1||e.kind!=="legacy-epic-terminal-root-accepted"||typeof e.epicId!="string"||!/^P[0-9]+$/.test(e.epicId)||typeof e.planSha256!="string"||!/^[a-f0-9]{64}$/.test(e.planSha256)||typeof e.baseHeadSha!="string"||!/^[a-f0-9]{40}$/.test(e.baseHeadSha)||typeof e.qaEvidenceDigest!="string"||!/^[a-f0-9]{64}$/.test(e.qaEvidenceDigest)||typeof e.acceptedAt!="string"||!Number.isFinite(Date.parse(e.acceptedAt)))throw new Error("legacy-epic-terminal-journal-invalid");return e}function Fe(t,r,a){return Object.freeze({schemaVersion:1,kind:"legacy-epic-terminal-root-accepted",epicId:t.epicId,baseHeadSha:t.baseHeadSha,storyCount:t.storyCount,planSha256:r.planSha256,qaEvidenceDigest:r.qaEvidenceDigest,reused:a,rootAcceptanceRecorded:!0,quotaReleaseAuthorized:!1,serverAccountingAccepted:!1,localOnly:!0,publicSafe:!0})}async function fr(t){const r=await Ne(t);if(!r.ok)return r;try{const a=D(t.projectRoot),e=A(me(a),"neocortex-yoloop"),s=A(e,"legacy-epic-terminal");if(!H(s))return n("legacy-epic-terminal-acceptance-missing");await je(s,e);const o=await Ze(s,`${h(t.epicId)}.accepted.json`);if(!o)return n("legacy-epic-terminal-acceptance-missing");if(o.epicId!==t.epicId||o.baseHeadSha!==r.value.baseHeadSha||o.planSha256!==h(JSON.stringify(r.value)))return n("legacy-epic-terminal-acceptance-drift");const l=await Ne(t);return!l.ok||h(JSON.stringify(l.value))!==o.planSha256?n("legacy-epic-terminal-acceptance-drift"):{ok:!0,value:Fe(l.value,o,!0)}}catch(a){const e=a instanceof Error?a.message:"";return n(e.startsWith("legacy-epic-terminal-")?e:"legacy-epic-terminal-acceptance-unavailable")}}async function hr(t){if(!t.authorize)return n("legacy-epic-terminal-authorization-required");if(!/^P[0-9]+$/.test(t.epicId))return n("legacy-epic-terminal-target-invalid");const r=`legacy-epic-terminal-${re(16).toString("hex")}`,a=await se(t.projectRoot,{kind:"direct",storyId:t.epicId,sessionId:r,idempotencyKey:r,expiresAt:new Date(Date.now()+60*6e4).toISOString(),persistent:!1});if(!a.ok)return n(a.reasonCode);try{const e=D(t.projectRoot),s=await Ne(t);if(!s.ok)return s;const o=await T(await G(e),"legacy-epic-terminal"),l=`${h(t.epicId)}.accepted.json`,p=h(JSON.stringify(s.value)),y=await Ze(o,l);if(y){if(y.epicId!==t.epicId||y.baseHeadSha!==s.value.baseHeadSha||y.planSha256!==p)return n("legacy-epic-terminal-acceptance-conflict");const f=await Ne(t);return!f.ok||h(JSON.stringify(f.value))!==p?n("legacy-epic-terminal-acceptance-drift"):{ok:!0,value:Fe(f.value,y,!0)}}let i;try{if(t.verifyQa)i=await t.verifyQa(s.value);else{const f=await ze(e,{headSha:s.value.baseHeadSha,graphRevision:h(`legacy-epic-terminal\0${p}`)}),m=Le(f);if(!m||m.executionHeadSha!==s.value.baseHeadSha)return n("legacy-epic-terminal-qa-invalid");i=m.executionEvidenceDigest}}catch{return n("legacy-epic-terminal-qa-failed")}if(!/^[a-f0-9]{64}$/.test(i))return n("legacy-epic-terminal-qa-invalid");const d=await Ne(t);if(!d.ok||h(JSON.stringify(d.value))!==p)return n("legacy-epic-terminal-post-qa-drift");const c={schemaVersion:1,kind:"legacy-epic-terminal-root-accepted",epicId:t.epicId,planSha256:p,baseHeadSha:s.value.baseHeadSha,qaEvidenceDigest:i,acceptedAt:new Date().toISOString()};await L(o,l,`${JSON.stringify(c)}
70
- `);const b=await Ze(o,l);if(!b||JSON.stringify(b)!==JSON.stringify(c))return n("legacy-epic-terminal-acceptance-write-raced");const w=await Ne(t);return!w.ok||h(JSON.stringify(w.value))!==p?n("legacy-epic-terminal-post-accept-drift"):{ok:!0,value:Fe(w.value,c,!1)}}catch(e){const s=e instanceof Error?e.message:"";return n(s.startsWith("legacy-epic-terminal-")?s:"legacy-epic-terminal-acceptance-unavailable")}finally{await oe(a.path,a.owner).catch(()=>{})}}export{hr as acceptLegacyEpicFinalization,er as acceptLegacyStoryCommitC08,ar as acceptLegacyStoryPrC10,pr as acceptLegacyStoryPrPostMergeC12,nr as acceptLegacyStoryPrReviewC11,tr as acceptLegacyStorySyncC09,rr as createLegacyStoryPr,lr as mergeLegacyStoryPrC12,or as observeLegacyStoryPrReviewC11,Ne as planLegacyEpicFinalization,Ct as planLegacyStoryCommitC08,be as planLegacyStoryPr,yt as planLegacyStoryPrMergeC12,pt as planLegacyStoryPrMergeDecisionC12,Rt as planLegacyStoryPrPostMergeC12,yr as planLegacyStoryPrRemoteDebtBeforePrepareC12,ir as planLegacyStoryPrReview,_t as planLegacyStorySyncC09,cr as prepareLegacyStoryPrMergeC12,sr as readAcceptedLegacyStoryPrC10,xt as readAcceptedLegacyStoryPrReviewC11,fr as readLegacyEpicTerminalAcceptance,dr as recordLegacyStoryPrRemoteDebtC12};
1
+ /** Root-only, provider-verified PR effect for a legacy story branch. */
2
+ import { execFileSync } from 'node:child_process';
3
+ import { createHash, randomBytes } from 'node:crypto';
4
+ import { closeSync, constants, existsSync, fstatSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, realpathSync, rmdirSync, unlinkSync, writeFileSync } from 'node:fs';
5
+ import { tmpdir } from 'node:os';
6
+ import { isAbsolute, join } from 'node:path';
7
+ import { parseDocument } from 'yaml';
8
+ import { projectC12ScopedCiFollowUpDebt, summarizeC12RemoteRollup } from '../../../shared/dist/merge-readiness/c12-remote-observation.js';
9
+ import { projectC12ScopedCiRegistryUpdate } from '../../../shared/dist/merge-readiness/c12-registry-projection.js';
10
+ import { resolveBranchUnit } from '../../../shared/dist/yoloop/branch-unit.js';
11
+ import { acquireGitLifecyclePhysicalIntegrationLock, assertGitLifecyclePrivateDirectory, assertGitLifecyclePrivateFile, ensureGitLifecyclePrivateChildDirectory, gitLifecycleRuntimeDirectory, releaseGitLifecyclePhysicalIntegrationLock } from './git-lifecycle-store.js';
12
+ import { anchoredAtomicWriteFile, anchoredCompareAndSwapFile } from './secure-filesystem-boundary.js';
13
+ import { GIT_LIFECYCLE_CREATE_PR_STEP_ID, GIT_LIFECYCLE_MERGE_PR_STEP_ID, GIT_LIFECYCLE_REVIEW_PR_STEP_ID, GIT_LIFECYCLE_COMMIT_PUSH_STEP_ID, GIT_LIFECYCLE_SYNC_MAIN_STEP_ID, GIT_LIFECYCLE_UPDATE_PROJECT_MEMORY_STEP_ID } from './git-lifecycle-step-ids.js';
14
+ import { executeP205TransientAuthority, projectP205TransientExecutionEvidence } from './git-lifecycle-store.js';
15
+ import { readCompletedLegacyStoryBranchPlan } from './legacy-story-branch-setup.js';
16
+ function fail(reasonCode) { return { ok: false, reasonCode }; }
17
+ function record(value) {
18
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
19
+ }
20
+ function git(cwd, ...args) {
21
+ return execFileSync('git', args, { cwd, encoding: 'utf8', timeout: 15_000,
22
+ stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 2 * 1024 * 1024 }).trim();
23
+ }
24
+ function gitRaw(cwd, ...args) {
25
+ return execFileSync('git', args, { cwd, encoding: 'utf8', timeout: 15_000,
26
+ stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 2 * 1024 * 1024 });
27
+ }
28
+ function safeRead(path, limit) {
29
+ const fd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
30
+ try {
31
+ const stat = fstatSync(fd);
32
+ if (!stat.isFile() || stat.size > limit)
33
+ throw new Error('legacy-story-pr-file-unsafe');
34
+ return readFileSync(fd, 'utf8');
35
+ }
36
+ finally {
37
+ closeSync(fd);
38
+ }
39
+ }
40
+ function singleRemoteSha(cwd, branch) {
41
+ const rows = git(cwd, 'ls-remote', '--heads', 'origin', `refs/heads/${branch}`).split('\n').filter(Boolean);
42
+ if (rows.length !== 1)
43
+ throw new Error('legacy-story-pr-remote-ref-missing');
44
+ const match = rows[0].match(/^([a-f0-9]{40})\s+refs\/heads\/(.+)$/);
45
+ if (!match || match[2] !== branch)
46
+ throw new Error('legacy-story-pr-remote-ref-invalid');
47
+ return match[1];
48
+ }
49
+ function repository(cwd) {
50
+ const remote = git(cwd, 'config', '--get', 'remote.origin.url');
51
+ const match = remote.match(/^(?:https:\/\/github\.com\/|git@github\.com:)([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+?)(?:\.git)?$/);
52
+ if (!match || match[1].includes('..') || match[2].includes('..')) {
53
+ throw new Error('legacy-story-pr-repository-invalid');
54
+ }
55
+ return `${match[1]}/${match[2]}`;
56
+ }
57
+ function gitCommonDirectory(cwd) {
58
+ const raw = git(cwd, 'rev-parse', '--git-common-dir');
59
+ return realpathSync(isAbsolute(raw) ? raw : join(cwd, raw));
60
+ }
61
+ function selectedPr(plan, items) {
62
+ if (!Array.isArray(items) || items.length >= 100)
63
+ throw new Error('legacy-story-pr-list-incomplete');
64
+ const relevant = items.filter((item) => item.headRefName === plan.branch);
65
+ if (relevant.length > 1 || relevant.some((item) => item.state !== 'OPEN'
66
+ || item.baseRefName !== plan.baseBranch || item.headRefOid !== plan.headSha)) {
67
+ throw new Error('legacy-story-pr-provider-conflict');
68
+ }
69
+ const item = relevant[0];
70
+ if (!item)
71
+ return null;
72
+ if (!Number.isSafeInteger(item.number) || item.number <= 0
73
+ || item.url.toLowerCase() !== `https://github.com/${plan.repository}/pull/${item.number}`.toLowerCase()) {
74
+ throw new Error('legacy-story-pr-provider-invalid');
75
+ }
76
+ return item;
77
+ }
78
+ async function recordCreateAttempt(projectRoot, plan) {
79
+ const directory = await ensureGitLifecyclePrivateChildDirectory(await gitLifecycleRuntimeDirectory(projectRoot), 'legacy-story-pr-intents');
80
+ const name = `${createHash('sha256').update(plan.storyId).digest('hex')}.json`;
81
+ const path = join(directory, name);
82
+ const intent = { schemaVersion: 1, kind: 'legacy-story-pr-create-attempt',
83
+ storyId: plan.storyId, epicId: plan.epicId, repository: plan.repository,
84
+ branch: plan.branch, baseBranch: plan.baseBranch, headSha: plan.headSha };
85
+ if (existsSync(path)) {
86
+ await assertGitLifecyclePrivateFile(path, directory);
87
+ if (safeRead(path, 4096) !== `${JSON.stringify(intent)}\n`) {
88
+ throw new Error('legacy-story-pr-attempt-conflict');
89
+ }
90
+ return false;
91
+ }
92
+ await anchoredAtomicWriteFile(directory, name, `${JSON.stringify(intent)}\n`);
93
+ await assertGitLifecyclePrivateFile(path, directory);
94
+ return true;
95
+ }
96
+ /** Root-owned C08 basis. Only a cleanly staged, same-story write set may
97
+ * cross into a future physical commit. This plan neither commits nor pushes. */
98
+ export function planLegacyStoryCommitC08(options) {
99
+ if (!/^[A-Za-z0-9][A-Za-z0-9_-]*\.[A-Za-z0-9._-]+$/.test(options.storyId)) {
100
+ return fail('legacy-story-commit-target-invalid');
101
+ }
102
+ try {
103
+ const cwd = realpathSync(options.projectRoot);
104
+ if (realpathSync(git(cwd, 'rev-parse', '--show-toplevel')) !== cwd
105
+ || gitRaw(cwd, 'diff', '--name-only', '-z') !== ''
106
+ || gitRaw(cwd, 'ls-files', '--others', '--exclude-standard', '-z') !== '') {
107
+ return fail('legacy-story-commit-checkout-invalid');
108
+ }
109
+ const changedPaths = gitRaw(cwd, 'diff', '--cached', '--name-only', '-z').split('\0').filter(Boolean);
110
+ if (changedPaths.length === 0 || new Set(changedPaths).size !== changedPaths.length
111
+ || changedPaths.some((path) => path.length > 1024 || path.startsWith('/')
112
+ || path.includes('\\') || path.includes('\0') || path.split('/').includes('..')
113
+ || path === '.git' || path.startsWith('.git/'))) {
114
+ return fail('legacy-story-commit-write-set-invalid');
115
+ }
116
+ if (gitRaw(cwd, 'ls-files', '-u', '-z') !== '' || changedPaths.some((path) => {
117
+ const entries = gitRaw(cwd, 'ls-files', '-s', '-z', '--', path).split('\0').filter(Boolean);
118
+ return entries.length > 1 || entries.some((entry) => {
119
+ const match = entry.match(/^(100644|100755) [a-f0-9]{40} 0\t(.+)$/);
120
+ return !match || match[2] !== path;
121
+ });
122
+ }))
123
+ return fail('legacy-story-commit-index-unsafe');
124
+ git(cwd, 'diff', '--cached', '--check');
125
+ const stateRaw = gitRaw(cwd, 'show', ':.neocortex/state.json');
126
+ const storyPath = `docs/stories/${options.storyId}.story.md`;
127
+ const storyRaw = gitRaw(cwd, 'show', `:${storyPath}`);
128
+ if (safeRead(join(cwd, '.neocortex/state.json'), 2 * 1024 * 1024) !== stateRaw
129
+ || safeRead(join(cwd, storyPath), 2 * 1024 * 1024) !== storyRaw) {
130
+ return fail('legacy-story-commit-preimage-drift');
131
+ }
132
+ const state = record(JSON.parse(stateRaw));
133
+ const stories = record(state?.stories);
134
+ const story = record(stories?.[options.storyId]);
135
+ const epicId = story?.epic_id;
136
+ const epic = typeof epicId === 'string' ? record(record(state?.epics)?.[epicId]) : null;
137
+ const ids = epic?.stories;
138
+ if (!story || story.id !== options.storyId || typeof epicId !== 'string'
139
+ || !epic || epic.id !== epicId || epic.branch_mode === 'epic'
140
+ || story.status !== 'memory-updated' || story.last_step !== GIT_LIFECYCLE_UPDATE_PROJECT_MEMORY_STEP_ID
141
+ || !Array.isArray(story.steps_completed)
142
+ || !story.steps_completed.includes(GIT_LIFECYCLE_UPDATE_PROJECT_MEMORY_STEP_ID)
143
+ || story.steps_completed.includes(GIT_LIFECYCLE_COMMIT_PUSH_STEP_ID)
144
+ || !Number.isSafeInteger(story.issue_number) || story.issue_number <= 0
145
+ || !Array.isArray(ids) || ids.length === 0 || new Set(ids).size !== ids.length
146
+ || !ids.includes(options.storyId)
147
+ || ids.some((id) => typeof id !== 'string' || record(stories?.[id])?.epic_id !== epicId)
148
+ || Object.entries(stories ?? {}).some(([id, value]) => record(value)?.epic_id === epicId && !ids.includes(id))) {
149
+ return fail('legacy-story-commit-state-invalid');
150
+ }
151
+ const unit = resolveBranchUnit(epic, ids.map((id) => ({ ...record(stories?.[id]), id })));
152
+ const branch = `story/${epicId}/${options.storyId}`;
153
+ const baseBranch = record(state?.config)?.default_branch;
154
+ if (unit.mode !== 'story' || story.branch_name !== branch
155
+ || git(cwd, 'branch', '--show-current') !== branch
156
+ || typeof baseBranch !== 'string' || baseBranch === branch
157
+ || git(cwd, 'check-ref-format', '--branch', baseBranch) !== baseBranch) {
158
+ return fail('legacy-story-commit-branch-invalid');
159
+ }
160
+ // The staged story is not an authority to widen its own write set. Pin it
161
+ // to the committed source specification before admitting staged paths.
162
+ const committedState = record(JSON.parse(gitRaw(cwd, 'show', 'HEAD:.neocortex/state.json')));
163
+ const committedStory = record(record(committedState?.stories)?.[options.storyId]);
164
+ const allowed = Array.isArray(committedStory?.files_to_modify)
165
+ ? committedStory.files_to_modify : null;
166
+ if (!allowed || allowed.length === 0 || new Set(allowed).size !== allowed.length
167
+ || committedStory?.epic_id !== epicId
168
+ || JSON.stringify(story.files_to_modify) !== JSON.stringify(allowed)
169
+ || allowed.some((path) => typeof path !== 'string' || path.length > 1024
170
+ || path.startsWith('/') || path.includes('\\') || path.split('/').includes('..')
171
+ || path === '.git' || path.startsWith('.git/'))
172
+ || !changedPaths.every((path) => allowed.includes(path))
173
+ || !changedPaths.includes('.neocortex/state.json') || !changedPaths.includes(storyPath)
174
+ || !allowed.includes('.neocortex/state.json') || !allowed.includes(storyPath)) {
175
+ return fail('legacy-story-commit-write-set-unauthorized');
176
+ }
177
+ const frontmatter = storyRaw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);
178
+ if (!frontmatter)
179
+ return fail('legacy-story-commit-frontmatter-invalid');
180
+ const document = parseDocument(frontmatter[1], { strict: true, uniqueKeys: true });
181
+ const fields = record(document.toJS());
182
+ const stepsKeys = ['stepsCompleted', 'steps_completed'].filter((key) => document.has(key));
183
+ const lastKeys = ['lastStep', 'last_step'].filter((key) => document.has(key));
184
+ const branchKeys = ['branchName', 'branch_name'].filter((key) => document.has(key));
185
+ const issueKeys = ['issueNumber', 'issue_number'].filter((key) => document.has(key));
186
+ if (document.errors.length > 0 || document.get('id') !== options.storyId
187
+ || (document.get('epic_id') ?? document.get('epic')) !== epicId
188
+ || document.get('status') !== 'memory-updated'
189
+ || document.has('currentStatus') && document.get('currentStatus') !== 'memory-updated'
190
+ || stepsKeys.length !== 1 || lastKeys.length !== 1 || branchKeys.length !== 1
191
+ || issueKeys.length !== 1
192
+ || JSON.stringify(fields?.[stepsKeys[0]]) !== JSON.stringify(story.steps_completed)
193
+ || fields?.[lastKeys[0]] !== GIT_LIFECYCLE_UPDATE_PROJECT_MEMORY_STEP_ID
194
+ || fields?.[branchKeys[0]] !== branch
195
+ || fields?.[issueKeys[0]] !== story.issue_number) {
196
+ return fail('legacy-story-commit-frontmatter-invalid');
197
+ }
198
+ const headSha = git(cwd, 'rev-parse', 'HEAD');
199
+ // C01 creates the story branch locally. C08 is its first remote push;
200
+ // an existing remote ref is ambiguous and cannot be silently adopted.
201
+ const remoteRows = git(cwd, 'ls-remote', '--heads', 'origin', `refs/heads/${branch}`)
202
+ .split('\n').filter(Boolean);
203
+ const baseSha = singleRemoteSha(cwd, baseBranch);
204
+ if (!/^[a-f0-9]{40}$/.test(headSha) || remoteRows.length !== 0
205
+ || git(cwd, 'rev-parse', `refs/remotes/origin/${baseBranch}`) !== baseSha) {
206
+ return fail('legacy-story-commit-remote-drift');
207
+ }
208
+ return { ok: true, value: Object.freeze({ schemaVersion: 1,
209
+ kind: 'legacy-story-c08-commit-plan', storyId: options.storyId, epicId,
210
+ repository: repository(cwd), branch, baseBranch, headSha, remoteHeadSha: null,
211
+ baseSha, indexDigestSha256: digest(gitRaw(cwd, 'ls-files', '-s', '-z')),
212
+ stagedPatchSha256: digest(gitRaw(cwd, 'diff', '--cached', '--binary', '--full-index')),
213
+ changedPaths: Object.freeze(changedPaths), stateSha256: digest(stateRaw),
214
+ storySha256: digest(storyRaw),
215
+ stateIndexBlobSha: git(cwd, 'rev-parse', ':.neocortex/state.json'),
216
+ storyIndexBlobSha: git(cwd, 'rev-parse', `:${storyPath}`),
217
+ physicalCommit: false,
218
+ checkpointAccepted: false, localOnly: true }) };
219
+ }
220
+ catch {
221
+ return fail('legacy-story-commit-physical-unavailable');
222
+ }
223
+ }
224
+ function projectLegacyStoryCommitC08(cwd, plan) {
225
+ const stateBefore = gitRaw(cwd, 'show', plan.stateIndexBlobSha);
226
+ const storyBefore = gitRaw(cwd, 'show', plan.storyIndexBlobSha);
227
+ if (git(cwd, 'cat-file', '-t', plan.stateIndexBlobSha) !== 'blob'
228
+ || git(cwd, 'cat-file', '-t', plan.storyIndexBlobSha) !== 'blob'
229
+ || digest(stateBefore) !== plan.stateSha256 || digest(storyBefore) !== plan.storySha256) {
230
+ throw new Error('legacy-story-commit-preimage-invalid');
231
+ }
232
+ const state = record(JSON.parse(stateBefore));
233
+ const story = record(record(state?.stories)?.[plan.storyId]);
234
+ if (!story || story.epic_id !== plan.epicId || story.status !== 'memory-updated'
235
+ || story.last_step !== GIT_LIFECYCLE_UPDATE_PROJECT_MEMORY_STEP_ID
236
+ || !Array.isArray(story.steps_completed)
237
+ || !story.steps_completed.includes(GIT_LIFECYCLE_UPDATE_PROJECT_MEMORY_STEP_ID)
238
+ || story.steps_completed.includes(GIT_LIFECYCLE_COMMIT_PUSH_STEP_ID)) {
239
+ throw new Error('legacy-story-commit-projection-state-invalid');
240
+ }
241
+ const priorSteps = story.steps_completed;
242
+ story.status = 'committed';
243
+ story.last_step = GIT_LIFECYCLE_COMMIT_PUSH_STEP_ID;
244
+ story.steps_completed = [...priorSteps, GIT_LIFECYCLE_COMMIT_PUSH_STEP_ID];
245
+ const stateAfter = `${JSON.stringify(state, null, 2)}\n`;
246
+ const frontmatter = storyBefore.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);
247
+ if (!frontmatter)
248
+ throw new Error('legacy-story-commit-projection-frontmatter-invalid');
249
+ const document = parseDocument(frontmatter[1], { strict: true, uniqueKeys: true });
250
+ const fields = record(document.toJS());
251
+ const stepsKeys = ['stepsCompleted', 'steps_completed'].filter((key) => document.has(key));
252
+ const lastKeys = ['lastStep', 'last_step'].filter((key) => document.has(key));
253
+ if (document.errors.length > 0 || document.get('id') !== plan.storyId
254
+ || (document.get('epic_id') ?? document.get('epic')) !== plan.epicId
255
+ || document.get('status') !== 'memory-updated'
256
+ || stepsKeys.length !== 1 || lastKeys.length !== 1
257
+ || JSON.stringify(fields?.[stepsKeys[0]]) !== JSON.stringify(priorSteps)
258
+ || fields?.[lastKeys[0]] !== GIT_LIFECYCLE_UPDATE_PROJECT_MEMORY_STEP_ID) {
259
+ throw new Error('legacy-story-commit-projection-frontmatter-invalid');
260
+ }
261
+ document.set('status', 'committed');
262
+ if (document.has('currentStatus'))
263
+ document.set('currentStatus', 'committed');
264
+ document.set(stepsKeys[0], story.steps_completed);
265
+ document.set(lastKeys[0], GIT_LIFECYCLE_COMMIT_PUSH_STEP_ID);
266
+ const storyAfter = `---\n${document.toString({ lineWidth: 0 })}---\n${storyBefore.slice(frontmatter[0].length)}`;
267
+ return { stateBefore, storyBefore, stateAfter, storyAfter };
268
+ }
269
+ async function verifyLegacyStoryC01BeforeCommit(cwd, plan, seam) {
270
+ if (seam)
271
+ return seam();
272
+ const completed = await readCompletedLegacyStoryBranchPlan({ projectRoot: cwd, storyId: plan.storyId });
273
+ if (!completed.ok || completed.value.epicId !== plan.epicId
274
+ || completed.value.branch !== plan.branch || completed.value.baseBranch !== plan.baseBranch
275
+ || completed.value.worktreePath !== cwd)
276
+ return false;
277
+ try {
278
+ git(cwd, 'merge-base', '--is-ancestor', completed.value.baseHeadSha, plan.headSha);
279
+ }
280
+ catch {
281
+ return false;
282
+ }
283
+ const directory = await ensureGitLifecyclePrivateChildDirectory(await gitLifecycleRuntimeDirectory(cwd), 'legacy-story-branch-intents');
284
+ const path = join(directory, `${digest(plan.storyId)}.c01.accepted.json`);
285
+ if (!existsSync(path))
286
+ return false;
287
+ await assertGitLifecyclePrivateFile(path, directory);
288
+ const marker = record(JSON.parse(safeRead(path, 4096)));
289
+ const state = record(JSON.parse(gitRaw(cwd, 'show', plan.stateIndexBlobSha)));
290
+ const story = record(record(state?.stories)?.[plan.storyId]);
291
+ return !!marker && marker.schemaVersion === 1
292
+ && marker.kind === 'legacy-story-c01-acceptance-intent'
293
+ && marker.storyId === plan.storyId
294
+ && marker.planDigest === digest(JSON.stringify(completed.value))
295
+ && marker.repository === plan.repository
296
+ && marker.issueNumber === story?.issue_number;
297
+ }
298
+ function c08OtherIndex(cwd, plan) {
299
+ return plan.changedPaths.filter((path) => path !== '.neocortex/state.json'
300
+ && path !== `docs/stories/${plan.storyId}.story.md`).map((path) => {
301
+ const entries = gitRaw(cwd, 'ls-files', '-s', '-z', '--', path).split('\0').filter(Boolean);
302
+ if (entries.length > 1)
303
+ throw new Error('legacy-story-commit-index-invalid');
304
+ return { path, entry: entries[0] ?? null };
305
+ });
306
+ }
307
+ /** Root-owned C08 effect: one locally committed story checkpoint, full P205 on
308
+ * that immutable HEAD, then a first non-force remote push and exact readback. */
309
+ export async function acceptLegacyStoryCommitC08(options) {
310
+ if (!options.authorize)
311
+ return fail('legacy-story-commit-authorization-required');
312
+ if (!/^[A-Za-z0-9][A-Za-z0-9_-]*\.[A-Za-z0-9._-]+$/.test(options.storyId)) {
313
+ return fail('legacy-story-commit-target-invalid');
314
+ }
315
+ const sessionId = `legacy-c08-${randomBytes(16).toString('hex')}`;
316
+ const lock = await acquireGitLifecyclePhysicalIntegrationLock(options.projectRoot, {
317
+ kind: 'direct', storyId: options.storyId, sessionId, idempotencyKey: sessionId,
318
+ expiresAt: new Date(Date.now() + 60 * 60_000).toISOString(), persistent: false,
319
+ });
320
+ if (!lock.ok)
321
+ return fail(lock.reasonCode);
322
+ try {
323
+ const cwd = realpathSync(options.projectRoot);
324
+ if (realpathSync(git(cwd, 'rev-parse', '--show-toplevel')) !== cwd) {
325
+ return fail('legacy-story-commit-checkout-invalid');
326
+ }
327
+ const directory = await ensureGitLifecyclePrivateChildDirectory(await gitLifecycleRuntimeDirectory(cwd), 'legacy-story-pr-intents');
328
+ const prefix = digest(options.storyId);
329
+ const intentName = `${prefix}.c08.commit.json`;
330
+ const acceptedName = `${prefix}.c08.commit.accepted.json`;
331
+ let intentRaw = await readC09PrivateJson(directory, intentName, 64 * 1024);
332
+ if (!intentRaw) {
333
+ const planned = planLegacyStoryCommitC08(options);
334
+ if (!planned.ok)
335
+ return planned;
336
+ const plan = planned.value;
337
+ if (!await verifyLegacyStoryC01BeforeCommit(cwd, plan, options.verifyC01)) {
338
+ return fail('legacy-story-commit-c01-unverified');
339
+ }
340
+ const projection = projectLegacyStoryCommitC08(cwd, plan);
341
+ const intent = { schemaVersion: 1, kind: 'legacy-story-c08-commit-intent',
342
+ plan, stateAfterSha256: digest(projection.stateAfter),
343
+ storyAfterSha256: digest(projection.storyAfter), otherIndex: c08OtherIndex(cwd, plan) };
344
+ await anchoredAtomicWriteFile(directory, intentName, `${JSON.stringify(intent)}\n`);
345
+ await assertGitLifecyclePrivateFile(join(directory, intentName), directory);
346
+ intentRaw = intent;
347
+ }
348
+ const planRaw = record(intentRaw.plan);
349
+ if (intentRaw.schemaVersion !== 1 || intentRaw.kind !== 'legacy-story-c08-commit-intent'
350
+ || !planRaw || planRaw.schemaVersion !== 1 || planRaw.kind !== 'legacy-story-c08-commit-plan'
351
+ || planRaw.storyId !== options.storyId || typeof planRaw.epicId !== 'string'
352
+ || planRaw.branch !== `story/${planRaw.epicId}/${options.storyId}`
353
+ || typeof planRaw.repository !== 'string' || typeof planRaw.baseBranch !== 'string'
354
+ || typeof planRaw.headSha !== 'string' || !/^[a-f0-9]{40}$/.test(planRaw.headSha)
355
+ || typeof planRaw.stateIndexBlobSha !== 'string' || !/^[a-f0-9]{40}$/.test(planRaw.stateIndexBlobSha)
356
+ || typeof planRaw.storyIndexBlobSha !== 'string' || !/^[a-f0-9]{40}$/.test(planRaw.storyIndexBlobSha)
357
+ || !Array.isArray(planRaw.changedPaths) || !planRaw.changedPaths.includes('.neocortex/state.json')
358
+ || !planRaw.changedPaths.includes(`docs/stories/${options.storyId}.story.md`)
359
+ || planRaw.remoteHeadSha !== null || planRaw.physicalCommit !== false
360
+ || planRaw.checkpointAccepted !== false
361
+ || typeof intentRaw.stateAfterSha256 !== 'string'
362
+ || !/^[a-f0-9]{64}$/.test(intentRaw.stateAfterSha256)
363
+ || typeof intentRaw.storyAfterSha256 !== 'string'
364
+ || !/^[a-f0-9]{64}$/.test(intentRaw.storyAfterSha256)
365
+ || !Array.isArray(intentRaw.otherIndex)
366
+ || intentRaw.otherIndex.length !== planRaw.changedPaths.length - 2
367
+ || intentRaw.otherIndex.some((item) => {
368
+ const value = record(item);
369
+ return !value || typeof value.path !== 'string'
370
+ || !planRaw.changedPaths.includes(value.path)
371
+ || value.path === '.neocortex/state.json'
372
+ || value.path === `docs/stories/${options.storyId}.story.md`
373
+ || value.entry !== null && (typeof value.entry !== 'string'
374
+ || !/^(100644|100755) [a-f0-9]{40} 0\t/.test(value.entry)
375
+ || !value.entry.endsWith(`\t${value.path}`));
376
+ })
377
+ || !/^[a-f0-9]{64}$/.test(String(planRaw.indexDigestSha256))
378
+ || !/^[a-f0-9]{64}$/.test(String(planRaw.stagedPatchSha256))
379
+ || !/^[a-f0-9]{64}$/.test(String(planRaw.stateSha256))
380
+ || !/^[a-f0-9]{64}$/.test(String(planRaw.storySha256))
381
+ || new Set(planRaw.changedPaths).size !== planRaw.changedPaths.length
382
+ || planRaw.changedPaths.some((path) => typeof path !== 'string'
383
+ || path.length > 1024 || path.startsWith('/') || path.includes('\\')
384
+ || path.split('/').includes('..') || path === '.git' || path.startsWith('.git/'))
385
+ || repository(cwd) !== planRaw.repository || git(cwd, 'branch', '--show-current') !== planRaw.branch) {
386
+ return fail('legacy-story-commit-intent-invalid');
387
+ }
388
+ const plan = planRaw;
389
+ if (!await verifyLegacyStoryC01BeforeCommit(cwd, plan, options.verifyC01)) {
390
+ return fail('legacy-story-commit-c01-unverified');
391
+ }
392
+ const projection = projectLegacyStoryCommitC08(cwd, plan);
393
+ if (digest(projection.stateAfter) !== intentRaw.stateAfterSha256
394
+ || digest(projection.storyAfter) !== intentRaw.storyAfterSha256
395
+ || JSON.stringify(c08OtherIndex(cwd, plan)) !== JSON.stringify(intentRaw.otherIndex)) {
396
+ return fail('legacy-story-commit-intent-drift');
397
+ }
398
+ const intentSha256 = digest(JSON.stringify(intentRaw));
399
+ const acceptedRaw = await readC09PrivateJson(directory, acceptedName, 4096);
400
+ const accepted = acceptedRaw;
401
+ if (accepted && (accepted.schemaVersion !== 1 || accepted.kind !== 'legacy-story-c08-commit-accepted'
402
+ || accepted.intentSha256 !== intentSha256
403
+ || !/^[a-f0-9]{40}$/.test(accepted.checkpointHeadSha)
404
+ || !/^[a-f0-9]{64}$/.test(accepted.qaSha256))) {
405
+ return fail('legacy-story-commit-accepted-invalid');
406
+ }
407
+ let headSha = git(cwd, 'rev-parse', 'HEAD');
408
+ if (headSha === plan.headSha) {
409
+ if (accepted)
410
+ return fail('legacy-story-commit-accepted-physical-drift');
411
+ const currentPaths = gitRaw(cwd, 'diff', '--cached', '--name-only', '-z').split('\0').filter(Boolean);
412
+ const unstaged = gitRaw(cwd, 'diff', '--name-only', '-z').split('\0').filter(Boolean);
413
+ if (JSON.stringify(currentPaths) !== JSON.stringify(plan.changedPaths)
414
+ || unstaged.some((path) => path !== '.neocortex/state.json'
415
+ && path !== `docs/stories/${plan.storyId}.story.md`)
416
+ || gitRaw(cwd, 'ls-files', '--others', '--exclude-standard', '-z') !== ''
417
+ || git(cwd, 'ls-remote', '--heads', 'origin', `refs/heads/${plan.branch}`) !== ''
418
+ || singleRemoteSha(cwd, plan.baseBranch) !== plan.baseSha) {
419
+ return fail('legacy-story-commit-precommit-drift');
420
+ }
421
+ const stateCurrent = safeRead(join(cwd, '.neocortex/state.json'), 2 * 1024 * 1024);
422
+ const storyCurrent = safeRead(join(cwd, `docs/stories/${plan.storyId}.story.md`), 2 * 1024 * 1024);
423
+ const indexState = gitRaw(cwd, 'show', ':.neocortex/state.json');
424
+ const indexStory = gitRaw(cwd, 'show', `:docs/stories/${plan.storyId}.story.md`);
425
+ if (![projection.stateBefore, projection.stateAfter].includes(stateCurrent)
426
+ || ![projection.storyBefore, projection.storyAfter].includes(storyCurrent)
427
+ || ![projection.stateBefore, projection.stateAfter].includes(indexState)
428
+ || ![projection.storyBefore, projection.storyAfter].includes(indexStory)
429
+ || indexState === projection.stateAfter && stateCurrent !== projection.stateAfter
430
+ || indexStory === projection.storyAfter && storyCurrent !== projection.storyAfter) {
431
+ return fail('legacy-story-commit-preimage-drift');
432
+ }
433
+ if (stateCurrent === projection.stateBefore) {
434
+ await anchoredCompareAndSwapFile(join(cwd, '.neocortex'), 'state.json', projection.stateAfter, digest(projection.stateBefore));
435
+ await options.afterStateWrite?.();
436
+ }
437
+ if (storyCurrent === projection.storyBefore) {
438
+ await anchoredCompareAndSwapFile(join(cwd, 'docs/stories'), `${plan.storyId}.story.md`, projection.storyAfter, digest(projection.storyBefore));
439
+ }
440
+ if (safeRead(join(cwd, '.neocortex/state.json'), 2 * 1024 * 1024) !== projection.stateAfter
441
+ || safeRead(join(cwd, `docs/stories/${plan.storyId}.story.md`), 2 * 1024 * 1024)
442
+ !== projection.storyAfter)
443
+ return fail('legacy-story-commit-projection-drift');
444
+ git(cwd, 'add', '--', '.neocortex/state.json', `docs/stories/${plan.storyId}.story.md`);
445
+ git(cwd, 'diff', '--cached', '--check');
446
+ if (gitRaw(cwd, 'diff', '--name-only', '-z') !== ''
447
+ || gitRaw(cwd, 'ls-files', '--others', '--exclude-standard', '-z') !== ''
448
+ || JSON.stringify(c08OtherIndex(cwd, plan)) !== JSON.stringify(intentRaw.otherIndex)
449
+ || digest(gitRaw(cwd, 'show', ':.neocortex/state.json')) !== intentRaw.stateAfterSha256
450
+ || digest(gitRaw(cwd, 'show', `:docs/stories/${plan.storyId}.story.md`))
451
+ !== intentRaw.storyAfterSha256)
452
+ return fail('legacy-story-commit-index-drift');
453
+ git(cwd, 'commit', '-m', `chore(story): checkpoint ${plan.storyId} C08`, '-m', `Neocortex-Legacy-Commit: ${plan.storyId}`);
454
+ headSha = git(cwd, 'rev-parse', 'HEAD');
455
+ await options.afterCommit?.();
456
+ }
457
+ const parents = git(cwd, 'rev-list', '--parents', '-n', '1', headSha).split(' ');
458
+ const changed = gitRaw(cwd, 'diff-tree', '--no-commit-id', '--name-only', '-r', '-z', headSha)
459
+ .split('\0').filter(Boolean);
460
+ if (JSON.stringify(parents) !== JSON.stringify([headSha, plan.headSha])
461
+ || JSON.stringify(changed) !== JSON.stringify(plan.changedPaths)
462
+ || intentRaw.otherIndex.some(({ path, entry }) => {
463
+ const committed = gitRaw(cwd, 'ls-tree', '-z', headSha, '--', path)
464
+ .split('\0').filter(Boolean);
465
+ return entry === null ? committed.length !== 0
466
+ : committed.length !== 1
467
+ || committed[0] !== entry.replace(/^(100644|100755) ([a-f0-9]{40}) 0\t/, '$1 blob $2\t');
468
+ })
469
+ || digest(gitRaw(cwd, 'show', `${headSha}:.neocortex/state.json`)) !== intentRaw.stateAfterSha256
470
+ || digest(gitRaw(cwd, 'show', `${headSha}:docs/stories/${plan.storyId}.story.md`))
471
+ !== intentRaw.storyAfterSha256
472
+ || !gitRaw(cwd, 'log', '-1', '--format=%B', headSha).split('\n').map((line) => line.trim())
473
+ .includes(`Neocortex-Legacy-Commit: ${plan.storyId}`)
474
+ || gitRaw(cwd, 'status', '--porcelain=v1', '-uall') !== '') {
475
+ return fail('legacy-story-commit-checkpoint-invalid');
476
+ }
477
+ const remoteNow = git(cwd, 'ls-remote', '--heads', 'origin', `refs/heads/${plan.branch}`);
478
+ if (remoteNow !== '' && remoteNow !== `${headSha}\trefs/heads/${plan.branch}`) {
479
+ return fail('legacy-story-commit-remote-drift');
480
+ }
481
+ let qaSha256 = accepted?.qaSha256;
482
+ if (!qaSha256) {
483
+ try {
484
+ qaSha256 = options.verifyQa ? await options.verifyQa(headSha) : await (async () => {
485
+ const authority = await executeP205TransientAuthority(cwd, { headSha,
486
+ graphRevision: digest(`legacy-story-c08\0${intentSha256}`) });
487
+ const evidence = projectP205TransientExecutionEvidence(authority);
488
+ if (!evidence || evidence.executionHeadSha !== headSha)
489
+ throw new Error('qa-invalid');
490
+ return evidence.executionEvidenceDigest;
491
+ })();
492
+ }
493
+ catch {
494
+ return fail('legacy-story-commit-qa-failed');
495
+ }
496
+ if (!/^[a-f0-9]{64}$/.test(qaSha256))
497
+ return fail('legacy-story-commit-qa-failed');
498
+ }
499
+ if (git(cwd, 'rev-parse', 'HEAD') !== headSha
500
+ || gitRaw(cwd, 'status', '--porcelain=v1', '-uall') !== '') {
501
+ return fail('legacy-story-commit-final-drift');
502
+ }
503
+ if (remoteNow === '')
504
+ git(cwd, 'push', '--porcelain', 'origin', `${headSha}:refs/heads/${plan.branch}`);
505
+ if (git(cwd, 'ls-remote', '--heads', 'origin', `refs/heads/${plan.branch}`)
506
+ !== `${headSha}\trefs/heads/${plan.branch}`)
507
+ return fail('legacy-story-commit-push-unverified');
508
+ await options.afterPush?.();
509
+ const next = { schemaVersion: 1, kind: 'legacy-story-c08-commit-accepted',
510
+ intentSha256, checkpointHeadSha: headSha, qaSha256 };
511
+ if (accepted && JSON.stringify(accepted) !== JSON.stringify(next)) {
512
+ return fail('legacy-story-commit-accepted-conflict');
513
+ }
514
+ if (!accepted) {
515
+ await anchoredAtomicWriteFile(directory, acceptedName, `${JSON.stringify(next)}\n`);
516
+ await assertGitLifecyclePrivateFile(join(directory, acceptedName), directory);
517
+ }
518
+ return { ok: true, value: Object.freeze({ schemaVersion: 1,
519
+ kind: 'legacy-story-c08-commit-accepted', storyId: plan.storyId, epicId: plan.epicId,
520
+ branch: plan.branch, checkpointHeadSha: headSha, qaSha256,
521
+ reused: Boolean(accepted), physicalCommit: true, checkpointAccepted: true, publicSafe: true }) };
522
+ }
523
+ catch (error) {
524
+ const reason = error instanceof Error ? error.message : '';
525
+ return fail(reason.startsWith('legacy-story-commit-') ? reason
526
+ : 'legacy-story-commit-effect-uncertain');
527
+ }
528
+ finally {
529
+ await releaseGitLifecyclePhysicalIntegrationLock(lock.path, lock.owner).catch(() => undefined);
530
+ }
531
+ }
532
+ /** Read-only C09 basis. A merge-base observation is not a synchronized story:
533
+ * only a later root-owned Git/state checkpoint may turn C08 into C09. */
534
+ export function planLegacyStorySyncC09(options) {
535
+ if (!/^[A-Za-z0-9][A-Za-z0-9_-]*\.[A-Za-z0-9._-]+$/.test(options.storyId)) {
536
+ return fail('legacy-story-sync-target-invalid');
537
+ }
538
+ try {
539
+ const cwd = realpathSync(options.projectRoot);
540
+ if (realpathSync(git(cwd, 'rev-parse', '--show-toplevel')) !== cwd
541
+ || gitRaw(cwd, 'status', '--porcelain=v1', '-uall') !== '') {
542
+ return fail('legacy-story-sync-checkout-invalid');
543
+ }
544
+ const stateRaw = gitRaw(cwd, 'show', 'HEAD:.neocortex/state.json');
545
+ const storyPath = `docs/stories/${options.storyId}.story.md`;
546
+ const storyRaw = gitRaw(cwd, 'show', `HEAD:${storyPath}`);
547
+ if (safeRead(join(cwd, '.neocortex/state.json'), 2 * 1024 * 1024) !== stateRaw
548
+ || safeRead(join(cwd, storyPath), 2 * 1024 * 1024) !== storyRaw) {
549
+ return fail('legacy-story-sync-committed-content-drift');
550
+ }
551
+ const state = record(JSON.parse(stateRaw));
552
+ const stories = record(state?.stories);
553
+ const epics = record(state?.epics);
554
+ const story = record(stories?.[options.storyId]);
555
+ const epicId = story?.epic_id;
556
+ const epic = typeof epicId === 'string' ? record(epics?.[epicId]) : null;
557
+ if (!story || story.id !== options.storyId || typeof epicId !== 'string'
558
+ || !epic || epic.id !== epicId || epic.branch_mode === 'epic'
559
+ || story.status !== 'committed' || story.last_step !== GIT_LIFECYCLE_COMMIT_PUSH_STEP_ID
560
+ || !Array.isArray(story.steps_completed)
561
+ || !story.steps_completed.includes(GIT_LIFECYCLE_COMMIT_PUSH_STEP_ID)
562
+ || story.steps_completed.includes(GIT_LIFECYCLE_SYNC_MAIN_STEP_ID)
563
+ || !Number.isSafeInteger(story.issue_number) || story.issue_number <= 0) {
564
+ return fail('legacy-story-sync-state-invalid');
565
+ }
566
+ const ids = Array.isArray(epic.stories) ? epic.stories : null;
567
+ if (!ids || ids.length === 0 || new Set(ids).size !== ids.length
568
+ || !ids.includes(options.storyId)
569
+ || ids.some((id) => typeof id !== 'string' || record(stories?.[id])?.epic_id !== epicId)
570
+ || Object.entries(stories ?? {}).some(([id, value]) => record(value)?.epic_id === epicId && !ids.includes(id))) {
571
+ return fail('legacy-story-sync-topology-invalid');
572
+ }
573
+ const unit = resolveBranchUnit(epic, ids.map((id) => ({ ...record(stories?.[id]), id })));
574
+ const branch = `story/${epicId}/${options.storyId}`;
575
+ const config = record(state?.config);
576
+ const baseBranch = config?.default_branch;
577
+ if (unit.mode !== 'story' || story.branch_name !== branch
578
+ || git(cwd, 'branch', '--show-current') !== branch
579
+ || typeof baseBranch !== 'string' || baseBranch === branch
580
+ || git(cwd, 'check-ref-format', '--branch', baseBranch) !== baseBranch) {
581
+ return fail('legacy-story-sync-branch-invalid');
582
+ }
583
+ const frontmatter = storyRaw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);
584
+ if (!frontmatter)
585
+ return fail('legacy-story-sync-frontmatter-invalid');
586
+ const document = parseDocument(frontmatter[1], { strict: true, uniqueKeys: true });
587
+ const fields = record(document.toJS());
588
+ const stepsKey = ['stepsCompleted', 'steps_completed'].filter((key) => document.has(key));
589
+ const lastKey = ['lastStep', 'last_step'].filter((key) => document.has(key));
590
+ const branchKey = ['branchName', 'branch_name'].filter((key) => document.has(key));
591
+ if (document.errors.length > 0 || document.get('id') !== options.storyId
592
+ || (document.get('epic_id') ?? document.get('epic')) !== epicId
593
+ || document.get('status') !== 'committed'
594
+ || document.has('currentStatus') && document.get('currentStatus') !== 'committed'
595
+ || stepsKey.length > 1 || lastKey.length > 1 || branchKey.length > 1
596
+ || stepsKey.length === 1 && JSON.stringify(fields?.[stepsKey[0]]) !== JSON.stringify(story.steps_completed)
597
+ || lastKey.length === 1 && fields?.[lastKey[0]] !== GIT_LIFECYCLE_COMMIT_PUSH_STEP_ID
598
+ || branchKey.length === 1 && fields?.[branchKey[0]] !== branch) {
599
+ return fail('legacy-story-sync-frontmatter-invalid');
600
+ }
601
+ const headSha = git(cwd, 'rev-parse', 'HEAD');
602
+ const baseSha = singleRemoteSha(cwd, baseBranch);
603
+ if (!/^[a-f0-9]{40}$/.test(headSha) || singleRemoteSha(cwd, branch) !== headSha
604
+ || git(cwd, 'rev-parse', `refs/remotes/origin/${baseBranch}`) !== baseSha) {
605
+ return fail('legacy-story-sync-remote-drift');
606
+ }
607
+ const mergeBaseSha = git(cwd, 'merge-base', headSha, baseSha);
608
+ if (!/^[a-f0-9]{40}$/.test(mergeBaseSha))
609
+ return fail('legacy-story-sync-ancestry-invalid');
610
+ return { ok: true, value: Object.freeze({ schemaVersion: 1,
611
+ kind: 'legacy-story-c09-sync-plan', storyId: options.storyId, epicId,
612
+ repository: repository(cwd), branch, baseBranch, headSha, baseSha,
613
+ mergeBaseSha, stateSha256: digest(stateRaw), storySha256: digest(storyRaw),
614
+ baseAlreadyAncestor: mergeBaseSha === baseSha,
615
+ physicalSync: false, checkpointAccepted: false, localOnly: true }) };
616
+ }
617
+ catch {
618
+ return fail('legacy-story-sync-physical-unavailable');
619
+ }
620
+ }
621
+ function projectLegacyStorySyncC09(cwd, plan) {
622
+ const stateBefore = safeRead(join(cwd, '.neocortex/state.json'), 2 * 1024 * 1024);
623
+ const storyPath = `docs/stories/${plan.storyId}.story.md`;
624
+ const storyBefore = safeRead(join(cwd, storyPath), 2 * 1024 * 1024);
625
+ const originalState = gitRaw(cwd, 'show', `${plan.headSha}:.neocortex/state.json`);
626
+ const originalStory = gitRaw(cwd, 'show', `${plan.headSha}:${storyPath}`);
627
+ if (digest(originalState) !== plan.stateSha256 || digest(originalStory) !== plan.storySha256
628
+ || storyBefore !== originalStory) {
629
+ throw new Error('legacy-story-sync-preimage-drift');
630
+ }
631
+ const state = record(JSON.parse(stateBefore));
632
+ const original = record(JSON.parse(originalState));
633
+ const stories = record(state?.stories);
634
+ const originalStoryState = record(record(original?.stories)?.[plan.storyId]);
635
+ const story = record(stories?.[plan.storyId]);
636
+ const epic = record(record(state?.epics)?.[plan.epicId]);
637
+ const ids = epic?.stories;
638
+ if (!story || !originalStoryState || JSON.stringify(story) !== JSON.stringify(originalStoryState)
639
+ || story.status !== 'committed' || story.last_step !== GIT_LIFECYCLE_COMMIT_PUSH_STEP_ID
640
+ || story.branch_name !== plan.branch || !Array.isArray(story.steps_completed)
641
+ || !story.steps_completed.includes(GIT_LIFECYCLE_COMMIT_PUSH_STEP_ID)
642
+ || story.steps_completed.includes(GIT_LIFECYCLE_SYNC_MAIN_STEP_ID)
643
+ || !epic || epic.id !== plan.epicId || epic.branch_mode === 'epic'
644
+ || !Array.isArray(ids) || !ids.includes(plan.storyId)
645
+ || new Set(ids).size !== ids.length
646
+ || ids.some((id) => typeof id !== 'string' || record(stories?.[id])?.epic_id !== plan.epicId)
647
+ || record(state?.config)?.default_branch !== plan.baseBranch) {
648
+ throw new Error('legacy-story-sync-merged-state-invalid');
649
+ }
650
+ story.status = 'synced';
651
+ story.last_step = GIT_LIFECYCLE_SYNC_MAIN_STEP_ID;
652
+ story.steps_completed = [...story.steps_completed, GIT_LIFECYCLE_SYNC_MAIN_STEP_ID];
653
+ story.sync_base_sha = plan.baseSha;
654
+ const stateAfter = `${JSON.stringify(state, null, 2)}\n`;
655
+ const frontmatter = storyBefore.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);
656
+ if (!frontmatter)
657
+ throw new Error('legacy-story-sync-frontmatter-invalid');
658
+ const document = parseDocument(frontmatter[1], { strict: true, uniqueKeys: true });
659
+ const fields = record(document.toJS());
660
+ const stepsKeys = ['stepsCompleted', 'steps_completed'].filter((key) => document.has(key));
661
+ const lastKeys = ['lastStep', 'last_step'].filter((key) => document.has(key));
662
+ if (document.errors.length > 0 || document.get('id') !== plan.storyId
663
+ || (document.get('epic_id') ?? document.get('epic')) !== plan.epicId
664
+ || document.get('status') !== 'committed'
665
+ || stepsKeys.length !== 1 || lastKeys.length !== 1
666
+ || JSON.stringify(fields?.[stepsKeys[0]]) !== JSON.stringify(originalStoryState.steps_completed)
667
+ || fields?.[lastKeys[0]] !== GIT_LIFECYCLE_COMMIT_PUSH_STEP_ID) {
668
+ throw new Error('legacy-story-sync-frontmatter-invalid');
669
+ }
670
+ document.set('status', 'synced');
671
+ if (document.has('currentStatus'))
672
+ document.set('currentStatus', 'synced');
673
+ document.set(stepsKeys[0], [...originalStoryState.steps_completed, GIT_LIFECYCLE_SYNC_MAIN_STEP_ID]);
674
+ document.set(lastKeys[0], GIT_LIFECYCLE_SYNC_MAIN_STEP_ID);
675
+ document.set('syncBaseSha', plan.baseSha);
676
+ const storyAfter = `---\n${document.toString({ lineWidth: 0 })}---\n${storyBefore.slice(frontmatter[0].length)}`;
677
+ return { stateBefore, stateAfter, storyBefore, storyAfter };
678
+ }
679
+ async function readC09PrivateJson(directory, name, limit) {
680
+ const path = join(directory, name);
681
+ if (!existsSync(path))
682
+ return null;
683
+ await assertGitLifecyclePrivateFile(path, directory);
684
+ const value = record(JSON.parse(safeRead(path, limit)));
685
+ if (!value)
686
+ throw new Error('legacy-story-sync-journal-invalid');
687
+ return value;
688
+ }
689
+ /** Root-only C09 physical sync. The merge and checkpoint are prepared in a
690
+ * detached private worktree; the live story branch moves only after candidate
691
+ * QA and an exact non-force remote readback. An interrupted push is reconciled
692
+ * from the private intent rather than replaying the merge. */
693
+ export async function acceptLegacyStorySyncC09(options) {
694
+ if (!options.authorize)
695
+ return fail('legacy-story-sync-authorization-required');
696
+ if (!/^[A-Za-z0-9][A-Za-z0-9_-]*\.[A-Za-z0-9._-]+$/.test(options.storyId)) {
697
+ return fail('legacy-story-sync-target-invalid');
698
+ }
699
+ const sessionId = `legacy-c09-sync-${randomBytes(16).toString('hex')}`;
700
+ const lock = await acquireGitLifecyclePhysicalIntegrationLock(options.projectRoot, {
701
+ kind: 'direct', storyId: options.storyId, sessionId, idempotencyKey: sessionId,
702
+ expiresAt: new Date(Date.now() + 60 * 60_000).toISOString(), persistent: false,
703
+ });
704
+ if (!lock.ok)
705
+ return fail(lock.reasonCode);
706
+ try {
707
+ const cwd = realpathSync(options.projectRoot);
708
+ if (realpathSync(git(cwd, 'rev-parse', '--show-toplevel')) !== cwd) {
709
+ return fail('legacy-story-sync-checkout-invalid');
710
+ }
711
+ const directory = await ensureGitLifecyclePrivateChildDirectory(await gitLifecycleRuntimeDirectory(cwd), 'legacy-story-pr-intents');
712
+ const prefix = digest(options.storyId);
713
+ const intentName = `${prefix}.c09.sync.json`;
714
+ const preparedName = `${prefix}.c09.sync.prepared.json`;
715
+ const acceptedName = `${prefix}.c09.sync.accepted.json`;
716
+ let rawIntent = await readC09PrivateJson(directory, intentName, 8192);
717
+ if (!rawIntent) {
718
+ const planned = planLegacyStorySyncC09(options);
719
+ if (!planned.ok)
720
+ return planned;
721
+ const intent = { schemaVersion: 1, kind: 'legacy-story-c09-sync-intent',
722
+ plan: planned.value };
723
+ await anchoredAtomicWriteFile(directory, intentName, `${JSON.stringify(intent)}\n`);
724
+ rawIntent = intent;
725
+ }
726
+ const plan = record(rawIntent.plan);
727
+ if (rawIntent.schemaVersion !== 1 || rawIntent.kind !== 'legacy-story-c09-sync-intent'
728
+ || !plan || plan.schemaVersion !== 1 || plan.kind !== 'legacy-story-c09-sync-plan'
729
+ || plan.storyId !== options.storyId || typeof plan.epicId !== 'string'
730
+ || plan.branch !== `story/${plan.epicId}/${options.storyId}`
731
+ || typeof plan.baseBranch !== 'string' || typeof plan.repository !== 'string'
732
+ || typeof plan.headSha !== 'string' || !/^[a-f0-9]{40}$/.test(plan.headSha)
733
+ || typeof plan.baseSha !== 'string' || !/^[a-f0-9]{40}$/.test(plan.baseSha)
734
+ || typeof plan.stateSha256 !== 'string' || !/^[a-f0-9]{64}$/.test(plan.stateSha256)
735
+ || typeof plan.storySha256 !== 'string' || !/^[a-f0-9]{64}$/.test(plan.storySha256)
736
+ || typeof plan.baseAlreadyAncestor !== 'boolean'
737
+ || plan.physicalSync !== false || plan.checkpointAccepted !== false
738
+ || git(cwd, 'branch', '--show-current') !== plan.branch
739
+ || repository(cwd) !== plan.repository) {
740
+ return fail('legacy-story-sync-intent-invalid');
741
+ }
742
+ const bound = plan;
743
+ const intentSha256 = digest(JSON.stringify(rawIntent));
744
+ const preparedRaw = await readC09PrivateJson(directory, preparedName, 4096);
745
+ let prepared = preparedRaw;
746
+ if (prepared && (prepared.schemaVersion !== 1 || prepared.kind !== 'legacy-story-c09-sync-prepared'
747
+ || prepared.intentSha256 !== intentSha256
748
+ || !/^[a-f0-9]{40}$/.test(prepared.treeSha)
749
+ || !/^[a-f0-9]{64}$/.test(prepared.stateSha256)
750
+ || !/^[a-f0-9]{64}$/.test(prepared.storySha256))) {
751
+ return fail('legacy-story-sync-prepared-invalid');
752
+ }
753
+ const acceptedRaw = await readC09PrivateJson(directory, acceptedName, 4096);
754
+ const accepted = acceptedRaw;
755
+ if (accepted && (accepted.schemaVersion !== 1 || accepted.kind !== 'legacy-story-c09-sync-accepted'
756
+ || accepted.intentSha256 !== intentSha256
757
+ || !/^[a-f0-9]{40}$/.test(accepted.checkpointHeadSha)
758
+ || !/^[a-f0-9]{64}$/.test(accepted.qaSha256))) {
759
+ return fail('legacy-story-sync-accepted-invalid');
760
+ }
761
+ const baseHeadSha = git(cwd, 'rev-parse', 'HEAD');
762
+ if (gitRaw(cwd, 'status', '--porcelain=v1', '-uall') !== ''
763
+ || singleRemoteSha(cwd, bound.baseBranch) !== bound.baseSha
764
+ || digest(gitRaw(cwd, 'show', `${bound.headSha}:.neocortex/state.json`)) !== bound.stateSha256
765
+ || digest(gitRaw(cwd, 'show', `${bound.headSha}:docs/stories/${bound.storyId}.story.md`)) !== bound.storySha256) {
766
+ return fail('legacy-story-sync-physical-drift');
767
+ }
768
+ const candidatePath = join(directory, `${prefix}.c09.sync.checkout`);
769
+ if (!existsSync(candidatePath)) {
770
+ execFileSync('git', ['worktree', 'add', '--detach', candidatePath,
771
+ accepted?.checkpointHeadSha ?? bound.headSha], { cwd, encoding: 'utf8', timeout: 60_000,
772
+ stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 32 * 1024 });
773
+ }
774
+ if (!lstatSync(candidatePath).isDirectory() || realpathSync(candidatePath) !== candidatePath
775
+ || realpathSync(git(candidatePath, 'rev-parse', '--show-toplevel')) !== candidatePath
776
+ || gitCommonDirectory(candidatePath) !== gitCommonDirectory(cwd)
777
+ || git(candidatePath, 'branch', '--show-current') !== '') {
778
+ return fail('legacy-story-sync-candidate-invalid');
779
+ }
780
+ let headSha = git(candidatePath, 'rev-parse', 'HEAD');
781
+ if (headSha === bound.headSha) {
782
+ if (prepared)
783
+ return fail('legacy-story-sync-prepared-uncommitted');
784
+ if (baseHeadSha !== bound.headSha || singleRemoteSha(cwd, bound.branch) !== bound.headSha
785
+ || gitRaw(candidatePath, 'status', '--porcelain=v1', '-uall') !== '') {
786
+ return fail('legacy-story-sync-preparation-drift');
787
+ }
788
+ if (!bound.baseAlreadyAncestor) {
789
+ try {
790
+ git(candidatePath, 'merge', '--no-ff', '--no-commit', bound.baseSha);
791
+ }
792
+ catch {
793
+ return fail('legacy-story-sync-merge-conflict');
794
+ }
795
+ if (gitRaw(candidatePath, 'ls-files', '-u') !== '') {
796
+ return fail('legacy-story-sync-merge-conflict');
797
+ }
798
+ }
799
+ const projection = projectLegacyStorySyncC09(candidatePath, bound);
800
+ await anchoredCompareAndSwapFile(join(candidatePath, '.neocortex'), 'state.json', projection.stateAfter, digest(projection.stateBefore));
801
+ await anchoredCompareAndSwapFile(join(candidatePath, 'docs/stories'), `${bound.storyId}.story.md`, projection.storyAfter, digest(projection.storyBefore));
802
+ git(candidatePath, 'add', '--', '.neocortex/state.json', `docs/stories/${bound.storyId}.story.md`);
803
+ git(candidatePath, 'diff', '--cached', '--check');
804
+ prepared = { schemaVersion: 1, kind: 'legacy-story-c09-sync-prepared', intentSha256,
805
+ treeSha: git(candidatePath, 'write-tree'), stateSha256: digest(projection.stateAfter),
806
+ storySha256: digest(projection.storyAfter) };
807
+ await anchoredAtomicWriteFile(directory, preparedName, `${JSON.stringify(prepared)}\n`);
808
+ await assertGitLifecyclePrivateFile(join(directory, preparedName), directory);
809
+ git(candidatePath, 'commit', '-m', `chore(story): sync ${bound.storyId} with ${bound.baseBranch}`, '-m', `Neocortex-Legacy-Sync: ${bound.storyId}`);
810
+ headSha = git(candidatePath, 'rev-parse', 'HEAD');
811
+ await options.afterCommit?.();
812
+ }
813
+ const parents = git(candidatePath, 'rev-list', '--parents', '-n', '1', headSha).split(' ');
814
+ const expectedParents = bound.baseAlreadyAncestor
815
+ ? [headSha, bound.headSha] : [headSha, bound.headSha, bound.baseSha];
816
+ const stateAtHead = record(JSON.parse(gitRaw(candidatePath, 'show', `${headSha}:.neocortex/state.json`)));
817
+ const storyAtHead = record(record(stateAtHead?.stories)?.[bound.storyId]);
818
+ const frontmatterAtHead = gitRaw(candidatePath, 'show', `${headSha}:docs/stories/${bound.storyId}.story.md`);
819
+ if (!prepared || git(candidatePath, 'rev-parse', `${headSha}^{tree}`) !== prepared.treeSha
820
+ || digest(gitRaw(candidatePath, 'show', `${headSha}:.neocortex/state.json`)) !== prepared.stateSha256
821
+ || digest(frontmatterAtHead) !== prepared.storySha256
822
+ || JSON.stringify(parents) !== JSON.stringify(expectedParents)
823
+ || !storyAtHead || storyAtHead.status !== 'synced'
824
+ || storyAtHead.last_step !== GIT_LIFECYCLE_SYNC_MAIN_STEP_ID
825
+ || storyAtHead.sync_base_sha !== bound.baseSha
826
+ || !Array.isArray(storyAtHead.steps_completed)
827
+ || !storyAtHead.steps_completed.includes(GIT_LIFECYCLE_SYNC_MAIN_STEP_ID)
828
+ || !frontmatterAtHead.startsWith('---\n')
829
+ || !gitRaw(candidatePath, 'log', '-1', '--format=%B', headSha).split('\n').map((line) => line.trim())
830
+ .includes(`Neocortex-Legacy-Sync: ${bound.storyId}`)
831
+ || gitRaw(candidatePath, 'status', '--porcelain=v1', '-uall') !== '') {
832
+ return fail('legacy-story-sync-checkpoint-invalid');
833
+ }
834
+ if (accepted && accepted.checkpointHeadSha !== headSha) {
835
+ return fail('legacy-story-sync-accepted-conflict');
836
+ }
837
+ const remoteStorySha = singleRemoteSha(cwd, bound.branch);
838
+ if (![bound.headSha, headSha].includes(baseHeadSha)
839
+ || ![bound.headSha, headSha].includes(remoteStorySha)
840
+ || accepted && (baseHeadSha !== headSha || remoteStorySha !== headSha)) {
841
+ return fail('legacy-story-sync-remote-drift');
842
+ }
843
+ let qaSha256 = accepted?.qaSha256;
844
+ if (!qaSha256) {
845
+ if (!options.verifyQa) {
846
+ try {
847
+ installP205CandidateDependencies(candidatePath);
848
+ }
849
+ catch {
850
+ return fail('legacy-story-sync-dependencies-failed');
851
+ }
852
+ }
853
+ const qaPlan = { schemaVersion: 1, kind: 'legacy-story-pr-plan',
854
+ storyId: bound.storyId, epicId: bound.epicId, repository: bound.repository,
855
+ branch: bound.branch, baseBranch: bound.baseBranch, headSha,
856
+ baseSha: bound.baseSha, qaAuthority: 'root-review-required', localOnly: true };
857
+ try {
858
+ qaSha256 = await verifyLocalQa(qaPlan, candidatePath, options.verifyQa);
859
+ }
860
+ catch {
861
+ return fail('legacy-story-sync-qa-failed');
862
+ }
863
+ }
864
+ if (git(candidatePath, 'rev-parse', 'HEAD') !== headSha
865
+ || gitRaw(candidatePath, 'status', '--porcelain=v1', '-uall') !== ''
866
+ || git(cwd, 'rev-parse', 'HEAD') !== baseHeadSha
867
+ || gitRaw(cwd, 'status', '--porcelain=v1', '-uall') !== ''
868
+ || singleRemoteSha(cwd, bound.baseBranch) !== bound.baseSha) {
869
+ return fail('legacy-story-sync-final-drift');
870
+ }
871
+ if (remoteStorySha === bound.headSha) {
872
+ git(cwd, 'push', '--porcelain', 'origin', `${headSha}:refs/heads/${bound.branch}`);
873
+ }
874
+ if (singleRemoteSha(cwd, bound.branch) !== headSha) {
875
+ return fail('legacy-story-sync-push-unverified');
876
+ }
877
+ await options.afterPush?.();
878
+ if (git(cwd, 'rev-parse', 'HEAD') === bound.headSha)
879
+ git(cwd, 'merge', '--ff-only', headSha);
880
+ if (git(cwd, 'rev-parse', 'HEAD') !== headSha
881
+ || gitRaw(cwd, 'status', '--porcelain=v1', '-uall') !== ''
882
+ || safeRead(join(cwd, '.neocortex/state.json'), 2 * 1024 * 1024)
883
+ !== gitRaw(cwd, 'show', `${headSha}:.neocortex/state.json`)
884
+ || safeRead(join(cwd, `docs/stories/${bound.storyId}.story.md`), 2 * 1024 * 1024)
885
+ !== frontmatterAtHead) {
886
+ return fail('legacy-story-sync-local-readback-invalid');
887
+ }
888
+ const next = { schemaVersion: 1, kind: 'legacy-story-c09-sync-accepted',
889
+ intentSha256, checkpointHeadSha: headSha, qaSha256 };
890
+ if (accepted && JSON.stringify(accepted) !== JSON.stringify(next)) {
891
+ return fail('legacy-story-sync-accepted-conflict');
892
+ }
893
+ if (!accepted) {
894
+ await anchoredAtomicWriteFile(directory, acceptedName, `${JSON.stringify(next)}\n`);
895
+ await assertGitLifecyclePrivateFile(join(directory, acceptedName), directory);
896
+ }
897
+ return { ok: true, value: Object.freeze({ schemaVersion: 1,
898
+ kind: 'legacy-story-c09-sync-accepted', storyId: bound.storyId,
899
+ epicId: bound.epicId, branch: bound.branch, baseSha: bound.baseSha,
900
+ checkpointHeadSha: headSha, qaSha256, reused: Boolean(accepted),
901
+ physicalSync: true, checkpointAccepted: true, publicSafe: true }) };
902
+ }
903
+ catch (error) {
904
+ const reason = error instanceof Error ? error.message : '';
905
+ return fail(reason.startsWith('legacy-story-sync-') ? reason
906
+ : 'legacy-story-sync-effect-uncertain');
907
+ }
908
+ finally {
909
+ await releaseGitLifecyclePhysicalIntegrationLock(lock.path, lock.owner).catch(() => undefined);
910
+ }
911
+ }
912
+ function inspectLegacyStoryPr(options, allowCheckpointPatch) {
913
+ try {
914
+ const cwd = realpathSync(options.projectRoot);
915
+ if (!/^[A-Za-z0-9][A-Za-z0-9_-]*\.[A-Za-z0-9._-]+$/.test(options.storyId)
916
+ || realpathSync(git(cwd, 'rev-parse', '--show-toplevel')) !== cwd
917
+ || !allowCheckpointPatch && git(cwd, 'status', '--porcelain=v1', '-uall') !== '') {
918
+ return fail('legacy-story-pr-checkout-invalid');
919
+ }
920
+ const stateRaw = gitRaw(cwd, 'show', 'HEAD:.neocortex/state.json');
921
+ if (!allowCheckpointPatch && stateRaw !== safeRead(join(cwd, '.neocortex/state.json'), 2 * 1024 * 1024)) {
922
+ return fail('legacy-story-pr-state-not-committed');
923
+ }
924
+ const state = record(JSON.parse(stateRaw));
925
+ const stories = record(state?.stories);
926
+ const epics = record(state?.epics);
927
+ const story = record(stories?.[options.storyId]);
928
+ const epicId = story?.epic_id;
929
+ const epic = typeof epicId === 'string' ? record(epics?.[epicId]) : null;
930
+ if (!story || story.id !== options.storyId || typeof epicId !== 'string'
931
+ || !epic || epic.id !== epicId || epic.branch_mode === 'epic'
932
+ || story.status !== 'synced' || story.last_step !== GIT_LIFECYCLE_SYNC_MAIN_STEP_ID
933
+ || !Array.isArray(story.steps_completed)
934
+ || !story.steps_completed.includes(GIT_LIFECYCLE_SYNC_MAIN_STEP_ID)
935
+ || story.steps_completed.includes(GIT_LIFECYCLE_CREATE_PR_STEP_ID))
936
+ return fail('legacy-story-pr-state-invalid');
937
+ const ids = Array.isArray(epic.stories) ? epic.stories : null;
938
+ if (!ids || ids.length === 0 || new Set(ids).size !== ids.length
939
+ || !ids.includes(options.storyId)
940
+ || ids.some((id) => typeof id !== 'string' || record(stories?.[id])?.epic_id !== epicId)
941
+ || Object.entries(stories ?? {}).some(([id, value]) => record(value)?.epic_id === epicId && !ids.includes(id))) {
942
+ return fail('legacy-story-pr-topology-invalid');
943
+ }
944
+ const unit = resolveBranchUnit(epic, ids.map((id) => ({ ...record(stories?.[id]), id })));
945
+ const branch = `story/${epicId}/${options.storyId}`;
946
+ const config = record(state?.config);
947
+ const baseBranch = config?.default_branch;
948
+ if (unit.mode !== 'story' || story.branch_name !== branch
949
+ || git(cwd, 'branch', '--show-current') !== branch
950
+ || typeof baseBranch !== 'string' || baseBranch === branch
951
+ || git(cwd, 'check-ref-format', '--branch', baseBranch) !== baseBranch) {
952
+ return fail('legacy-story-pr-branch-invalid');
953
+ }
954
+ const storyFile = git(cwd, 'show', `HEAD:docs/stories/${options.storyId}.story.md`);
955
+ const frontmatter = storyFile.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);
956
+ if (!frontmatter)
957
+ return fail('legacy-story-pr-frontmatter-invalid');
958
+ const document = parseDocument(frontmatter[1], { strict: true, uniqueKeys: true });
959
+ if (document.errors.length > 0 || document.get('id') !== options.storyId
960
+ || (document.get('epic_id') ?? document.get('epic')) !== epicId
961
+ || document.get('status') !== 'synced')
962
+ return fail('legacy-story-pr-frontmatter-invalid');
963
+ const headSha = git(cwd, 'rev-parse', 'HEAD');
964
+ const baseSha = singleRemoteSha(cwd, baseBranch);
965
+ if (!/^[a-f0-9]{40}$/.test(headSha) || singleRemoteSha(cwd, branch) !== headSha
966
+ || git(cwd, 'rev-parse', `refs/remotes/origin/${baseBranch}`) !== baseSha) {
967
+ return fail('legacy-story-pr-remote-drift');
968
+ }
969
+ git(cwd, 'merge-base', '--is-ancestor', baseSha, headSha);
970
+ return { ok: true, value: Object.freeze({ schemaVersion: 1, kind: 'legacy-story-pr-plan',
971
+ storyId: options.storyId, epicId, repository: repository(cwd), branch, baseBranch,
972
+ headSha, baseSha, qaAuthority: 'root-review-required', localOnly: true }) };
973
+ }
974
+ catch {
975
+ return fail('legacy-story-pr-physical-unavailable');
976
+ }
977
+ }
978
+ export function planLegacyStoryPr(options) {
979
+ return inspectLegacyStoryPr(options, false);
980
+ }
981
+ async function verifyLocalQa(plan, projectRoot, seam) {
982
+ const qaDigest = seam ? await seam(plan) : await (async () => {
983
+ const authority = await executeP205TransientAuthority(projectRoot, {
984
+ headSha: plan.headSha,
985
+ graphRevision: digest(`legacy-story-pr-c10\0${JSON.stringify(plan)}`),
986
+ });
987
+ const evidence = projectP205TransientExecutionEvidence(authority);
988
+ if (!evidence || evidence.executionHeadSha !== plan.headSha) {
989
+ throw new Error('legacy-story-pr-checkpoint-qa-invalid');
990
+ }
991
+ return evidence.executionEvidenceDigest;
992
+ })();
993
+ if (!/^[a-f0-9]{64}$/.test(qaDigest))
994
+ throw new Error('legacy-story-pr-checkpoint-qa-invalid');
995
+ return qaDigest;
996
+ }
997
+ /** Detached checkpoint worktrees do not inherit the operator's dependencies.
998
+ * Keep every P205 package root inside the private candidate, never in the
999
+ * operator's checkout. A missing or substituted manifest fails closed. */
1000
+ function installP205CandidateDependencies(candidatePath) {
1001
+ for (const relativeRoot of ['.', 'packages/server', 'portal', 'admin', 'landing']) {
1002
+ const dependencyRoot = join(candidatePath, relativeRoot);
1003
+ const manifest = join(dependencyRoot, 'package.json');
1004
+ const lockfile = join(dependencyRoot, 'package-lock.json');
1005
+ if (!existsSync(dependencyRoot) || !lstatSync(dependencyRoot).isDirectory()
1006
+ || realpathSync(dependencyRoot) !== dependencyRoot
1007
+ || !existsSync(manifest) || !lstatSync(manifest).isFile()
1008
+ || !existsSync(lockfile) || !lstatSync(lockfile).isFile()) {
1009
+ throw new Error('legacy-story-pr-candidate-dependencies-missing');
1010
+ }
1011
+ try {
1012
+ execFileSync('npm', ['ci', '--include=dev', '--no-audit', '--no-fund'], {
1013
+ cwd: dependencyRoot, encoding: 'utf8', timeout: 10 * 60_000,
1014
+ stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 1024 * 1024,
1015
+ });
1016
+ }
1017
+ catch {
1018
+ throw new Error('legacy-story-pr-candidate-dependencies-failed');
1019
+ }
1020
+ }
1021
+ }
1022
+ const defaultProvider = {
1023
+ async list(repo, branch, _baseBranch) {
1024
+ const raw = execFileSync('gh', ['pr', 'list', '--repo', repo, '--state', 'all', '--head', branch,
1025
+ '--json', 'number,state,headRefName,headRefOid,baseRefName,url', '--limit', '100'], { encoding: 'utf8', timeout: 15_000, stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 256 * 1024 });
1026
+ return JSON.parse(raw);
1027
+ },
1028
+ async create(repo, branch, baseBranch, title, bodyFile) {
1029
+ execFileSync('gh', ['pr', 'create', '--repo', repo, '--head', branch, '--base', baseBranch,
1030
+ '--title', title, '--body-file', bodyFile], { encoding: 'utf8', timeout: 30_000, stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 16 * 1024 });
1031
+ },
1032
+ };
1033
+ const defaultMergeProvider = {
1034
+ async view(repo, number) {
1035
+ const raw = execFileSync('gh', ['pr', 'view', String(number), '--repo', repo, '--json',
1036
+ 'number,state,headRefName,headRefOid,baseRefName,baseRefOid,isDraft,mergeable,mergeCommit,statusCheckRollup,url'], { encoding: 'utf8', timeout: 15_000, stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 256 * 1024 });
1037
+ return JSON.parse(raw);
1038
+ },
1039
+ async merge(repo, number, expectedHeadSha) {
1040
+ execFileSync('gh', ['api', '--method', 'PUT', `repos/${repo}/pulls/${number}/merge`,
1041
+ '-f', `sha=${expectedHeadSha}`, '-f', 'merge_method=merge'], { encoding: 'utf8', timeout: 30_000, stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 16 * 1024 });
1042
+ },
1043
+ };
1044
+ export async function createLegacyStoryPr(options) {
1045
+ if (!options.authorize)
1046
+ return fail('legacy-story-pr-authorization-required');
1047
+ let reviewedBody;
1048
+ try {
1049
+ if (lstatSync(options.bodyFile).isSymbolicLink())
1050
+ return fail('legacy-story-pr-body-unsafe');
1051
+ const body = safeRead(options.bodyFile, 32 * 1024);
1052
+ if (!body.trim() || body.includes('\0'))
1053
+ return fail('legacy-story-pr-body-unsafe');
1054
+ reviewedBody = body;
1055
+ }
1056
+ catch {
1057
+ return fail('legacy-story-pr-body-unsafe');
1058
+ }
1059
+ const before = planLegacyStoryPr(options);
1060
+ if (!before.ok)
1061
+ return before;
1062
+ try {
1063
+ await verifyLocalQa(before.value, options.projectRoot, options.verifyQa);
1064
+ }
1065
+ catch {
1066
+ return fail('legacy-story-pr-checkpoint-qa-failed');
1067
+ }
1068
+ const provider = options.provider ?? defaultProvider;
1069
+ const sessionId = `legacy-c10-${randomBytes(16).toString('hex')}`;
1070
+ const lock = await acquireGitLifecyclePhysicalIntegrationLock(options.projectRoot, {
1071
+ kind: 'direct', storyId: options.storyId, sessionId, idempotencyKey: sessionId,
1072
+ expiresAt: new Date(Date.now() + 5 * 60_000).toISOString(), persistent: false,
1073
+ });
1074
+ if (!lock.ok)
1075
+ return fail(lock.reasonCode);
1076
+ try {
1077
+ const underLock = planLegacyStoryPr(options);
1078
+ if (!underLock.ok || JSON.stringify(underLock.value) !== JSON.stringify(before.value)) {
1079
+ return fail('legacy-story-pr-pre-effect-race');
1080
+ }
1081
+ const listed = await provider.list(before.value.repository, before.value.branch, before.value.baseBranch);
1082
+ let item = selectedPr(before.value, listed);
1083
+ const reused = item !== null;
1084
+ if (!item) {
1085
+ const current = planLegacyStoryPr(options);
1086
+ if (!current.ok || JSON.stringify(current.value) !== JSON.stringify(before.value)) {
1087
+ return fail('legacy-story-pr-pre-effect-race');
1088
+ }
1089
+ if (!(await recordCreateAttempt(options.projectRoot, before.value))) {
1090
+ return fail('legacy-story-pr-create-attempt-unresolved');
1091
+ }
1092
+ const stagingDirectory = mkdtempSync(join(tmpdir(), 'legacy-story-pr-body-'));
1093
+ const pinnedBodyFile = join(stagingDirectory, 'body.md');
1094
+ try {
1095
+ writeFileSync(pinnedBodyFile, reviewedBody, { flag: 'wx', mode: 0o600 });
1096
+ await provider.create(before.value.repository, before.value.branch, before.value.baseBranch, `[YOLO] Story ${before.value.storyId}`, pinnedBodyFile);
1097
+ }
1098
+ finally {
1099
+ try {
1100
+ unlinkSync(pinnedBodyFile);
1101
+ }
1102
+ catch { /* The file may not have been created. */ }
1103
+ rmdirSync(stagingDirectory);
1104
+ }
1105
+ const after = planLegacyStoryPr(options);
1106
+ if (!after.ok || JSON.stringify(after.value) !== JSON.stringify(before.value)) {
1107
+ return fail('legacy-story-pr-post-effect-race');
1108
+ }
1109
+ item = selectedPr(before.value, await provider.list(before.value.repository, before.value.branch, before.value.baseBranch));
1110
+ if (!item)
1111
+ return fail('legacy-story-pr-readback-unavailable');
1112
+ }
1113
+ const finalPlan = planLegacyStoryPr(options);
1114
+ if (!finalPlan.ok || JSON.stringify(finalPlan.value) !== JSON.stringify(before.value)) {
1115
+ return fail('legacy-story-pr-post-effect-race');
1116
+ }
1117
+ return { ok: true, value: Object.freeze({ schemaVersion: 1,
1118
+ kind: 'legacy-story-pr-provider-receipt', storyId: before.value.storyId,
1119
+ epicId: before.value.epicId, repository: before.value.repository,
1120
+ branch: before.value.branch, baseBranch: before.value.baseBranch,
1121
+ headSha: before.value.headSha, number: item.number, url: item.url,
1122
+ reused, checkpointAccepted: false, publicSafe: true }) };
1123
+ }
1124
+ catch (error) {
1125
+ const reason = error instanceof Error ? error.message : '';
1126
+ return fail(reason.startsWith('legacy-story-pr-') ? reason : 'legacy-story-pr-provider-unavailable');
1127
+ }
1128
+ finally {
1129
+ await releaseGitLifecyclePhysicalIntegrationLock(lock.path, lock.owner).catch(() => undefined);
1130
+ }
1131
+ }
1132
+ function digest(value) { return createHash('sha256').update(value).digest('hex'); }
1133
+ function projectC10(cwd, plan, number) {
1134
+ const stateBefore = gitRaw(cwd, 'show', 'HEAD:.neocortex/state.json');
1135
+ const state = record(JSON.parse(stateBefore));
1136
+ const story = record(record(state?.stories)?.[plan.storyId]);
1137
+ if (!story || story.status !== 'synced' || story.last_step !== GIT_LIFECYCLE_SYNC_MAIN_STEP_ID
1138
+ || !Array.isArray(story.steps_completed) || !story.steps_completed.includes(GIT_LIFECYCLE_SYNC_MAIN_STEP_ID)
1139
+ || story.steps_completed.includes(GIT_LIFECYCLE_CREATE_PR_STEP_ID)
1140
+ || story.pr_number != null)
1141
+ throw new Error('legacy-story-pr-checkpoint-state-conflict');
1142
+ const completedSteps = story.steps_completed;
1143
+ story.status = 'pr-created';
1144
+ story.last_step = GIT_LIFECYCLE_CREATE_PR_STEP_ID;
1145
+ story.steps_completed = [...completedSteps, GIT_LIFECYCLE_CREATE_PR_STEP_ID];
1146
+ story.pr_number = number;
1147
+ const stateAfter = `${JSON.stringify(state, null, 2)}\n`;
1148
+ const storyPath = `docs/stories/${plan.storyId}.story.md`;
1149
+ const storyBefore = gitRaw(cwd, 'show', `HEAD:${storyPath}`);
1150
+ const frontmatter = storyBefore.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);
1151
+ if (!frontmatter)
1152
+ throw new Error('legacy-story-pr-checkpoint-frontmatter-invalid');
1153
+ const document = parseDocument(frontmatter[1], { strict: true, uniqueKeys: true });
1154
+ const fields = record(document.toJS());
1155
+ const stepsKey = ['stepsCompleted', 'steps_completed'].filter((key) => document.has(key));
1156
+ const lastKey = ['lastStep', 'last_step'].filter((key) => document.has(key));
1157
+ const prKey = ['prNumber', 'pr_number'].filter((key) => document.has(key));
1158
+ const branchKey = ['branchName', 'branch_name'].filter((key) => document.has(key));
1159
+ if (document.errors.length > 0 || document.get('id') !== plan.storyId
1160
+ || (document.get('epic_id') ?? document.get('epic')) !== plan.epicId
1161
+ || document.get('status') !== 'synced' || stepsKey.length > 1 || lastKey.length > 1
1162
+ || prKey.length > 1 || branchKey.length > 1
1163
+ || document.has('currentStatus') && document.get('currentStatus') !== 'synced'
1164
+ || stepsKey.length === 1 && (!Array.isArray(fields?.[stepsKey[0]])
1165
+ || JSON.stringify(fields?.[stepsKey[0]]) !== JSON.stringify(completedSteps))
1166
+ || lastKey.length === 1 && fields?.[lastKey[0]] !== GIT_LIFECYCLE_SYNC_MAIN_STEP_ID
1167
+ || branchKey.length === 1 && fields?.[branchKey[0]] !== plan.branch
1168
+ || prKey.length === 1 && fields?.[prKey[0]] != null) {
1169
+ throw new Error('legacy-story-pr-checkpoint-frontmatter-conflict');
1170
+ }
1171
+ document.set('status', 'pr-created');
1172
+ if (document.has('currentStatus'))
1173
+ document.set('currentStatus', 'pr-created');
1174
+ document.set(stepsKey[0] ?? 'stepsCompleted', [
1175
+ ...(stepsKey.length === 1 ? fields[stepsKey[0]] : completedSteps),
1176
+ GIT_LIFECYCLE_CREATE_PR_STEP_ID,
1177
+ ]);
1178
+ document.set(lastKey[0] ?? 'lastStep', GIT_LIFECYCLE_CREATE_PR_STEP_ID);
1179
+ document.set(prKey[0] ?? 'prNumber', number);
1180
+ const storyAfter = `---\n${document.toString({ lineWidth: 0 })}---\n${storyBefore.slice(frontmatter[0].length)}`;
1181
+ return { stateBefore, stateAfter, storyBefore, storyAfter, storyPath };
1182
+ }
1183
+ function physicalC10(cwd, plan, projection) {
1184
+ const current = inspectLegacyStoryPr({ projectRoot: cwd, storyId: plan.storyId }, true);
1185
+ if (!current.ok || JSON.stringify(current.value) !== JSON.stringify(plan)) {
1186
+ throw new Error('legacy-story-pr-checkpoint-physical-drift');
1187
+ }
1188
+ const stateCurrent = safeRead(join(cwd, '.neocortex/state.json'), 2 * 1024 * 1024);
1189
+ const storyCurrent = safeRead(join(cwd, projection.storyPath), 2 * 1024 * 1024);
1190
+ if (![projection.stateBefore, projection.stateAfter].includes(stateCurrent)
1191
+ || ![projection.storyBefore, projection.storyAfter].includes(storyCurrent)) {
1192
+ throw new Error('legacy-story-pr-checkpoint-preimage-conflict');
1193
+ }
1194
+ const changed = gitRaw(cwd, 'status', '--porcelain=v1', '-uall').split('\n').filter(Boolean);
1195
+ if (changed.some((line) => !line.startsWith(' M ')
1196
+ || !['.neocortex/state.json', projection.storyPath].includes(line.slice(3)))) {
1197
+ throw new Error('legacy-story-pr-checkpoint-worktree-dirty');
1198
+ }
1199
+ return { stateCurrent, storyCurrent };
1200
+ }
1201
+ async function readC10Journal(directory, name) {
1202
+ const path = join(directory, name);
1203
+ if (!existsSync(path))
1204
+ return null;
1205
+ await assertGitLifecyclePrivateFile(path, directory);
1206
+ const raw = safeRead(path, 4096);
1207
+ const value = record(JSON.parse(raw));
1208
+ if (!value || value.schemaVersion !== 1 || value.kind !== 'legacy-story-c10-acceptance-intent'
1209
+ || typeof value.qaDigest !== 'string' || !/^[a-f0-9]{64}$/.test(value.qaDigest)) {
1210
+ throw new Error('legacy-story-pr-checkpoint-journal-invalid');
1211
+ }
1212
+ return value;
1213
+ }
1214
+ async function writeC10Journal(directory, name, journal) {
1215
+ await anchoredAtomicWriteFile(directory, name, `${JSON.stringify(journal)}\n`);
1216
+ await assertGitLifecyclePrivateFile(join(directory, name), directory);
1217
+ }
1218
+ /** Accepts only an exact, still-open provider PR after physical local QA. */
1219
+ export async function acceptLegacyStoryPrC10(options) {
1220
+ if (!Number.isSafeInteger(options.prNumber) || options.prNumber <= 0) {
1221
+ return fail('legacy-story-pr-checkpoint-number-invalid');
1222
+ }
1223
+ const clean = planLegacyStoryPr(options);
1224
+ let qaDigest = null;
1225
+ if (clean.ok) {
1226
+ try {
1227
+ qaDigest = await verifyLocalQa(clean.value, options.projectRoot, options.verifyQa);
1228
+ }
1229
+ catch {
1230
+ return fail('legacy-story-pr-checkpoint-qa-failed');
1231
+ }
1232
+ }
1233
+ const sessionId = `legacy-c10-accept-${randomBytes(16).toString('hex')}`;
1234
+ const lock = await acquireGitLifecyclePhysicalIntegrationLock(options.projectRoot, {
1235
+ kind: 'direct', storyId: options.storyId, sessionId, idempotencyKey: sessionId,
1236
+ expiresAt: new Date(Date.now() + 5 * 60_000).toISOString(), persistent: false,
1237
+ });
1238
+ if (!lock.ok)
1239
+ return fail(lock.reasonCode);
1240
+ try {
1241
+ const cwd = realpathSync(options.projectRoot);
1242
+ const current = inspectLegacyStoryPr(options, true);
1243
+ if (!current.ok || clean.ok && JSON.stringify(current.value) !== JSON.stringify(clean.value)) {
1244
+ return fail('legacy-story-pr-checkpoint-plan-drift');
1245
+ }
1246
+ const plan = current.value;
1247
+ const projection = projectC10(cwd, plan, options.prNumber);
1248
+ const before = physicalC10(cwd, plan, projection);
1249
+ const directory = await ensureGitLifecyclePrivateChildDirectory(await gitLifecycleRuntimeDirectory(cwd), 'legacy-story-pr-intents');
1250
+ const prefix = digest(plan.storyId);
1251
+ const journalName = `${prefix}.c10.json`;
1252
+ const acceptedName = `${prefix}.c10.accepted.json`;
1253
+ const prior = await readC10Journal(directory, journalName);
1254
+ const accepted = await readC10Journal(directory, acceptedName);
1255
+ if (!prior && (!clean.ok || !qaDigest || before.stateCurrent !== projection.stateBefore
1256
+ || before.storyCurrent !== projection.storyBefore)) {
1257
+ return fail('legacy-story-pr-checkpoint-unjournaled-drift');
1258
+ }
1259
+ if (accepted && (!prior || JSON.stringify(accepted) !== JSON.stringify(prior))) {
1260
+ return fail('legacy-story-pr-checkpoint-accepted-conflict');
1261
+ }
1262
+ const item = selectedPr(plan, await (options.provider ?? defaultProvider).list(plan.repository, plan.branch, plan.baseBranch));
1263
+ if (!item || item.number !== options.prNumber)
1264
+ return fail('legacy-story-pr-checkpoint-provider-mismatch');
1265
+ const journal = {
1266
+ schemaVersion: 1, kind: 'legacy-story-c10-acceptance-intent', storyId: plan.storyId,
1267
+ planDigest: digest(JSON.stringify(plan)), repository: plan.repository,
1268
+ number: item.number, url: item.url, qaDigest: prior?.qaDigest ?? qaDigest,
1269
+ stateBefore: digest(projection.stateBefore), stateAfter: digest(projection.stateAfter),
1270
+ storyBefore: digest(projection.storyBefore), storyAfter: digest(projection.storyAfter),
1271
+ };
1272
+ if (prior && JSON.stringify(prior) !== JSON.stringify(journal)) {
1273
+ return fail('legacy-story-pr-checkpoint-journal-conflict');
1274
+ }
1275
+ if (!prior)
1276
+ await writeC10Journal(directory, journalName, journal);
1277
+ const prewrite = physicalC10(cwd, plan, projection);
1278
+ if (accepted && (prewrite.stateCurrent !== projection.stateAfter
1279
+ || prewrite.storyCurrent !== projection.storyAfter)) {
1280
+ return fail('legacy-story-pr-checkpoint-accepted-physical-mismatch');
1281
+ }
1282
+ if (prewrite.stateCurrent === projection.stateBefore) {
1283
+ await anchoredCompareAndSwapFile(join(cwd, '.neocortex'), 'state.json', projection.stateAfter, journal.stateBefore);
1284
+ await options.afterStatePersist?.();
1285
+ }
1286
+ const middle = physicalC10(cwd, plan, projection);
1287
+ if (middle.storyCurrent === projection.storyBefore) {
1288
+ await anchoredCompareAndSwapFile(join(cwd, 'docs/stories'), `${plan.storyId}.story.md`, projection.storyAfter, journal.storyBefore);
1289
+ await options.afterStoryPersist?.();
1290
+ }
1291
+ const after = physicalC10(cwd, plan, projection);
1292
+ if (after.stateCurrent !== projection.stateAfter || after.storyCurrent !== projection.storyAfter) {
1293
+ return fail('legacy-story-pr-checkpoint-post-write-mismatch');
1294
+ }
1295
+ if (!accepted)
1296
+ await writeC10Journal(directory, acceptedName, journal);
1297
+ return { ok: true, value: Object.freeze({
1298
+ schemaVersion: 1, kind: 'legacy-story-c10-checkpoint', storyId: plan.storyId,
1299
+ epicId: plan.epicId, branch: plan.branch, repository: plan.repository,
1300
+ number: item.number, url: item.url, headSha: plan.headSha,
1301
+ stateSha256: journal.stateAfter, storySha256: journal.storyAfter,
1302
+ reused: Boolean(accepted), checkpointAccepted: true, localOnly: true,
1303
+ }) };
1304
+ }
1305
+ catch (error) {
1306
+ const reason = error instanceof Error ? error.message : '';
1307
+ return fail(reason.startsWith('legacy-story-pr-') ? reason
1308
+ : reason.includes('secure-filesystem-cas-conflict') ? 'legacy-story-pr-checkpoint-write-raced'
1309
+ : 'legacy-story-pr-checkpoint-failed');
1310
+ }
1311
+ finally {
1312
+ await releaseGitLifecyclePhysicalIntegrationLock(lock.path, lock.owner).catch(() => undefined);
1313
+ }
1314
+ }
1315
+ /** Read-only C10 predecessor for the root-owned C11 review. */
1316
+ export async function readAcceptedLegacyStoryPrC10(options) {
1317
+ if (!Number.isSafeInteger(options.prNumber) || options.prNumber <= 0) {
1318
+ return fail('legacy-story-pr-checkpoint-number-invalid');
1319
+ }
1320
+ try {
1321
+ const cwd = realpathSync(options.projectRoot);
1322
+ const current = inspectLegacyStoryPr(options, true);
1323
+ if (!current.ok)
1324
+ return current;
1325
+ const plan = current.value;
1326
+ const projection = projectC10(cwd, plan, options.prNumber);
1327
+ const physical = physicalC10(cwd, plan, projection);
1328
+ if (physical.stateCurrent !== projection.stateAfter
1329
+ || physical.storyCurrent !== projection.storyAfter) {
1330
+ return fail('legacy-story-pr-checkpoint-not-accepted');
1331
+ }
1332
+ const directory = await ensureGitLifecyclePrivateChildDirectory(await gitLifecycleRuntimeDirectory(cwd), 'legacy-story-pr-intents');
1333
+ const prefix = digest(plan.storyId);
1334
+ const prior = await readC10Journal(directory, `${prefix}.c10.json`);
1335
+ const accepted = await readC10Journal(directory, `${prefix}.c10.accepted.json`);
1336
+ if (!prior || !accepted || JSON.stringify(prior) !== JSON.stringify(accepted)) {
1337
+ return fail('legacy-story-pr-checkpoint-not-accepted');
1338
+ }
1339
+ const item = selectedPr(plan, await (options.provider ?? defaultProvider).list(plan.repository, plan.branch, plan.baseBranch));
1340
+ if (!item || item.number !== options.prNumber) {
1341
+ return fail('legacy-story-pr-checkpoint-provider-mismatch');
1342
+ }
1343
+ const expected = {
1344
+ schemaVersion: 1, kind: 'legacy-story-c10-acceptance-intent', storyId: plan.storyId,
1345
+ planDigest: digest(JSON.stringify(plan)), repository: plan.repository,
1346
+ number: item.number, url: item.url, qaDigest: accepted.qaDigest,
1347
+ stateBefore: digest(projection.stateBefore), stateAfter: digest(projection.stateAfter),
1348
+ storyBefore: digest(projection.storyBefore), storyAfter: digest(projection.storyAfter),
1349
+ };
1350
+ if (JSON.stringify(accepted) !== JSON.stringify(expected)) {
1351
+ return fail('legacy-story-pr-checkpoint-journal-conflict');
1352
+ }
1353
+ return { ok: true, value: Object.freeze({ plan, receipt: Object.freeze({
1354
+ schemaVersion: 1, kind: 'legacy-story-c10-checkpoint', storyId: plan.storyId,
1355
+ epicId: plan.epicId, branch: plan.branch, repository: plan.repository,
1356
+ number: item.number, url: item.url, headSha: plan.headSha,
1357
+ stateSha256: accepted.stateAfter, storySha256: accepted.storyAfter,
1358
+ reused: true, checkpointAccepted: true, localOnly: true,
1359
+ }) }) };
1360
+ }
1361
+ catch (error) {
1362
+ const reason = error instanceof Error ? error.message : '';
1363
+ return fail(reason.startsWith('legacy-story-pr-') ? reason : 'legacy-story-pr-checkpoint-read-failed');
1364
+ }
1365
+ }
1366
+ function reviewDiff(cwd, plan) {
1367
+ const raw = gitRaw(cwd, 'diff', '--raw', '-z', plan.baseSha, plan.headSha);
1368
+ const paths = gitRaw(cwd, 'diff', '--name-only', '-z', plan.baseSha, plan.headSha)
1369
+ .split('\0').filter(Boolean);
1370
+ if (paths.length === 0 || paths.length > 256 || paths.some((path) => !safeReviewPath(path))) {
1371
+ throw new Error('legacy-story-pr-review-diff-invalid');
1372
+ }
1373
+ return { diffSha256: digest(raw), changedPaths: Object.freeze(paths) };
1374
+ }
1375
+ function safeReviewPath(path) {
1376
+ return path.length > 0 && path.length <= 240
1377
+ && /^[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)*$/.test(path)
1378
+ && path.split('/').every((part) => part !== '.' && part !== '..');
1379
+ }
1380
+ function projectC11(plan, c10, prNumber, reviewSha256) {
1381
+ const state = record(JSON.parse(c10.stateAfter));
1382
+ const story = record(record(state?.stories)?.[plan.storyId]);
1383
+ if (!story || story.status !== 'pr-created' || story.last_step !== GIT_LIFECYCLE_CREATE_PR_STEP_ID
1384
+ || story.pr_number !== prNumber || story.review_request != null
1385
+ || !Array.isArray(story.steps_completed)
1386
+ || !story.steps_completed.includes(GIT_LIFECYCLE_CREATE_PR_STEP_ID)
1387
+ || story.steps_completed.includes(GIT_LIFECYCLE_REVIEW_PR_STEP_ID)) {
1388
+ throw new Error('legacy-story-pr-review-state-conflict');
1389
+ }
1390
+ story.status = 'pr-reviewed';
1391
+ story.last_step = GIT_LIFECYCLE_REVIEW_PR_STEP_ID;
1392
+ story.steps_completed = [...story.steps_completed, GIT_LIFECYCLE_REVIEW_PR_STEP_ID];
1393
+ story.review_request = { schemaVersion: 1, kind: 'git-pull-request', gitPullRequest: true, prNumber,
1394
+ headSha: plan.headSha, reviewEvidenceDigest: reviewSha256 };
1395
+ const stateAfter = `${JSON.stringify(state, null, 2)}\n`;
1396
+ const frontmatter = c10.storyAfter.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);
1397
+ if (!frontmatter)
1398
+ throw new Error('legacy-story-pr-review-frontmatter-invalid');
1399
+ const document = parseDocument(frontmatter[1], { strict: true, uniqueKeys: true });
1400
+ const fields = record(document.toJS());
1401
+ const stepsKey = ['stepsCompleted', 'steps_completed'].filter((key) => document.has(key));
1402
+ const lastKey = ['lastStep', 'last_step'].filter((key) => document.has(key));
1403
+ const frontmatterSteps = fields?.[stepsKey[0]];
1404
+ if (document.errors.length > 0 || document.get('id') !== plan.storyId
1405
+ || (document.get('epic_id') ?? document.get('epic')) !== plan.epicId
1406
+ || document.get('status') !== 'pr-created' || stepsKey.length !== 1 || lastKey.length !== 1
1407
+ || document.has('currentStatus') && document.get('currentStatus') !== 'pr-created'
1408
+ || fields?.[lastKey[0]] !== GIT_LIFECYCLE_CREATE_PR_STEP_ID
1409
+ || !Array.isArray(frontmatterSteps)
1410
+ || !frontmatterSteps.includes(GIT_LIFECYCLE_CREATE_PR_STEP_ID)) {
1411
+ throw new Error('legacy-story-pr-review-frontmatter-conflict');
1412
+ }
1413
+ document.set('status', 'pr-reviewed');
1414
+ if (document.has('currentStatus'))
1415
+ document.set('currentStatus', 'pr-reviewed');
1416
+ document.set(stepsKey[0], [...frontmatterSteps, GIT_LIFECYCLE_REVIEW_PR_STEP_ID]);
1417
+ document.set(lastKey[0], GIT_LIFECYCLE_REVIEW_PR_STEP_ID);
1418
+ const storyAfter = `---\n${document.toString({ lineWidth: 0 })}---\n${c10.storyAfter.slice(frontmatter[0].length)}`;
1419
+ return { stateBefore: c10.stateAfter, stateAfter, storyBefore: c10.storyAfter,
1420
+ storyAfter, storyPath: c10.storyPath };
1421
+ }
1422
+ async function inspectLegacyReviewBasis(options) {
1423
+ const cwd = realpathSync(options.projectRoot);
1424
+ const inspected = inspectLegacyStoryPr(options, true);
1425
+ if (!inspected.ok)
1426
+ throw new Error(inspected.reasonCode);
1427
+ const plan = inspected.value;
1428
+ if (!Number.isSafeInteger(options.prNumber) || options.prNumber <= 0) {
1429
+ throw new Error('legacy-story-pr-review-number-invalid');
1430
+ }
1431
+ const c10 = projectC10(cwd, plan, options.prNumber);
1432
+ const directory = await ensureGitLifecyclePrivateChildDirectory(await gitLifecycleRuntimeDirectory(cwd), 'legacy-story-pr-intents');
1433
+ const prefix = digest(plan.storyId);
1434
+ const prior = await readC10Journal(directory, `${prefix}.c10.json`);
1435
+ const accepted = await readC10Journal(directory, `${prefix}.c10.accepted.json`);
1436
+ if (!prior || !accepted || JSON.stringify(prior) !== JSON.stringify(accepted)) {
1437
+ throw new Error('legacy-story-pr-review-c10-unaccepted');
1438
+ }
1439
+ const item = selectedPr(plan, await (options.provider ?? defaultProvider).list(plan.repository, plan.branch, plan.baseBranch));
1440
+ if (!item || item.number !== options.prNumber) {
1441
+ throw new Error('legacy-story-pr-review-provider-mismatch');
1442
+ }
1443
+ const expected = {
1444
+ schemaVersion: 1, kind: 'legacy-story-c10-acceptance-intent', storyId: plan.storyId,
1445
+ planDigest: digest(JSON.stringify(plan)), repository: plan.repository,
1446
+ number: item.number, url: item.url, qaDigest: accepted.qaDigest,
1447
+ stateBefore: digest(c10.stateBefore), stateAfter: digest(c10.stateAfter),
1448
+ storyBefore: digest(c10.storyBefore), storyAfter: digest(c10.storyAfter),
1449
+ };
1450
+ if (JSON.stringify(accepted) !== JSON.stringify(expected)) {
1451
+ throw new Error('legacy-story-pr-review-c10-journal-conflict');
1452
+ }
1453
+ const diff = reviewDiff(cwd, plan);
1454
+ const reviewPlan = Object.freeze({
1455
+ schemaVersion: 1, kind: 'legacy-story-pr-review-plan', storyId: plan.storyId,
1456
+ epicId: plan.epicId, repository: plan.repository, branch: plan.branch,
1457
+ baseBranch: plan.baseBranch, baseSha: plan.baseSha, headSha: plan.headSha,
1458
+ prNumber: options.prNumber, diffSha256: diff.diffSha256,
1459
+ changedPaths: diff.changedPaths, checkpointAccepted: false, localOnly: true,
1460
+ });
1461
+ return { cwd, plan, c10, directory, reviewPlan };
1462
+ }
1463
+ function physicalC11(cwd, plan, projection) {
1464
+ const inspected = inspectLegacyStoryPr({ projectRoot: cwd, storyId: plan.storyId }, true);
1465
+ if (!inspected.ok || JSON.stringify(inspected.value) !== JSON.stringify(plan)) {
1466
+ throw new Error('legacy-story-pr-review-physical-drift');
1467
+ }
1468
+ const stateCurrent = safeRead(join(cwd, '.neocortex/state.json'), 2 * 1024 * 1024);
1469
+ const storyCurrent = safeRead(join(cwd, projection.storyPath), 2 * 1024 * 1024);
1470
+ if (![projection.stateBefore, projection.stateAfter].includes(stateCurrent)
1471
+ || ![projection.storyBefore, projection.storyAfter].includes(storyCurrent)) {
1472
+ throw new Error('legacy-story-pr-review-preimage-conflict');
1473
+ }
1474
+ const changed = gitRaw(cwd, 'status', '--porcelain=v1', '-uall').split('\n').filter(Boolean);
1475
+ if (changed.some((line) => !line.startsWith(' M ')
1476
+ || !['.neocortex/state.json', projection.storyPath].includes(line.slice(3)))) {
1477
+ throw new Error('legacy-story-pr-review-worktree-dirty');
1478
+ }
1479
+ return { stateCurrent, storyCurrent };
1480
+ }
1481
+ function validateReviewEvidence(cwd, headSha, items) {
1482
+ if (!Array.isArray(items) || items.length < 1 || items.length > 64)
1483
+ return false;
1484
+ const ids = new Set();
1485
+ return items.every((item) => {
1486
+ const value = record(item);
1487
+ if (!value || Object.keys(value).sort().join(',') !== 'disposition,id,path,sha256'
1488
+ || typeof value.id !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/.test(value.id)
1489
+ || ids.has(value.id) || typeof value.path !== 'string' || !safeReviewPath(value.path)
1490
+ || typeof value.sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(value.sha256)
1491
+ || value.disposition !== 'passed' && value.disposition !== 'not-applicable')
1492
+ return false;
1493
+ ids.add(value.id);
1494
+ try {
1495
+ return digest(gitRaw(cwd, 'show', `${headSha}:${value.path}`)) === value.sha256;
1496
+ }
1497
+ catch {
1498
+ return false;
1499
+ }
1500
+ });
1501
+ }
1502
+ /** Only bounded, HEAD-bound P3 follow-ups may accompany an approved root
1503
+ * verdict. They become tracked debt in C12's exact review artifact. */
1504
+ function validateNonBlockingReviewFindings(basis, items) {
1505
+ if (!Array.isArray(items) || items.length > 16)
1506
+ return false;
1507
+ const actions = new Map([
1508
+ ['documentation-gap', 'update-docs'],
1509
+ ['test-gap', 'add-regression-test'],
1510
+ ['minor-maintainability', 'simplify-code'],
1511
+ ]);
1512
+ const ids = new Set();
1513
+ return items.every((item) => {
1514
+ const finding = record(item);
1515
+ if (!finding || Object.keys(finding).sort().join(',') !== 'actionCode,id,path,reasonCode,severity,sha256'
1516
+ || typeof finding.id !== 'string' || !/^F[1-9][0-9]{0,2}$/.test(finding.id)
1517
+ || ids.has(finding.id) || finding.severity !== 'P3'
1518
+ || typeof finding.reasonCode !== 'string'
1519
+ || finding.actionCode !== actions.get(finding.reasonCode)
1520
+ || typeof finding.path !== 'string' || !basis.reviewPlan.changedPaths.includes(finding.path)
1521
+ || typeof finding.sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(finding.sha256))
1522
+ return false;
1523
+ ids.add(finding.id);
1524
+ try {
1525
+ return digest(gitRaw(basis.cwd, 'show', `${basis.reviewPlan.headSha}:${finding.path}`))
1526
+ === finding.sha256;
1527
+ }
1528
+ catch {
1529
+ return false;
1530
+ }
1531
+ });
1532
+ }
1533
+ function validateReviewReportRaw(raw, basis) {
1534
+ if (Buffer.byteLength(raw) > 32 * 1024)
1535
+ throw new Error('legacy-story-pr-review-report-unsafe');
1536
+ const value = record(JSON.parse(raw));
1537
+ const plan = basis.reviewPlan;
1538
+ if (!value || Object.keys(value).sort().join(',') !== [
1539
+ 'acceptanceEvidence', 'baseSha', 'blockingFindings', 'diffSha256', 'epicId',
1540
+ 'headSha', 'kind', 'nonBlockingFindings', 'prNumber', 'remoteObservations',
1541
+ 'schemaVersion', 'sddEvidence', 'storyId', 'verdict',
1542
+ ].sort().join(',') || value.schemaVersion !== 1 || value.kind !== 'legacy-story-pr-review'
1543
+ || value.storyId !== plan.storyId || value.epicId !== plan.epicId
1544
+ || value.prNumber !== plan.prNumber || value.headSha !== plan.headSha
1545
+ || value.baseSha !== plan.baseSha || value.diffSha256 !== plan.diffSha256
1546
+ || value.verdict !== 'approved'
1547
+ || !Array.isArray(value.blockingFindings) || value.blockingFindings.length !== 0
1548
+ || !validateNonBlockingReviewFindings(basis, value.nonBlockingFindings)
1549
+ || !Array.isArray(value.remoteObservations) || value.remoteObservations.length > 1
1550
+ || value.remoteObservations.some((observation) => {
1551
+ const summary = record(observation);
1552
+ return !summary || !Array.isArray(summary.reasonCodes) || summary.reasonCodes.length === 0
1553
+ || projectC12ScopedCiFollowUpDebt(observation, plan.storyId, plan.prNumber) === null;
1554
+ })
1555
+ || !validateReviewEvidence(basis.cwd, plan.headSha, value.acceptanceEvidence)
1556
+ || !validateReviewEvidence(basis.cwd, plan.headSha, value.sddEvidence)) {
1557
+ throw new Error('legacy-story-pr-review-report-invalid');
1558
+ }
1559
+ return { report: value, raw, sha256: digest(raw) };
1560
+ }
1561
+ function readReviewReport(path, basis) {
1562
+ if (lstatSync(path).isSymbolicLink())
1563
+ throw new Error('legacy-story-pr-review-report-unsafe');
1564
+ return validateReviewReportRaw(safeRead(path, 32 * 1024), basis);
1565
+ }
1566
+ /** Remote-only CI is observational at C11; C12 must reobserve and durably
1567
+ * record exact categorical debt before any non-green merge attempt. */
1568
+ async function inspectC11RemoteObservation(basis, provider) {
1569
+ const observed = await provider.view(basis.plan.repository, basis.reviewPlan.prNumber);
1570
+ const plan = basis.reviewPlan;
1571
+ if (observed.number !== plan.prNumber || observed.state !== 'OPEN'
1572
+ || observed.headRefName !== plan.branch || observed.headRefOid !== plan.headSha
1573
+ || observed.baseRefName !== plan.baseBranch || observed.baseRefOid !== plan.baseSha
1574
+ || observed.url.toLowerCase() !== `https://github.com/${plan.repository}/pull/${plan.prNumber}`.toLowerCase()) {
1575
+ throw new Error('legacy-story-pr-review-remote-provider-mismatch');
1576
+ }
1577
+ const summary = summarizeC12RemoteRollup(observed.statusCheckRollup);
1578
+ if (!summary.ok)
1579
+ throw new Error('legacy-story-pr-review-remote-observation-unclassified');
1580
+ return summary.value;
1581
+ }
1582
+ async function verifyC11RemoteObservation(basis, report, provider) {
1583
+ if (report.remoteObservations.length === 0)
1584
+ return;
1585
+ const summary = await inspectC11RemoteObservation(basis, provider);
1586
+ if (JSON.stringify(summary) !== JSON.stringify(report.remoteObservations[0])) {
1587
+ throw new Error('legacy-story-pr-review-remote-observation-drift');
1588
+ }
1589
+ }
1590
+ /** Read-only categorical PR observation for the root C11 report. No raw check
1591
+ * name, URL or provider text is returned, and C12 must reobserve independently. */
1592
+ export async function observeLegacyStoryPrReviewC11(options) {
1593
+ try {
1594
+ const basis = await inspectLegacyReviewBasis(options);
1595
+ const current = physicalC11(basis.cwd, basis.plan, projectC11(basis.plan, basis.c10, options.prNumber, '0'.repeat(64)));
1596
+ if (current.stateCurrent !== basis.c10.stateAfter || current.storyCurrent !== basis.c10.storyAfter) {
1597
+ return fail('legacy-story-pr-review-already-transitioned');
1598
+ }
1599
+ const observation = await inspectC11RemoteObservation(basis, options.mergeProvider ?? defaultMergeProvider);
1600
+ return { ok: true, value: Object.freeze({ schemaVersion: 1,
1601
+ kind: 'legacy-story-c11-remote-observation', storyId: basis.plan.storyId,
1602
+ prNumber: options.prNumber, observation, checkpointAccepted: false,
1603
+ mergeAuthorized: false, publicSafe: true }) };
1604
+ }
1605
+ catch (error) {
1606
+ const reason = error instanceof Error ? error.message : '';
1607
+ return fail(reason.startsWith('legacy-story-pr-') ? reason : 'legacy-story-pr-review-remote-observe-failed');
1608
+ }
1609
+ }
1610
+ async function readC11Journal(directory, name) {
1611
+ const path = join(directory, name);
1612
+ if (!existsSync(path))
1613
+ return null;
1614
+ await assertGitLifecyclePrivateFile(path, directory);
1615
+ const value = record(JSON.parse(safeRead(path, 64 * 1024)));
1616
+ if (!value || value.schemaVersion !== 1 || value.kind !== 'legacy-story-c11-acceptance-intent'
1617
+ || typeof value.reviewSha256 !== 'string' || !/^[a-f0-9]{64}$/.test(value.reviewSha256)
1618
+ || typeof value.qaSha256 !== 'string' || !/^[a-f0-9]{64}$/.test(value.qaSha256)
1619
+ || typeof value.reviewRaw !== 'string' || digest(value.reviewRaw) !== value.reviewSha256) {
1620
+ throw new Error('legacy-story-pr-review-journal-invalid');
1621
+ }
1622
+ return value;
1623
+ }
1624
+ async function writeC11Journal(directory, name, journal) {
1625
+ await anchoredAtomicWriteFile(directory, name, `${JSON.stringify(journal)}\n`);
1626
+ await assertGitLifecyclePrivateFile(join(directory, name), directory);
1627
+ }
1628
+ export async function planLegacyStoryPrReview(options) {
1629
+ try {
1630
+ const basis = await inspectLegacyReviewBasis(options);
1631
+ const current = physicalC11(basis.cwd, basis.plan, projectC11(basis.plan, basis.c10, options.prNumber, '0'.repeat(64)));
1632
+ if (current.stateCurrent !== basis.c10.stateAfter || current.storyCurrent !== basis.c10.storyAfter) {
1633
+ return fail('legacy-story-pr-review-already-transitioned');
1634
+ }
1635
+ return { ok: true, value: basis.reviewPlan };
1636
+ }
1637
+ catch (error) {
1638
+ const reason = error instanceof Error ? error.message : '';
1639
+ return fail(reason.startsWith('legacy-story-pr-') ? reason : 'legacy-story-pr-review-plan-failed');
1640
+ }
1641
+ }
1642
+ /** Root-only C11 acceptance. The caller supplies an independently reviewed,
1643
+ * public-safe report; neither the server selection nor a sandbox return can
1644
+ * approve this checkpoint. It never commits, pushes, or merges the PR. */
1645
+ export async function acceptLegacyStoryPrReviewC11(options) {
1646
+ if (!options.authorize)
1647
+ return fail('legacy-story-pr-review-authorization-required');
1648
+ let initial;
1649
+ let reviewed;
1650
+ let freshQa = null;
1651
+ try {
1652
+ initial = await inspectLegacyReviewBasis(options);
1653
+ reviewed = readReviewReport(options.reportFile, initial);
1654
+ await verifyC11RemoteObservation(initial, reviewed.report, options.mergeProvider ?? defaultMergeProvider);
1655
+ const c11 = projectC11(initial.plan, initial.c10, options.prNumber, reviewed.sha256);
1656
+ const current = physicalC11(initial.cwd, initial.plan, c11);
1657
+ if (current.stateCurrent === initial.c10.stateAfter
1658
+ && current.storyCurrent === initial.c10.storyAfter) {
1659
+ try {
1660
+ freshQa = await verifyLocalQa(initial.plan, initial.cwd, options.verifyQa);
1661
+ }
1662
+ catch {
1663
+ throw new Error('legacy-story-pr-review-qa-failed');
1664
+ }
1665
+ }
1666
+ }
1667
+ catch (error) {
1668
+ const reason = error instanceof Error ? error.message : '';
1669
+ return fail(reason.startsWith('legacy-story-pr-') ? reason : 'legacy-story-pr-review-preflight-failed');
1670
+ }
1671
+ const sessionId = `legacy-c11-accept-${randomBytes(16).toString('hex')}`;
1672
+ const lock = await acquireGitLifecyclePhysicalIntegrationLock(options.projectRoot, {
1673
+ kind: 'direct', storyId: options.storyId, sessionId, idempotencyKey: sessionId,
1674
+ expiresAt: new Date(Date.now() + 5 * 60_000).toISOString(), persistent: false,
1675
+ });
1676
+ if (!lock.ok)
1677
+ return fail(lock.reasonCode);
1678
+ try {
1679
+ const basis = await inspectLegacyReviewBasis(options);
1680
+ if (JSON.stringify(basis.reviewPlan) !== JSON.stringify(initial.reviewPlan)) {
1681
+ return fail('legacy-story-pr-review-plan-drift');
1682
+ }
1683
+ const report = readReviewReport(options.reportFile, basis);
1684
+ if (report.sha256 !== reviewed.sha256)
1685
+ return fail('legacy-story-pr-review-report-drift');
1686
+ await verifyC11RemoteObservation(basis, report.report, options.mergeProvider ?? defaultMergeProvider);
1687
+ const c11 = projectC11(basis.plan, basis.c10, options.prNumber, report.sha256);
1688
+ const before = physicalC11(basis.cwd, basis.plan, c11);
1689
+ const prefix = digest(basis.plan.storyId);
1690
+ const intentName = `${prefix}.c11.json`;
1691
+ const acceptedName = `${prefix}.c11.accepted.json`;
1692
+ const prior = await readC11Journal(basis.directory, intentName);
1693
+ const accepted = await readC11Journal(basis.directory, acceptedName);
1694
+ if (!prior && (!freshQa || before.stateCurrent !== c11.stateBefore
1695
+ || before.storyCurrent !== c11.storyBefore)) {
1696
+ return fail('legacy-story-pr-review-unjournaled-drift');
1697
+ }
1698
+ if (accepted && (!prior || JSON.stringify(accepted) !== JSON.stringify(prior))) {
1699
+ return fail('legacy-story-pr-review-accepted-conflict');
1700
+ }
1701
+ const journal = {
1702
+ schemaVersion: 1, kind: 'legacy-story-c11-acceptance-intent', storyId: basis.plan.storyId,
1703
+ planDigest: digest(JSON.stringify(basis.reviewPlan)), prNumber: options.prNumber,
1704
+ reviewSha256: report.sha256, qaSha256: prior?.qaSha256 ?? freshQa,
1705
+ reviewRaw: report.raw,
1706
+ stateBefore: digest(c11.stateBefore), stateAfter: digest(c11.stateAfter),
1707
+ storyBefore: digest(c11.storyBefore), storyAfter: digest(c11.storyAfter),
1708
+ };
1709
+ if (prior && JSON.stringify(prior) !== JSON.stringify(journal)) {
1710
+ return fail('legacy-story-pr-review-journal-conflict');
1711
+ }
1712
+ if (!prior)
1713
+ await writeC11Journal(basis.directory, intentName, journal);
1714
+ const prewrite = physicalC11(basis.cwd, basis.plan, c11);
1715
+ if (accepted && (prewrite.stateCurrent !== c11.stateAfter
1716
+ || prewrite.storyCurrent !== c11.storyAfter)) {
1717
+ return fail('legacy-story-pr-review-accepted-physical-mismatch');
1718
+ }
1719
+ if (prewrite.stateCurrent === c11.stateBefore) {
1720
+ await anchoredCompareAndSwapFile(join(basis.cwd, '.neocortex'), 'state.json', c11.stateAfter, journal.stateBefore);
1721
+ await options.afterStatePersist?.();
1722
+ }
1723
+ const middle = physicalC11(basis.cwd, basis.plan, c11);
1724
+ if (middle.storyCurrent === c11.storyBefore) {
1725
+ await anchoredCompareAndSwapFile(join(basis.cwd, 'docs/stories'), `${basis.plan.storyId}.story.md`, c11.storyAfter, journal.storyBefore);
1726
+ await options.afterStoryPersist?.();
1727
+ }
1728
+ const after = physicalC11(basis.cwd, basis.plan, c11);
1729
+ if (after.stateCurrent !== c11.stateAfter || after.storyCurrent !== c11.storyAfter) {
1730
+ return fail('legacy-story-pr-review-post-write-mismatch');
1731
+ }
1732
+ if (!accepted)
1733
+ await writeC11Journal(basis.directory, acceptedName, journal);
1734
+ return { ok: true, value: Object.freeze({
1735
+ schemaVersion: 1, kind: 'legacy-story-c11-checkpoint', storyId: basis.plan.storyId,
1736
+ epicId: basis.plan.epicId, branch: basis.plan.branch, prNumber: options.prNumber,
1737
+ headSha: basis.plan.headSha, reviewSha256: journal.reviewSha256,
1738
+ qaSha256: journal.qaSha256, stateSha256: journal.stateAfter,
1739
+ storySha256: journal.storyAfter, reused: Boolean(accepted),
1740
+ checkpointAccepted: true, localOnly: true,
1741
+ }) };
1742
+ }
1743
+ catch (error) {
1744
+ const reason = error instanceof Error ? error.message : '';
1745
+ return fail(reason.startsWith('legacy-story-pr-') ? reason
1746
+ : reason.includes('secure-filesystem-cas-conflict') ? 'legacy-story-pr-review-write-raced'
1747
+ : 'legacy-story-pr-review-accept-failed');
1748
+ }
1749
+ finally {
1750
+ await releaseGitLifecyclePhysicalIntegrationLock(lock.path, lock.owner).catch(() => undefined);
1751
+ }
1752
+ }
1753
+ /** Independent readback for the eventual C12 integration boundary. A saved
1754
+ * status or provider PR alone is never a review verdict. */
1755
+ export async function readAcceptedLegacyStoryPrReviewC11(options) {
1756
+ try {
1757
+ const basis = await inspectLegacyReviewBasis(options);
1758
+ const prefix = digest(basis.plan.storyId);
1759
+ const prior = await readC11Journal(basis.directory, `${prefix}.c11.json`);
1760
+ const accepted = await readC11Journal(basis.directory, `${prefix}.c11.accepted.json`);
1761
+ if (!prior || !accepted || JSON.stringify(prior) !== JSON.stringify(accepted)) {
1762
+ return fail('legacy-story-pr-review-not-accepted');
1763
+ }
1764
+ const reviewed = validateReviewReportRaw(accepted.reviewRaw, basis);
1765
+ const projection = projectC11(basis.plan, basis.c10, options.prNumber, reviewed.sha256);
1766
+ const expected = {
1767
+ schemaVersion: 1, kind: 'legacy-story-c11-acceptance-intent', storyId: basis.plan.storyId,
1768
+ planDigest: digest(JSON.stringify(basis.reviewPlan)), prNumber: options.prNumber,
1769
+ reviewSha256: reviewed.sha256, qaSha256: accepted.qaSha256,
1770
+ reviewRaw: reviewed.raw,
1771
+ stateBefore: digest(projection.stateBefore), stateAfter: digest(projection.stateAfter),
1772
+ storyBefore: digest(projection.storyBefore), storyAfter: digest(projection.storyAfter),
1773
+ };
1774
+ if (JSON.stringify(accepted) !== JSON.stringify(expected)) {
1775
+ return fail('legacy-story-pr-review-journal-conflict');
1776
+ }
1777
+ const current = physicalC11(basis.cwd, basis.plan, projection);
1778
+ if (current.stateCurrent !== projection.stateAfter
1779
+ || current.storyCurrent !== projection.storyAfter) {
1780
+ return fail('legacy-story-pr-review-physical-not-accepted');
1781
+ }
1782
+ return { ok: true, value: Object.freeze({ plan: basis.reviewPlan, receipt: Object.freeze({
1783
+ schemaVersion: 1, kind: 'legacy-story-c11-checkpoint', storyId: basis.plan.storyId,
1784
+ epicId: basis.plan.epicId, branch: basis.plan.branch, prNumber: options.prNumber,
1785
+ headSha: basis.plan.headSha, reviewSha256: accepted.reviewSha256,
1786
+ qaSha256: accepted.qaSha256, stateSha256: accepted.stateAfter,
1787
+ storySha256: accepted.storyAfter, reused: true,
1788
+ checkpointAccepted: true, localOnly: true,
1789
+ }) }) };
1790
+ }
1791
+ catch (error) {
1792
+ const reason = error instanceof Error ? error.message : '';
1793
+ return fail(reason.startsWith('legacy-story-pr-') ? reason : 'legacy-story-pr-review-read-failed');
1794
+ }
1795
+ }
1796
+ function renderLegacyStoryReviewArtifact(report, reviewSha256, qaSha256) {
1797
+ const evidence = (items) => items
1798
+ .map((item) => `- ${item.id}: ${item.disposition}; ${item.path}; sha256:${item.sha256}`)
1799
+ .join('\n');
1800
+ const debt = report.nonBlockingFindings.length === 0 ? 'None.'
1801
+ : report.nonBlockingFindings.map((item) => `- ${item.id}: ${item.severity}; ${item.reasonCode}; ${item.actionCode}; ${item.path}; sha256:${item.sha256}`).join('\n');
1802
+ return `# ${report.storyId} — PR review\n\nVerdict: approved\n\nPR: ${report.prNumber}\nReviewed HEAD: ${report.headSha}\nBase HEAD: ${report.baseSha}\nDiff SHA-256: ${report.diffSha256}\nRoot review SHA-256: ${reviewSha256}\nLocal QA SHA-256: ${qaSha256}\n\n## Acceptance evidence\n\n${evidence(report.acceptanceEvidence)}\n\n## SDD evidence\n\n${evidence(report.sddEvidence)}\n\n## Non-blocking review debt\n\n${debt}\n`;
1803
+ }
1804
+ /** Plan only: the old PR is not ready for physical merge while C10/C11 remain
1805
+ * uncommitted. This validates the exact review artifact to integrate next. */
1806
+ export async function planLegacyStoryPrMergeC12(options) {
1807
+ try {
1808
+ const accepted = await readAcceptedLegacyStoryPrReviewC11(options);
1809
+ if (!accepted.ok)
1810
+ return accepted;
1811
+ const basis = await inspectLegacyReviewBasis(options);
1812
+ const prefix = digest(basis.plan.storyId);
1813
+ const journal = await readC11Journal(basis.directory, `${prefix}.c11.accepted.json`);
1814
+ if (!journal || journal.reviewSha256 !== accepted.value.receipt.reviewSha256
1815
+ || journal.qaSha256 !== accepted.value.receipt.qaSha256) {
1816
+ return fail('legacy-story-pr-merge-review-drift');
1817
+ }
1818
+ const reviewed = validateReviewReportRaw(journal.reviewRaw, basis);
1819
+ const reviewArtifactPath = `.neocortex/stories/${basis.plan.storyId}/review.md`;
1820
+ if (existsSync(join(basis.cwd, reviewArtifactPath))) {
1821
+ return fail('legacy-story-pr-merge-review-artifact-conflict');
1822
+ }
1823
+ try {
1824
+ git(basis.cwd, 'cat-file', '-e', `${basis.plan.headSha}:${reviewArtifactPath}`);
1825
+ return fail('legacy-story-pr-merge-review-artifact-conflict');
1826
+ }
1827
+ catch { /* The exact review artifact is not in the PR HEAD yet. */ }
1828
+ const reviewArtifact = renderLegacyStoryReviewArtifact(reviewed.report, journal.reviewSha256, journal.qaSha256);
1829
+ return { ok: true, value: Object.freeze({
1830
+ schemaVersion: 1, kind: 'legacy-story-pr-merge-plan', storyId: basis.plan.storyId,
1831
+ epicId: basis.plan.epicId, repository: basis.plan.repository,
1832
+ branch: basis.plan.branch, baseBranch: basis.plan.baseBranch,
1833
+ baseSha: basis.plan.baseSha, originalHeadSha: basis.plan.headSha,
1834
+ prNumber: options.prNumber, reviewArtifactPath,
1835
+ reviewArtifactSha256: digest(reviewArtifact),
1836
+ reviewSha256: journal.reviewSha256, qaSha256: journal.qaSha256,
1837
+ stateSha256: journal.stateAfter, storySha256: journal.storyAfter,
1838
+ nextAuthority: 'root-review-commit-and-new-head-qa-required',
1839
+ physicalMerge: false, localOnly: true,
1840
+ }) };
1841
+ }
1842
+ catch (error) {
1843
+ const reason = error instanceof Error ? error.message : '';
1844
+ return fail(reason.startsWith('legacy-story-pr-') ? reason : 'legacy-story-pr-merge-plan-failed');
1845
+ }
1846
+ }
1847
+ async function readC12Intent(directory, name) {
1848
+ const path = join(directory, name);
1849
+ if (!existsSync(path))
1850
+ return null;
1851
+ await assertGitLifecyclePrivateFile(path, directory);
1852
+ const value = record(JSON.parse(safeRead(path, 64 * 1024)));
1853
+ const plan = record(value?.plan);
1854
+ if (!value || value.schemaVersion !== 1 || value.kind !== 'legacy-story-c12-review-commit-intent'
1855
+ || !plan || plan.kind !== 'legacy-story-pr-merge-plan'
1856
+ || typeof plan.reviewArtifactSha256 !== 'string'
1857
+ || typeof value.reviewArtifact !== 'string'
1858
+ || digest(value.reviewArtifact) !== plan.reviewArtifactSha256) {
1859
+ throw new Error('legacy-story-pr-merge-intent-invalid');
1860
+ }
1861
+ if (value.remoteDebt !== undefined) {
1862
+ const debt = record(value.remoteDebt);
1863
+ const observation = record(debt?.observation);
1864
+ if (!debt || Object.keys(debt).sort().join(',') !== [
1865
+ 'added', 'itemIds', 'observation', 'observedAt', 'registryAfterSha256',
1866
+ 'registryBeforeSha256', 'registryExisted',
1867
+ ].sort().join(',') || !observation || debt.registryExisted !== true
1868
+ || typeof debt.observedAt !== 'string' || !Number.isFinite(Date.parse(debt.observedAt))
1869
+ || new Date(debt.observedAt).toISOString() !== debt.observedAt
1870
+ || typeof debt.registryBeforeSha256 !== 'string' || !/^[a-f0-9]{64}$/.test(debt.registryBeforeSha256)
1871
+ || typeof debt.registryAfterSha256 !== 'string' || !/^[a-f0-9]{64}$/.test(debt.registryAfterSha256)
1872
+ || !Number.isSafeInteger(debt.added) || debt.added < 0
1873
+ || !Array.isArray(debt.itemIds) || debt.itemIds.some((id) => typeof id !== 'string')) {
1874
+ throw new Error('legacy-story-pr-merge-intent-invalid');
1875
+ }
1876
+ }
1877
+ return value;
1878
+ }
1879
+ async function readC12Prepared(directory, name) {
1880
+ const path = join(directory, name);
1881
+ if (!existsSync(path))
1882
+ return null;
1883
+ await assertGitLifecyclePrivateFile(path, directory);
1884
+ const value = record(JSON.parse(safeRead(path, 4096)));
1885
+ if (!value || value.schemaVersion !== 1 || value.kind !== 'legacy-story-c12-prepared'
1886
+ || typeof value.intentSha256 !== 'string' || !/^[a-f0-9]{64}$/.test(value.intentSha256)
1887
+ || typeof value.candidateHeadSha !== 'string' || !/^[a-f0-9]{40}$/.test(value.candidateHeadSha)
1888
+ || typeof value.qaSha256 !== 'string' || !/^[a-f0-9]{64}$/.test(value.qaSha256)) {
1889
+ throw new Error('legacy-story-pr-merge-prepared-journal-invalid');
1890
+ }
1891
+ return value;
1892
+ }
1893
+ function reviewArtifactDirectory(cwd, storyId) {
1894
+ const folders = [join(cwd, '.neocortex'), join(cwd, '.neocortex/stories'),
1895
+ join(cwd, '.neocortex/stories', storyId)];
1896
+ for (const folder of folders) {
1897
+ if (!existsSync(folder))
1898
+ mkdirSync(folder, { mode: 0o700 });
1899
+ if (!lstatSync(folder).isDirectory() || realpathSync(folder) !== folder) {
1900
+ throw new Error('legacy-story-pr-merge-review-directory-unsafe');
1901
+ }
1902
+ }
1903
+ return folders[2];
1904
+ }
1905
+ const C12_REGISTRY_PATH = '.neocortex/tech-debt-registry.json';
1906
+ const C12_EMPTY_REGISTRY = '{"schema_version":2,"items":[]}';
1907
+ function c12RegistryAtOriginal(cwd, originalHeadSha) {
1908
+ const entry = gitRaw(cwd, 'ls-tree', originalHeadSha, '--', C12_REGISTRY_PATH).trim();
1909
+ if (!entry)
1910
+ return { existed: false, raw: C12_EMPTY_REGISTRY };
1911
+ if (!/^100644 blob [a-f0-9]{40}\t\.neocortex\/tech-debt-registry\.json$/.test(entry)) {
1912
+ throw new Error('legacy-story-pr-merge-remote-debt-registry-unsafe');
1913
+ }
1914
+ return { existed: true, raw: gitRaw(cwd, 'show', `${originalHeadSha}:${C12_REGISTRY_PATH}`) };
1915
+ }
1916
+ function c12RegistryProjection(cwd, intent) {
1917
+ const debt = intent.remoteDebt;
1918
+ if (!debt)
1919
+ return null;
1920
+ const before = c12RegistryAtOriginal(cwd, intent.plan.originalHeadSha);
1921
+ if (!before.existed || digest(before.raw) !== debt.registryBeforeSha256) {
1922
+ throw new Error('legacy-story-pr-merge-remote-debt-registry-drift');
1923
+ }
1924
+ const projection = projectC12ScopedCiRegistryUpdate({ registryRaw: before.raw,
1925
+ observation: debt.observation, storyId: intent.plan.storyId,
1926
+ prNumber: intent.plan.prNumber, nowIso: debt.observedAt });
1927
+ if (!projection || projection.registryAfterSha256 !== debt.registryAfterSha256
1928
+ || projection.added !== debt.added
1929
+ || JSON.stringify(projection.itemIds) !== JSON.stringify(debt.itemIds)
1930
+ || debt.observation.reasonCodes.length === 0) {
1931
+ throw new Error('legacy-story-pr-merge-remote-debt-intent-invalid');
1932
+ }
1933
+ return projection;
1934
+ }
1935
+ function exactC12Paths(intent) {
1936
+ const plan = intent.plan;
1937
+ return ['.neocortex/state.json', `docs/stories/${plan.storyId}.story.md`,
1938
+ plan.reviewArtifactPath, ...(intent.remoteDebt?.added ? [C12_REGISTRY_PATH] : [])].sort();
1939
+ }
1940
+ function assertC12Candidate(cwd, intent, candidateHeadSha) {
1941
+ const plan = intent.plan;
1942
+ if (git(cwd, 'rev-parse', `${candidateHeadSha}^`) !== plan.originalHeadSha
1943
+ || git(cwd, 'rev-list', '--count', `${plan.originalHeadSha}..${candidateHeadSha}`) !== '1') {
1944
+ throw new Error('legacy-story-pr-merge-candidate-parent-invalid');
1945
+ }
1946
+ const changed = gitRaw(cwd, 'diff-tree', '--no-commit-id', '--name-only', '-r', '-z', candidateHeadSha)
1947
+ .split('\0').filter(Boolean).sort();
1948
+ const projection = c12RegistryProjection(cwd, intent);
1949
+ if (JSON.stringify(changed) !== JSON.stringify(exactC12Paths(intent))
1950
+ || digest(gitRaw(cwd, 'show', `${candidateHeadSha}:.neocortex/state.json`)) !== plan.stateSha256
1951
+ || digest(gitRaw(cwd, 'show', `${candidateHeadSha}:docs/stories/${plan.storyId}.story.md`)) !== plan.storySha256
1952
+ || digest(gitRaw(cwd, 'show', `${candidateHeadSha}:${plan.reviewArtifactPath}`)) !== plan.reviewArtifactSha256
1953
+ || (projection && digest(gitRaw(cwd, 'show', `${candidateHeadSha}:${C12_REGISTRY_PATH}`))
1954
+ !== projection.registryAfterSha256)
1955
+ || !gitRaw(cwd, 'log', '-1', '--format=%B', candidateHeadSha)
1956
+ .split('\n').map((line) => line.trim()).includes(`Neocortex-Legacy-Review: ${plan.storyId}`)) {
1957
+ throw new Error('legacy-story-pr-merge-candidate-invalid');
1958
+ }
1959
+ }
1960
+ function inspectC12Intent(cwd, intent, journal, providerItems) {
1961
+ const plan = intent.plan;
1962
+ if (repository(cwd) !== plan.repository || git(cwd, 'branch', '--show-current') !== plan.branch
1963
+ || singleRemoteSha(cwd, plan.baseBranch) !== plan.baseSha
1964
+ || journal.storyId !== plan.storyId || journal.reviewSha256 !== plan.reviewSha256
1965
+ || journal.qaSha256 !== plan.qaSha256 || journal.stateAfter !== plan.stateSha256
1966
+ || journal.storyAfter !== plan.storySha256) {
1967
+ throw new Error('legacy-story-pr-merge-intent-drift');
1968
+ }
1969
+ const headSha = git(cwd, 'rev-parse', 'HEAD');
1970
+ const remoteSha = singleRemoteSha(cwd, plan.branch);
1971
+ if (headSha !== plan.originalHeadSha)
1972
+ assertC12Candidate(cwd, intent, headSha);
1973
+ if (remoteSha !== plan.originalHeadSha && remoteSha !== headSha) {
1974
+ throw new Error('legacy-story-pr-merge-remote-drift');
1975
+ }
1976
+ const providerPlan = { schemaVersion: 1, kind: 'legacy-story-pr-plan',
1977
+ storyId: plan.storyId, epicId: plan.epicId, repository: plan.repository,
1978
+ branch: plan.branch, baseBranch: plan.baseBranch, baseSha: plan.baseSha,
1979
+ headSha: remoteSha, qaAuthority: 'root-review-required', localOnly: true };
1980
+ const item = selectedPr(providerPlan, providerItems);
1981
+ if (!item || item.number !== plan.prNumber) {
1982
+ throw new Error('legacy-story-pr-merge-provider-drift');
1983
+ }
1984
+ const stateCurrent = safeRead(join(cwd, '.neocortex/state.json'), 2 * 1024 * 1024);
1985
+ const storyCurrent = safeRead(join(cwd, `docs/stories/${plan.storyId}.story.md`), 2 * 1024 * 1024);
1986
+ if (digest(stateCurrent) !== plan.stateSha256 || digest(storyCurrent) !== plan.storySha256) {
1987
+ throw new Error('legacy-story-pr-merge-checkpoint-drift');
1988
+ }
1989
+ const projection = c12RegistryProjection(cwd, intent);
1990
+ if (projection) {
1991
+ const registryPath = join(cwd, C12_REGISTRY_PATH);
1992
+ const registryExists = existsSync(registryPath);
1993
+ const currentDigest = registryExists ? digest(safeRead(registryPath, 2 * 1024 * 1024)) : null;
1994
+ if (headSha === plan.originalHeadSha
1995
+ ? currentDigest !== projection.registryBeforeSha256
1996
+ && currentDigest !== projection.registryAfterSha256
1997
+ : currentDigest !== projection.registryAfterSha256) {
1998
+ throw new Error('legacy-story-pr-merge-remote-debt-registry-drift');
1999
+ }
2000
+ }
2001
+ const status = gitRaw(cwd, 'status', '--porcelain=v1', '-uall').split('\n').filter(Boolean);
2002
+ if (headSha === plan.originalHeadSha) {
2003
+ const allowed = new Set(exactC12Paths(intent));
2004
+ if (status.some((line) => !allowed.has(line.slice(3))
2005
+ || ![' M', 'M ', 'MM', '??', 'A '].includes(line.slice(0, 2)))) {
2006
+ throw new Error('legacy-story-pr-merge-worktree-dirty');
2007
+ }
2008
+ }
2009
+ else if (status.length > 0) {
2010
+ throw new Error('legacy-story-pr-merge-worktree-dirty');
2011
+ }
2012
+ return { headSha, remoteSha };
2013
+ }
2014
+ /** Commit only the reviewed C10/C11 metadata and public review artifact to
2015
+ * the existing PR branch, then run P205 on that new immutable HEAD and push
2016
+ * without force. This prepares C12; it does not merge or close the story. */
2017
+ export async function prepareLegacyStoryPrMergeC12(options) {
2018
+ if (!options.authorize)
2019
+ return fail('legacy-story-pr-merge-authorization-required');
2020
+ if (!Number.isSafeInteger(options.prNumber) || options.prNumber <= 0
2021
+ || !/^[A-Za-z0-9][A-Za-z0-9_-]*\.[A-Za-z0-9._-]+$/.test(options.storyId)) {
2022
+ return fail('legacy-story-pr-merge-target-invalid');
2023
+ }
2024
+ const sessionId = `legacy-c12-prepare-${randomBytes(16).toString('hex')}`;
2025
+ const lock = await acquireGitLifecyclePhysicalIntegrationLock(options.projectRoot, {
2026
+ kind: 'direct', storyId: options.storyId, sessionId, idempotencyKey: sessionId,
2027
+ expiresAt: new Date(Date.now() + 60 * 60_000).toISOString(), persistent: false,
2028
+ });
2029
+ if (!lock.ok)
2030
+ return fail(lock.reasonCode);
2031
+ try {
2032
+ const cwd = realpathSync(options.projectRoot);
2033
+ const directory = await ensureGitLifecyclePrivateChildDirectory(await gitLifecycleRuntimeDirectory(cwd), 'legacy-story-pr-intents');
2034
+ const prefix = digest(options.storyId);
2035
+ const c11Prior = await readC11Journal(directory, `${prefix}.c11.json`);
2036
+ const c11Accepted = await readC11Journal(directory, `${prefix}.c11.accepted.json`);
2037
+ if (!c11Prior || !c11Accepted || JSON.stringify(c11Prior) !== JSON.stringify(c11Accepted)) {
2038
+ return fail('legacy-story-pr-merge-review-unaccepted');
2039
+ }
2040
+ const intentName = `${prefix}.c12.json`;
2041
+ const preparedName = `${prefix}.c12.prepared.json`;
2042
+ let intent = await readC12Intent(directory, intentName);
2043
+ if (!intent) {
2044
+ const planned = await planLegacyStoryPrMergeC12(options);
2045
+ if (!planned.ok)
2046
+ return planned;
2047
+ const basis = await inspectLegacyReviewBasis(options);
2048
+ const reviewed = validateReviewReportRaw(c11Accepted.reviewRaw, basis);
2049
+ const reviewArtifact = renderLegacyStoryReviewArtifact(reviewed.report, c11Accepted.reviewSha256, c11Accepted.qaSha256);
2050
+ let remoteDebt;
2051
+ if (options.recordRemoteDebt) {
2052
+ const plan = planned.value;
2053
+ const observed = await (options.mergeProvider ?? defaultMergeProvider)
2054
+ .view(plan.repository, plan.prNumber);
2055
+ assertC12MergeObservation(plan, plan.originalHeadSha, observed, 'OPEN');
2056
+ const summarized = summarizeC12RemoteRollup(observed.statusCheckRollup);
2057
+ if (!summarized.ok)
2058
+ return fail('legacy-story-pr-merge-remote-observation-unclassified');
2059
+ if (summarized.value.reasonCodes.length === 0) {
2060
+ return fail('legacy-story-pr-merge-remote-debt-not-required');
2061
+ }
2062
+ const before = c12RegistryAtOriginal(cwd, plan.originalHeadSha);
2063
+ if (!before.existed)
2064
+ return fail('legacy-story-pr-merge-remote-debt-registry-missing');
2065
+ const registryPath = join(cwd, C12_REGISTRY_PATH);
2066
+ const worktreeRaw = existsSync(registryPath)
2067
+ ? safeRead(registryPath, 2 * 1024 * 1024) : null;
2068
+ if (worktreeRaw !== before.raw) {
2069
+ return fail('legacy-story-pr-merge-remote-debt-registry-drift');
2070
+ }
2071
+ const observedAt = new Date().toISOString();
2072
+ const projection = projectC12ScopedCiRegistryUpdate({ registryRaw: before.raw,
2073
+ observation: summarized.value, storyId: plan.storyId,
2074
+ prNumber: plan.prNumber, nowIso: observedAt });
2075
+ if (!projection)
2076
+ return fail('legacy-story-pr-merge-remote-debt-registry-invalid');
2077
+ remoteDebt = { observation: summarized.value, observedAt,
2078
+ registryExisted: before.existed,
2079
+ registryBeforeSha256: projection.registryBeforeSha256,
2080
+ registryAfterSha256: projection.registryAfterSha256,
2081
+ added: projection.added, itemIds: projection.itemIds };
2082
+ }
2083
+ intent = { schemaVersion: 1, kind: 'legacy-story-c12-review-commit-intent',
2084
+ plan: planned.value, reviewArtifact, ...(remoteDebt ? { remoteDebt } : {}) };
2085
+ await anchoredAtomicWriteFile(directory, intentName, `${JSON.stringify(intent)}\n`);
2086
+ await assertGitLifecyclePrivateFile(join(directory, intentName), directory);
2087
+ }
2088
+ const plan = intent.plan;
2089
+ if (plan.storyId !== options.storyId || plan.prNumber !== options.prNumber
2090
+ || digest(intent.reviewArtifact) !== plan.reviewArtifactSha256
2091
+ || Boolean(intent.remoteDebt) !== Boolean(options.recordRemoteDebt)) {
2092
+ return fail('legacy-story-pr-merge-intent-conflict');
2093
+ }
2094
+ const provider = options.provider ?? defaultProvider;
2095
+ const inspect = async () => inspectC12Intent(cwd, intent, c11Accepted, await provider.list(plan.repository, plan.branch, plan.baseBranch));
2096
+ let current = await inspect();
2097
+ const intentSha256 = digest(JSON.stringify(intent));
2098
+ const accepted = await readC12Prepared(directory, preparedName);
2099
+ if (accepted) {
2100
+ if (accepted.intentSha256 !== intentSha256
2101
+ || accepted.candidateHeadSha !== current.headSha
2102
+ || current.remoteSha !== current.headSha) {
2103
+ return fail('legacy-story-pr-merge-prepared-conflict');
2104
+ }
2105
+ return { ok: true, value: Object.freeze({ schemaVersion: 1,
2106
+ kind: 'legacy-story-c12-prepared', storyId: plan.storyId, epicId: plan.epicId,
2107
+ branch: plan.branch,
2108
+ prNumber: plan.prNumber, originalHeadSha: plan.originalHeadSha,
2109
+ candidateHeadSha: accepted.candidateHeadSha, qaSha256: accepted.qaSha256,
2110
+ registryRecorded: Boolean(intent.remoteDebt),
2111
+ reused: true, physicalMerge: false, localOnly: true }) };
2112
+ }
2113
+ if (current.headSha === plan.originalHeadSha) {
2114
+ if (current.remoteSha !== plan.originalHeadSha) {
2115
+ return fail('legacy-story-pr-merge-remote-drift');
2116
+ }
2117
+ const projection = c12RegistryProjection(cwd, intent);
2118
+ if (projection) {
2119
+ const observed = await (options.mergeProvider ?? defaultMergeProvider)
2120
+ .view(plan.repository, plan.prNumber);
2121
+ assertC12MergeObservation(plan, plan.originalHeadSha, observed, 'OPEN');
2122
+ const summarized = summarizeC12RemoteRollup(observed.statusCheckRollup);
2123
+ if (!summarized.ok || summarized.value.digest !== intent.remoteDebt.observation.digest
2124
+ || JSON.stringify(summarized.value) !== JSON.stringify(intent.remoteDebt.observation)) {
2125
+ return fail('legacy-story-pr-merge-remote-debt-observation-drift');
2126
+ }
2127
+ if (projection.added > 0) {
2128
+ const registryPath = join(cwd, C12_REGISTRY_PATH);
2129
+ const currentRaw = existsSync(registryPath)
2130
+ ? safeRead(registryPath, 2 * 1024 * 1024) : null;
2131
+ if (currentRaw === projection.registryAfterRaw) { /* recovered after interrupted write */ }
2132
+ else if (currentRaw === c12RegistryAtOriginal(cwd, plan.originalHeadSha).raw) {
2133
+ const registryDir = join(cwd, '.neocortex');
2134
+ if (!lstatSync(registryDir).isDirectory() || realpathSync(registryDir) !== registryDir) {
2135
+ return fail('legacy-story-pr-merge-remote-debt-registry-unsafe');
2136
+ }
2137
+ await anchoredCompareAndSwapFile(registryDir, 'tech-debt-registry.json', projection.registryAfterRaw, projection.registryBeforeSha256);
2138
+ }
2139
+ else
2140
+ return fail('legacy-story-pr-merge-remote-debt-registry-drift');
2141
+ }
2142
+ }
2143
+ const artifactDir = reviewArtifactDirectory(cwd, plan.storyId);
2144
+ const artifactPath = join(artifactDir, 'review.md');
2145
+ if (existsSync(artifactPath)) {
2146
+ if (lstatSync(artifactPath).isSymbolicLink()
2147
+ || safeRead(artifactPath, 32 * 1024) !== intent.reviewArtifact) {
2148
+ return fail('legacy-story-pr-merge-review-artifact-conflict');
2149
+ }
2150
+ }
2151
+ else {
2152
+ await anchoredAtomicWriteFile(artifactDir, 'review.md', intent.reviewArtifact);
2153
+ }
2154
+ current = await inspect();
2155
+ git(cwd, 'add', '--', ...exactC12Paths(intent));
2156
+ const staged = gitRaw(cwd, 'diff', '--cached', '--name-only', '-z').split('\0').filter(Boolean).sort();
2157
+ if (JSON.stringify(staged) !== JSON.stringify(exactC12Paths(intent))
2158
+ || digest(gitRaw(cwd, 'show', ':.neocortex/state.json')) !== plan.stateSha256
2159
+ || digest(gitRaw(cwd, 'show', `:docs/stories/${plan.storyId}.story.md`)) !== plan.storySha256
2160
+ || digest(gitRaw(cwd, 'show', `:${plan.reviewArtifactPath}`)) !== plan.reviewArtifactSha256
2161
+ || (projection && digest(gitRaw(cwd, 'show', `:${C12_REGISTRY_PATH}`))
2162
+ !== projection.registryAfterSha256)
2163
+ || gitRaw(cwd, 'diff', '--name-only', '-z') !== '') {
2164
+ return fail('legacy-story-pr-merge-staged-scope-invalid');
2165
+ }
2166
+ git(cwd, 'diff', '--cached', '--check');
2167
+ git(cwd, 'commit', '-m', `chore(story): record reviewed PR ${plan.storyId}`, '-m', `Neocortex-Legacy-Review: ${plan.storyId}`);
2168
+ current = await inspect();
2169
+ await options.afterCommit?.();
2170
+ }
2171
+ if (current.headSha === plan.originalHeadSha)
2172
+ return fail('legacy-story-pr-merge-commit-missing');
2173
+ const qaPlan = { schemaVersion: 1, kind: 'legacy-story-pr-plan',
2174
+ storyId: plan.storyId, epicId: plan.epicId, repository: plan.repository,
2175
+ branch: plan.branch, baseBranch: plan.baseBranch, headSha: current.headSha,
2176
+ baseSha: plan.baseSha, qaAuthority: 'root-review-required', localOnly: true };
2177
+ let qaSha256;
2178
+ try {
2179
+ qaSha256 = await verifyLocalQa(qaPlan, cwd, options.verifyQa);
2180
+ }
2181
+ catch {
2182
+ return fail('legacy-story-pr-merge-new-head-qa-failed');
2183
+ }
2184
+ current = await inspect();
2185
+ if (current.headSha === plan.originalHeadSha)
2186
+ return fail('legacy-story-pr-merge-candidate-raced');
2187
+ if (current.remoteSha === plan.originalHeadSha) {
2188
+ git(cwd, 'push', '--porcelain', 'origin', `${current.headSha}:refs/heads/${plan.branch}`);
2189
+ }
2190
+ current = await inspect();
2191
+ if (current.remoteSha !== current.headSha)
2192
+ return fail('legacy-story-pr-merge-push-unverified');
2193
+ await options.afterPush?.();
2194
+ const prepared = { schemaVersion: 1, kind: 'legacy-story-c12-prepared',
2195
+ intentSha256, candidateHeadSha: current.headSha, qaSha256 };
2196
+ await anchoredAtomicWriteFile(directory, preparedName, `${JSON.stringify(prepared)}\n`);
2197
+ await assertGitLifecyclePrivateFile(join(directory, preparedName), directory);
2198
+ return { ok: true, value: Object.freeze({ schemaVersion: 1,
2199
+ kind: 'legacy-story-c12-prepared', storyId: plan.storyId, epicId: plan.epicId,
2200
+ branch: plan.branch,
2201
+ prNumber: plan.prNumber, originalHeadSha: plan.originalHeadSha,
2202
+ candidateHeadSha: current.headSha, qaSha256,
2203
+ registryRecorded: Boolean(intent.remoteDebt),
2204
+ reused: false, physicalMerge: false, localOnly: true }) };
2205
+ }
2206
+ catch (error) {
2207
+ const reason = error instanceof Error ? error.message : '';
2208
+ return fail(reason.startsWith('legacy-story-pr-') ? reason : 'legacy-story-pr-merge-prepare-failed');
2209
+ }
2210
+ finally {
2211
+ await releaseGitLifecyclePhysicalIntegrationLock(lock.path, lock.owner).catch(() => undefined);
2212
+ }
2213
+ }
2214
+ /** Read-only C12 observation before the reviewed metadata is committed. This
2215
+ * gives the root the canonical debt input at the only point where a future
2216
+ * tracked-registry write can join the reviewed PR candidate commit. */
2217
+ export async function planLegacyStoryPrRemoteDebtBeforePrepareC12(options) {
2218
+ if (!Number.isSafeInteger(options.prNumber) || options.prNumber <= 0) {
2219
+ return fail('legacy-story-pr-merge-target-invalid');
2220
+ }
2221
+ const planned = await planLegacyStoryPrMergeC12(options);
2222
+ if (!planned.ok)
2223
+ return planned;
2224
+ try {
2225
+ const plan = planned.value;
2226
+ const observed = await (options.mergeProvider ?? defaultMergeProvider)
2227
+ .view(plan.repository, plan.prNumber);
2228
+ assertC12MergeObservation(plan, plan.originalHeadSha, observed, 'OPEN');
2229
+ const summarized = summarizeC12RemoteRollup(observed.statusCheckRollup);
2230
+ if (!summarized.ok)
2231
+ return fail('legacy-story-pr-merge-remote-observation-unclassified');
2232
+ const followUpDebt = projectC12ScopedCiFollowUpDebt(summarized.value, plan.storyId, plan.prNumber);
2233
+ if (!followUpDebt)
2234
+ return fail('legacy-story-pr-merge-remote-debt-projection-invalid');
2235
+ if (followUpDebt.length === 0)
2236
+ return fail('legacy-story-pr-merge-remote-debt-not-required');
2237
+ const registryPath = join(realpathSync(options.projectRoot), '.neocortex/tech-debt-registry.json');
2238
+ const registryExists = existsSync(registryPath);
2239
+ const registryRaw = registryExists ? safeRead(registryPath, 2 * 1024 * 1024)
2240
+ : '{"schema_version":2,"items":[]}';
2241
+ const registryProjection = projectC12ScopedCiRegistryUpdate({
2242
+ registryRaw, observation: summarized.value, storyId: plan.storyId,
2243
+ prNumber: plan.prNumber, nowIso: new Date().toISOString(),
2244
+ });
2245
+ if (!registryProjection)
2246
+ return fail('legacy-story-pr-merge-remote-debt-registry-invalid');
2247
+ return { ok: true, value: Object.freeze({ schemaVersion: 1,
2248
+ kind: 'legacy-story-c12-pre-prepare-debt-plan', storyId: plan.storyId,
2249
+ epicId: plan.epicId, prNumber: plan.prNumber,
2250
+ originalHeadSha: plan.originalHeadSha, baseSha: plan.baseSha,
2251
+ remoteObservationDigest: summarized.value.digest,
2252
+ reasonCodes: summarized.value.reasonCodes, followUpDebt,
2253
+ registryExists, registryBeforeSha256: registryProjection.registryBeforeSha256,
2254
+ registryProjectedItemIds: registryProjection.itemIds,
2255
+ registryProjectedAdded: registryProjection.added,
2256
+ registryProjectedExisting: registryProjection.existing,
2257
+ registryMutationAuthorized: false, canMerge: false, publicSafe: true }) };
2258
+ }
2259
+ catch (error) {
2260
+ const reason = error instanceof Error ? error.message : '';
2261
+ return fail(reason.startsWith('legacy-story-pr-') ? reason : 'legacy-story-pr-merge-pre-prepare-debt-uncertain');
2262
+ }
2263
+ }
2264
+ function recordedC12RootDecision(plan, candidateHeadSha, remote, registrySha256, registryItemIds) {
2265
+ const followUpDebt = projectC12ScopedCiFollowUpDebt(remote, plan.storyId, plan.prNumber);
2266
+ if (!followUpDebt || followUpDebt.length === 0)
2267
+ return null;
2268
+ return Object.freeze({ schemaVersion: 1, kind: 'legacy-story-c12-root-merge-decision',
2269
+ storyId: plan.storyId, epicId: plan.epicId, prNumber: plan.prNumber,
2270
+ candidateHeadSha, baseSha: plan.baseSha, reviewSha256: plan.reviewSha256,
2271
+ remoteObservationDigest: remote.digest, canMerge: true,
2272
+ blockingChecks: [],
2273
+ nonBlockingChecks: Object.freeze(followUpDebt.map((debt) => Object.freeze({
2274
+ checkName: debt.checkName, bucket: 'nonBlockingChecks',
2275
+ reason: debt.reason, origin: 'remote_ci_cd', safeToContinueYoloop: true,
2276
+ }))), followUpDebt,
2277
+ registrySha256, registryItemIds, publicSafe: true });
2278
+ }
2279
+ function validC12RootDecision(value, intent, candidateHeadSha) {
2280
+ const plan = intent.plan;
2281
+ const decision = record(value);
2282
+ if (decision?.registrySha256 !== undefined) {
2283
+ const debt = intent.remoteDebt;
2284
+ const expected = debt && recordedC12RootDecision(plan, candidateHeadSha, debt.observation, debt.registryAfterSha256, debt.itemIds);
2285
+ return !!expected && JSON.stringify(decision) === JSON.stringify(expected);
2286
+ }
2287
+ if (!decision || JSON.stringify(Object.keys(decision).sort()) !== JSON.stringify([
2288
+ 'baseSha', 'blockingChecks', 'canMerge', 'candidateHeadSha', 'epicId', 'followUpDebt',
2289
+ 'kind', 'nonBlockingChecks', 'prNumber', 'remoteObservationDigest', 'reviewSha256', 'schemaVersion', 'storyId',
2290
+ ].sort()))
2291
+ return false;
2292
+ return decision.schemaVersion === 1 && decision.kind === 'legacy-story-c12-root-merge-decision'
2293
+ && decision.storyId === plan.storyId && decision.epicId === plan.epicId
2294
+ && decision.prNumber === plan.prNumber && decision.candidateHeadSha === candidateHeadSha
2295
+ && decision.baseSha === plan.baseSha && decision.reviewSha256 === plan.reviewSha256
2296
+ && typeof decision.remoteObservationDigest === 'string'
2297
+ && /^[a-f0-9]{64}$/.test(decision.remoteObservationDigest)
2298
+ && decision.canMerge === true && Array.isArray(decision.blockingChecks)
2299
+ && decision.blockingChecks.length === 0 && Array.isArray(decision.nonBlockingChecks)
2300
+ && decision.nonBlockingChecks.length === 0 && Array.isArray(decision.followUpDebt)
2301
+ && decision.followUpDebt.length === 0;
2302
+ }
2303
+ async function readC12MergeAttempt(directory, name) {
2304
+ const path = join(directory, name);
2305
+ if (!existsSync(path))
2306
+ return null;
2307
+ await assertGitLifecyclePrivateFile(path, directory);
2308
+ const value = record(JSON.parse(safeRead(path, 4096)));
2309
+ if (!value || value.schemaVersion !== 1 || value.kind !== 'legacy-story-c12-merge-attempt'
2310
+ || typeof value.intentSha256 !== 'string' || !/^[a-f0-9]{64}$/.test(value.intentSha256)
2311
+ || typeof value.candidateHeadSha !== 'string' || !/^[a-f0-9]{40}$/.test(value.candidateHeadSha)
2312
+ || typeof value.qaSha256 !== 'string' || !/^[a-f0-9]{64}$/.test(value.qaSha256)
2313
+ || typeof value.remoteObservationDigest !== 'string'
2314
+ || !/^[a-f0-9]{64}$/.test(value.remoteObservationDigest)) {
2315
+ throw new Error('legacy-story-pr-merge-attempt-invalid');
2316
+ }
2317
+ return value;
2318
+ }
2319
+ async function readC12Merged(directory, name) {
2320
+ const path = join(directory, name);
2321
+ if (!existsSync(path))
2322
+ return null;
2323
+ await assertGitLifecyclePrivateFile(path, directory);
2324
+ const value = record(JSON.parse(safeRead(path, 4096)));
2325
+ if (!value || value.schemaVersion !== 1 || value.kind !== 'legacy-story-c12-merged'
2326
+ || typeof value.intentSha256 !== 'string' || !/^[a-f0-9]{64}$/.test(value.intentSha256)
2327
+ || typeof value.candidateHeadSha !== 'string' || !/^[a-f0-9]{40}$/.test(value.candidateHeadSha)
2328
+ || typeof value.qaSha256 !== 'string' || !/^[a-f0-9]{64}$/.test(value.qaSha256)
2329
+ || typeof value.remoteObservationDigest !== 'string'
2330
+ || !/^[a-f0-9]{64}$/.test(value.remoteObservationDigest)
2331
+ || typeof value.mergeCommitSha !== 'string' || !/^[a-f0-9]{40}$/.test(value.mergeCommitSha)
2332
+ || typeof value.baseHeadSha !== 'string' || !/^[a-f0-9]{40}$/.test(value.baseHeadSha)) {
2333
+ throw new Error('legacy-story-pr-merge-journal-invalid');
2334
+ }
2335
+ return value;
2336
+ }
2337
+ function assertC12MergeObservation(plan, candidateHeadSha, observed, expectedState) {
2338
+ if (observed.number !== plan.prNumber || observed.state !== expectedState
2339
+ || observed.headRefName !== plan.branch || observed.headRefOid !== candidateHeadSha
2340
+ || observed.baseRefName !== plan.baseBranch
2341
+ || observed.url.toLowerCase() !== `https://github.com/${plan.repository}/pull/${plan.prNumber}`.toLowerCase()) {
2342
+ throw new Error('legacy-story-pr-merge-provider-drift');
2343
+ }
2344
+ if (expectedState === 'OPEN' && (observed.baseRefOid !== plan.baseSha
2345
+ || observed.isDraft !== false || observed.mergeable !== 'MERGEABLE')) {
2346
+ throw new Error('legacy-story-pr-merge-provider-not-ready');
2347
+ }
2348
+ }
2349
+ /** The root decision is an attempt authority, never a claim that GitHub will
2350
+ * merge. Every non-green categorical reason must already be committed as debt. */
2351
+ function assertC12DecisionRemoteRollup(observed, decision, intent) {
2352
+ const summary = summarizeC12RemoteRollup(observed.statusCheckRollup);
2353
+ if (!summary.ok) {
2354
+ throw new Error('legacy-story-pr-merge-remote-observation-unclassified');
2355
+ }
2356
+ if (!('registrySha256' in decision) && summary.value.reasonCodes.length > 0) {
2357
+ throw new Error('legacy-story-pr-merge-remote-observation-unclassified');
2358
+ }
2359
+ if (summary.value.digest !== decision.remoteObservationDigest) {
2360
+ throw new Error('legacy-story-pr-merge-remote-observation-drift');
2361
+ }
2362
+ if ('registrySha256' in decision) {
2363
+ if (!intent.remoteDebt
2364
+ || JSON.stringify(summary.value) !== JSON.stringify(intent.remoteDebt.observation)) {
2365
+ throw new Error('legacy-story-pr-merge-remote-debt-observation-drift');
2366
+ }
2367
+ }
2368
+ return summary.value;
2369
+ }
2370
+ function assertC12MergedPhysical(cwd, intent, prepared, observed) {
2371
+ const plan = intent.plan;
2372
+ assertC12MergeObservation(plan, prepared.candidateHeadSha, observed, 'MERGED');
2373
+ const mergeCommitSha = observed.mergeCommit?.oid;
2374
+ if (!mergeCommitSha || !/^[a-f0-9]{40}$/.test(mergeCommitSha)
2375
+ || git(cwd, 'rev-parse', 'HEAD') !== prepared.candidateHeadSha
2376
+ || gitRaw(cwd, 'status', '--porcelain=v1', '-uall') !== '') {
2377
+ throw new Error('legacy-story-pr-merge-physical-invalid');
2378
+ }
2379
+ assertC12Candidate(cwd, intent, prepared.candidateHeadSha);
2380
+ git(cwd, 'fetch', '--no-tags', 'origin', `refs/heads/${plan.baseBranch}`);
2381
+ const baseHeadSha = git(cwd, 'rev-parse', 'FETCH_HEAD');
2382
+ if (singleRemoteSha(cwd, plan.baseBranch) !== baseHeadSha
2383
+ || !/^[a-f0-9]{40}$/.test(baseHeadSha)) {
2384
+ throw new Error('legacy-story-pr-merge-base-readback-drift');
2385
+ }
2386
+ const parents = git(cwd, 'rev-list', '--parents', '-n', '1', mergeCommitSha).split(' ');
2387
+ if (parents.length !== 3 || parents[0] !== mergeCommitSha
2388
+ || parents[1] !== plan.baseSha || parents[2] !== prepared.candidateHeadSha) {
2389
+ throw new Error('legacy-story-pr-merge-commit-parent-invalid');
2390
+ }
2391
+ git(cwd, 'merge-base', '--is-ancestor', mergeCommitSha, baseHeadSha);
2392
+ return { mergeCommitSha, baseHeadSha };
2393
+ }
2394
+ /** Root-only readback of the prepared PR candidate. Remote-only findings may
2395
+ * authorize an attempt only when the exact categorical debt is already in
2396
+ * the reviewed candidate registry; this does not guarantee provider merge. */
2397
+ export async function planLegacyStoryPrMergeDecisionC12(options) {
2398
+ if (!Number.isSafeInteger(options.prNumber) || options.prNumber <= 0
2399
+ || !/^[A-Za-z0-9][A-Za-z0-9_-]*\.[A-Za-z0-9._-]+$/.test(options.storyId)) {
2400
+ return fail('legacy-story-pr-merge-target-invalid');
2401
+ }
2402
+ try {
2403
+ const cwd = realpathSync(options.projectRoot);
2404
+ const commonRaw = git(cwd, 'rev-parse', '--git-common-dir');
2405
+ const commonDir = realpathSync(isAbsolute(commonRaw) ? commonRaw : join(cwd, commonRaw));
2406
+ const runtimeDir = join(commonDir, 'neocortex-yoloop');
2407
+ const directory = join(runtimeDir, 'legacy-story-pr-intents');
2408
+ if (!existsSync(runtimeDir) || !existsSync(directory))
2409
+ return fail('legacy-story-pr-merge-not-prepared');
2410
+ if (!lstatSync(runtimeDir).isDirectory() || realpathSync(runtimeDir) !== runtimeDir) {
2411
+ return fail('legacy-story-pr-merge-runtime-unsafe');
2412
+ }
2413
+ await assertGitLifecyclePrivateDirectory(directory, runtimeDir);
2414
+ const prefix = digest(options.storyId);
2415
+ const intent = await readC12Intent(directory, `${prefix}.c12.json`);
2416
+ const prepared = await readC12Prepared(directory, `${prefix}.c12.prepared.json`);
2417
+ const c11 = await readC11Journal(directory, `${prefix}.c11.accepted.json`);
2418
+ if (!intent || !prepared || !c11)
2419
+ return fail('legacy-story-pr-merge-not-prepared');
2420
+ const plan = intent.plan;
2421
+ if (plan.storyId !== options.storyId || plan.prNumber !== options.prNumber
2422
+ || prepared.intentSha256 !== digest(JSON.stringify(intent))
2423
+ || c11.reviewSha256 !== plan.reviewSha256 || c11.qaSha256 !== plan.qaSha256) {
2424
+ return fail('legacy-story-pr-merge-prepared-drift');
2425
+ }
2426
+ const current = inspectC12Intent(cwd, intent, c11, await (options.provider ?? defaultProvider).list(plan.repository, plan.branch, plan.baseBranch));
2427
+ if (current.headSha !== prepared.candidateHeadSha
2428
+ || current.remoteSha !== prepared.candidateHeadSha) {
2429
+ return fail('legacy-story-pr-merge-remote-drift');
2430
+ }
2431
+ assertC12Candidate(cwd, intent, prepared.candidateHeadSha);
2432
+ const observation = await (options.mergeProvider ?? defaultMergeProvider).view(plan.repository, plan.prNumber);
2433
+ assertC12MergeObservation(plan, prepared.candidateHeadSha, observation, 'OPEN');
2434
+ const summarized = summarizeC12RemoteRollup(observation.statusCheckRollup);
2435
+ if (!summarized.ok)
2436
+ return fail('legacy-story-pr-merge-remote-observation-unclassified');
2437
+ const remote = summarized.value;
2438
+ if (remote.reasonCodes.length > 0) {
2439
+ const debt = intent.remoteDebt;
2440
+ if (debt && JSON.stringify(remote) === JSON.stringify(debt.observation)) {
2441
+ const projection = c12RegistryProjection(cwd, intent);
2442
+ if (!projection)
2443
+ return fail('legacy-story-pr-merge-remote-debt-registry-invalid');
2444
+ const decision = recordedC12RootDecision(plan, prepared.candidateHeadSha, remote, projection.registryAfterSha256, projection.itemIds);
2445
+ if (!decision)
2446
+ return fail('legacy-story-pr-merge-remote-debt-projection-invalid');
2447
+ return { ok: true, value: decision };
2448
+ }
2449
+ return { ok: true, value: Object.freeze({ schemaVersion: 1,
2450
+ kind: 'legacy-story-c12-remote-debt-required', storyId: plan.storyId,
2451
+ epicId: plan.epicId, prNumber: plan.prNumber,
2452
+ candidateHeadSha: prepared.candidateHeadSha, baseSha: plan.baseSha,
2453
+ reviewSha256: plan.reviewSha256, remoteObservationDigest: remote.digest,
2454
+ reasonCodes: remote.reasonCodes, reasonCounts: remote.reasonCounts,
2455
+ canMerge: false, publicSafe: true }) };
2456
+ }
2457
+ return { ok: true, value: Object.freeze({ schemaVersion: 1,
2458
+ kind: 'legacy-story-c12-root-merge-decision', storyId: plan.storyId,
2459
+ epicId: plan.epicId, prNumber: plan.prNumber,
2460
+ candidateHeadSha: prepared.candidateHeadSha, baseSha: plan.baseSha,
2461
+ reviewSha256: plan.reviewSha256, remoteObservationDigest: remote.digest,
2462
+ canMerge: true, blockingChecks: [],
2463
+ nonBlockingChecks: [], followUpDebt: [] }) };
2464
+ }
2465
+ catch (error) {
2466
+ const reason = error instanceof Error ? error.message : '';
2467
+ return fail(reason.startsWith('legacy-story-pr-') ? reason : 'legacy-story-pr-merge-decision-uncertain');
2468
+ }
2469
+ }
2470
+ /** Persist an exact categorized remote observation before any C12 effect.
2471
+ * This is a private replay-safe evidence journal, not the canonical scoped-CI
2472
+ * registry writer and never merge authorization. */
2473
+ export async function recordLegacyStoryPrRemoteDebtC12(options) {
2474
+ if (!Number.isSafeInteger(options.prNumber) || options.prNumber <= 0
2475
+ || !/^[A-Za-z0-9][A-Za-z0-9_-]*\.[A-Za-z0-9._-]+$/.test(options.storyId)) {
2476
+ return fail('legacy-story-pr-merge-target-invalid');
2477
+ }
2478
+ const preflight = await planLegacyStoryPrMergeDecisionC12(options);
2479
+ if (!preflight.ok)
2480
+ return fail(preflight.reasonCode);
2481
+ if (preflight.value.canMerge)
2482
+ return fail('legacy-story-pr-merge-remote-debt-not-required');
2483
+ const sessionId = `legacy-c12-remote-debt-${randomBytes(16).toString('hex')}`;
2484
+ const lock = await acquireGitLifecyclePhysicalIntegrationLock(options.projectRoot, {
2485
+ kind: 'direct', storyId: options.storyId, sessionId, idempotencyKey: sessionId,
2486
+ expiresAt: new Date(Date.now() + 5 * 60_000).toISOString(), persistent: false,
2487
+ });
2488
+ if (!lock.ok)
2489
+ return fail(lock.reasonCode);
2490
+ try {
2491
+ const planned = await planLegacyStoryPrMergeDecisionC12(options);
2492
+ if (!planned.ok)
2493
+ return fail(planned.reasonCode);
2494
+ if (planned.value.canMerge)
2495
+ return fail('legacy-story-pr-merge-remote-debt-not-required');
2496
+ if (JSON.stringify(planned.value) !== JSON.stringify(preflight.value)) {
2497
+ return fail('legacy-story-pr-merge-remote-debt-observation-drift');
2498
+ }
2499
+ const plan = planned.value;
2500
+ const cwd = realpathSync(options.projectRoot);
2501
+ const repositoryName = repository(cwd);
2502
+ const currentObservation = await (options.mergeProvider ?? defaultMergeProvider)
2503
+ .view(repositoryName, plan.prNumber);
2504
+ if (currentObservation.number !== plan.prNumber || currentObservation.state !== 'OPEN'
2505
+ || currentObservation.headRefOid !== plan.candidateHeadSha
2506
+ || currentObservation.baseRefOid !== plan.baseSha
2507
+ || currentObservation.headRefName !== `story/${plan.epicId}/${plan.storyId}`
2508
+ || currentObservation.isDraft !== false || currentObservation.mergeable !== 'MERGEABLE'
2509
+ || currentObservation.url.toLowerCase()
2510
+ !== `https://github.com/${repositoryName}/pull/${plan.prNumber}`.toLowerCase()) {
2511
+ return fail('legacy-story-pr-merge-remote-debt-observation-drift');
2512
+ }
2513
+ const currentRemote = summarizeC12RemoteRollup(currentObservation.statusCheckRollup);
2514
+ if (!currentRemote.ok || currentRemote.value.digest !== plan.remoteObservationDigest) {
2515
+ return fail('legacy-story-pr-merge-remote-debt-observation-drift');
2516
+ }
2517
+ const followUpDebt = projectC12ScopedCiFollowUpDebt(currentRemote.value, plan.storyId, plan.prNumber);
2518
+ if (!followUpDebt || followUpDebt.length === 0) {
2519
+ return fail('legacy-story-pr-merge-remote-debt-projection-invalid');
2520
+ }
2521
+ const directory = await ensureGitLifecyclePrivateChildDirectory(await gitLifecycleRuntimeDirectory(cwd), 'legacy-story-pr-intents');
2522
+ const journal = { schemaVersion: 1,
2523
+ kind: 'legacy-story-c12-remote-debt-observation',
2524
+ planSha256: digest(JSON.stringify(plan)), storyId: plan.storyId,
2525
+ epicId: plan.epicId, prNumber: plan.prNumber,
2526
+ candidateHeadSha: plan.candidateHeadSha,
2527
+ remoteObservationDigest: plan.remoteObservationDigest,
2528
+ reasonCodes: plan.reasonCodes, reasonCounts: plan.reasonCounts };
2529
+ const name = `${digest(plan.storyId)}.${plan.candidateHeadSha}.${plan.remoteObservationDigest}.c12.remote-debt.json`;
2530
+ const path = join(directory, name);
2531
+ const reused = existsSync(path);
2532
+ if (reused) {
2533
+ await assertGitLifecyclePrivateFile(path, directory);
2534
+ const stored = record(JSON.parse(safeRead(path, 4096)));
2535
+ if (!stored || JSON.stringify(stored) !== JSON.stringify(journal)) {
2536
+ return fail('legacy-story-pr-merge-remote-debt-journal-conflict');
2537
+ }
2538
+ }
2539
+ else {
2540
+ await anchoredAtomicWriteFile(directory, name, `${JSON.stringify(journal)}\n`);
2541
+ await assertGitLifecyclePrivateFile(path, directory);
2542
+ }
2543
+ return { ok: true, value: Object.freeze({ schemaVersion: 1,
2544
+ kind: 'legacy-story-c12-remote-debt-journaled', storyId: plan.storyId,
2545
+ epicId: plan.epicId, prNumber: plan.prNumber,
2546
+ candidateHeadSha: plan.candidateHeadSha,
2547
+ remoteObservationDigest: plan.remoteObservationDigest,
2548
+ reasonCodes: plan.reasonCodes, reasonCounts: plan.reasonCounts,
2549
+ followUpDebt,
2550
+ reused, durableObservation: true,
2551
+ registryRecorded: false, canMerge: false, publicSafe: true }) };
2552
+ }
2553
+ catch (error) {
2554
+ const reason = error instanceof Error ? error.message : '';
2555
+ return fail(reason.startsWith('legacy-story-pr-') ? reason : 'legacy-story-pr-merge-remote-debt-uncertain');
2556
+ }
2557
+ finally {
2558
+ await releaseGitLifecyclePhysicalIntegrationLock(lock.path, lock.owner).catch(() => undefined);
2559
+ }
2560
+ }
2561
+ /** One root-owned immediate provider merge attempt. An uncertain effect is
2562
+ * never retried automatically; later calls only reconcile physical truth. */
2563
+ export async function mergeLegacyStoryPrC12(options) {
2564
+ if (!options.authorize)
2565
+ return fail('legacy-story-pr-merge-authorization-required');
2566
+ if (!Number.isSafeInteger(options.prNumber) || options.prNumber <= 0
2567
+ || !/^[A-Za-z0-9][A-Za-z0-9_-]*\.[A-Za-z0-9._-]+$/.test(options.storyId)) {
2568
+ return fail('legacy-story-pr-merge-target-invalid');
2569
+ }
2570
+ const sessionId = `legacy-c12-merge-${randomBytes(16).toString('hex')}`;
2571
+ const lock = await acquireGitLifecyclePhysicalIntegrationLock(options.projectRoot, {
2572
+ kind: 'direct', storyId: options.storyId, sessionId, idempotencyKey: sessionId,
2573
+ expiresAt: new Date(Date.now() + 60 * 60_000).toISOString(), persistent: false,
2574
+ });
2575
+ if (!lock.ok)
2576
+ return fail(lock.reasonCode);
2577
+ try {
2578
+ const cwd = realpathSync(options.projectRoot);
2579
+ const directory = await ensureGitLifecyclePrivateChildDirectory(await gitLifecycleRuntimeDirectory(cwd), 'legacy-story-pr-intents');
2580
+ const prefix = digest(options.storyId);
2581
+ const intent = await readC12Intent(directory, `${prefix}.c12.json`);
2582
+ const prepared = await readC12Prepared(directory, `${prefix}.c12.prepared.json`);
2583
+ const c11 = await readC11Journal(directory, `${prefix}.c11.accepted.json`);
2584
+ if (!intent || !prepared || !c11)
2585
+ return fail('legacy-story-pr-merge-not-prepared');
2586
+ const plan = intent.plan;
2587
+ const intentSha256 = digest(JSON.stringify(intent));
2588
+ if (plan.storyId !== options.storyId || plan.prNumber !== options.prNumber
2589
+ || prepared.intentSha256 !== intentSha256
2590
+ || repository(cwd) !== plan.repository
2591
+ || git(cwd, 'branch', '--show-current') !== plan.branch
2592
+ || git(cwd, 'rev-parse', 'HEAD') !== prepared.candidateHeadSha
2593
+ || c11.reviewSha256 !== plan.reviewSha256 || c11.qaSha256 !== plan.qaSha256
2594
+ || c11.stateAfter !== plan.stateSha256 || c11.storyAfter !== plan.storySha256
2595
+ || digest(safeRead(join(cwd, '.neocortex/state.json'), 2 * 1024 * 1024)) !== plan.stateSha256
2596
+ || digest(safeRead(join(cwd, `docs/stories/${plan.storyId}.story.md`), 2 * 1024 * 1024)) !== plan.storySha256
2597
+ || gitRaw(cwd, 'status', '--porcelain=v1', '-uall') !== '') {
2598
+ return fail('legacy-story-pr-merge-prepared-drift');
2599
+ }
2600
+ assertC12Candidate(cwd, intent, prepared.candidateHeadSha);
2601
+ const attemptName = `${prefix}.c12.merge-attempt.json`;
2602
+ const mergedName = `${prefix}.c12.merged.json`;
2603
+ const attempt = await readC12MergeAttempt(directory, attemptName);
2604
+ const merged = await readC12Merged(directory, mergedName);
2605
+ const merger = options.mergeProvider ?? defaultMergeProvider;
2606
+ const rootDecision = options.rootDecision;
2607
+ if ((!attempt || rootDecision)
2608
+ && !validC12RootDecision(rootDecision, intent, prepared.candidateHeadSha)) {
2609
+ return fail('legacy-story-pr-merge-root-decision-required');
2610
+ }
2611
+ if (attempt && (attempt.intentSha256 !== intentSha256
2612
+ || attempt.candidateHeadSha !== prepared.candidateHeadSha
2613
+ || (rootDecision && attempt.remoteObservationDigest !== rootDecision.remoteObservationDigest))) {
2614
+ return fail('legacy-story-pr-merge-attempt-conflict');
2615
+ }
2616
+ if (merged && (!attempt || merged.intentSha256 !== intentSha256
2617
+ || merged.candidateHeadSha !== prepared.candidateHeadSha
2618
+ || merged.qaSha256 !== attempt.qaSha256
2619
+ || merged.remoteObservationDigest !== attempt.remoteObservationDigest)) {
2620
+ return fail('legacy-story-pr-merge-journal-conflict');
2621
+ }
2622
+ if (!attempt) {
2623
+ if (!rootDecision)
2624
+ return fail('legacy-story-pr-merge-root-decision-required');
2625
+ const provider = options.provider ?? defaultProvider;
2626
+ const current = inspectC12Intent(cwd, intent, c11, await provider.list(plan.repository, plan.branch, plan.baseBranch));
2627
+ if (current.headSha !== prepared.candidateHeadSha
2628
+ || current.remoteSha !== prepared.candidateHeadSha) {
2629
+ return fail('legacy-story-pr-merge-remote-drift');
2630
+ }
2631
+ const firstObservation = await merger.view(plan.repository, plan.prNumber);
2632
+ assertC12MergeObservation(plan, prepared.candidateHeadSha, firstObservation, 'OPEN');
2633
+ assertC12DecisionRemoteRollup(firstObservation, rootDecision, intent);
2634
+ const qaPlan = { schemaVersion: 1, kind: 'legacy-story-pr-plan',
2635
+ storyId: plan.storyId, epicId: plan.epicId, repository: plan.repository,
2636
+ branch: plan.branch, baseBranch: plan.baseBranch, headSha: prepared.candidateHeadSha,
2637
+ baseSha: plan.baseSha, qaAuthority: 'root-review-required', localOnly: true };
2638
+ let qaSha256;
2639
+ try {
2640
+ qaSha256 = await verifyLocalQa(qaPlan, cwd, options.verifyQa);
2641
+ }
2642
+ catch {
2643
+ return fail('legacy-story-pr-merge-final-qa-failed');
2644
+ }
2645
+ const refreshed = inspectC12Intent(cwd, intent, c11, await provider.list(plan.repository, plan.branch, plan.baseBranch));
2646
+ if (refreshed.headSha !== prepared.candidateHeadSha
2647
+ || refreshed.remoteSha !== prepared.candidateHeadSha) {
2648
+ return fail('legacy-story-pr-merge-remote-drift');
2649
+ }
2650
+ const finalObservation = await merger.view(plan.repository, plan.prNumber);
2651
+ assertC12MergeObservation(plan, prepared.candidateHeadSha, finalObservation, 'OPEN');
2652
+ const finalRemote = assertC12DecisionRemoteRollup(finalObservation, rootDecision, intent);
2653
+ const next = { schemaVersion: 1, kind: 'legacy-story-c12-merge-attempt',
2654
+ intentSha256, candidateHeadSha: prepared.candidateHeadSha, qaSha256,
2655
+ remoteObservationDigest: finalRemote.digest };
2656
+ await anchoredAtomicWriteFile(directory, attemptName, `${JSON.stringify(next)}\n`);
2657
+ await assertGitLifecyclePrivateFile(join(directory, attemptName), directory);
2658
+ try {
2659
+ await merger.merge(plan.repository, plan.prNumber, prepared.candidateHeadSha);
2660
+ }
2661
+ catch { /* An uncertain or provider-denied attempt is reconciled below; never repeated. */ }
2662
+ await options.afterMerge?.();
2663
+ }
2664
+ const currentAttempt = attempt ?? await readC12MergeAttempt(directory, attemptName);
2665
+ if (!currentAttempt)
2666
+ return fail('legacy-story-pr-merge-attempt-missing');
2667
+ const observed = await merger.view(plan.repository, plan.prNumber);
2668
+ if (observed.state !== 'MERGED')
2669
+ return fail('legacy-story-pr-merge-effect-not-confirmed');
2670
+ const physical = assertC12MergedPhysical(cwd, intent, prepared, observed);
2671
+ if (merged) {
2672
+ if (merged.mergeCommitSha !== physical.mergeCommitSha) {
2673
+ return fail('legacy-story-pr-merge-journal-conflict');
2674
+ }
2675
+ git(cwd, 'merge-base', '--is-ancestor', merged.baseHeadSha, physical.baseHeadSha);
2676
+ }
2677
+ else {
2678
+ const journal = { ...currentAttempt, kind: 'legacy-story-c12-merged',
2679
+ mergeCommitSha: physical.mergeCommitSha, baseHeadSha: physical.baseHeadSha };
2680
+ await anchoredAtomicWriteFile(directory, mergedName, `${JSON.stringify(journal)}\n`);
2681
+ await assertGitLifecyclePrivateFile(join(directory, mergedName), directory);
2682
+ }
2683
+ return { ok: true, value: Object.freeze({ schemaVersion: 1, kind: 'legacy-story-c12-merged',
2684
+ storyId: plan.storyId, epicId: plan.epicId, prNumber: plan.prNumber, branch: plan.branch,
2685
+ candidateHeadSha: prepared.candidateHeadSha, mergeCommitSha: physical.mergeCommitSha,
2686
+ baseHeadSha: physical.baseHeadSha, qaSha256: currentAttempt.qaSha256,
2687
+ remoteObservationDigest: currentAttempt.remoteObservationDigest,
2688
+ reused: !!merged, physicalMerge: true, checkpointAccepted: false, localOnly: true }) };
2689
+ }
2690
+ catch (error) {
2691
+ const reason = error instanceof Error ? error.message : '';
2692
+ return fail(reason.startsWith('legacy-story-pr-') ? reason : 'legacy-story-pr-merge-effect-uncertain');
2693
+ }
2694
+ finally {
2695
+ await releaseGitLifecyclePhysicalIntegrationLock(lock.path, lock.owner).catch(() => undefined);
2696
+ }
2697
+ }
2698
+ const defaultIssueProvider = {
2699
+ async view(repositoryName, issueNumber) {
2700
+ const raw = execFileSync('gh', ['issue', 'view', String(issueNumber), '--repo', repositoryName,
2701
+ '--json', 'number,title,state,url'], { encoding: 'utf8', timeout: 15_000,
2702
+ stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 16 * 1024 });
2703
+ return JSON.parse(raw);
2704
+ },
2705
+ async close(repositoryName, issueNumber) {
2706
+ execFileSync('gh', ['issue', 'close', String(issueNumber), '--repo', repositoryName,
2707
+ '--reason', 'completed'], { encoding: 'utf8', timeout: 30_000,
2708
+ stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 16 * 1024 });
2709
+ },
2710
+ };
2711
+ function projectC12PostMergeCheckpoint(cwd, plan, nowIso) {
2712
+ const stateBefore = gitRaw(cwd, 'show', `${plan.mergeCommitSha}:.neocortex/state.json`);
2713
+ const storyPath = `docs/stories/${plan.storyId}.story.md`;
2714
+ const storyBefore = gitRaw(cwd, 'show', `${plan.mergeCommitSha}:${storyPath}`);
2715
+ if (digest(stateBefore) !== plan.stateSha256 || digest(storyBefore) !== plan.storySha256) {
2716
+ throw new Error('legacy-story-pr-post-merge-preimage-drift');
2717
+ }
2718
+ const state = record(JSON.parse(stateBefore));
2719
+ const stories = record(state?.stories);
2720
+ const epics = record(state?.epics);
2721
+ const story = record(stories?.[plan.storyId]);
2722
+ const epic = record(epics?.[plan.epicId]);
2723
+ const steps = story?.steps_completed;
2724
+ const epicIds = epic?.stories;
2725
+ if (!story || !epic || epic.id !== plan.epicId
2726
+ || story.id !== plan.storyId || story.epic_id !== plan.epicId
2727
+ || story.status !== 'pr-reviewed' || story.last_step !== GIT_LIFECYCLE_REVIEW_PR_STEP_ID
2728
+ || story.pr_number !== plan.prNumber || story.issue_number !== plan.issueNumber
2729
+ || story.branch_name !== `story/${plan.epicId}/${plan.storyId}`
2730
+ || !Array.isArray(steps) || !steps.includes(GIT_LIFECYCLE_REVIEW_PR_STEP_ID)
2731
+ || steps.includes(GIT_LIFECYCLE_MERGE_PR_STEP_ID)
2732
+ || !Array.isArray(epicIds) || !epicIds.includes(plan.storyId)
2733
+ || new Set(epicIds).size !== epicIds.length
2734
+ || epicIds.some((id) => typeof id !== 'string'
2735
+ || record(stories?.[id])?.epic_id !== plan.epicId)) {
2736
+ throw new Error('legacy-story-pr-post-merge-state-conflict');
2737
+ }
2738
+ story.status = 'done';
2739
+ story.last_step = GIT_LIFECYCLE_MERGE_PR_STEP_ID;
2740
+ story.steps_completed = [...steps, GIT_LIFECYCLE_MERGE_PR_STEP_ID];
2741
+ story.merged = true;
2742
+ story.physicalMergeStatus = 'merged';
2743
+ story.merge_commit_sha = plan.mergeCommitSha;
2744
+ story.issue_state = 'closed';
2745
+ const completed = epicIds.filter((id) => record(stories?.[id])?.status === 'done').length;
2746
+ epic.total_stories = epicIds.length;
2747
+ epic.completed_stories = completed;
2748
+ if (completed === epicIds.length) {
2749
+ epic.status = 'done';
2750
+ if (epic.completed_at == null)
2751
+ epic.completed_at = nowIso;
2752
+ }
2753
+ else if (epic.status === 'done') {
2754
+ throw new Error('legacy-story-pr-post-merge-epic-conflict');
2755
+ }
2756
+ const stateAfter = `${JSON.stringify(state, null, 2)}\n`;
2757
+ const frontmatter = storyBefore.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);
2758
+ if (!frontmatter)
2759
+ throw new Error('legacy-story-pr-post-merge-frontmatter-invalid');
2760
+ const document = parseDocument(frontmatter[1], { strict: true, uniqueKeys: true });
2761
+ const fields = record(document.toJS());
2762
+ const stepsKeys = ['stepsCompleted', 'steps_completed'].filter((key) => document.has(key));
2763
+ const lastKeys = ['lastStep', 'last_step'].filter((key) => document.has(key));
2764
+ const issueKeys = ['issueNumber', 'issue_number'].filter((key) => document.has(key));
2765
+ const prKeys = ['prNumber', 'pr_number'].filter((key) => document.has(key));
2766
+ if (document.errors.length > 0 || document.get('id') !== plan.storyId
2767
+ || (document.get('epic_id') ?? document.get('epic')) !== plan.epicId
2768
+ || document.get('status') !== 'pr-reviewed'
2769
+ || document.has('currentStatus') && document.get('currentStatus') !== 'pr-reviewed'
2770
+ || stepsKeys.length !== 1 || lastKeys.length !== 1
2771
+ || issueKeys.length > 1 || prKeys.length > 1
2772
+ || JSON.stringify(fields?.[stepsKeys[0]]) !== JSON.stringify(steps)
2773
+ || fields?.[lastKeys[0]] !== GIT_LIFECYCLE_REVIEW_PR_STEP_ID
2774
+ || issueKeys.length === 1 && fields?.[issueKeys[0]] !== plan.issueNumber
2775
+ || prKeys.length === 1 && fields?.[prKeys[0]] !== plan.prNumber) {
2776
+ throw new Error('legacy-story-pr-post-merge-frontmatter-conflict');
2777
+ }
2778
+ document.set('status', 'done');
2779
+ if (document.has('currentStatus'))
2780
+ document.set('currentStatus', 'done');
2781
+ document.set(stepsKeys[0], [...steps, GIT_LIFECYCLE_MERGE_PR_STEP_ID]);
2782
+ document.set(lastKeys[0], GIT_LIFECYCLE_MERGE_PR_STEP_ID);
2783
+ document.set('mergeCommitSha', plan.mergeCommitSha);
2784
+ document.set('issueState', 'closed');
2785
+ const storyAfter = `---\n${document.toString({ lineWidth: 0 })}---\n${storyBefore.slice(frontmatter[0].length)}`;
2786
+ return { stateBefore, stateAfter, storyBefore, storyAfter };
2787
+ }
2788
+ /** A provider's MERGED flag is not the C12 state checkpoint. This read-only
2789
+ * preflight requires the exact merge commit at the current base HEAD, the
2790
+ * accepted C11 bytes, and the still-open same-story issue before any write. */
2791
+ export async function planLegacyStoryPrPostMergeC12(options) {
2792
+ if (!Number.isSafeInteger(options.prNumber) || options.prNumber <= 0
2793
+ || !/^[A-Za-z0-9][A-Za-z0-9_-]*\.[A-Za-z0-9._-]+$/.test(options.storyId)) {
2794
+ return fail('legacy-story-pr-post-merge-target-invalid');
2795
+ }
2796
+ try {
2797
+ const cwd = realpathSync(options.projectRoot);
2798
+ if (realpathSync(git(cwd, 'rev-parse', '--show-toplevel')) !== cwd
2799
+ || gitRaw(cwd, 'status', '--porcelain=v1', '-uall') !== '') {
2800
+ return fail('legacy-story-pr-post-merge-checkout-invalid');
2801
+ }
2802
+ const commonRaw = git(cwd, 'rev-parse', '--git-common-dir');
2803
+ const commonDir = realpathSync(isAbsolute(commonRaw) ? commonRaw : join(cwd, commonRaw));
2804
+ const runtimeDir = join(commonDir, 'neocortex-yoloop');
2805
+ const directory = join(runtimeDir, 'legacy-story-pr-intents');
2806
+ if (!existsSync(runtimeDir) || !existsSync(directory)) {
2807
+ return fail('legacy-story-pr-post-merge-not-journaled');
2808
+ }
2809
+ if (!lstatSync(runtimeDir).isDirectory() || realpathSync(runtimeDir) !== runtimeDir) {
2810
+ return fail('legacy-story-pr-post-merge-runtime-unsafe');
2811
+ }
2812
+ await assertGitLifecyclePrivateDirectory(directory, runtimeDir);
2813
+ const prefix = digest(options.storyId);
2814
+ const intent = await readC12Intent(directory, `${prefix}.c12.json`);
2815
+ const prepared = await readC12Prepared(directory, `${prefix}.c12.prepared.json`);
2816
+ const attempt = await readC12MergeAttempt(directory, `${prefix}.c12.merge-attempt.json`);
2817
+ const merged = await readC12Merged(directory, `${prefix}.c12.merged.json`);
2818
+ if (!intent || !prepared || !attempt || !merged) {
2819
+ return fail('legacy-story-pr-post-merge-not-journaled');
2820
+ }
2821
+ const plan = intent.plan;
2822
+ if (plan.storyId !== options.storyId || plan.prNumber !== options.prNumber
2823
+ || prepared.intentSha256 !== digest(JSON.stringify(intent))
2824
+ || attempt.intentSha256 !== prepared.intentSha256
2825
+ || merged.intentSha256 !== attempt.intentSha256
2826
+ || attempt.candidateHeadSha !== prepared.candidateHeadSha
2827
+ || merged.candidateHeadSha !== attempt.candidateHeadSha
2828
+ || merged.qaSha256 !== attempt.qaSha256
2829
+ || merged.remoteObservationDigest !== attempt.remoteObservationDigest
2830
+ || repository(cwd) !== plan.repository
2831
+ || git(cwd, 'branch', '--show-current') !== plan.baseBranch) {
2832
+ return fail('legacy-story-pr-post-merge-journal-drift');
2833
+ }
2834
+ const headSha = git(cwd, 'rev-parse', 'HEAD');
2835
+ if (headSha !== merged.mergeCommitSha || singleRemoteSha(cwd, plan.baseBranch) !== headSha) {
2836
+ return fail('legacy-story-pr-post-merge-base-drift');
2837
+ }
2838
+ const observed = await (options.mergeProvider ?? defaultMergeProvider).view(plan.repository, plan.prNumber);
2839
+ assertC12MergeObservation(plan, prepared.candidateHeadSha, observed, 'MERGED');
2840
+ if (observed.mergeCommit?.oid !== merged.mergeCommitSha) {
2841
+ return fail('legacy-story-pr-post-merge-provider-drift');
2842
+ }
2843
+ const parents = git(cwd, 'rev-list', '--parents', '-n', '1', headSha).split(' ');
2844
+ if (parents.length !== 3 || parents[0] !== headSha
2845
+ || parents[1] !== plan.baseSha || parents[2] !== prepared.candidateHeadSha) {
2846
+ return fail('legacy-story-pr-post-merge-parents-invalid');
2847
+ }
2848
+ const stateRaw = gitRaw(cwd, 'show', 'HEAD:.neocortex/state.json');
2849
+ const storyPath = `docs/stories/${plan.storyId}.story.md`;
2850
+ const storyRaw = gitRaw(cwd, 'show', `HEAD:${storyPath}`);
2851
+ if (digest(stateRaw) !== plan.stateSha256 || digest(storyRaw) !== plan.storySha256
2852
+ || safeRead(join(cwd, '.neocortex/state.json'), 2 * 1024 * 1024) !== stateRaw
2853
+ || safeRead(join(cwd, storyPath), 2 * 1024 * 1024) !== storyRaw) {
2854
+ return fail('legacy-story-pr-post-merge-content-drift');
2855
+ }
2856
+ const state = record(JSON.parse(stateRaw));
2857
+ const story = record(record(state?.stories)?.[plan.storyId]);
2858
+ const issueNumber = story?.issue_number;
2859
+ if (!story || story.id !== plan.storyId || story.epic_id !== plan.epicId
2860
+ || story.status !== 'pr-reviewed' || story.last_step !== GIT_LIFECYCLE_REVIEW_PR_STEP_ID
2861
+ || story.pr_number !== plan.prNumber || story.branch_name !== plan.branch
2862
+ || !Array.isArray(story.steps_completed)
2863
+ || !story.steps_completed.includes(GIT_LIFECYCLE_REVIEW_PR_STEP_ID)
2864
+ || !Number.isSafeInteger(issueNumber) || issueNumber <= 0) {
2865
+ return fail('legacy-story-pr-post-merge-state-invalid');
2866
+ }
2867
+ const frontmatter = storyRaw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);
2868
+ if (!frontmatter)
2869
+ return fail('legacy-story-pr-post-merge-frontmatter-invalid');
2870
+ const document = parseDocument(frontmatter[1], { strict: true, uniqueKeys: true });
2871
+ if (document.errors.length > 0 || document.get('id') !== plan.storyId
2872
+ || (document.get('epic_id') ?? document.get('epic')) !== plan.epicId
2873
+ || document.get('status') !== 'pr-reviewed') {
2874
+ return fail('legacy-story-pr-post-merge-frontmatter-invalid');
2875
+ }
2876
+ const issue = await (options.issueProvider ?? defaultIssueProvider).view(plan.repository, issueNumber);
2877
+ if (issue.number !== issueNumber || issue.state !== 'OPEN'
2878
+ || issue.title !== `[YOLO] Story ${plan.storyId} - Workflow Run`
2879
+ || issue.url.toLowerCase() !== `https://github.com/${plan.repository}/issues/${issueNumber}`.toLowerCase()) {
2880
+ return fail('legacy-story-pr-post-merge-issue-invalid');
2881
+ }
2882
+ return { ok: true, value: Object.freeze({ schemaVersion: 1,
2883
+ kind: 'legacy-story-c12-post-merge-plan', storyId: plan.storyId, epicId: plan.epicId,
2884
+ repository: plan.repository, baseBranch: plan.baseBranch, prNumber: plan.prNumber,
2885
+ issueNumber: issueNumber, candidateHeadSha: prepared.candidateHeadSha,
2886
+ mergeCommitSha: merged.mergeCommitSha, stateSha256: plan.stateSha256,
2887
+ storySha256: plan.storySha256,
2888
+ nextAuthority: 'root-state-checkpoint-and-issue-closure-required',
2889
+ checkpointAccepted: false, localOnly: true }) };
2890
+ }
2891
+ catch (error) {
2892
+ const reason = error instanceof Error ? error.message : '';
2893
+ return fail(reason.startsWith('legacy-story-pr-') ? reason
2894
+ : 'legacy-story-pr-post-merge-physical-unavailable');
2895
+ }
2896
+ }
2897
+ async function readC12PostMergeIntent(directory, name) {
2898
+ const path = join(directory, name);
2899
+ if (!existsSync(path))
2900
+ return null;
2901
+ await assertGitLifecyclePrivateFile(path, directory);
2902
+ const value = record(JSON.parse(safeRead(path, 8192)));
2903
+ const plan = record(value?.plan);
2904
+ if (!value || value.schemaVersion !== 1 || value.kind !== 'legacy-story-c12-post-merge-intent'
2905
+ || !plan || plan.schemaVersion !== 1 || plan.kind !== 'legacy-story-c12-post-merge-plan'
2906
+ || typeof value.nowIso !== 'string' || !Number.isFinite(Date.parse(value.nowIso))
2907
+ || typeof value.stateAfterSha256 !== 'string' || !/^[a-f0-9]{64}$/.test(value.stateAfterSha256)
2908
+ || typeof value.storyAfterSha256 !== 'string' || !/^[a-f0-9]{64}$/.test(value.storyAfterSha256)) {
2909
+ throw new Error('legacy-story-pr-post-merge-intent-invalid');
2910
+ }
2911
+ return value;
2912
+ }
2913
+ async function readC12PostMergeAccepted(directory, name) {
2914
+ const path = join(directory, name);
2915
+ if (!existsSync(path))
2916
+ return null;
2917
+ await assertGitLifecyclePrivateFile(path, directory);
2918
+ const value = record(JSON.parse(safeRead(path, 4096)));
2919
+ if (!value || value.schemaVersion !== 1 || value.kind !== 'legacy-story-c12-post-merge-accepted'
2920
+ || typeof value.intentSha256 !== 'string' || !/^[a-f0-9]{64}$/.test(value.intentSha256)
2921
+ || typeof value.checkpointHeadSha !== 'string' || !/^[a-f0-9]{40}$/.test(value.checkpointHeadSha)
2922
+ || typeof value.qaSha256 !== 'string' || !/^[a-f0-9]{64}$/.test(value.qaSha256)) {
2923
+ throw new Error('legacy-story-pr-post-merge-accepted-invalid');
2924
+ }
2925
+ return value;
2926
+ }
2927
+ /** Root-owned C12 terminal checkpoint. The private intent precedes issue close,
2928
+ * which is re-read on replay. A story becomes done only after an exact two-file
2929
+ * base commit, fresh P205, non-force push and provider/issue readback. */
2930
+ export async function acceptLegacyStoryPrPostMergeC12(options) {
2931
+ if (!options.authorize)
2932
+ return fail('legacy-story-pr-post-merge-authorization-required');
2933
+ if (!Number.isSafeInteger(options.prNumber) || options.prNumber <= 0
2934
+ || !/^[A-Za-z0-9][A-Za-z0-9_-]*\.[A-Za-z0-9._-]+$/.test(options.storyId)) {
2935
+ return fail('legacy-story-pr-post-merge-target-invalid');
2936
+ }
2937
+ const sessionId = `legacy-c12-post-merge-${randomBytes(16).toString('hex')}`;
2938
+ const lock = await acquireGitLifecyclePhysicalIntegrationLock(options.projectRoot, {
2939
+ kind: 'direct', storyId: options.storyId, sessionId, idempotencyKey: sessionId,
2940
+ expiresAt: new Date(Date.now() + 60 * 60_000).toISOString(), persistent: false,
2941
+ });
2942
+ if (!lock.ok)
2943
+ return fail(lock.reasonCode);
2944
+ try {
2945
+ const cwd = realpathSync(options.projectRoot);
2946
+ if (realpathSync(git(cwd, 'rev-parse', '--show-toplevel')) !== cwd) {
2947
+ return fail('legacy-story-pr-post-merge-checkout-invalid');
2948
+ }
2949
+ const directory = await ensureGitLifecyclePrivateChildDirectory(await gitLifecycleRuntimeDirectory(cwd), 'legacy-story-pr-intents');
2950
+ const prefix = digest(options.storyId);
2951
+ const intentName = `${prefix}.c12.post-merge.json`;
2952
+ const acceptedName = `${prefix}.c12.post-merge.accepted.json`;
2953
+ let intent = await readC12PostMergeIntent(directory, intentName);
2954
+ if (!intent) {
2955
+ const planned = await planLegacyStoryPrPostMergeC12(options);
2956
+ if (!planned.ok)
2957
+ return planned;
2958
+ const nowIso = new Date().toISOString();
2959
+ const stable = projectC12PostMergeCheckpoint(cwd, planned.value, nowIso);
2960
+ intent = { schemaVersion: 1, kind: 'legacy-story-c12-post-merge-intent',
2961
+ plan: planned.value, nowIso,
2962
+ stateAfterSha256: digest(stable.stateAfter),
2963
+ storyAfterSha256: digest(stable.storyAfter) };
2964
+ await anchoredAtomicWriteFile(directory, intentName, `${JSON.stringify(intent)}\n`);
2965
+ await assertGitLifecyclePrivateFile(join(directory, intentName), directory);
2966
+ }
2967
+ const plan = intent.plan;
2968
+ const c12Intent = await readC12Intent(directory, `${prefix}.c12.json`);
2969
+ const prepared = await readC12Prepared(directory, `${prefix}.c12.prepared.json`);
2970
+ const attempt = await readC12MergeAttempt(directory, `${prefix}.c12.merge-attempt.json`);
2971
+ const merged = await readC12Merged(directory, `${prefix}.c12.merged.json`);
2972
+ if (!c12Intent || !prepared || !attempt || !merged
2973
+ || plan.storyId !== options.storyId || plan.prNumber !== options.prNumber
2974
+ || plan.repository !== c12Intent.plan.repository
2975
+ || plan.epicId !== c12Intent.plan.epicId
2976
+ || plan.baseBranch !== c12Intent.plan.baseBranch
2977
+ || plan.candidateHeadSha !== prepared.candidateHeadSha
2978
+ || plan.mergeCommitSha !== merged.mergeCommitSha
2979
+ || plan.stateSha256 !== c12Intent.plan.stateSha256
2980
+ || plan.storySha256 !== c12Intent.plan.storySha256
2981
+ || prepared.intentSha256 !== digest(JSON.stringify(c12Intent))
2982
+ || attempt.intentSha256 !== prepared.intentSha256
2983
+ || merged.intentSha256 !== attempt.intentSha256
2984
+ || merged.qaSha256 !== attempt.qaSha256
2985
+ || merged.remoteObservationDigest !== attempt.remoteObservationDigest
2986
+ || repository(cwd) !== plan.repository
2987
+ || git(cwd, 'branch', '--show-current') !== plan.baseBranch) {
2988
+ return fail('legacy-story-pr-post-merge-intent-drift');
2989
+ }
2990
+ const projection = projectC12PostMergeCheckpoint(cwd, plan, intent.nowIso);
2991
+ if (digest(projection.stateAfter) !== intent.stateAfterSha256
2992
+ || digest(projection.storyAfter) !== intent.storyAfterSha256) {
2993
+ return fail('legacy-story-pr-post-merge-projection-drift');
2994
+ }
2995
+ const observed = await (options.mergeProvider ?? defaultMergeProvider).view(plan.repository, plan.prNumber);
2996
+ assertC12MergeObservation(c12Intent.plan, prepared.candidateHeadSha, observed, 'MERGED');
2997
+ if (observed.mergeCommit?.oid !== plan.mergeCommitSha) {
2998
+ return fail('legacy-story-pr-post-merge-provider-drift');
2999
+ }
3000
+ const parents = git(cwd, 'rev-list', '--parents', '-n', '1', plan.mergeCommitSha).split(' ');
3001
+ if (parents.length !== 3 || parents[0] !== plan.mergeCommitSha
3002
+ || parents[1] !== c12Intent.plan.baseSha || parents[2] !== plan.candidateHeadSha) {
3003
+ return fail('legacy-story-pr-post-merge-parents-invalid');
3004
+ }
3005
+ const accepted = await readC12PostMergeAccepted(directory, acceptedName);
3006
+ const intentSha256 = digest(JSON.stringify(intent));
3007
+ if (accepted && accepted.intentSha256 !== intentSha256) {
3008
+ return fail('legacy-story-pr-post-merge-accepted-conflict');
3009
+ }
3010
+ const baseHeadSha = git(cwd, 'rev-parse', 'HEAD');
3011
+ let remoteSha = singleRemoteSha(cwd, plan.baseBranch);
3012
+ if (gitRaw(cwd, 'status', '--porcelain=v1', '-uall') !== '') {
3013
+ return fail('legacy-story-pr-post-merge-base-worktree-dirty');
3014
+ }
3015
+ const candidatePath = join(directory, `${prefix}.c12.post-merge.checkout`);
3016
+ if (!existsSync(candidatePath)) {
3017
+ execFileSync('git', ['worktree', 'add', '--detach', candidatePath,
3018
+ accepted?.checkpointHeadSha ?? plan.mergeCommitSha], { cwd, encoding: 'utf8',
3019
+ timeout: 60_000, stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 32 * 1024 });
3020
+ }
3021
+ if (!lstatSync(candidatePath).isDirectory() || realpathSync(candidatePath) !== candidatePath
3022
+ || realpathSync(git(candidatePath, 'rev-parse', '--show-toplevel')) !== candidatePath
3023
+ || gitCommonDirectory(candidatePath) !== gitCommonDirectory(cwd)
3024
+ || git(candidatePath, 'branch', '--show-current') !== '') {
3025
+ return fail('legacy-story-pr-post-merge-candidate-checkout-invalid');
3026
+ }
3027
+ let headSha = git(candidatePath, 'rev-parse', 'HEAD');
3028
+ if (headSha !== plan.mergeCommitSha && (git(candidatePath, 'rev-parse', `${headSha}^`) !== plan.mergeCommitSha
3029
+ || JSON.stringify(gitRaw(candidatePath, 'diff-tree', '--no-commit-id', '--name-only', '-r', '-z', headSha)
3030
+ .split('\0').filter(Boolean).sort()) !== JSON.stringify([
3031
+ '.neocortex/state.json', `docs/stories/${plan.storyId}.story.md`
3032
+ ].sort())
3033
+ || digest(gitRaw(candidatePath, 'show', `${headSha}:.neocortex/state.json`)) !== intent.stateAfterSha256
3034
+ || digest(gitRaw(candidatePath, 'show', `${headSha}:docs/stories/${plan.storyId}.story.md`)) !== intent.storyAfterSha256
3035
+ || !gitRaw(candidatePath, 'log', '-1', '--format=%B', headSha).split('\n').map((line) => line.trim())
3036
+ .includes(`Neocortex-Legacy-Merge: ${plan.storyId}`))) {
3037
+ return fail('legacy-story-pr-post-merge-checkpoint-commit-invalid');
3038
+ }
3039
+ if (![plan.mergeCommitSha, headSha].includes(baseHeadSha)
3040
+ || ![plan.mergeCommitSha, headSha].includes(remoteSha)
3041
+ || accepted && (accepted.checkpointHeadSha !== headSha || remoteSha !== headSha)) {
3042
+ return fail('legacy-story-pr-post-merge-base-drift');
3043
+ }
3044
+ const status = gitRaw(candidatePath, 'status', '--porcelain=v1', '-uall').split('\n').filter(Boolean);
3045
+ const allowed = new Set(['.neocortex/state.json', `docs/stories/${plan.storyId}.story.md`]);
3046
+ if (headSha !== plan.mergeCommitSha && status.length > 0
3047
+ || status.some((line) => !allowed.has(line.slice(3))
3048
+ || ![' M', 'M ', 'MM'].includes(line.slice(0, 2)))) {
3049
+ return fail('legacy-story-pr-post-merge-worktree-dirty');
3050
+ }
3051
+ const currentState = safeRead(join(candidatePath, '.neocortex/state.json'), 2 * 1024 * 1024);
3052
+ const currentStory = safeRead(join(candidatePath, `docs/stories/${plan.storyId}.story.md`), 2 * 1024 * 1024);
3053
+ if (![projection.stateBefore, projection.stateAfter].includes(currentState)
3054
+ || ![projection.storyBefore, projection.storyAfter].includes(currentStory)
3055
+ || headSha !== plan.mergeCommitSha && (currentState !== projection.stateAfter
3056
+ || currentStory !== projection.storyAfter)) {
3057
+ return fail('legacy-story-pr-post-merge-content-drift');
3058
+ }
3059
+ const issueProvider = options.issueProvider ?? defaultIssueProvider;
3060
+ const issueIdentity = async () => {
3061
+ const issue = await issueProvider.view(plan.repository, plan.issueNumber);
3062
+ if (issue.number !== plan.issueNumber
3063
+ || issue.title !== `[YOLO] Story ${plan.storyId} - Workflow Run`
3064
+ || issue.url.toLowerCase() !== `https://github.com/${plan.repository}/issues/${plan.issueNumber}`.toLowerCase()
3065
+ || !['OPEN', 'CLOSED'].includes(issue.state)) {
3066
+ throw new Error('legacy-story-pr-post-merge-issue-invalid');
3067
+ }
3068
+ return issue.state;
3069
+ };
3070
+ if (accepted && await issueIdentity() !== 'CLOSED') {
3071
+ return fail('legacy-story-pr-post-merge-accepted-issue-drift');
3072
+ }
3073
+ if (!accepted && baseHeadSha === plan.mergeCommitSha && remoteSha === plan.mergeCommitSha) {
3074
+ if (status.length > 0) {
3075
+ return fail('legacy-story-pr-post-merge-pre-issue-drift');
3076
+ }
3077
+ const mergedQaPlan = { schemaVersion: 1,
3078
+ kind: 'legacy-story-pr-plan', storyId: plan.storyId, epicId: plan.epicId,
3079
+ repository: plan.repository, branch: plan.baseBranch,
3080
+ baseBranch: plan.baseBranch, headSha: plan.mergeCommitSha,
3081
+ baseSha: c12Intent.plan.baseSha, qaAuthority: 'root-review-required', localOnly: true };
3082
+ try {
3083
+ await verifyLocalQa(mergedQaPlan, cwd, options.verifyQa);
3084
+ }
3085
+ catch {
3086
+ return fail('legacy-story-pr-post-merge-pre-issue-qa-failed');
3087
+ }
3088
+ if (git(cwd, 'rev-parse', 'HEAD') !== plan.mergeCommitSha
3089
+ || singleRemoteSha(cwd, plan.baseBranch) !== plan.mergeCommitSha
3090
+ || gitRaw(cwd, 'status', '--porcelain=v1', '-uall') !== '') {
3091
+ return fail('legacy-story-pr-post-merge-pre-issue-drift');
3092
+ }
3093
+ }
3094
+ if (headSha === plan.mergeCommitSha) {
3095
+ if (currentState === projection.stateBefore) {
3096
+ await anchoredCompareAndSwapFile(join(candidatePath, '.neocortex'), 'state.json', projection.stateAfter, digest(projection.stateBefore));
3097
+ }
3098
+ if (currentStory === projection.storyBefore) {
3099
+ await anchoredCompareAndSwapFile(join(candidatePath, 'docs/stories'), `${plan.storyId}.story.md`, projection.storyAfter, digest(projection.storyBefore));
3100
+ }
3101
+ if (safeRead(join(candidatePath, '.neocortex/state.json'), 2 * 1024 * 1024) !== projection.stateAfter
3102
+ || safeRead(join(candidatePath, `docs/stories/${plan.storyId}.story.md`), 2 * 1024 * 1024) !== projection.storyAfter) {
3103
+ return fail('legacy-story-pr-post-merge-write-drift');
3104
+ }
3105
+ git(candidatePath, 'add', '--', '.neocortex/state.json', `docs/stories/${plan.storyId}.story.md`);
3106
+ const staged = gitRaw(candidatePath, 'diff', '--cached', '--name-only', '-z').split('\0').filter(Boolean).sort();
3107
+ if (JSON.stringify(staged) !== JSON.stringify([...allowed].sort())
3108
+ || gitRaw(candidatePath, 'diff', '--name-only', '-z') !== '') {
3109
+ return fail('legacy-story-pr-post-merge-staged-scope-invalid');
3110
+ }
3111
+ git(candidatePath, 'diff', '--cached', '--check');
3112
+ git(candidatePath, 'commit', '-m', `chore(story): accept merged PR ${plan.storyId}`, '-m', `Neocortex-Legacy-Merge: ${plan.storyId}`);
3113
+ headSha = git(candidatePath, 'rev-parse', 'HEAD');
3114
+ await options.afterCommit?.();
3115
+ }
3116
+ if (gitRaw(candidatePath, 'status', '--porcelain=v1', '-uall') !== '') {
3117
+ return fail('legacy-story-pr-post-merge-worktree-dirty');
3118
+ }
3119
+ const qaPlan = { schemaVersion: 1, kind: 'legacy-story-pr-plan',
3120
+ storyId: plan.storyId, epicId: plan.epicId, repository: plan.repository,
3121
+ branch: plan.baseBranch, baseBranch: plan.baseBranch,
3122
+ headSha, baseSha: plan.mergeCommitSha, qaAuthority: 'root-review-required', localOnly: true };
3123
+ let qaSha256 = accepted?.qaSha256;
3124
+ if (!qaSha256) {
3125
+ // A detached Git worktree has no node_modules. P205 also runs commands
3126
+ // from the server, portal, admin and landing package roots. Materialize
3127
+ // their pinned dependencies inside this private candidate only; never
3128
+ // replace the operator's base checkout dependencies for checkpoint QA.
3129
+ if (!options.verifyQa) {
3130
+ try {
3131
+ installP205CandidateDependencies(candidatePath);
3132
+ }
3133
+ catch (error) {
3134
+ return fail(error instanceof Error && error.message.endsWith('-missing')
3135
+ ? 'legacy-story-pr-post-merge-dependencies-missing'
3136
+ : 'legacy-story-pr-post-merge-dependencies-failed');
3137
+ }
3138
+ }
3139
+ try {
3140
+ qaSha256 = await verifyLocalQa(qaPlan, candidatePath, options.verifyQa);
3141
+ }
3142
+ catch {
3143
+ return fail('legacy-story-pr-post-merge-qa-failed');
3144
+ }
3145
+ }
3146
+ if (git(candidatePath, 'rev-parse', 'HEAD') !== headSha
3147
+ || gitRaw(candidatePath, 'status', '--porcelain=v1', '-uall') !== ''
3148
+ || git(cwd, 'rev-parse', 'HEAD') !== baseHeadSha
3149
+ || gitRaw(cwd, 'status', '--porcelain=v1', '-uall') !== '') {
3150
+ return fail('legacy-story-pr-post-merge-final-drift');
3151
+ }
3152
+ if (await issueIdentity() === 'OPEN') {
3153
+ if (singleRemoteSha(cwd, plan.baseBranch) !== plan.mergeCommitSha) {
3154
+ return fail('legacy-story-pr-post-merge-pre-issue-drift');
3155
+ }
3156
+ await issueProvider.close(plan.repository, plan.issueNumber);
3157
+ await options.afterIssueClose?.();
3158
+ }
3159
+ if (await issueIdentity() !== 'CLOSED')
3160
+ return fail('legacy-story-pr-post-merge-issue-not-closed');
3161
+ const finalObserved = await (options.mergeProvider ?? defaultMergeProvider).view(plan.repository, plan.prNumber);
3162
+ assertC12MergeObservation(c12Intent.plan, prepared.candidateHeadSha, finalObserved, 'MERGED');
3163
+ if (finalObserved.mergeCommit?.oid !== plan.mergeCommitSha) {
3164
+ return fail('legacy-story-pr-post-merge-provider-drift');
3165
+ }
3166
+ remoteSha = singleRemoteSha(cwd, plan.baseBranch);
3167
+ if (remoteSha === plan.mergeCommitSha) {
3168
+ git(cwd, 'push', '--porcelain', 'origin', `${headSha}:refs/heads/${plan.baseBranch}`);
3169
+ }
3170
+ if (singleRemoteSha(cwd, plan.baseBranch) !== headSha) {
3171
+ return fail('legacy-story-pr-post-merge-push-unverified');
3172
+ }
3173
+ await options.afterPush?.();
3174
+ if (git(cwd, 'rev-parse', 'HEAD') === plan.mergeCommitSha) {
3175
+ git(cwd, 'merge', '--ff-only', headSha);
3176
+ }
3177
+ if (git(cwd, 'rev-parse', 'HEAD') !== headSha
3178
+ || gitRaw(cwd, 'status', '--porcelain=v1', '-uall') !== ''
3179
+ || safeRead(join(cwd, '.neocortex/state.json'), 2 * 1024 * 1024) !== projection.stateAfter
3180
+ || safeRead(join(cwd, `docs/stories/${plan.storyId}.story.md`), 2 * 1024 * 1024) !== projection.storyAfter) {
3181
+ return fail('legacy-story-pr-post-merge-base-readback-invalid');
3182
+ }
3183
+ const next = { schemaVersion: 1,
3184
+ kind: 'legacy-story-c12-post-merge-accepted', intentSha256,
3185
+ checkpointHeadSha: headSha, qaSha256 };
3186
+ if (accepted && JSON.stringify(accepted) !== JSON.stringify(next)) {
3187
+ return fail('legacy-story-pr-post-merge-accepted-conflict');
3188
+ }
3189
+ if (!accepted) {
3190
+ await anchoredAtomicWriteFile(directory, acceptedName, `${JSON.stringify(next)}\n`);
3191
+ await assertGitLifecyclePrivateFile(join(directory, acceptedName), directory);
3192
+ }
3193
+ return { ok: true, value: Object.freeze({ schemaVersion: 1,
3194
+ kind: 'legacy-story-c12-post-merge-accepted', storyId: plan.storyId,
3195
+ epicId: plan.epicId, prNumber: plan.prNumber, issueNumber: plan.issueNumber,
3196
+ mergeCommitSha: plan.mergeCommitSha, checkpointHeadSha: headSha, qaSha256,
3197
+ reused: Boolean(accepted), physicalMerge: true, issueClosed: true,
3198
+ checkpointAccepted: true, publicSafe: true }) };
3199
+ }
3200
+ catch (error) {
3201
+ const reason = error instanceof Error ? error.message : '';
3202
+ return fail(reason.startsWith('legacy-story-pr-') ? reason
3203
+ : reason.includes('secure-filesystem-cas-conflict') ? 'legacy-story-pr-post-merge-write-raced'
3204
+ : 'legacy-story-pr-post-merge-effect-uncertain');
3205
+ }
3206
+ finally {
3207
+ await releaseGitLifecyclePhysicalIntegrationLock(lock.path, lock.owner).catch(() => undefined);
3208
+ }
3209
+ }
3210
+ async function verifyLegacyTerminalStory(options) {
3211
+ const { cwd, directory, storyId, epicId, repository: repo, baseBranch } = options;
3212
+ const prefix = digest(storyId);
3213
+ const c12Intent = await readC12Intent(directory, `${prefix}.c12.json`);
3214
+ const prepared = await readC12Prepared(directory, `${prefix}.c12.prepared.json`);
3215
+ const attempt = await readC12MergeAttempt(directory, `${prefix}.c12.merge-attempt.json`);
3216
+ const merged = await readC12Merged(directory, `${prefix}.c12.merged.json`);
3217
+ const postIntent = await readC12PostMergeIntent(directory, `${prefix}.c12.post-merge.json`);
3218
+ const accepted = await readC12PostMergeAccepted(directory, `${prefix}.c12.post-merge.accepted.json`);
3219
+ if (!c12Intent || !prepared || !attempt || !merged || !postIntent || !accepted) {
3220
+ return fail('legacy-epic-terminal-story-receipt-missing');
3221
+ }
3222
+ const plan = c12Intent.plan;
3223
+ const postPlan = postIntent.plan;
3224
+ if (plan.storyId !== storyId || plan.epicId !== epicId || plan.repository !== repo
3225
+ || plan.baseBranch !== baseBranch || postPlan.storyId !== storyId
3226
+ || postPlan.epicId !== epicId || postPlan.repository !== repo
3227
+ || postPlan.baseBranch !== baseBranch || postPlan.prNumber !== plan.prNumber
3228
+ || postPlan.candidateHeadSha !== prepared.candidateHeadSha
3229
+ || postPlan.mergeCommitSha !== merged.mergeCommitSha
3230
+ || postPlan.stateSha256 !== plan.stateSha256
3231
+ || postPlan.storySha256 !== plan.storySha256
3232
+ || prepared.intentSha256 !== digest(JSON.stringify(c12Intent))
3233
+ || attempt.intentSha256 !== prepared.intentSha256
3234
+ || merged.intentSha256 !== attempt.intentSha256
3235
+ || attempt.candidateHeadSha !== prepared.candidateHeadSha
3236
+ || merged.candidateHeadSha !== attempt.candidateHeadSha
3237
+ || merged.qaSha256 !== attempt.qaSha256
3238
+ || merged.remoteObservationDigest !== attempt.remoteObservationDigest
3239
+ || accepted.intentSha256 !== digest(JSON.stringify(postIntent))) {
3240
+ return fail('legacy-epic-terminal-story-receipt-drift');
3241
+ }
3242
+ const checkpointHeadSha = accepted.checkpointHeadSha;
3243
+ const checkpointParent = git(cwd, 'rev-parse', `${checkpointHeadSha}^`);
3244
+ const mergeParents = git(cwd, 'rev-list', '--parents', '-n', '1', merged.mergeCommitSha).split(' ');
3245
+ const changed = gitRaw(cwd, 'diff-tree', '--no-commit-id', '--name-only', '-r', '-z', checkpointHeadSha)
3246
+ .split('\0').filter(Boolean).sort();
3247
+ const storyPath = `docs/stories/${storyId}.story.md`;
3248
+ if (checkpointParent !== merged.mergeCommitSha
3249
+ || mergeParents.length !== 3 || mergeParents[0] !== merged.mergeCommitSha
3250
+ || mergeParents[1] !== plan.baseSha || mergeParents[2] !== prepared.candidateHeadSha
3251
+ || JSON.stringify(changed) !== JSON.stringify(['.neocortex/state.json', storyPath].sort())
3252
+ || !gitRaw(cwd, 'log', '-1', '--format=%B', checkpointHeadSha).split('\n')
3253
+ .map((line) => line.trim()).includes(`Neocortex-Legacy-Merge: ${storyId}`)) {
3254
+ return fail('legacy-epic-terminal-story-git-drift');
3255
+ }
3256
+ if (!options.firstParentHeads.has(checkpointHeadSha)) {
3257
+ return fail('legacy-epic-terminal-story-not-on-base');
3258
+ }
3259
+ const stateRaw = gitRaw(cwd, 'show', `${checkpointHeadSha}:.neocortex/state.json`);
3260
+ const storyRaw = gitRaw(cwd, 'show', `${checkpointHeadSha}:${storyPath}`);
3261
+ if (digest(stateRaw) !== postIntent.stateAfterSha256
3262
+ || digest(storyRaw) !== postIntent.storyAfterSha256) {
3263
+ return fail('legacy-epic-terminal-story-checkpoint-drift');
3264
+ }
3265
+ const state = record(JSON.parse(stateRaw));
3266
+ const story = record(record(state?.stories)?.[storyId]);
3267
+ const frontmatter = storyRaw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);
3268
+ const document = frontmatter ? parseDocument(frontmatter[1], { strict: true, uniqueKeys: true }) : null;
3269
+ const fields = document ? record(document.toJS()) : null;
3270
+ const stepsKeys = ['stepsCompleted', 'steps_completed'].filter((key) => document?.has(key));
3271
+ const lastKeys = ['lastStep', 'last_step'].filter((key) => document?.has(key));
3272
+ if (!story || story.id !== storyId || story.epic_id !== epicId
3273
+ || story.status !== 'done' || story.last_step !== GIT_LIFECYCLE_MERGE_PR_STEP_ID
3274
+ || !Array.isArray(story.steps_completed)
3275
+ || !story.steps_completed.includes(GIT_LIFECYCLE_MERGE_PR_STEP_ID)
3276
+ || story.pr_number !== plan.prNumber || story.issue_number !== postPlan.issueNumber
3277
+ || story.issue_state !== 'closed' || story.merged !== true
3278
+ || story.physicalMergeStatus !== 'merged'
3279
+ || story.merge_commit_sha !== merged.mergeCommitSha
3280
+ || story.branch_name !== `story/${epicId}/${storyId}`
3281
+ || !document || document.errors.length > 0 || document.get('id') !== storyId
3282
+ || (document.get('epic_id') ?? document.get('epic')) !== epicId
3283
+ || document.get('status') !== 'done'
3284
+ || document.has('currentStatus') && document.get('currentStatus') !== 'done'
3285
+ || stepsKeys.length !== 1 || lastKeys.length !== 1
3286
+ || JSON.stringify(fields?.[stepsKeys[0]]) !== JSON.stringify(story.steps_completed)
3287
+ || fields?.[lastKeys[0]] !== GIT_LIFECYCLE_MERGE_PR_STEP_ID
3288
+ || fields?.mergeCommitSha !== merged.mergeCommitSha
3289
+ || fields?.issueState !== 'closed') {
3290
+ return fail('legacy-epic-terminal-story-state-drift');
3291
+ }
3292
+ const observed = await options.mergeProvider.view(repo, plan.prNumber);
3293
+ assertC12MergeObservation(plan, prepared.candidateHeadSha, observed, 'MERGED');
3294
+ if (observed.mergeCommit?.oid !== merged.mergeCommitSha) {
3295
+ return fail('legacy-epic-terminal-story-provider-drift');
3296
+ }
3297
+ const issue = await options.issueProvider.view(repo, postPlan.issueNumber);
3298
+ if (issue.number !== postPlan.issueNumber || issue.state !== 'CLOSED'
3299
+ || issue.title !== `[YOLO] Story ${storyId} - Workflow Run`
3300
+ || issue.url.toLowerCase() !== `https://github.com/${repo}/issues/${postPlan.issueNumber}`.toLowerCase()) {
3301
+ return fail('legacy-epic-terminal-story-issue-drift');
3302
+ }
3303
+ return { ok: true, value: Object.freeze({ storyId, checkpointHeadSha,
3304
+ storyRowSha256: digest(JSON.stringify(story)), storyDocumentSha256: digest(storyRaw),
3305
+ qaSha256: accepted.qaSha256 }) };
3306
+ }
3307
+ /** Read-only, root-owned precursor for legacy epic accounting. It proves all
3308
+ * accepted C12 checkpoints at the exact pushed base HEAD but never frees quota
3309
+ * or declares the server workflow complete. A later accounting transaction must
3310
+ * independently revalidate this basis at its own effect boundary. */
3311
+ export async function planLegacyEpicFinalization(options) {
3312
+ if (!/^P[0-9]+$/.test(options.epicId))
3313
+ return fail('legacy-epic-terminal-target-invalid');
3314
+ try {
3315
+ const cwd = realpathSync(options.projectRoot);
3316
+ if (realpathSync(git(cwd, 'rev-parse', '--show-toplevel')) !== cwd
3317
+ || gitRaw(cwd, 'status', '--porcelain=v1', '-uall') !== '') {
3318
+ return fail('legacy-epic-terminal-checkout-invalid');
3319
+ }
3320
+ for (const directory of ['.neocortex', 'docs', 'docs/stories']) {
3321
+ const path = join(cwd, directory);
3322
+ if (!existsSync(path) || !lstatSync(path).isDirectory() || realpathSync(path) !== path) {
3323
+ return fail('legacy-epic-terminal-checkout-unsafe');
3324
+ }
3325
+ }
3326
+ const stateRaw = gitRaw(cwd, 'show', 'HEAD:.neocortex/state.json');
3327
+ if (safeRead(join(cwd, '.neocortex/state.json'), 2 * 1024 * 1024) !== stateRaw) {
3328
+ return fail('legacy-epic-terminal-state-drift');
3329
+ }
3330
+ const state = record(JSON.parse(stateRaw));
3331
+ const epics = record(state?.epics);
3332
+ const stories = record(state?.stories);
3333
+ const epic = record(epics?.[options.epicId]);
3334
+ const config = record(state?.config);
3335
+ const storyIds = epic?.stories;
3336
+ const baseBranch = config?.default_branch;
3337
+ if (!epic || epic.id !== options.epicId || epic.branch_mode !== 'story'
3338
+ || epic.status !== 'done' || !Array.isArray(storyIds)
3339
+ || storyIds.length === 0 || storyIds.length > 128
3340
+ || new Set(storyIds).size !== storyIds.length
3341
+ || storyIds.some((id) => typeof id !== 'string' || !/^P[0-9]+\.[0-9]+(?:\.[0-9]+)*$/.test(id))
3342
+ || !stories || Object.entries(stories).some(([id, value]) => record(value)?.epic_id === options.epicId && !storyIds.includes(id))
3343
+ || epic.total_stories !== storyIds.length || epic.completed_stories !== storyIds.length
3344
+ || typeof baseBranch !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._/-]*$/.test(baseBranch)
3345
+ || git(cwd, 'branch', '--show-current') !== baseBranch) {
3346
+ return fail('legacy-epic-terminal-topology-invalid');
3347
+ }
3348
+ const repo = repository(cwd);
3349
+ const baseHeadSha = git(cwd, 'rev-parse', 'HEAD');
3350
+ if (singleRemoteSha(cwd, baseBranch) !== baseHeadSha) {
3351
+ return fail('legacy-epic-terminal-base-not-pushed');
3352
+ }
3353
+ const runtimeDir = join(gitCommonDirectory(cwd), 'neocortex-yoloop');
3354
+ const directory = join(runtimeDir, 'legacy-story-pr-intents');
3355
+ if (!existsSync(runtimeDir) || !existsSync(directory)
3356
+ || !lstatSync(runtimeDir).isDirectory() || realpathSync(runtimeDir) !== runtimeDir) {
3357
+ return fail('legacy-epic-terminal-receipts-missing');
3358
+ }
3359
+ await assertGitLifecyclePrivateDirectory(directory, runtimeDir);
3360
+ const firstParentHeads = new Set(git(cwd, 'rev-list', '--first-parent', '--max-count=4096', baseHeadSha)
3361
+ .split('\n').filter(Boolean));
3362
+ const verified = [];
3363
+ for (const storyId of storyIds) {
3364
+ const result = await verifyLegacyTerminalStory({ cwd, directory, storyId,
3365
+ epicId: options.epicId, repository: repo, baseBranch, firstParentHeads,
3366
+ mergeProvider: options.mergeProvider ?? defaultMergeProvider,
3367
+ issueProvider: options.issueProvider ?? defaultIssueProvider });
3368
+ if (!result.ok)
3369
+ return result;
3370
+ const current = record(stories[storyId]);
3371
+ const currentDocument = safeRead(join(cwd, `docs/stories/${storyId}.story.md`), 2 * 1024 * 1024);
3372
+ if (!current || digest(JSON.stringify(current)) !== result.value.storyRowSha256
3373
+ || digest(currentDocument) !== result.value.storyDocumentSha256) {
3374
+ return fail('legacy-epic-terminal-story-current-drift');
3375
+ }
3376
+ verified.push(result.value);
3377
+ }
3378
+ if (!verified.some((item) => item.checkpointHeadSha === baseHeadSha)
3379
+ || git(cwd, 'rev-parse', 'HEAD') !== baseHeadSha
3380
+ || singleRemoteSha(cwd, baseBranch) !== baseHeadSha
3381
+ || gitRaw(cwd, 'status', '--porcelain=v1', '-uall') !== '') {
3382
+ return fail('legacy-epic-terminal-base-final-drift');
3383
+ }
3384
+ return { ok: true, value: Object.freeze({ schemaVersion: 1,
3385
+ kind: 'legacy-epic-terminal-plan', epicId: options.epicId,
3386
+ repositorySha256: digest(repo), baseBranchSha256: digest(baseBranch),
3387
+ baseHeadSha, storyCount: verified.length,
3388
+ storyEvidenceSha256: digest(JSON.stringify(verified)),
3389
+ physicalStoriesVerified: true, quotaReleaseAuthorized: false,
3390
+ checkpointAccepted: false, localOnly: true, publicSafe: true }) };
3391
+ }
3392
+ catch (error) {
3393
+ const reason = error instanceof Error ? error.message : '';
3394
+ return fail(reason.startsWith('legacy-epic-terminal-') ? reason
3395
+ : 'legacy-epic-terminal-physical-unavailable');
3396
+ }
3397
+ }
3398
+ async function readLegacyEpicTerminalJournal(directory, name) {
3399
+ const path = join(directory, name);
3400
+ if (!existsSync(path))
3401
+ return null;
3402
+ await assertGitLifecyclePrivateFile(path, directory);
3403
+ const value = record(JSON.parse(safeRead(path, 4096)));
3404
+ if (!value || Object.keys(value).sort().join(',') !== [
3405
+ 'schemaVersion', 'kind', 'epicId', 'planSha256', 'baseHeadSha', 'qaEvidenceDigest', 'acceptedAt',
3406
+ ].sort().join(',') || value.schemaVersion !== 1 || value.kind !== 'legacy-epic-terminal-root-accepted'
3407
+ || typeof value.epicId !== 'string' || !/^P[0-9]+$/.test(value.epicId)
3408
+ || typeof value.planSha256 !== 'string' || !/^[a-f0-9]{64}$/.test(value.planSha256)
3409
+ || typeof value.baseHeadSha !== 'string' || !/^[a-f0-9]{40}$/.test(value.baseHeadSha)
3410
+ || typeof value.qaEvidenceDigest !== 'string' || !/^[a-f0-9]{64}$/.test(value.qaEvidenceDigest)
3411
+ || typeof value.acceptedAt !== 'string' || !Number.isFinite(Date.parse(value.acceptedAt))) {
3412
+ throw new Error('legacy-epic-terminal-journal-invalid');
3413
+ }
3414
+ return value;
3415
+ }
3416
+ function projectLegacyEpicTerminalAcceptance(plan, journal, reused) {
3417
+ return Object.freeze({ schemaVersion: 1, kind: 'legacy-epic-terminal-root-accepted',
3418
+ epicId: plan.epicId, baseHeadSha: plan.baseHeadSha, storyCount: plan.storyCount,
3419
+ planSha256: journal.planSha256, qaEvidenceDigest: journal.qaEvidenceDigest, reused,
3420
+ rootAcceptanceRecorded: true, quotaReleaseAuthorized: false,
3421
+ serverAccountingAccepted: false, localOnly: true, publicSafe: true });
3422
+ }
3423
+ /** Rechecks the physical C12 chain before projecting a private root QA record.
3424
+ * This is local evidence only; the server must independently bind a future
3425
+ * quota transaction to fresh physical truth at its own effect boundary. */
3426
+ export async function readLegacyEpicTerminalAcceptance(options) {
3427
+ const planned = await planLegacyEpicFinalization(options);
3428
+ if (!planned.ok)
3429
+ return planned;
3430
+ try {
3431
+ const cwd = realpathSync(options.projectRoot);
3432
+ const runtimeDir = join(gitCommonDirectory(cwd), 'neocortex-yoloop');
3433
+ const directory = join(runtimeDir, 'legacy-epic-terminal');
3434
+ if (!existsSync(directory))
3435
+ return fail('legacy-epic-terminal-acceptance-missing');
3436
+ await assertGitLifecyclePrivateDirectory(directory, runtimeDir);
3437
+ const journal = await readLegacyEpicTerminalJournal(directory, `${digest(options.epicId)}.accepted.json`);
3438
+ if (!journal)
3439
+ return fail('legacy-epic-terminal-acceptance-missing');
3440
+ if (journal.epicId !== options.epicId || journal.baseHeadSha !== planned.value.baseHeadSha
3441
+ || journal.planSha256 !== digest(JSON.stringify(planned.value))) {
3442
+ return fail('legacy-epic-terminal-acceptance-drift');
3443
+ }
3444
+ const final = await planLegacyEpicFinalization(options);
3445
+ if (!final.ok || digest(JSON.stringify(final.value)) !== journal.planSha256) {
3446
+ return fail('legacy-epic-terminal-acceptance-drift');
3447
+ }
3448
+ return { ok: true, value: projectLegacyEpicTerminalAcceptance(final.value, journal, true) };
3449
+ }
3450
+ catch (error) {
3451
+ const reason = error instanceof Error ? error.message : '';
3452
+ return fail(reason.startsWith('legacy-epic-terminal-') ? reason
3453
+ : 'legacy-epic-terminal-acceptance-unavailable');
3454
+ }
3455
+ }
3456
+ /** Explicit root acceptance runs fresh P205 on the exact pushed terminal HEAD
3457
+ * and persists only a private digest-bound local record. No server accounting
3458
+ * or quota effect is implied by this function or its public projection. */
3459
+ export async function acceptLegacyEpicFinalization(options) {
3460
+ if (!options.authorize)
3461
+ return fail('legacy-epic-terminal-authorization-required');
3462
+ if (!/^P[0-9]+$/.test(options.epicId))
3463
+ return fail('legacy-epic-terminal-target-invalid');
3464
+ const sessionId = `legacy-epic-terminal-${randomBytes(16).toString('hex')}`;
3465
+ const lock = await acquireGitLifecyclePhysicalIntegrationLock(options.projectRoot, {
3466
+ kind: 'direct', storyId: options.epicId, sessionId, idempotencyKey: sessionId,
3467
+ expiresAt: new Date(Date.now() + 60 * 60_000).toISOString(), persistent: false,
3468
+ });
3469
+ if (!lock.ok)
3470
+ return fail(lock.reasonCode);
3471
+ try {
3472
+ const cwd = realpathSync(options.projectRoot);
3473
+ const planned = await planLegacyEpicFinalization(options);
3474
+ if (!planned.ok)
3475
+ return planned;
3476
+ const directory = await ensureGitLifecyclePrivateChildDirectory(await gitLifecycleRuntimeDirectory(cwd), 'legacy-epic-terminal');
3477
+ const name = `${digest(options.epicId)}.accepted.json`;
3478
+ const planSha256 = digest(JSON.stringify(planned.value));
3479
+ const existing = await readLegacyEpicTerminalJournal(directory, name);
3480
+ if (existing) {
3481
+ if (existing.epicId !== options.epicId || existing.baseHeadSha !== planned.value.baseHeadSha
3482
+ || existing.planSha256 !== planSha256)
3483
+ return fail('legacy-epic-terminal-acceptance-conflict');
3484
+ const final = await planLegacyEpicFinalization(options);
3485
+ if (!final.ok || digest(JSON.stringify(final.value)) !== planSha256) {
3486
+ return fail('legacy-epic-terminal-acceptance-drift');
3487
+ }
3488
+ return { ok: true, value: projectLegacyEpicTerminalAcceptance(final.value, existing, true) };
3489
+ }
3490
+ let qaEvidenceDigest;
3491
+ try {
3492
+ if (options.verifyQa)
3493
+ qaEvidenceDigest = await options.verifyQa(planned.value);
3494
+ else {
3495
+ const authority = await executeP205TransientAuthority(cwd, {
3496
+ headSha: planned.value.baseHeadSha,
3497
+ graphRevision: digest(`legacy-epic-terminal\0${planSha256}`),
3498
+ });
3499
+ const evidence = projectP205TransientExecutionEvidence(authority);
3500
+ if (!evidence || evidence.executionHeadSha !== planned.value.baseHeadSha) {
3501
+ return fail('legacy-epic-terminal-qa-invalid');
3502
+ }
3503
+ qaEvidenceDigest = evidence.executionEvidenceDigest;
3504
+ }
3505
+ }
3506
+ catch {
3507
+ return fail('legacy-epic-terminal-qa-failed');
3508
+ }
3509
+ if (!/^[a-f0-9]{64}$/.test(qaEvidenceDigest))
3510
+ return fail('legacy-epic-terminal-qa-invalid');
3511
+ const refreshed = await planLegacyEpicFinalization(options);
3512
+ if (!refreshed.ok || digest(JSON.stringify(refreshed.value)) !== planSha256) {
3513
+ return fail('legacy-epic-terminal-post-qa-drift');
3514
+ }
3515
+ const journal = { schemaVersion: 1,
3516
+ kind: 'legacy-epic-terminal-root-accepted', epicId: options.epicId,
3517
+ planSha256, baseHeadSha: planned.value.baseHeadSha, qaEvidenceDigest,
3518
+ acceptedAt: new Date().toISOString() };
3519
+ await anchoredAtomicWriteFile(directory, name, `${JSON.stringify(journal)}\n`);
3520
+ const readback = await readLegacyEpicTerminalJournal(directory, name);
3521
+ if (!readback || JSON.stringify(readback) !== JSON.stringify(journal)) {
3522
+ return fail('legacy-epic-terminal-acceptance-write-raced');
3523
+ }
3524
+ const final = await planLegacyEpicFinalization(options);
3525
+ if (!final.ok || digest(JSON.stringify(final.value)) !== planSha256) {
3526
+ return fail('legacy-epic-terminal-post-accept-drift');
3527
+ }
3528
+ return { ok: true, value: projectLegacyEpicTerminalAcceptance(final.value, journal, false) };
3529
+ }
3530
+ catch (error) {
3531
+ const reason = error instanceof Error ? error.message : '';
3532
+ return fail(reason.startsWith('legacy-epic-terminal-') ? reason
3533
+ : 'legacy-epic-terminal-acceptance-unavailable');
3534
+ }
3535
+ finally {
3536
+ await releaseGitLifecyclePhysicalIntegrationLock(lock.path, lock.owner).catch(() => undefined);
3537
+ }
3538
+ }