@ornexus/neocortex 4.60.73 → 4.60.74
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.
- package/dist/release-source.json +1 -1
- package/dist/sbom.cdx.json +1216 -1210
- package/install.ps1 +1 -1
- package/install.sh +1 -1
- package/package.json +2 -2
- package/packages/client/dist/adapters/adapter-registry.js +1 -108
- package/packages/client/dist/adapters/antigravity-adapter.js +2 -77
- package/packages/client/dist/adapters/claude-code-adapter.js +3 -79
- package/packages/client/dist/adapters/codex-adapter.js +2 -80
- package/packages/client/dist/adapters/cursor-adapter.js +4 -115
- package/packages/client/dist/adapters/gemini-adapter.js +2 -71
- package/packages/client/dist/adapters/grok-adapter.js +2 -42
- package/packages/client/dist/adapters/index.js +1 -22
- package/packages/client/dist/adapters/platform-detector.js +1 -212
- package/packages/client/dist/adapters/target-adapter.js +0 -12
- package/packages/client/dist/adapters/vscode-adapter.js +2 -72
- package/packages/client/dist/agent/refresh-stubs.js +2 -417
- package/packages/client/dist/agent/update-agent-yaml.js +1 -102
- package/packages/client/dist/agent/update-description.js +1 -251
- package/packages/client/dist/cache/crypto-utils.js +1 -78
- package/packages/client/dist/cache/encrypted-cache.js +1 -108
- package/packages/client/dist/cache/in-memory-asset-cache.js +1 -73
- package/packages/client/dist/cache/index.js +1 -13
- package/packages/client/dist/cache/protected-pi-boundary.js +1 -44
- package/packages/client/dist/checkpoint/checkpoint-client-reader.js +2 -127
- package/packages/client/dist/checkpoint/index.js +1 -11
- package/packages/client/dist/checkpoint/shared-checkpoint-types.js +1 -32
- package/packages/client/dist/cli.js +5 -358
- package/packages/client/dist/commands/activate.js +8 -394
- package/packages/client/dist/commands/architecture-answers.js +7 -274
- package/packages/client/dist/commands/architecture-memory-terminal.js +4 -253
- package/packages/client/dist/commands/architecture-non-ui-continuation.js +1 -100
- package/packages/client/dist/commands/architecture-root-binding.js +1 -51
- package/packages/client/dist/commands/cache-status.js +2 -112
- package/packages/client/dist/commands/config.js +1 -88
- package/packages/client/dist/commands/doctor.js +2 -635
- package/packages/client/dist/commands/invoke.js +143 -7200
- package/packages/client/dist/commands/legacy-epic-terminal.js +1 -71
- package/packages/client/dist/commands/legacy-story-branch.js +1 -96
- package/packages/client/dist/commands/legacy-story-pr.js +1 -276
- package/packages/client/dist/commands/mutation-qa-checkpoint.js +5 -285
- package/packages/client/dist/commands/mutation-qa-gate.js +1 -54
- package/packages/client/dist/commands/mutation-qa-git.js +1 -41
- package/packages/client/dist/commands/mutation-qa-policy.js +1 -102
- package/packages/client/dist/commands/mutation-qa-run.js +3 -219
- package/packages/client/dist/commands/mutation-qa-verdict.js +1 -149
- package/packages/client/dist/commands/mutation-qa.js +1 -220
- package/packages/client/dist/commands/native-agent.js +2 -136
- package/packages/client/dist/commands/planning-diagnosis.js +1 -43
- package/packages/client/dist/commands/planning-generation.js +1 -53
- package/packages/client/dist/commands/planning-memory.js +1 -40
- package/packages/client/dist/commands/planning-research.js +1 -42
- package/packages/client/dist/commands/private-session.js +1 -75
- package/packages/client/dist/commands/property-qa.js +1 -139
- package/packages/client/dist/commands/qa-control.js +1 -16
- package/packages/client/dist/commands/qa-scan-browser-launch.js +1 -55
- package/packages/client/dist/commands/qa-scan-browser-route.js +1 -28
- package/packages/client/dist/commands/qa-scan-capture.js +5 -663
- package/packages/client/dist/commands/qa-scan-discovery.js +3 -376
- package/packages/client/dist/commands/qa-scan-review.js +5 -826
- package/packages/client/dist/commands/qa-synthetic-backend-capture.js +1 -266
- package/packages/client/dist/commands/qa-synthetic-backend-review.js +1 -297
- package/packages/client/dist/commands/qa-synthetic-backend.js +2 -340
- package/packages/client/dist/commands/qa-synthetic-broker-port.js +1 -85
- package/packages/client/dist/commands/qa-synthetic-browser.js +1 -39
- package/packages/client/dist/commands/refactor-characterization.js +21 -671
- package/packages/client/dist/commands/refactor-commit.js +13 -399
- package/packages/client/dist/commands/refactor-control.js +3 -142
- package/packages/client/dist/commands/refactor-cpp-assertions.js +3 -93
- package/packages/client/dist/commands/refactor-review.js +5 -283
- package/packages/client/dist/commands/refactor-root.js +7 -588
- package/packages/client/dist/commands/refresh-memory.js +3 -156
- package/packages/client/dist/commands/shared-architecture-non-ui.js +1 -24
- package/packages/client/dist/commands/story-checkpoint.js +1 -171
- package/packages/client/dist/config/profile-directory.js +1 -13
- package/packages/client/dist/config/resolver-selection.js +1 -278
- package/packages/client/dist/config/secure-config.js +12 -281
- package/packages/client/dist/config.js +2 -223
- package/packages/client/dist/constants.js +1 -25
- package/packages/client/dist/context/context-collector.js +2 -222
- package/packages/client/dist/context/context-sanitizer.js +1 -145
- package/packages/client/dist/continuity/autonomous-governance.js +1 -285
- package/packages/client/dist/continuity/continuity-client-state-store.js +1 -1587
- package/packages/client/dist/continuity/invoke-hooks.js +1 -129
- package/packages/client/dist/continuity/migrations/001-initial-schema.js +2 -12
- package/packages/client/dist/continuity/migrations/002-processing-output-summary-indexes.js +2 -12
- package/packages/client/dist/continuity/migrations/003-headless-execution-attempts.js +2 -12
- package/packages/client/dist/continuity/migrations/004-outbox-delivery-state.js +2 -12
- package/packages/client/dist/continuity/migrations/005-bounded-headless-queue.js +2 -12
- package/packages/client/dist/continuity/migrations/006-native-agent-dispatches.js +2 -10
- package/packages/client/dist/continuity/native-agent-store.js +1 -69
- package/packages/client/dist/continuity/native-agent-types.js +1 -90
- package/packages/client/dist/continuity/outbox.js +1 -117
- package/packages/client/dist/continuity/runner-bootstrap-policy.js +1 -172
- package/packages/client/dist/continuity/sqlite-store.js +81 -2612
- package/packages/client/dist/effects/invoke-effect-gateways.js +1 -62
- package/packages/client/dist/errors/error-messages.js +2 -228
- package/packages/client/dist/errors/frontmatter-diagnostics.js +1 -125
- package/packages/client/dist/evidence/collector.js +1 -347
- package/packages/client/dist/evidence/index.js +1 -1
- package/packages/client/dist/evidence/types.js +0 -2
- package/packages/client/dist/fs/atomic-state-replace.js +1 -152
- package/packages/client/dist/generated/command-registry.generated.js +1 -4076
- package/packages/client/dist/generated/public-projections.generated.js +1 -149
- package/packages/client/dist/git/direct-yolo-c01-retry.js +4 -138
- package/packages/client/dist/git/direct-yolo-checkpoint.js +4 -1234
- package/packages/client/dist/git/epic-operational-state.js +2 -672
- package/packages/client/dist/git/git-lifecycle-step-ids.js +1 -14
- package/packages/client/dist/git/git-lifecycle-store.d.ts +23 -0
- package/packages/client/dist/git/git-lifecycle-store.js +10 -1981
- package/packages/client/dist/git/legacy-story-branch-checkpoint.js +9 -259
- package/packages/client/dist/git/legacy-story-branch-setup.js +4 -348
- package/packages/client/dist/git/legacy-story-pr.js +70 -3538
- package/packages/client/dist/git/secure-filesystem-boundary.js +5 -182
- package/packages/client/dist/graph-retrieval/pre-command-hook.js +1 -284
- package/packages/client/dist/graph-retrieval/shared-graph-retrieval-contract.js +1 -237
- package/packages/client/dist/i18n/first-run.js +2 -60
- package/packages/client/dist/index.js +1 -47
- package/packages/client/dist/invocation/invoke-input.js +1 -24
- package/packages/client/dist/license/index.js +1 -5
- package/packages/client/dist/license/license-client.js +1 -304
- package/packages/client/dist/machine/fingerprint.js +2 -163
- package/packages/client/dist/machine/index.js +1 -5
- package/packages/client/dist/memory/project-memory-path-scope.js +1 -62
- package/packages/client/dist/memory/project-memory-writer.js +40 -1007
- package/packages/client/dist/memory/shared-project-memory-types.js +2 -253
- package/packages/client/dist/migrations/epic-lifecycle-backfill.js +3 -1060
- package/packages/client/dist/migrations/project-identity-capability.js +1 -50
- package/packages/client/dist/migrations/project-identity.js +7 -493
- package/packages/client/dist/migrations/state-json.js +4 -393
- package/packages/client/dist/policy/architecture-policy.js +2 -184
- package/packages/client/dist/policy/index.js +1 -7
- package/packages/client/dist/policy/shared-policy-types.js +0 -38
- package/packages/client/dist/policy/subagent-approval-policy.js +1 -55
- package/packages/client/dist/private-execution/clean-baseline.js +2 -279
- package/packages/client/dist/private-execution/codex-executor.js +4 -574
- package/packages/client/dist/private-execution/codex-host-runtime.js +1 -137
- package/packages/client/dist/private-execution/codex-lifecycle-guard.js +1 -97
- package/packages/client/dist/private-execution/codex-scoped-permissions.js +1 -128
- package/packages/client/dist/private-execution/delivery.js +13 -410
- package/packages/client/dist/private-execution/diagnosis-root-review.js +2 -294
- package/packages/client/dist/private-execution/frozen-handoff.js +6 -19
- package/packages/client/dist/private-execution/implementation-validation.js +2 -106
- package/packages/client/dist/private-execution/isolated-admission.js +23 -1432
- package/packages/client/dist/private-execution/isolated-session.js +5 -725
- package/packages/client/dist/private-execution/isolated-workspace.js +1 -122
- package/packages/client/dist/private-execution/pact-consumer-runtime.js +3 -409
- package/packages/client/dist/private-execution/pact-test-source.js +1 -258
- package/packages/client/dist/private-execution/pact-toolchain.js +3 -290
- package/packages/client/dist/private-execution/pact-write-set.js +1 -172
- package/packages/client/dist/private-execution/planning-diagnosis-checkpoint.js +5 -406
- package/packages/client/dist/private-execution/planning-generation-checkpoint.js +5 -462
- package/packages/client/dist/private-execution/planning-memory-checkpoint.js +3 -139
- package/packages/client/dist/private-execution/planning-research-checkpoint.js +4 -325
- package/packages/client/dist/private-execution/private-report-store.js +1 -42
- package/packages/client/dist/private-execution/property-test-candidate.js +1 -103
- package/packages/client/dist/private-execution/property-test-choices.js +3 -147
- package/packages/client/dist/private-execution/property-test-plan.js +7 -271
- package/packages/client/dist/private-execution/property-test-review.js +4 -258
- package/packages/client/dist/private-execution/property-test-runtime.js +9 -372
- package/packages/client/dist/private-execution/property-test-source.js +1 -139
- package/packages/client/dist/private-execution/root-checkpoint.js +1 -80
- package/packages/client/dist/private-execution/root-memory-git-evidence.js +3 -135
- package/packages/client/dist/private-execution/root-memory-recovery.js +1 -93
- package/packages/client/dist/private-execution/root-memory-request.js +1 -29
- package/packages/client/dist/private-execution/root-memory-scope.js +1 -178
- package/packages/client/dist/private-execution/root-memory-writer.js +1 -102
- package/packages/client/dist/private-execution/session-lease.js +1 -63
- package/packages/client/dist/private-execution/shared-private-admission-types.js +1 -50
- package/packages/client/dist/private-execution/shared-project-validation.js +1 -66
- package/packages/client/dist/private-execution/shared-root-memory-types.js +1 -61
- package/packages/client/dist/private-execution/ux-source-validation.js +1 -372
- package/packages/client/dist/private-execution/ux-write-set.js +2 -191
- package/packages/client/dist/registry/registry-reconciliation.js +1 -234
- package/packages/client/dist/resilience/circuit-breaker.js +1 -170
- package/packages/client/dist/resilience/degradation-manager.js +1 -164
- package/packages/client/dist/resilience/freshness-indicator.js +1 -100
- package/packages/client/dist/resilience/index.js +1 -8
- package/packages/client/dist/resilience/recovery-detector.js +1 -74
- package/packages/client/dist/resolvers/asset-resolver.js +0 -13
- package/packages/client/dist/resolvers/local-resolver.js +8 -255
- package/packages/client/dist/resolvers/remote-resolver.js +1 -233
- package/packages/client/dist/runner/binary.js +1 -404
- package/packages/client/dist/runner/cli.js +17 -1571
- package/packages/client/dist/runner/headless/adapters.js +1 -49
- package/packages/client/dist/runner/headless/claude-code-adapter.js +1 -79
- package/packages/client/dist/runner/headless/continuous-story-workspace.js +11 -1628
- package/packages/client/dist/runner/headless/epic-git-reconciliation-inventory.js +1 -69
- package/packages/client/dist/runner/headless/epic-merge-candidate-qa.d.ts +1 -1
- package/packages/client/dist/runner/headless/epic-merge-candidate-qa.js +2 -110
- package/packages/client/dist/runner/headless/epic-terminal-git-cli-adapter.d.ts +2 -0
- package/packages/client/dist/runner/headless/epic-terminal-git-cli-adapter.js +10 -1236
- package/packages/client/dist/runner/headless/epic-terminal-git-runner.d.ts +18 -2
- package/packages/client/dist/runner/headless/epic-terminal-git-runner.js +2 -318
- package/packages/client/dist/runner/headless/fake-adapter.js +1 -62
- package/packages/client/dist/runner/headless/managed-profile.js +1 -132
- package/packages/client/dist/runner/headless/opencode-adapter.js +2 -145
- package/packages/client/dist/runner/headless/prepared-story-bundle.js +1 -43
- package/packages/client/dist/runner/headless/resolver.js +1 -231
- package/packages/client/dist/runner/headless/run-yolo.js +1 -885
- package/packages/client/dist/runner/headless/run-yoloop.d.ts +1 -0
- package/packages/client/dist/runner/headless/run-yoloop.js +2 -2574
- package/packages/client/dist/runner/headless/types.js +2 -574
- package/packages/client/dist/runner/local-console.js +13 -396
- package/packages/client/dist/runner/portal-sync-worker.js +1 -1487
- package/packages/client/dist/runner/project-identity.js +1 -106
- package/packages/client/dist/runner/scheduler.js +7 -995
- package/packages/client/dist/runner-cli.js +2 -18
- package/packages/client/dist/state/invocation-shape.js +1 -13
- package/packages/client/dist/state/project-state-snapshot.js +1 -1731
- package/packages/client/dist/state/state-json-repair.js +3 -785
- package/packages/client/dist/state/structural-dag-projection.js +1 -118
- package/packages/client/dist/telemetry/index.js +1 -5
- package/packages/client/dist/telemetry/offline-queue.js +1 -131
- package/packages/client/dist/tier/index.js +1 -5
- package/packages/client/dist/tier/tier-aware-client.js +1 -405
- package/packages/client/dist/types/index.js +1 -38
- package/packages/client/dist/version.js +1 -27
- package/packages/client/dist/yolo/change-evidence-collector.js +1 -126
- package/packages/client/dist/yolo/invoke-envelope-capability.js +1 -62
- package/packages/client/dist/yolo/spec-envelope-collector.js +5 -184
- package/packages/client/dist/yoloop/discovery-hook.js +2 -300
- package/packages/client/dist/yoloop/index.js +1 -8
- package/packages/client/dist/yoloop/invoke-hooks.js +12 -669
- package/packages/client/dist/yoloop/shared-discover-epics.js +1 -1007
- package/packages/client/dist/yoloop/shared-yoloop-types.js +1 -33
- package/packages/client/dist/yoloop/target-normalization.js +1 -180
- package/packages/client/dist/yoloop/yoloop-client-state-store.js +1 -1920
- package/packages/shared/dist/yolo/change-evidence.js +1 -1
- package/packages/shared/dist/yoloop/epic-terminal-git.js +144 -2
- package/targets-stubs/antigravity/gemini.md +1 -1
- package/targets-stubs/antigravity/skill/SKILL.md +1 -1
- package/targets-stubs/claude-code/neocortex-root.agent.yaml +1 -1
- package/targets-stubs/claude-code/neocortex-root.md +2 -2
- package/targets-stubs/claude-code/neocortex.agent.yaml +1 -1
- package/targets-stubs/claude-code/neocortex.md +2 -2
- package/targets-stubs/codex/AGENTS.md +1 -1
- package/targets-stubs/cursor/agent.md +2 -2
- package/targets-stubs/gemini-cli/agent.md +2 -2
- package/targets-stubs/opencode/neocortex-root.md +1 -1
- package/targets-stubs/vscode/neocortex.agent.md +2 -2
|
@@ -1,3538 +1,70 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
function
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
function
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
function gitCommonDirectory(cwd) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
}
|
|
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};
|