oh-my-codex 0.21.0 → 0.21.2
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/Cargo.toml +1 -1
- package/README.md +4 -2
- package/dist/cli/__tests__/detached-launch-diagnostics-3578.test.d.ts +2 -0
- package/dist/cli/__tests__/detached-launch-diagnostics-3578.test.d.ts.map +1 -0
- package/dist/cli/__tests__/detached-launch-diagnostics-3578.test.js +592 -0
- package/dist/cli/__tests__/detached-launch-diagnostics-3578.test.js.map +1 -0
- package/dist/cli/__tests__/detached-leader-teardown.test.js +270 -33
- package/dist/cli/__tests__/detached-leader-teardown.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-warning-copy.test.js +133 -5
- package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
- package/dist/cli/__tests__/index.test.js +543 -2
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/issue-windows-cancel-identity.test.d.ts +2 -0
- package/dist/cli/__tests__/issue-windows-cancel-identity.test.d.ts.map +1 -0
- package/dist/cli/__tests__/issue-windows-cancel-identity.test.js +17 -0
- package/dist/cli/__tests__/issue-windows-cancel-identity.test.js.map +1 -0
- package/dist/cli/__tests__/launch-fallback.test.js +15 -3
- package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
- package/dist/cli/__tests__/package-bin-contract.test.js +20 -0
- package/dist/cli/__tests__/package-bin-contract.test.js.map +1 -1
- package/dist/cli/__tests__/plugin-cache-stale-lock.test.d.ts +2 -0
- package/dist/cli/__tests__/plugin-cache-stale-lock.test.d.ts.map +1 -0
- package/dist/cli/__tests__/plugin-cache-stale-lock.test.js +76 -0
- package/dist/cli/__tests__/plugin-cache-stale-lock.test.js.map +1 -0
- package/dist/cli/__tests__/plugin-cache-symlink-provenance-3552.test.d.ts +2 -0
- package/dist/cli/__tests__/plugin-cache-symlink-provenance-3552.test.d.ts.map +1 -0
- package/dist/cli/__tests__/plugin-cache-symlink-provenance-3552.test.js +1662 -0
- package/dist/cli/__tests__/plugin-cache-symlink-provenance-3552.test.js.map +1 -0
- package/dist/cli/__tests__/plugin-launcher-provenance-3558.test.d.ts +2 -0
- package/dist/cli/__tests__/plugin-launcher-provenance-3558.test.d.ts.map +1 -0
- package/dist/cli/__tests__/plugin-launcher-provenance-3558.test.js +522 -0
- package/dist/cli/__tests__/plugin-launcher-provenance-3558.test.js.map +1 -0
- package/dist/cli/__tests__/ralplan-advisory.test.d.ts +2 -0
- package/dist/cli/__tests__/ralplan-advisory.test.d.ts.map +1 -0
- package/dist/cli/__tests__/ralplan-advisory.test.js +301 -0
- package/dist/cli/__tests__/ralplan-advisory.test.js.map +1 -0
- package/dist/cli/__tests__/ralplan.test.js +39 -0
- package/dist/cli/__tests__/ralplan.test.js.map +1 -1
- package/dist/cli/__tests__/resume.test.js +81 -6
- package/dist/cli/__tests__/resume.test.js.map +1 -1
- package/dist/cli/__tests__/session-scoped-runtime.test.js +423 -4
- package/dist/cli/__tests__/session-scoped-runtime.test.js.map +1 -1
- package/dist/cli/__tests__/setup-install-mode.test.js +60 -3
- package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
- package/dist/cli/__tests__/setup-refresh.test.js +9 -0
- package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
- package/dist/cli/__tests__/sparkshell-cli.test.js +4 -0
- package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
- package/dist/cli/__tests__/ultragoal.test.js +71 -0
- package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
- package/dist/cli/__tests__/windows-popup-loop-contract.test.js +30 -4
- package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -1
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +57 -8
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/index.d.ts +113 -7
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +1850 -185
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/native-assets.d.ts +2 -1
- package/dist/cli/native-assets.d.ts.map +1 -1
- package/dist/cli/native-assets.js +7 -7
- package/dist/cli/native-assets.js.map +1 -1
- package/dist/cli/plugin-marketplace.d.ts +89 -4
- package/dist/cli/plugin-marketplace.d.ts.map +1 -1
- package/dist/cli/plugin-marketplace.js +2135 -118
- package/dist/cli/plugin-marketplace.js.map +1 -1
- package/dist/cli/ralplan.d.ts +2 -1
- package/dist/cli/ralplan.d.ts.map +1 -1
- package/dist/cli/ralplan.js +149 -9
- package/dist/cli/ralplan.js.map +1 -1
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +7 -1
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/sparkshell.d.ts.map +1 -1
- package/dist/cli/sparkshell.js +9 -10
- package/dist/cli/sparkshell.js.map +1 -1
- package/dist/cli/update-worker.js +9 -0
- package/dist/cli/update-worker.js.map +1 -1
- package/dist/cli/update.d.ts +1 -0
- package/dist/cli/update.d.ts.map +1 -1
- package/dist/cli/update.js +9 -0
- package/dist/cli/update.js.map +1 -1
- package/dist/config/__tests__/generator-idempotent.test.js +118 -0
- package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
- package/dist/config/generator.d.ts.map +1 -1
- package/dist/config/generator.js +74 -4
- package/dist/config/generator.js.map +1 -1
- package/dist/hooks/__tests__/keyword-detector.test.js +74 -0
- package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
- package/dist/hooks/__tests__/ralplan-advisory-keyword.test.d.ts +2 -0
- package/dist/hooks/__tests__/ralplan-advisory-keyword.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/ralplan-advisory-keyword.test.js +250 -0
- package/dist/hooks/__tests__/ralplan-advisory-keyword.test.js.map +1 -0
- package/dist/hooks/__tests__/session.test.js +10 -1
- package/dist/hooks/__tests__/session.test.js.map +1 -1
- package/dist/hooks/keyword-detector.d.ts +8 -1
- package/dist/hooks/keyword-detector.d.ts.map +1 -1
- package/dist/hooks/keyword-detector.js +130 -9
- package/dist/hooks/keyword-detector.js.map +1 -1
- package/dist/hooks/session.d.ts +5 -0
- package/dist/hooks/session.d.ts.map +1 -1
- package/dist/hooks/session.js +17 -3
- package/dist/hooks/session.js.map +1 -1
- package/dist/hud/__tests__/index.test.js +16 -1
- package/dist/hud/__tests__/index.test.js.map +1 -1
- package/dist/hud/__tests__/reconcile.test.js +134 -7
- package/dist/hud/__tests__/reconcile.test.js.map +1 -1
- package/dist/hud/__tests__/render.test.js +57 -0
- package/dist/hud/__tests__/render.test.js.map +1 -1
- package/dist/hud/__tests__/session-attached.test.d.ts +2 -0
- package/dist/hud/__tests__/session-attached.test.d.ts.map +1 -0
- package/dist/hud/__tests__/session-attached.test.js +82 -0
- package/dist/hud/__tests__/session-attached.test.js.map +1 -0
- package/dist/hud/__tests__/state.test.js +94 -2
- package/dist/hud/__tests__/state.test.js.map +1 -1
- package/dist/hud/__tests__/tmux-split-realtmux.test.js +169 -2
- package/dist/hud/__tests__/tmux-split-realtmux.test.js.map +1 -1
- package/dist/hud/__tests__/tmux.test.js +104 -22
- package/dist/hud/__tests__/tmux.test.js.map +1 -1
- package/dist/hud/__tests__/types.test.js +9 -0
- package/dist/hud/__tests__/types.test.js.map +1 -1
- package/dist/hud/__tests__/watch-detached.test.d.ts +2 -0
- package/dist/hud/__tests__/watch-detached.test.d.ts.map +1 -0
- package/dist/hud/__tests__/watch-detached.test.js +383 -0
- package/dist/hud/__tests__/watch-detached.test.js.map +1 -0
- package/dist/hud/__tests__/watch-realtmux.test.d.ts +2 -0
- package/dist/hud/__tests__/watch-realtmux.test.d.ts.map +1 -0
- package/dist/hud/__tests__/watch-realtmux.test.js +322 -0
- package/dist/hud/__tests__/watch-realtmux.test.js.map +1 -0
- package/dist/hud/index.d.ts +3 -0
- package/dist/hud/index.d.ts.map +1 -1
- package/dist/hud/index.js +98 -20
- package/dist/hud/index.js.map +1 -1
- package/dist/hud/reconcile.d.ts.map +1 -1
- package/dist/hud/reconcile.js +68 -24
- package/dist/hud/reconcile.js.map +1 -1
- package/dist/hud/render.d.ts.map +1 -1
- package/dist/hud/render.js +20 -0
- package/dist/hud/render.js.map +1 -1
- package/dist/hud/session-attached.d.ts +15 -0
- package/dist/hud/session-attached.d.ts.map +1 -0
- package/dist/hud/session-attached.js +47 -0
- package/dist/hud/session-attached.js.map +1 -0
- package/dist/hud/state.d.ts +9 -1
- package/dist/hud/state.d.ts.map +1 -1
- package/dist/hud/state.js +140 -3
- package/dist/hud/state.js.map +1 -1
- package/dist/hud/tmux.d.ts +4 -0
- package/dist/hud/tmux.d.ts.map +1 -1
- package/dist/hud/tmux.js +86 -23
- package/dist/hud/tmux.js.map +1 -1
- package/dist/hud/types.d.ts +20 -0
- package/dist/hud/types.d.ts.map +1 -1
- package/dist/hud/types.js +3 -0
- package/dist/hud/types.js.map +1 -1
- package/dist/modes/base.d.ts +15 -2
- package/dist/modes/base.d.ts.map +1 -1
- package/dist/modes/base.js +105 -16
- package/dist/modes/base.js.map +1 -1
- package/dist/question/autopilot-wait.js +2 -2
- package/dist/question/autopilot-wait.js.map +1 -1
- package/dist/question/deep-interview.js +2 -2
- package/dist/question/deep-interview.js.map +1 -1
- package/dist/ralplan/__tests__/advisory-activation.test.d.ts +2 -0
- package/dist/ralplan/__tests__/advisory-activation.test.d.ts.map +1 -0
- package/dist/ralplan/__tests__/advisory-activation.test.js +700 -0
- package/dist/ralplan/__tests__/advisory-activation.test.js.map +1 -0
- package/dist/ralplan/__tests__/advisory-detached-transition.test.d.ts +2 -0
- package/dist/ralplan/__tests__/advisory-detached-transition.test.d.ts.map +1 -0
- package/dist/ralplan/__tests__/advisory-detached-transition.test.js +88 -0
- package/dist/ralplan/__tests__/advisory-detached-transition.test.js.map +1 -0
- package/dist/ralplan/__tests__/advisory.test.d.ts +2 -0
- package/dist/ralplan/__tests__/advisory.test.d.ts.map +1 -0
- package/dist/ralplan/__tests__/advisory.test.js +1087 -0
- package/dist/ralplan/__tests__/advisory.test.js.map +1 -0
- package/dist/ralplan/__tests__/runtime.test.js +416 -7
- package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
- package/dist/ralplan/advisory-activation-verifier.d.ts +19 -0
- package/dist/ralplan/advisory-activation-verifier.d.ts.map +1 -0
- package/dist/ralplan/advisory-activation-verifier.js +77 -0
- package/dist/ralplan/advisory-activation-verifier.js.map +1 -0
- package/dist/ralplan/advisory-activation.d.ts +34 -0
- package/dist/ralplan/advisory-activation.d.ts.map +1 -0
- package/dist/ralplan/advisory-activation.js +205 -0
- package/dist/ralplan/advisory-activation.js.map +1 -0
- package/dist/ralplan/advisory-contract.d.ts +103 -0
- package/dist/ralplan/advisory-contract.d.ts.map +1 -0
- package/dist/ralplan/advisory-contract.js +83 -0
- package/dist/ralplan/advisory-contract.js.map +1 -0
- package/dist/ralplan/advisory-detached-transition.d.ts +11 -0
- package/dist/ralplan/advisory-detached-transition.d.ts.map +1 -0
- package/dist/ralplan/advisory-detached-transition.js +21 -0
- package/dist/ralplan/advisory-detached-transition.js.map +1 -0
- package/dist/ralplan/advisory-evidence.d.ts +91 -0
- package/dist/ralplan/advisory-evidence.d.ts.map +1 -0
- package/dist/ralplan/advisory-evidence.js +393 -0
- package/dist/ralplan/advisory-evidence.js.map +1 -0
- package/dist/ralplan/advisory-lifecycle-validation.d.ts +11 -0
- package/dist/ralplan/advisory-lifecycle-validation.d.ts.map +1 -0
- package/dist/ralplan/advisory-lifecycle-validation.js +72 -0
- package/dist/ralplan/advisory-lifecycle-validation.js.map +1 -0
- package/dist/ralplan/advisory-postconditions.d.ts +3 -0
- package/dist/ralplan/advisory-postconditions.d.ts.map +1 -0
- package/dist/ralplan/advisory-postconditions.js +33 -0
- package/dist/ralplan/advisory-postconditions.js.map +1 -0
- package/dist/ralplan/advisory-recovery-journal.d.ts +56 -0
- package/dist/ralplan/advisory-recovery-journal.d.ts.map +1 -0
- package/dist/ralplan/advisory-recovery-journal.js +136 -0
- package/dist/ralplan/advisory-recovery-journal.js.map +1 -0
- package/dist/ralplan/advisory-storage.d.ts +24 -0
- package/dist/ralplan/advisory-storage.d.ts.map +1 -0
- package/dist/ralplan/advisory-storage.js +106 -0
- package/dist/ralplan/advisory-storage.js.map +1 -0
- package/dist/ralplan/advisory.d.ts +99 -0
- package/dist/ralplan/advisory.d.ts.map +1 -0
- package/dist/ralplan/advisory.js +914 -0
- package/dist/ralplan/advisory.js.map +1 -0
- package/dist/ralplan/documented-leader-preflight.d.ts +8 -0
- package/dist/ralplan/documented-leader-preflight.d.ts.map +1 -1
- package/dist/ralplan/documented-leader-preflight.js +60 -2
- package/dist/ralplan/documented-leader-preflight.js.map +1 -1
- package/dist/ralplan/runtime-advisory-lifecycle.d.ts +74 -0
- package/dist/ralplan/runtime-advisory-lifecycle.d.ts.map +1 -0
- package/dist/ralplan/runtime-advisory-lifecycle.js +352 -0
- package/dist/ralplan/runtime-advisory-lifecycle.js.map +1 -0
- package/dist/ralplan/runtime-contract.d.ts +126 -0
- package/dist/ralplan/runtime-contract.d.ts.map +1 -0
- package/dist/ralplan/runtime-contract.js +2 -0
- package/dist/ralplan/runtime-contract.js.map +1 -0
- package/dist/ralplan/runtime.d.ts +6 -109
- package/dist/ralplan/runtime.d.ts.map +1 -1
- package/dist/ralplan/runtime.js +174 -56
- package/dist/ralplan/runtime.js.map +1 -1
- package/dist/scripts/__tests__/postinstall.test.js +26 -0
- package/dist/scripts/__tests__/postinstall.test.js.map +1 -1
- package/dist/scripts/__tests__/ralplan-advisory-hook.test.d.ts +2 -0
- package/dist/scripts/__tests__/ralplan-advisory-hook.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/ralplan-advisory-hook.test.js +299 -0
- package/dist/scripts/__tests__/ralplan-advisory-hook.test.js.map +1 -0
- package/dist/scripts/__tests__/smoke-packed-install.test.js +102 -8
- package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
- package/dist/scripts/codex-native-hook.d.ts +20 -0
- package/dist/scripts/codex-native-hook.d.ts.map +1 -1
- package/dist/scripts/codex-native-hook.js +226 -15
- package/dist/scripts/codex-native-hook.js.map +1 -1
- package/dist/scripts/postinstall.d.ts +7 -0
- package/dist/scripts/postinstall.d.ts.map +1 -1
- package/dist/scripts/postinstall.js +40 -0
- package/dist/scripts/postinstall.js.map +1 -1
- package/dist/scripts/smoke-packed-install.d.ts +21 -5
- package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
- package/dist/scripts/smoke-packed-install.js +157 -57
- package/dist/scripts/smoke-packed-install.js.map +1 -1
- package/dist/state/__tests__/mode-binding-lease.test.d.ts +2 -0
- package/dist/state/__tests__/mode-binding-lease.test.d.ts.map +1 -0
- package/dist/state/__tests__/mode-binding-lease.test.js +498 -0
- package/dist/state/__tests__/mode-binding-lease.test.js.map +1 -0
- package/dist/state/__tests__/operations.test.js +1 -0
- package/dist/state/__tests__/operations.test.js.map +1 -1
- package/dist/state/__tests__/pinned-atomic-file.test.d.ts +2 -0
- package/dist/state/__tests__/pinned-atomic-file.test.d.ts.map +1 -0
- package/dist/state/__tests__/pinned-atomic-file.test.js +438 -0
- package/dist/state/__tests__/pinned-atomic-file.test.js.map +1 -0
- package/dist/state/__tests__/skill-active-exact-session-transaction.test.d.ts +2 -0
- package/dist/state/__tests__/skill-active-exact-session-transaction.test.d.ts.map +1 -0
- package/dist/state/__tests__/skill-active-exact-session-transaction.test.js +179 -0
- package/dist/state/__tests__/skill-active-exact-session-transaction.test.js.map +1 -0
- package/dist/state/__tests__/skill-active.test.js +29 -1
- package/dist/state/__tests__/skill-active.test.js.map +1 -1
- package/dist/state/mode-binding-lease-helper.d.ts +2 -0
- package/dist/state/mode-binding-lease-helper.d.ts.map +1 -0
- package/dist/state/mode-binding-lease-helper.js +623 -0
- package/dist/state/mode-binding-lease-helper.js.map +1 -0
- package/dist/state/mode-binding-lease.d.ts +25 -0
- package/dist/state/mode-binding-lease.d.ts.map +1 -0
- package/dist/state/mode-binding-lease.js +380 -0
- package/dist/state/mode-binding-lease.js.map +1 -0
- package/dist/state/operations.d.ts +29 -1
- package/dist/state/operations.d.ts.map +1 -1
- package/dist/state/operations.js +203 -122
- package/dist/state/operations.js.map +1 -1
- package/dist/state/pinned-atomic-file-darwin-client.d.ts +34 -0
- package/dist/state/pinned-atomic-file-darwin-client.d.ts.map +1 -0
- package/dist/state/pinned-atomic-file-darwin-client.js +174 -0
- package/dist/state/pinned-atomic-file-darwin-client.js.map +1 -0
- package/dist/state/pinned-atomic-file-darwin-helper.d.ts +2 -0
- package/dist/state/pinned-atomic-file-darwin-helper.d.ts.map +1 -0
- package/dist/state/pinned-atomic-file-darwin-helper.js +275 -0
- package/dist/state/pinned-atomic-file-darwin-helper.js.map +1 -0
- package/dist/state/pinned-atomic-file-darwin-janitor.d.ts +2 -0
- package/dist/state/pinned-atomic-file-darwin-janitor.d.ts.map +1 -0
- package/dist/state/pinned-atomic-file-darwin-janitor.js +272 -0
- package/dist/state/pinned-atomic-file-darwin-janitor.js.map +1 -0
- package/dist/state/pinned-atomic-file.d.ts +29 -0
- package/dist/state/pinned-atomic-file.d.ts.map +1 -0
- package/dist/state/pinned-atomic-file.js +536 -0
- package/dist/state/pinned-atomic-file.js.map +1 -0
- package/dist/state/process-identity.d.ts +30 -0
- package/dist/state/process-identity.d.ts.map +1 -0
- package/dist/state/process-identity.js +99 -0
- package/dist/state/process-identity.js.map +1 -0
- package/dist/state/skill-active.d.ts +18 -1
- package/dist/state/skill-active.d.ts.map +1 -1
- package/dist/state/skill-active.js +82 -5
- package/dist/state/skill-active.js.map +1 -1
- package/dist/state/workflow-transition-reconcile.js +1 -1
- package/dist/state/workflow-transition-reconcile.js.map +1 -1
- package/dist/team/__tests__/tmux-session.test.js +109 -0
- package/dist/team/__tests__/tmux-session.test.js.map +1 -1
- package/dist/team/tmux-session.d.ts.map +1 -1
- package/dist/team/tmux-session.js +10 -1
- package/dist/team/tmux-session.js.map +1 -1
- package/dist/utils/__tests__/file-durability.test.js +20 -0
- package/dist/utils/__tests__/file-durability.test.js.map +1 -1
- package/dist/utils/file-durability.d.ts +1 -1
- package/dist/utils/file-durability.d.ts.map +1 -1
- package/dist/utils/file-durability.js.map +1 -1
- package/dist/verification/__tests__/ci-rust-gates.test.js +12 -4
- package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
- package/dist/verification/__tests__/ci-trusted-publish.test.d.ts +2 -0
- package/dist/verification/__tests__/ci-trusted-publish.test.d.ts.map +1 -0
- package/dist/verification/__tests__/ci-trusted-publish.test.js +205 -0
- package/dist/verification/__tests__/ci-trusted-publish.test.js.map +1 -0
- package/dist/verification/__tests__/explore-harness-release-workflow.test.js +10 -5
- package/dist/verification/__tests__/explore-harness-release-workflow.test.js.map +1 -1
- package/dist/verification/__tests__/publish-npm-manual-retired.test.d.ts +2 -0
- package/dist/verification/__tests__/publish-npm-manual-retired.test.d.ts.map +1 -0
- package/dist/verification/__tests__/publish-npm-manual-retired.test.js +22 -0
- package/dist/verification/__tests__/publish-npm-manual-retired.test.js.map +1 -0
- package/package.json +1 -1
- package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/oh-my-codex/skills/doctor/SKILL.md +1 -1
- package/plugins/oh-my-codex/skills/omx-setup/SKILL.md +1 -1
- package/plugins/oh-my-codex/skills/ralplan/SKILL.md +10 -1
- package/plugins/oh-my-codex/skills/team/SKILL.md +2 -0
- package/skills/doctor/SKILL.md +1 -1
- package/skills/omx-setup/SKILL.md +1 -1
- package/skills/ralplan/SKILL.md +10 -1
- package/skills/team/SKILL.md +2 -0
- package/src/scripts/__tests__/postinstall.test.ts +28 -0
- package/src/scripts/__tests__/ralplan-advisory-hook.test.ts +320 -0
- package/src/scripts/__tests__/smoke-packed-install.test.ts +120 -7
- package/src/scripts/codex-native-hook.ts +220 -10
- package/src/scripts/postinstall.ts +46 -0
- package/src/scripts/smoke-packed-install.ts +175 -70
|
@@ -0,0 +1,914 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdir, readFile, realpath, rm, stat } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join, relative } from 'node:path';
|
|
4
|
+
import { getBaseStateDir } from '../state/paths.js';
|
|
5
|
+
import { describeAdvisoryActivationProjections, verifyPinnedJsonAndSync } from './advisory-activation-verifier.js';
|
|
6
|
+
import { classifyAdvisoryPrompt, validateAdvisoryInactiveState, } from './advisory-contract.js';
|
|
7
|
+
export * from './advisory-contract.js';
|
|
8
|
+
import { activationValid, committedJournalMatchesFence, completeLifecycleBinding, fenceStateSemanticsValid, fenceTransitionSemanticsValid, fenceValid, journalValid, releasedEventValid, } from './advisory-lifecycle-validation.js';
|
|
9
|
+
import { applyTerminalSkillReduction, commitInterruptedAdvisoryRecovery, createAdvisoryCloseoutJournal, revalidateAdvisoryEvidence, revalidateStoredAdvisoryEvidence, transitionAdvisoryJournalToRecoveryRequired, } from './advisory-recovery-journal.js';
|
|
10
|
+
import { advisoryEventsPath as ralplanAdvisoryEventsPath, advisoryGenerationDir as generationDir, advisoryRoot, canonicalAdvisoryCwd as canonicalCwd, emitAdvisoryEvent, readAdvisoryJson as readStrictJson, safeAdvisoryId as safeId, syncAdvisoryDirectory as syncDirectory, withAdvisoryCurrentLock as withRalplanAdvisoryCurrentLock, withAdvisoryGenerationLock as withGenerationLock, writeAdvisoryAtomic as writeAtomic, writeAdvisoryExclusive as writeExclusive, } from './advisory-storage.js';
|
|
11
|
+
export { ralplanAdvisoryEventsPath, withRalplanAdvisoryCurrentLock };
|
|
12
|
+
import { closeoutStepPostcondition } from './advisory-postconditions.js';
|
|
13
|
+
function sha256(value) {
|
|
14
|
+
return createHash('sha256').update(value).digest('hex');
|
|
15
|
+
}
|
|
16
|
+
async function prepareActivationIntentUnlocked(cwd, root, input) {
|
|
17
|
+
const pointerPath = join(root, 'current.json');
|
|
18
|
+
const intentPath = join(root, 'rollover-intent.json');
|
|
19
|
+
const current = await readStrictJson(pointerPath);
|
|
20
|
+
const expectedPredecessor = input.predecessorGenerationId;
|
|
21
|
+
const pendingRaw = await readStrictJson(intentPath);
|
|
22
|
+
if (pendingRaw)
|
|
23
|
+
throw new Error('ralplan_advisory_rollover_intent_pending_admin');
|
|
24
|
+
if (current && current.generation_id !== expectedPredecessor)
|
|
25
|
+
throw new Error('ralplan_advisory_current_cas_mismatch');
|
|
26
|
+
if (!current && expectedPredecessor)
|
|
27
|
+
throw new Error('ralplan_advisory_current_cas_mismatch');
|
|
28
|
+
const generationId = input.generationId ?? randomUUID();
|
|
29
|
+
if (!safeId(generationId))
|
|
30
|
+
throw new Error('ralplan_advisory_generation_id_invalid');
|
|
31
|
+
const createdAt = input.nowIso ?? new Date().toISOString();
|
|
32
|
+
const intent = {
|
|
33
|
+
schema_version: 1, ...(expectedPredecessor ? { predecessor_generation_id: expectedPredecessor } : {}), generation_id: generationId,
|
|
34
|
+
session_id: input.sessionId, root_thread_id: input.rootThreadId,
|
|
35
|
+
activation_turn_id: input.activationTurnId,
|
|
36
|
+
...(input.activationPrompt !== undefined ? { activation_prompt_sha256: sha256(input.activationPrompt) } : {}),
|
|
37
|
+
canonical_cwd: cwd, created_at: createdAt,
|
|
38
|
+
};
|
|
39
|
+
await writeExclusive(intentPath, intent);
|
|
40
|
+
return {
|
|
41
|
+
schema_version: 1, generation_id: generationId,
|
|
42
|
+
...(expectedPredecessor ? { predecessor_generation_id: expectedPredecessor } : {}),
|
|
43
|
+
canonical_cwd: cwd, session_id: input.sessionId, root_thread_id: input.rootThreadId,
|
|
44
|
+
activation_turn_id: input.activationTurnId,
|
|
45
|
+
...(input.activationPrompt !== undefined ? { activation_prompt_sha256: sha256(input.activationPrompt) } : {}),
|
|
46
|
+
created_at: createdAt,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** @internal Intent-store primitive. Only the central activation owner may call this. */
|
|
50
|
+
export async function prepareRalplanAdvisoryActivationInternal(input) {
|
|
51
|
+
const cwd = await canonicalCwd(input.cwd);
|
|
52
|
+
if (![input.sessionId, input.rootThreadId, input.activationTurnId].every(safeId))
|
|
53
|
+
throw new Error('ralplan_advisory_identity_missing');
|
|
54
|
+
const root = advisoryRoot(cwd, input.sessionId);
|
|
55
|
+
return withRalplanAdvisoryCurrentLock(cwd, input.sessionId, () => prepareActivationIntentUnlocked(cwd, root, input));
|
|
56
|
+
}
|
|
57
|
+
export async function readAuthorizedPendingRalplanActivation(input) {
|
|
58
|
+
const cwd = await canonicalCwd(input.cwd);
|
|
59
|
+
const pending = await readStrictJson(join(advisoryRoot(cwd, input.sessionId), 'rollover-intent.json'));
|
|
60
|
+
if (!pending)
|
|
61
|
+
return null;
|
|
62
|
+
const authorized = input.producer === 'native' && input.threadKind === 'root-or-drift'
|
|
63
|
+
&& pending.schema_version === 1 && pending.session_id === input.sessionId && pending.canonical_cwd === cwd
|
|
64
|
+
&& pending.root_thread_id === input.rootThreadId && pending.activation_turn_id === input.activationTurnId
|
|
65
|
+
&& typeof pending.activation_prompt_sha256 === 'string'
|
|
66
|
+
&& pending.activation_prompt_sha256 === sha256(input.prompt)
|
|
67
|
+
&& typeof pending.generation_id === 'string' && safeId(pending.generation_id)
|
|
68
|
+
&& typeof pending.created_at === 'string';
|
|
69
|
+
if (!authorized)
|
|
70
|
+
throw new Error('ralplan_advisory_pending_activation_authority_mismatch');
|
|
71
|
+
return {
|
|
72
|
+
schema_version: 1, generation_id: String(pending.generation_id),
|
|
73
|
+
...(typeof pending.predecessor_generation_id === 'string' ? { predecessor_generation_id: pending.predecessor_generation_id } : {}),
|
|
74
|
+
canonical_cwd: cwd, session_id: input.sessionId, root_thread_id: input.rootThreadId,
|
|
75
|
+
activation_turn_id: input.activationTurnId,
|
|
76
|
+
activation_prompt_sha256: String(pending.activation_prompt_sha256),
|
|
77
|
+
created_at: String(pending.created_at),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
async function commitBoundActivationIntent(cwd, sessionId, pending, failpoint, beforeCommit) {
|
|
81
|
+
const root = advisoryRoot(cwd, sessionId);
|
|
82
|
+
return withRalplanAdvisoryCurrentLock(cwd, sessionId, async () => {
|
|
83
|
+
const intentPath = join(root, 'rollover-intent.json');
|
|
84
|
+
const currentIntent = await readStrictJson(intentPath);
|
|
85
|
+
if (!currentIntent || JSON.stringify(currentIntent) !== JSON.stringify(pending)) {
|
|
86
|
+
throw new Error('ralplan_advisory_rollover_intent_changed');
|
|
87
|
+
}
|
|
88
|
+
await beforeCommit?.();
|
|
89
|
+
const generationId = String(pending.generation_id ?? '');
|
|
90
|
+
const rootThreadId = String(pending.root_thread_id ?? '');
|
|
91
|
+
const activationTurnId = String(pending.activation_turn_id ?? '');
|
|
92
|
+
if (!safeId(generationId) || !safeId(rootThreadId) || !safeId(activationTurnId)
|
|
93
|
+
|| pending.schema_version !== 1 || pending.session_id !== sessionId || pending.canonical_cwd !== cwd) {
|
|
94
|
+
throw new Error('ralplan_advisory_rollover_intent_invalid');
|
|
95
|
+
}
|
|
96
|
+
const dir = generationDir(cwd, sessionId, generationId);
|
|
97
|
+
await mkdir(dir, { recursive: true });
|
|
98
|
+
let activationRaw = await readStrictJson(join(dir, 'activation.json'));
|
|
99
|
+
if (!activationRaw) {
|
|
100
|
+
const activation = {
|
|
101
|
+
schema_version: 1, generation_id: generationId,
|
|
102
|
+
...(typeof pending.predecessor_generation_id === 'string' ? { predecessor_generation_id: pending.predecessor_generation_id } : {}),
|
|
103
|
+
canonical_cwd: cwd, session_id: sessionId, root_thread_id: rootThreadId,
|
|
104
|
+
activation_turn_id: activationTurnId,
|
|
105
|
+
...(typeof pending.activation_prompt_sha256 === 'string'
|
|
106
|
+
? { activation_prompt_sha256: pending.activation_prompt_sha256 } : {}),
|
|
107
|
+
created_at: String(pending.created_at ?? ''),
|
|
108
|
+
};
|
|
109
|
+
await writeExclusive(join(dir, 'activation.json'), activation);
|
|
110
|
+
activationRaw = activation;
|
|
111
|
+
}
|
|
112
|
+
await failpoint?.('rollover_activation');
|
|
113
|
+
if (!activationValid(activationRaw, cwd, sessionId, generationId))
|
|
114
|
+
throw new Error('ralplan_advisory_rollover_activation_invalid');
|
|
115
|
+
const pointerPath = join(root, 'current.json');
|
|
116
|
+
const current = await readStrictJson(pointerPath);
|
|
117
|
+
const predecessor = typeof pending.predecessor_generation_id === 'string' ? pending.predecessor_generation_id : undefined;
|
|
118
|
+
if (current && current.generation_id !== generationId && current.generation_id !== predecessor) {
|
|
119
|
+
throw new Error('ralplan_advisory_current_cas_mismatch');
|
|
120
|
+
}
|
|
121
|
+
if (!current || current.generation_id !== generationId) {
|
|
122
|
+
await writeAtomic(pointerPath, {
|
|
123
|
+
schema_version: 1, generation_id: generationId, ...(predecessor ? { predecessor_generation_id: predecessor } : {}),
|
|
124
|
+
session_id: sessionId, canonical_cwd: cwd, updated_at: String(pending.created_at ?? ''),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
await failpoint?.('rollover_pointer');
|
|
128
|
+
await rm(intentPath, { force: false });
|
|
129
|
+
await syncDirectory(root);
|
|
130
|
+
await failpoint?.('intent_committed');
|
|
131
|
+
return activationRaw;
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
async function verifyPreparedActivationMirrors(cwd, sessionId, generationId) {
|
|
135
|
+
const projections = describeAdvisoryActivationProjections(getBaseStateDir(cwd), sessionId, generationId);
|
|
136
|
+
for (const projection of [projections.mode, projections.run, projections.sessionSkill, projections.rootSkill]) {
|
|
137
|
+
await verifyPinnedJsonAndSync(projection.path, projection.predicate);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/** @internal Activation-store primitive. Only the central activation owner may call this. */
|
|
141
|
+
export async function commitPreparedRalplanAdvisoryActivationInternal(input) {
|
|
142
|
+
const cwd = await canonicalCwd(input.cwd);
|
|
143
|
+
const pending = await readStrictJson(join(advisoryRoot(cwd, input.sessionId), 'rollover-intent.json'));
|
|
144
|
+
if (!pending)
|
|
145
|
+
throw new Error('ralplan_advisory_rollover_intent_missing');
|
|
146
|
+
const authenticated = input.producer === 'native' && input.threadKind === 'root-or-drift'
|
|
147
|
+
&& input.rootThreadId === pending.root_thread_id
|
|
148
|
+
&& input.activationTurnId === pending.activation_turn_id;
|
|
149
|
+
if (!authenticated)
|
|
150
|
+
throw new Error('ralplan_advisory_pending_activation_authority_mismatch');
|
|
151
|
+
const bound = await readStrictJson(join(getBaseStateDir(cwd), 'sessions', input.sessionId, 'ralplan-state.json'));
|
|
152
|
+
if (!bound || bound.active !== true || bound.workflow_variant !== 'advisory'
|
|
153
|
+
|| bound.advisory_generation_id !== pending.generation_id) {
|
|
154
|
+
throw new Error('ralplan_advisory_start_binding_conflict');
|
|
155
|
+
}
|
|
156
|
+
return commitBoundActivationIntent(cwd, input.sessionId, pending, input.failpoint, () => verifyPreparedActivationMirrors(cwd, input.sessionId, String(pending.generation_id)));
|
|
157
|
+
}
|
|
158
|
+
async function readProjectionForGeneration(cwdInput, sessionId, generationId) {
|
|
159
|
+
const cwd = await canonicalCwd(cwdInput);
|
|
160
|
+
const dir = generationDir(cwd, sessionId, generationId);
|
|
161
|
+
try {
|
|
162
|
+
const canonicalDir = await realpath(dir);
|
|
163
|
+
if (canonicalDir !== dir || relative(advisoryRoot(cwd, sessionId), canonicalDir).startsWith('..'))
|
|
164
|
+
throw new Error('generation_path_invalid');
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return { activation: null, fence: null, journal: null, denyProductWrites: false, corruption: 'generation_missing_or_untrusted' };
|
|
168
|
+
}
|
|
169
|
+
let activationRaw;
|
|
170
|
+
try {
|
|
171
|
+
activationRaw = await readStrictJson(join(dir, 'activation.json'));
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return { activation: null, fence: null, journal: null, denyProductWrites: false, corruption: 'activation_corrupt' };
|
|
175
|
+
}
|
|
176
|
+
if (!activationRaw || !activationValid(activationRaw, cwd, sessionId, generationId)) {
|
|
177
|
+
return { activation: null, fence: null, journal: null, denyProductWrites: false, corruption: 'activation_invalid' };
|
|
178
|
+
}
|
|
179
|
+
const activation = activationRaw;
|
|
180
|
+
let base;
|
|
181
|
+
let journalRaw;
|
|
182
|
+
try {
|
|
183
|
+
base = await readStrictJson(join(dir, 'fence.json'));
|
|
184
|
+
journalRaw = await readStrictJson(join(dir, 'closeout-journal.json'));
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return { activation, fence: null, journal: null, denyProductWrites: false, corruption: 'fence_or_journal_corrupt' };
|
|
188
|
+
}
|
|
189
|
+
let fence = base ? base : null;
|
|
190
|
+
if (fence && !fenceValid(base, activation))
|
|
191
|
+
return { activation, fence: null, journal: null, denyProductWrites: false, corruption: 'fence_invalid' };
|
|
192
|
+
if (fence && (fence.state !== 'pending_closeout' || !fenceStateSemanticsValid(fence)
|
|
193
|
+
|| fence.sequence !== 0 || fence.previous_event_sha256 !== undefined
|
|
194
|
+
|| base?.release_turn_id !== undefined || base?.release_thread_id !== undefined || base?.authority_kind !== undefined)) {
|
|
195
|
+
return { activation, fence: null, journal: null, denyProductWrites: false, corruption: 'fence_base_semantics_invalid' };
|
|
196
|
+
}
|
|
197
|
+
let sequence = fence?.sequence ?? -1;
|
|
198
|
+
let priorDigest = fence ? sha256(`${JSON.stringify(fence)}\n`) : undefined;
|
|
199
|
+
let abandonedPredecessorBytes = null;
|
|
200
|
+
let releaseSeen = false;
|
|
201
|
+
for (let index = 1;; index += 1) {
|
|
202
|
+
let event;
|
|
203
|
+
try {
|
|
204
|
+
event = await readStrictJson(join(dir, `fence-event-${String(index).padStart(4, '0')}.json`));
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
return { activation, fence, journal: null, denyProductWrites: false, corruption: 'fence_event_corrupt' };
|
|
208
|
+
}
|
|
209
|
+
if (!event)
|
|
210
|
+
break;
|
|
211
|
+
if (event.state === 'released') {
|
|
212
|
+
if (!fence || !fenceValid(event, activation) || event.sequence !== sequence + 1
|
|
213
|
+
|| event.previous_event_sha256 !== priorDigest
|
|
214
|
+
|| !releasedEventValid(fence, event, journalRaw)) {
|
|
215
|
+
return { activation, fence, journal: null, denyProductWrites: true, corruption: 'fence_event_chain_invalid' };
|
|
216
|
+
}
|
|
217
|
+
// Valid release records are historical and inert in the non-authoritative
|
|
218
|
+
// projection. Keep the terminal predecessor as the planning result.
|
|
219
|
+
releaseSeen = true;
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
if (event.state === 'abandoned') {
|
|
223
|
+
const predecessorPath = index === 1 ? join(dir, 'fence.json') : join(dir, `fence-event-${String(index - 1).padStart(4, '0')}.json`);
|
|
224
|
+
abandonedPredecessorBytes = await readFile(predecessorPath).catch(() => null);
|
|
225
|
+
}
|
|
226
|
+
if (!fence || !fenceValid(event, activation) || event.sequence !== sequence + 1 || event.previous_event_sha256 !== priorDigest
|
|
227
|
+
|| !transitionAllowed(fence.state, event.state)
|
|
228
|
+
|| !fenceTransitionSemanticsValid(fence, event)) {
|
|
229
|
+
return { activation, fence, journal: null, denyProductWrites: false, corruption: 'fence_event_chain_invalid' };
|
|
230
|
+
}
|
|
231
|
+
fence = event;
|
|
232
|
+
sequence = fence.sequence;
|
|
233
|
+
priorDigest = sha256(`${JSON.stringify(fence)}\n`);
|
|
234
|
+
}
|
|
235
|
+
if (releaseSeen) {
|
|
236
|
+
let trailing;
|
|
237
|
+
try {
|
|
238
|
+
trailing = await readStrictJson(join(dir, `fence-event-${String(sequence + 1).padStart(4, '0')}.json`));
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
return { activation, fence, journal: null, denyProductWrites: true, corruption: 'fence_event_chain_invalid' };
|
|
242
|
+
}
|
|
243
|
+
if (trailing)
|
|
244
|
+
return { activation, fence, journal: null, denyProductWrites: true, corruption: 'fence_event_chain_invalid' };
|
|
245
|
+
}
|
|
246
|
+
const journal = journalRaw && journalValid(journalRaw, generationId) ? journalRaw : null;
|
|
247
|
+
if (journalRaw && !journal)
|
|
248
|
+
return { activation, fence, journal: null, denyProductWrites: false, corruption: 'journal_invalid' };
|
|
249
|
+
let adminRaw;
|
|
250
|
+
try {
|
|
251
|
+
adminRaw = await readStrictJson(join(dir, 'admin-event-0001.json'));
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
return { activation, fence, journal, denyProductWrites: false, corruption: 'admin_event_corrupt' };
|
|
255
|
+
}
|
|
256
|
+
const adminEvent = adminRaw;
|
|
257
|
+
const terminalFence = fence;
|
|
258
|
+
const priorFenceBytes = adminRaw ? abandonedPredecessorBytes : null;
|
|
259
|
+
const journalBytes = adminRaw && journal ? await readFile(join(dir, 'closeout-journal.json')).catch(() => null) : null;
|
|
260
|
+
if (adminRaw && (adminRaw.schema_version !== 1 || adminRaw.action !== 'abandon'
|
|
261
|
+
|| adminRaw.generation_id !== generationId || adminRaw.session_id !== sessionId
|
|
262
|
+
|| typeof adminRaw.root_thread_id !== 'string' || !safeId(adminRaw.root_thread_id)
|
|
263
|
+
|| typeof adminRaw.turn_id !== 'string' || !safeId(adminRaw.turn_id)
|
|
264
|
+
|| typeof adminRaw.created_at !== 'string' || typeof adminRaw.prior_fence_sha256 !== 'string'
|
|
265
|
+
|| (adminRaw.prior_journal_sha256 !== undefined && typeof adminRaw.prior_journal_sha256 !== 'string')
|
|
266
|
+
|| !['abandoned', 'released'].includes(String(terminalFence?.state)) || !priorFenceBytes || sha256(priorFenceBytes) !== adminRaw.prior_fence_sha256
|
|
267
|
+
|| (journalBytes ? sha256(journalBytes) : undefined) !== adminRaw.prior_journal_sha256)) {
|
|
268
|
+
return { activation, fence, journal, admin_event: null, denyProductWrites: false, corruption: 'admin_event_invalid' };
|
|
269
|
+
}
|
|
270
|
+
if (terminalFence?.state === 'closed' && !committedJournalMatchesFence(journal, terminalFence)) {
|
|
271
|
+
return { activation, fence, journal, denyProductWrites: false, corruption: 'closed_without_committed_journal' };
|
|
272
|
+
}
|
|
273
|
+
if (terminalFence?.state === 'closed' && (terminalFence.outcome !== 'approved' || terminalFence.integrity_status !== 'proven'
|
|
274
|
+
|| journal?.outcome !== 'approved' || journal.integrity_status !== 'proven'
|
|
275
|
+
|| !terminalFence.iteration_id || !terminalFence.plan_manifest_sha256 || !terminalFence.architect_review_sha256
|
|
276
|
+
|| !terminalFence.critic_review_sha256 || !terminalFence.evidence_bundle_sha256
|
|
277
|
+
|| journal.evidence_bundle_sha256 !== terminalFence.evidence_bundle_sha256)) {
|
|
278
|
+
return { activation, fence, journal, admin_event: adminEvent, denyProductWrites: false, corruption: 'approved_proven_binding_invalid' };
|
|
279
|
+
}
|
|
280
|
+
if (terminalFence?.state === 'abandoned'
|
|
281
|
+
&& !committedJournalMatchesFence(journal, terminalFence)
|
|
282
|
+
&& !adminEvent) {
|
|
283
|
+
return { activation, fence, journal, admin_event: null, denyProductWrites: false, corruption: 'abandoned_without_matching_journal_or_admin_event' };
|
|
284
|
+
}
|
|
285
|
+
return { activation, fence, journal, admin_event: adminEvent, denyProductWrites: false, corruption: null };
|
|
286
|
+
}
|
|
287
|
+
export async function readCurrentRalplanAdvisory(cwdInput, sessionId) {
|
|
288
|
+
const cwd = await canonicalCwd(cwdInput);
|
|
289
|
+
const root = advisoryRoot(cwd, sessionId);
|
|
290
|
+
let current;
|
|
291
|
+
try {
|
|
292
|
+
current = await readStrictJson(join(root, 'current.json'));
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
return { activation: null, fence: null, journal: null, denyProductWrites: false, corruption: 'current_corrupt' };
|
|
296
|
+
}
|
|
297
|
+
if (!current) {
|
|
298
|
+
try {
|
|
299
|
+
const rootStat = await stat(root);
|
|
300
|
+
if (rootStat.isDirectory())
|
|
301
|
+
return { activation: null, fence: null, journal: null, denyProductWrites: false, corruption: 'current_missing_with_advisory_state' };
|
|
302
|
+
return { activation: null, fence: null, journal: null, denyProductWrites: false, corruption: 'advisory_root_not_directory' };
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
if (error.code !== 'ENOENT') {
|
|
306
|
+
return { activation: null, fence: null, journal: null, denyProductWrites: false, corruption: 'current_state_stat_failed' };
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
if (current.schema_version !== 1 || current.session_id !== sessionId || current.canonical_cwd !== cwd
|
|
312
|
+
|| typeof current.generation_id !== 'string' || !safeId(current.generation_id)) {
|
|
313
|
+
return { activation: null, fence: null, journal: null, denyProductWrites: false, corruption: 'current_invalid' };
|
|
314
|
+
}
|
|
315
|
+
const projection = await readProjectionForGeneration(cwd, sessionId, current.generation_id);
|
|
316
|
+
const pendingRollover = await readStrictJson(join(root, 'rollover-intent.json')).catch(() => ({ invalid: true }));
|
|
317
|
+
if (pendingRollover) {
|
|
318
|
+
return { ...projection, denyProductWrites: false, corruption: 'rollover_pending_admin' };
|
|
319
|
+
}
|
|
320
|
+
if (projection.activation?.generation_id) {
|
|
321
|
+
let bound;
|
|
322
|
+
try {
|
|
323
|
+
bound = await readStrictJson(join(getBaseStateDir(cwd), 'sessions', sessionId, 'ralplan-state.json'));
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
return { ...projection, denyProductWrites: false, corruption: 'generation_mode_binding_corrupt' };
|
|
327
|
+
}
|
|
328
|
+
const canonicalBinding = Boolean(bound && bound.mode === 'ralplan' && bound.session_id === sessionId
|
|
329
|
+
&& bound.workflow_variant === 'advisory' && bound.advisory_generation_id === projection.activation.generation_id);
|
|
330
|
+
const hasAdvisoryMarkers = Boolean(bound && (bound.workflow_variant === 'advisory'
|
|
331
|
+
|| Object.prototype.hasOwnProperty.call(bound, 'advisory_generation_id')
|
|
332
|
+
|| Object.prototype.hasOwnProperty.call(bound, 'execution_handoff_authorized')
|
|
333
|
+
|| Object.prototype.hasOwnProperty.call(bound, 'host_verified')));
|
|
334
|
+
const genuineStandardRalplan = Boolean(bound && bound.mode === 'ralplan' && bound.session_id === sessionId && !hasAdvisoryMarkers);
|
|
335
|
+
if (projection.fence && ['closed', 'abandoned', 'recovery_required', 'released'].includes(projection.fence.state)) {
|
|
336
|
+
if (hasAdvisoryMarkers && !canonicalBinding) {
|
|
337
|
+
return { ...projection, denyProductWrites: true, corruption: 'generation_mode_binding_missing' };
|
|
338
|
+
}
|
|
339
|
+
if (bound?.workflow_variant === 'advisory' && bound.active === false) {
|
|
340
|
+
const inactiveError = validateAdvisoryInactiveState(bound, projection);
|
|
341
|
+
if (inactiveError)
|
|
342
|
+
return { ...projection, denyProductWrites: true, corruption: inactiveError };
|
|
343
|
+
}
|
|
344
|
+
if (genuineStandardRalplan || canonicalBinding)
|
|
345
|
+
return projection;
|
|
346
|
+
}
|
|
347
|
+
const publicationState = genuineStandardRalplan && bound?.active === true;
|
|
348
|
+
if (!projection.fence && !canonicalBinding && !publicationState) {
|
|
349
|
+
return { ...projection, denyProductWrites: false, corruption: 'inactive_without_closeout' };
|
|
350
|
+
}
|
|
351
|
+
if (projection.fence && !canonicalBinding) {
|
|
352
|
+
return { ...projection, denyProductWrites: false, corruption: 'generation_mode_binding_missing' };
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return projection;
|
|
356
|
+
}
|
|
357
|
+
function transitionAllowed(from, to) {
|
|
358
|
+
return (from === 'pending_closeout' && ['recovery_required', 'closed', 'abandoned'].includes(to))
|
|
359
|
+
|| (from === 'recovery_required' && to === 'abandoned')
|
|
360
|
+
|| (from === 'closed' && to === 'abandoned');
|
|
361
|
+
}
|
|
362
|
+
async function appendFenceEvent(cwd, fence, next) {
|
|
363
|
+
if (!transitionAllowed(fence.state, next.state))
|
|
364
|
+
throw new Error('ralplan_advisory_fence_transition_invalid');
|
|
365
|
+
const dir = generationDir(cwd, fence.session_id, fence.generation_id);
|
|
366
|
+
const event = {
|
|
367
|
+
...fence, ...next,
|
|
368
|
+
sequence: fence.sequence + 1,
|
|
369
|
+
previous_event_sha256: sha256(`${JSON.stringify(fence)}\n`),
|
|
370
|
+
};
|
|
371
|
+
await writeExclusive(join(dir, `fence-event-${String(event.sequence).padStart(4, '0')}.json`), event);
|
|
372
|
+
return event;
|
|
373
|
+
}
|
|
374
|
+
export async function prepareAdvisoryCloseout(input) {
|
|
375
|
+
const projection = await readProjectionForGeneration(input.cwd, input.sessionId, input.generationId);
|
|
376
|
+
if (projection.corruption)
|
|
377
|
+
throw new Error(`ralplan_advisory_${projection.corruption}`);
|
|
378
|
+
if (projection.fence) {
|
|
379
|
+
if (projection.fence.closing_turn_id !== input.closingTurnId)
|
|
380
|
+
throw new Error('ralplan_advisory_closeout_turn_mismatch');
|
|
381
|
+
if (projection.fence.iteration !== input.iteration
|
|
382
|
+
|| projection.fence.evidence_bundle_sha256 !== input.lifecycle?.evidence_bundle_sha256
|
|
383
|
+
|| projection.fence.iteration_id !== input.lifecycle?.iteration_id
|
|
384
|
+
|| projection.fence.plan_manifest_sha256 !== input.lifecycle?.plan_manifest_sha256
|
|
385
|
+
|| projection.fence.architect_review_sha256 !== input.lifecycle?.architect_review_sha256
|
|
386
|
+
|| projection.fence.critic_review_sha256 !== input.lifecycle?.critic_review_sha256) {
|
|
387
|
+
throw new Error('ralplan_advisory_closeout_binding_mismatch');
|
|
388
|
+
}
|
|
389
|
+
return projection.fence;
|
|
390
|
+
}
|
|
391
|
+
const fence = {
|
|
392
|
+
...projection.activation,
|
|
393
|
+
state: 'pending_closeout',
|
|
394
|
+
closing_turn_id: input.closingTurnId,
|
|
395
|
+
iteration: input.iteration,
|
|
396
|
+
...(input.lifecycle ? {
|
|
397
|
+
iteration_id: input.lifecycle.iteration_id,
|
|
398
|
+
plan_manifest_sha256: input.lifecycle.plan_manifest_sha256,
|
|
399
|
+
architect_review_sha256: input.lifecycle.architect_review_sha256,
|
|
400
|
+
critic_review_sha256: input.lifecycle.critic_review_sha256,
|
|
401
|
+
evidence_bundle_sha256: input.lifecycle.evidence_bundle_sha256,
|
|
402
|
+
} : {}),
|
|
403
|
+
updated_at: input.nowIso ?? new Date().toISOString(),
|
|
404
|
+
sequence: 0,
|
|
405
|
+
};
|
|
406
|
+
const fencePath = join(generationDir(projection.activation.canonical_cwd, input.sessionId, input.generationId), 'fence.json');
|
|
407
|
+
await input.beforeMutation?.('fence_create');
|
|
408
|
+
await writeExclusive(fencePath, fence);
|
|
409
|
+
await emitAdvisoryEvent(projection.activation.canonical_cwd, {
|
|
410
|
+
type: 'ralplan_advisory_fence_created', generationId: input.generationId, iteration: input.iteration,
|
|
411
|
+
transition: 'active->pending_closeout', checkpoint: 'fence_create', reason: 'closeout_started',
|
|
412
|
+
path: fencePath, digest: input.lifecycle?.evidence_bundle_sha256,
|
|
413
|
+
});
|
|
414
|
+
return fence;
|
|
415
|
+
}
|
|
416
|
+
function terminalState(outcome, integrity) {
|
|
417
|
+
if (integrity === 'unproven')
|
|
418
|
+
return 'recovery_required';
|
|
419
|
+
return outcome === 'approved' ? 'closed' : 'abandoned';
|
|
420
|
+
}
|
|
421
|
+
async function terminalizeRalplanAdvisoryUnlocked(options) {
|
|
422
|
+
const now = options.nowIso ?? new Date().toISOString();
|
|
423
|
+
const currentBinding = await readStrictJson(join(getBaseStateDir(await canonicalCwd(options.cwd)), 'sessions', options.sessionId, 'ralplan-state.json'));
|
|
424
|
+
if (!currentBinding || currentBinding.workflow_variant !== 'advisory'
|
|
425
|
+
|| currentBinding.advisory_generation_id !== options.generationId) {
|
|
426
|
+
throw new Error('ralplan_advisory_current_generation_changed');
|
|
427
|
+
}
|
|
428
|
+
if (options.outcome === 'approved' && options.integrityStatus === 'proven'
|
|
429
|
+
&& (!completeLifecycleBinding(options.lifecycle) || !options.revalidateEvidence)) {
|
|
430
|
+
throw new Error('ralplan_advisory_approved_proven_requires_complete_lifecycle_and_revalidation');
|
|
431
|
+
}
|
|
432
|
+
await options.failpoint?.('before-fence-create');
|
|
433
|
+
let fence = await prepareAdvisoryCloseout({ ...options, lifecycle: options.lifecycle, nowIso: now });
|
|
434
|
+
await options.failpoint?.('after-fence-create');
|
|
435
|
+
const dir = generationDir(fence.canonical_cwd, options.sessionId, options.generationId);
|
|
436
|
+
const journalPath = join(dir, 'closeout-journal.json');
|
|
437
|
+
let journalRaw = await readStrictJson(journalPath);
|
|
438
|
+
let journal;
|
|
439
|
+
if (journalRaw) {
|
|
440
|
+
if (!journalValid(journalRaw, options.generationId))
|
|
441
|
+
throw new Error('ralplan_advisory_journal_invalid');
|
|
442
|
+
journal = journalRaw;
|
|
443
|
+
if (journal.evidence_bundle_sha256 !== options.lifecycle?.evidence_bundle_sha256) {
|
|
444
|
+
throw new Error('ralplan_advisory_journal_binding_mismatch');
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
let terminalSkillUpdates;
|
|
449
|
+
if (options.terminalModeUpdates) {
|
|
450
|
+
const base = getBaseStateDir(fence.canonical_cwd);
|
|
451
|
+
const [currentMode, rootSkillState, sessionSkillState] = await Promise.all([
|
|
452
|
+
readStrictJson(join(base, 'sessions', options.sessionId, 'ralplan-state.json')),
|
|
453
|
+
readStrictJson(join(base, 'skill-active-state.json')),
|
|
454
|
+
readStrictJson(join(base, 'sessions', options.sessionId, 'skill-active-state.json')),
|
|
455
|
+
]);
|
|
456
|
+
const { projectRalplanTerminalSkillMirrors } = await import('../state/operations.js');
|
|
457
|
+
terminalSkillUpdates = projectRalplanTerminalSkillMirrors({
|
|
458
|
+
rootSkillState, sessionSkillState,
|
|
459
|
+
terminalState: { ...(currentMode ?? {}), ...options.terminalModeUpdates },
|
|
460
|
+
sessionId: options.sessionId, nowIso: now,
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
journal = createAdvisoryCloseoutJournal({
|
|
464
|
+
generationId: options.generationId,
|
|
465
|
+
outcome: options.outcome,
|
|
466
|
+
integrity: options.integrityStatus,
|
|
467
|
+
evidenceDigest: options.lifecycle?.evidence_bundle_sha256,
|
|
468
|
+
terminalModeUpdates: options.terminalModeUpdates,
|
|
469
|
+
terminalSkillUpdates,
|
|
470
|
+
now,
|
|
471
|
+
});
|
|
472
|
+
await options.beforeMutation?.('journal_prepare');
|
|
473
|
+
await writeAtomic(journalPath, journal);
|
|
474
|
+
}
|
|
475
|
+
await options.failpoint?.('journal-prepare');
|
|
476
|
+
const mark = async (step) => {
|
|
477
|
+
journal.steps[step] = 'applied';
|
|
478
|
+
journal.updated_at = new Date().toISOString();
|
|
479
|
+
await options.beforeMutation?.(`journal_step_${step}`);
|
|
480
|
+
await writeAtomic(journalPath, journal);
|
|
481
|
+
await emitAdvisoryEvent(fence.canonical_cwd, {
|
|
482
|
+
type: 'ralplan_advisory_closeout_step', generationId: fence.generation_id, iteration: fence.iteration,
|
|
483
|
+
transition: 'pending->applied', checkpoint: step, reason: 'closeout_checkpoint_applied',
|
|
484
|
+
path: journalPath, digest: journal.evidence_bundle_sha256,
|
|
485
|
+
});
|
|
486
|
+
await options.failpoint?.(step);
|
|
487
|
+
};
|
|
488
|
+
for (const step of ['session_mode', 'root_mode', 'session_skill', 'root_skill']) {
|
|
489
|
+
const expectedSkill = step === 'session_skill' || step === 'root_skill'
|
|
490
|
+
? journal.terminal_skill_updates?.[step]
|
|
491
|
+
: undefined;
|
|
492
|
+
const postconditionSatisfied = await closeoutStepPostcondition(fence.canonical_cwd, options.sessionId, step, journal.terminal_mode_updates, expectedSkill);
|
|
493
|
+
if ((step === 'session_skill' || step === 'root_skill') && postconditionSatisfied) {
|
|
494
|
+
if (journal.steps[step] !== 'applied')
|
|
495
|
+
await mark(step);
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
if (journal.steps[step] === 'applied' && postconditionSatisfied)
|
|
499
|
+
continue;
|
|
500
|
+
if ((step === 'session_skill' || step === 'root_skill') && expectedSkill !== undefined) {
|
|
501
|
+
await applyTerminalSkillReduction({
|
|
502
|
+
cwd: fence.canonical_cwd,
|
|
503
|
+
sessionId: options.sessionId,
|
|
504
|
+
terminalModeUpdates: journal.terminal_mode_updates ?? {},
|
|
505
|
+
terminalTimestamp: journal.terminal_timestamp,
|
|
506
|
+
beforeCommit: () => options.beforeMutation?.(`mirror_${step}`),
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
else {
|
|
510
|
+
await options.beforeMutation?.(`mirror_${step}`);
|
|
511
|
+
await options.applyStep?.(step, journal.terminal_mode_updates, journal.terminal_timestamp);
|
|
512
|
+
}
|
|
513
|
+
if (!await closeoutStepPostcondition(fence.canonical_cwd, options.sessionId, step, journal.terminal_mode_updates, expectedSkill)) {
|
|
514
|
+
throw new Error(`ralplan_advisory_${step}_postcondition_failed`);
|
|
515
|
+
}
|
|
516
|
+
await mark(step);
|
|
517
|
+
}
|
|
518
|
+
if (journal.steps.post_digest !== 'applied') {
|
|
519
|
+
const evidence = options.lifecycle && options.revalidateEvidence
|
|
520
|
+
? await revalidateAdvisoryEvidence(options.lifecycle.evidence_bundle_sha256, options.revalidateEvidence)
|
|
521
|
+
: null;
|
|
522
|
+
if (options.lifecycle && evidence?.kind !== 'matched') {
|
|
523
|
+
fence = await transitionAdvisoryJournalToRecoveryRequired({
|
|
524
|
+
fence,
|
|
525
|
+
journal,
|
|
526
|
+
journalPath,
|
|
527
|
+
evidence,
|
|
528
|
+
changedReason: 'evidence_digest_changed',
|
|
529
|
+
unreadableReason: 'evidence_unreadable',
|
|
530
|
+
terminalTimestamp: journal.terminal_timestamp,
|
|
531
|
+
appendFenceEvent: (next) => appendFenceEvent(fence.canonical_cwd, fence, next),
|
|
532
|
+
hooks: {
|
|
533
|
+
beforePrepare: () => options.beforeMutation?.('journal_recovery_prepare'),
|
|
534
|
+
afterPrepare: () => options.failpoint?.('recovery_wal'),
|
|
535
|
+
beforeFence: () => options.beforeMutation?.('fence_recovery_required'),
|
|
536
|
+
beforeCommit: () => options.beforeMutation?.('journal_recovery_commit'),
|
|
537
|
+
},
|
|
538
|
+
});
|
|
539
|
+
return readProjectionForGeneration(fence.canonical_cwd, options.sessionId, options.generationId);
|
|
540
|
+
}
|
|
541
|
+
await mark('post_digest');
|
|
542
|
+
}
|
|
543
|
+
if (journal.steps.journal_commit !== 'applied') {
|
|
544
|
+
journal.steps.journal_commit = 'applied';
|
|
545
|
+
journal.phase = 'committed';
|
|
546
|
+
journal.updated_at = new Date().toISOString();
|
|
547
|
+
await options.beforeMutation?.('journal_commit');
|
|
548
|
+
await writeAtomic(journalPath, journal);
|
|
549
|
+
await emitAdvisoryEvent(fence.canonical_cwd, {
|
|
550
|
+
type: 'ralplan_advisory_closeout_committed', generationId: fence.generation_id, iteration: fence.iteration,
|
|
551
|
+
transition: 'prepared->committed', checkpoint: 'journal_commit', reason: 'all_closeout_postconditions_satisfied',
|
|
552
|
+
path: journalPath, digest: journal.evidence_bundle_sha256,
|
|
553
|
+
});
|
|
554
|
+
await options.failpoint?.('journal_commit');
|
|
555
|
+
}
|
|
556
|
+
if (journal.steps.fence_terminal !== 'applied') {
|
|
557
|
+
const targetState = terminalState(journal.outcome, journal.integrity_status);
|
|
558
|
+
if (fence.state !== targetState) {
|
|
559
|
+
const priorState = fence.state;
|
|
560
|
+
await options.beforeMutation?.('fence_terminal');
|
|
561
|
+
fence = await appendFenceEvent(fence.canonical_cwd, fence, {
|
|
562
|
+
state: targetState, closing_turn_id: fence.closing_turn_id,
|
|
563
|
+
iteration: fence.iteration, outcome: journal.outcome, integrity_status: journal.integrity_status,
|
|
564
|
+
updated_at: journal.terminal_timestamp,
|
|
565
|
+
});
|
|
566
|
+
await emitAdvisoryEvent(fence.canonical_cwd, {
|
|
567
|
+
type: 'ralplan_advisory_fence_closed', generationId: fence.generation_id, iteration: fence.iteration,
|
|
568
|
+
transition: `${priorState}->${targetState}`, checkpoint: 'fence_terminal', reason: `closeout_${journal.outcome}`,
|
|
569
|
+
path: join(dir, `fence-event-${String(fence.sequence).padStart(4, '0')}.json`), digest: journal.evidence_bundle_sha256,
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
journal.steps.fence_terminal = 'applied';
|
|
573
|
+
journal.updated_at = new Date().toISOString();
|
|
574
|
+
await options.beforeMutation?.('journal_fence_terminal');
|
|
575
|
+
await writeAtomic(journalPath, journal);
|
|
576
|
+
await options.failpoint?.('fence_terminal');
|
|
577
|
+
}
|
|
578
|
+
return readProjectionForGeneration(fence.canonical_cwd, options.sessionId, options.generationId);
|
|
579
|
+
}
|
|
580
|
+
export async function terminalizeRalplanAdvisory(options) {
|
|
581
|
+
return withRalplanAdvisoryCurrentLock(options.cwd, options.sessionId, () => withGenerationLock(options.cwd, options.sessionId, options.generationId, () => terminalizeRalplanAdvisoryUnlocked(options)));
|
|
582
|
+
}
|
|
583
|
+
/** Replays the stored terminal mode patch after a crash, then completes the
|
|
584
|
+
* journal/fence tail. Every checkpoint is persisted before advancing. */
|
|
585
|
+
class AdvisoryLiveBindingConflictError extends Error {
|
|
586
|
+
diagnostic;
|
|
587
|
+
constructor(diagnostic) {
|
|
588
|
+
super(diagnostic);
|
|
589
|
+
this.diagnostic = diagnostic;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
async function liveAdvisoryBindingConflict(cwd, sessionId, generationId) {
|
|
593
|
+
let binding;
|
|
594
|
+
try {
|
|
595
|
+
binding = await readStrictJson(join(getBaseStateDir(cwd), 'sessions', sessionId, 'ralplan-state.json'));
|
|
596
|
+
}
|
|
597
|
+
catch {
|
|
598
|
+
return 'live_session_binding_unreadable';
|
|
599
|
+
}
|
|
600
|
+
return binding
|
|
601
|
+
&& (binding.workflow_variant !== 'advisory' || binding.advisory_generation_id !== generationId)
|
|
602
|
+
? 'live_session_binding_conflict'
|
|
603
|
+
: null;
|
|
604
|
+
}
|
|
605
|
+
async function reconcileRalplanAdvisoryUnlocked(cwd, sessionId, beforeReplayCheck) {
|
|
606
|
+
const projection = await readCurrentRalplanAdvisory(cwd, sessionId);
|
|
607
|
+
if (!projection || projection.corruption || !projection.fence || !projection.journal)
|
|
608
|
+
return projection;
|
|
609
|
+
const { fence, journal } = projection;
|
|
610
|
+
if (fence.state === 'recovery_required') {
|
|
611
|
+
if (journal.phase !== 'committed') {
|
|
612
|
+
await commitInterruptedAdvisoryRecovery(join(generationDir(cwd, sessionId, fence.generation_id), 'closeout-journal.json'), journal);
|
|
613
|
+
return readCurrentRalplanAdvisory(cwd, sessionId);
|
|
614
|
+
}
|
|
615
|
+
return projection;
|
|
616
|
+
}
|
|
617
|
+
const guardReplay = async (checkpoint) => {
|
|
618
|
+
await beforeReplayCheck?.(checkpoint);
|
|
619
|
+
const conflict = await liveAdvisoryBindingConflict(cwd, sessionId, fence.generation_id);
|
|
620
|
+
if (conflict)
|
|
621
|
+
throw new AdvisoryLiveBindingConflictError(conflict);
|
|
622
|
+
};
|
|
623
|
+
const immutableAdminRecovery = Boolean(projection.admin_event);
|
|
624
|
+
const journalPath = join(generationDir(fence.canonical_cwd, sessionId, fence.generation_id), 'closeout-journal.json');
|
|
625
|
+
let reconciled = false;
|
|
626
|
+
const emitReconciled = async (reason) => {
|
|
627
|
+
if (!reconciled)
|
|
628
|
+
return;
|
|
629
|
+
await guardReplay('emit_reconciled');
|
|
630
|
+
await emitAdvisoryEvent(fence.canonical_cwd, {
|
|
631
|
+
type: 'ralplan_advisory_closeout_reconciled', generationId: fence.generation_id, iteration: fence.iteration,
|
|
632
|
+
transition: `${fence.state}->${fence.state}`, checkpoint: 'reconcile', reason,
|
|
633
|
+
path: journalPath, digest: journal.evidence_bundle_sha256,
|
|
634
|
+
});
|
|
635
|
+
};
|
|
636
|
+
const mark = async (step) => {
|
|
637
|
+
journal.steps[step] = 'applied';
|
|
638
|
+
journal.updated_at = new Date().toISOString();
|
|
639
|
+
await guardReplay(`journal_step_${step}`);
|
|
640
|
+
await writeAtomic(journalPath, journal);
|
|
641
|
+
reconciled = true;
|
|
642
|
+
await guardReplay(`emit_step_${step}`);
|
|
643
|
+
await emitAdvisoryEvent(fence.canonical_cwd, {
|
|
644
|
+
type: 'ralplan_advisory_closeout_step', generationId: fence.generation_id, iteration: fence.iteration,
|
|
645
|
+
transition: 'pending->applied', checkpoint: step, reason: 'reconciled_checkpoint',
|
|
646
|
+
path: journalPath, digest: journal.evidence_bundle_sha256,
|
|
647
|
+
});
|
|
648
|
+
};
|
|
649
|
+
if (journal.terminal_mode_updates) {
|
|
650
|
+
const canonicalPatch = async () => {
|
|
651
|
+
const { updateModeState } = await import('../modes/base.js');
|
|
652
|
+
await updateModeState('ralplan', { ...journal.terminal_mode_updates, updated_at: journal.terminal_timestamp }, cwd, sessionId, (site) => guardReplay(`mode_commit_${site}`));
|
|
653
|
+
};
|
|
654
|
+
for (const step of ['session_mode', 'root_mode', 'session_skill', 'root_skill']) {
|
|
655
|
+
const expectedSkill = step === 'session_skill' || step === 'root_skill'
|
|
656
|
+
? journal.terminal_skill_updates?.[step]
|
|
657
|
+
: undefined;
|
|
658
|
+
if (!await closeoutStepPostcondition(cwd, sessionId, step, journal.terminal_mode_updates, expectedSkill)) {
|
|
659
|
+
reconciled = true;
|
|
660
|
+
if ((step === 'session_skill' || step === 'root_skill') && expectedSkill !== undefined) {
|
|
661
|
+
await applyTerminalSkillReduction({
|
|
662
|
+
cwd,
|
|
663
|
+
sessionId,
|
|
664
|
+
terminalModeUpdates: journal.terminal_mode_updates ?? {},
|
|
665
|
+
terminalTimestamp: journal.terminal_timestamp,
|
|
666
|
+
beforeCommit: () => guardReplay(`mirror_${step}`),
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
else {
|
|
670
|
+
await guardReplay(`mirror_${step}`);
|
|
671
|
+
await canonicalPatch();
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
if (!await closeoutStepPostcondition(cwd, sessionId, step, journal.terminal_mode_updates, expectedSkill)) {
|
|
675
|
+
throw new Error(`ralplan_advisory_reconcile_${step}_postcondition_failed`);
|
|
676
|
+
}
|
|
677
|
+
if (!immutableAdminRecovery && journal.steps[step] !== 'applied')
|
|
678
|
+
await mark(step);
|
|
679
|
+
}
|
|
680
|
+
if (immutableAdminRecovery) {
|
|
681
|
+
await emitReconciled('admin_recovery_mirrors_repaired');
|
|
682
|
+
return readCurrentRalplanAdvisory(cwd, sessionId);
|
|
683
|
+
}
|
|
684
|
+
if (journal.steps.post_digest !== 'applied') {
|
|
685
|
+
if (fence.evidence_bundle_sha256) {
|
|
686
|
+
const evidence = await revalidateStoredAdvisoryEvidence({ cwd, sessionId, fence });
|
|
687
|
+
if (evidence.kind !== 'matched') {
|
|
688
|
+
await transitionAdvisoryJournalToRecoveryRequired({
|
|
689
|
+
fence,
|
|
690
|
+
journal,
|
|
691
|
+
journalPath,
|
|
692
|
+
evidence,
|
|
693
|
+
changedReason: 'reconcile_evidence_digest_changed',
|
|
694
|
+
unreadableReason: 'reconcile_evidence_unreadable',
|
|
695
|
+
terminalTimestamp: () => new Date().toISOString(),
|
|
696
|
+
appendFenceEvent: (next) => appendFenceEvent(fence.canonical_cwd, fence, next),
|
|
697
|
+
hooks: {
|
|
698
|
+
beforePrepare: () => guardReplay('journal_recovery_prepare'),
|
|
699
|
+
beforeEmit: () => guardReplay('emit_digest_mismatch'),
|
|
700
|
+
beforeFence: () => guardReplay('fence_recovery_required'),
|
|
701
|
+
},
|
|
702
|
+
});
|
|
703
|
+
return readCurrentRalplanAdvisory(cwd, sessionId);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
await mark('post_digest');
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
if (immutableAdminRecovery) {
|
|
710
|
+
await emitReconciled('admin_recovery_preserved');
|
|
711
|
+
return readCurrentRalplanAdvisory(cwd, sessionId);
|
|
712
|
+
}
|
|
713
|
+
const prerequisiteSteps = ['session_mode', 'root_mode', 'session_skill', 'root_skill', 'post_digest'];
|
|
714
|
+
if (!prerequisiteSteps.every((step) => journal.steps[step] === 'applied')) {
|
|
715
|
+
await emitReconciled('partial_closeout_replay');
|
|
716
|
+
return readCurrentRalplanAdvisory(cwd, sessionId);
|
|
717
|
+
}
|
|
718
|
+
if (journal.phase !== 'committed') {
|
|
719
|
+
journal.phase = 'committed';
|
|
720
|
+
journal.steps.journal_commit = 'applied';
|
|
721
|
+
journal.updated_at = new Date().toISOString();
|
|
722
|
+
await guardReplay('journal_commit');
|
|
723
|
+
await writeAtomic(journalPath, journal);
|
|
724
|
+
reconciled = true;
|
|
725
|
+
await guardReplay('emit_journal_commit');
|
|
726
|
+
await emitAdvisoryEvent(fence.canonical_cwd, {
|
|
727
|
+
type: 'ralplan_advisory_closeout_committed', generationId: fence.generation_id, iteration: fence.iteration,
|
|
728
|
+
transition: 'prepared->committed', checkpoint: 'journal_commit', reason: 'reconciled_closeout_commit',
|
|
729
|
+
path: journalPath, digest: journal.evidence_bundle_sha256,
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
const target = terminalState(journal.outcome, journal.integrity_status);
|
|
733
|
+
if (fence.state !== target) {
|
|
734
|
+
await guardReplay('fence_terminal');
|
|
735
|
+
const terminalFence = await appendFenceEvent(fence.canonical_cwd, fence, {
|
|
736
|
+
state: target, closing_turn_id: fence.closing_turn_id, iteration: fence.iteration,
|
|
737
|
+
outcome: journal.outcome, integrity_status: journal.integrity_status, updated_at: journal.terminal_timestamp,
|
|
738
|
+
});
|
|
739
|
+
reconciled = true;
|
|
740
|
+
await guardReplay('emit_fence_terminal');
|
|
741
|
+
await emitAdvisoryEvent(fence.canonical_cwd, {
|
|
742
|
+
type: 'ralplan_advisory_fence_closed', generationId: fence.generation_id, iteration: fence.iteration,
|
|
743
|
+
transition: `${fence.state}->${target}`, checkpoint: 'fence_terminal', reason: 'reconciled_terminal_fence',
|
|
744
|
+
path: join(dirname(journalPath), `fence-event-${String(terminalFence.sequence).padStart(4, '0')}.json`), digest: journal.evidence_bundle_sha256,
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
if (journal.steps.fence_terminal !== 'applied') {
|
|
748
|
+
journal.steps.fence_terminal = 'applied';
|
|
749
|
+
journal.updated_at = new Date().toISOString();
|
|
750
|
+
await guardReplay('journal_fence_terminal');
|
|
751
|
+
await writeAtomic(journalPath, journal);
|
|
752
|
+
reconciled = true;
|
|
753
|
+
}
|
|
754
|
+
await emitReconciled('stored_closeout_replayed');
|
|
755
|
+
return readCurrentRalplanAdvisory(cwd, sessionId);
|
|
756
|
+
}
|
|
757
|
+
export async function reconcileRalplanAdvisory(cwd, sessionId, authority) {
|
|
758
|
+
const cwdCanonical = await canonicalCwd(cwd);
|
|
759
|
+
const projection = await readCurrentRalplanAdvisory(cwdCanonical, sessionId);
|
|
760
|
+
if (!projection?.activation?.generation_id)
|
|
761
|
+
return projection;
|
|
762
|
+
const initialConflict = await liveAdvisoryBindingConflict(cwdCanonical, sessionId, projection.activation.generation_id);
|
|
763
|
+
if (initialConflict)
|
|
764
|
+
return { ...projection, corruption: initialConflict };
|
|
765
|
+
if (projection.corruption)
|
|
766
|
+
return projection;
|
|
767
|
+
return withRalplanAdvisoryCurrentLock(cwdCanonical, sessionId, () => withGenerationLock(cwd, sessionId, projection.activation.generation_id, async () => {
|
|
768
|
+
const conflict = await liveAdvisoryBindingConflict(cwdCanonical, sessionId, projection.activation.generation_id);
|
|
769
|
+
if (conflict)
|
|
770
|
+
return { ...projection, corruption: conflict };
|
|
771
|
+
try {
|
|
772
|
+
return await reconcileRalplanAdvisoryUnlocked(cwd, sessionId, authority?.beforeReplayCheck);
|
|
773
|
+
}
|
|
774
|
+
catch (error) {
|
|
775
|
+
if (error instanceof AdvisoryLiveBindingConflictError) {
|
|
776
|
+
const current = await readCurrentRalplanAdvisory(cwdCanonical, sessionId) ?? projection;
|
|
777
|
+
return { ...current, corruption: error.diagnostic };
|
|
778
|
+
}
|
|
779
|
+
throw error;
|
|
780
|
+
}
|
|
781
|
+
}));
|
|
782
|
+
}
|
|
783
|
+
export async function observeRalplanAdvisoryPrompt(input) {
|
|
784
|
+
// Prompt observation is deliberately read-only. In particular, an explicit
|
|
785
|
+
// execution request must not reconcile state, append an event, or mint any
|
|
786
|
+
// durable authorization. Lifecycle mutations below are reserved for the
|
|
787
|
+
// explicit administrative abandon/replan/new-advisory intents.
|
|
788
|
+
let projection = await readCurrentRalplanAdvisory(input.cwd, input.sessionId);
|
|
789
|
+
if (!projection)
|
|
790
|
+
return { intent: 'unrelated', projection: null };
|
|
791
|
+
if (!projection.activation)
|
|
792
|
+
return { intent: 'unrelated', projection };
|
|
793
|
+
if (projection.activation?.generation_id) {
|
|
794
|
+
const bindingConflict = await liveAdvisoryBindingConflict(projection.activation.canonical_cwd, input.sessionId, projection.activation.generation_id);
|
|
795
|
+
if (bindingConflict) {
|
|
796
|
+
return { intent: 'unrelated', projection: { ...projection, corruption: bindingConflict } };
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
if (input.threadId !== projection.activation.root_thread_id) {
|
|
800
|
+
return { intent: 'unrelated', projection };
|
|
801
|
+
}
|
|
802
|
+
if (input.producer !== 'native' || input.isSubagentPromptSubmit
|
|
803
|
+
|| input.markedContinuation || input.synthetic || input.reservedInput || !safeId(input.turnId) || !safeId(input.threadId)
|
|
804
|
+
|| input.turnId === projection.fence?.closing_turn_id) {
|
|
805
|
+
return { intent: 'unrelated', projection };
|
|
806
|
+
}
|
|
807
|
+
const intent = classifyAdvisoryPrompt(input.prompt);
|
|
808
|
+
if (intent === 'execute') {
|
|
809
|
+
return { intent, projection };
|
|
810
|
+
}
|
|
811
|
+
if (intent === 'unrelated')
|
|
812
|
+
return { intent, projection };
|
|
813
|
+
// Administrative lifecycle intents may mutate durable state, so they require
|
|
814
|
+
// both the authenticated root classification and the exact current Advisory
|
|
815
|
+
// session binding. Historical Advisory evidence must stay inert after another
|
|
816
|
+
// workflow has replaced that binding.
|
|
817
|
+
if (input.producer !== 'native' || input.threadKind !== 'root-or-drift') {
|
|
818
|
+
return { intent: 'unrelated', projection };
|
|
819
|
+
}
|
|
820
|
+
if (projection.corruption || !projection.fence)
|
|
821
|
+
return { intent: 'unrelated', projection };
|
|
822
|
+
let binding;
|
|
823
|
+
try {
|
|
824
|
+
binding = await readStrictJson(join(getBaseStateDir(projection.activation.canonical_cwd), 'sessions', input.sessionId, 'ralplan-state.json'));
|
|
825
|
+
}
|
|
826
|
+
catch {
|
|
827
|
+
return { intent: 'unrelated', projection };
|
|
828
|
+
}
|
|
829
|
+
if (!binding || binding.mode !== 'ralplan' || binding.session_id !== input.sessionId
|
|
830
|
+
|| (binding.active !== true && binding.active !== false)
|
|
831
|
+
|| (binding.thread_id !== undefined && binding.thread_id !== projection.activation.root_thread_id)
|
|
832
|
+
|| binding.workflow_variant !== 'advisory'
|
|
833
|
+
|| binding.advisory_generation_id !== projection.activation.generation_id) {
|
|
834
|
+
return { intent: 'unrelated', projection };
|
|
835
|
+
}
|
|
836
|
+
projection = await reconcileRalplanAdvisory(input.cwd, input.sessionId, {
|
|
837
|
+
producer: input.producer,
|
|
838
|
+
threadKind: input.threadKind,
|
|
839
|
+
rootThreadId: input.threadId,
|
|
840
|
+
activationTurnId: input.turnId,
|
|
841
|
+
}) ?? projection;
|
|
842
|
+
if (projection.corruption || !projection.fence)
|
|
843
|
+
return { intent: 'unrelated', projection };
|
|
844
|
+
if (intent === 'abandon' && ['recovery_required', 'closed'].includes(projection.fence.state)) {
|
|
845
|
+
return {
|
|
846
|
+
intent,
|
|
847
|
+
projection: await administrativelyAbandonRalplanAdvisory({
|
|
848
|
+
cwd: input.cwd, sessionId: input.sessionId, generationId: projection.activation.generation_id,
|
|
849
|
+
rootThreadId: input.threadId, turnId: input.turnId,
|
|
850
|
+
}),
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
if (intent === 'replan' || intent === 'new_advisory') {
|
|
854
|
+
if (projection.fence.state === 'pending_closeout') {
|
|
855
|
+
return { intent: 'unrelated', projection };
|
|
856
|
+
}
|
|
857
|
+
if (projection.fence.state === 'recovery_required') {
|
|
858
|
+
await administrativelyAbandonRalplanAdvisory({
|
|
859
|
+
cwd: input.cwd, sessionId: input.sessionId, generationId: projection.activation.generation_id,
|
|
860
|
+
rootThreadId: input.threadId, turnId: input.turnId,
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
return { intent, projection };
|
|
864
|
+
}
|
|
865
|
+
return { intent, projection };
|
|
866
|
+
}
|
|
867
|
+
export async function administrativelyAbandonRalplanAdvisory(input) {
|
|
868
|
+
if (![input.sessionId, input.generationId, input.rootThreadId, input.turnId].every(safeId)) {
|
|
869
|
+
throw new Error('ralplan_advisory_admin_identity_missing');
|
|
870
|
+
}
|
|
871
|
+
return withGenerationLock(input.cwd, input.sessionId, input.generationId, async () => {
|
|
872
|
+
let projection = await readCurrentRalplanAdvisory(input.cwd, input.sessionId);
|
|
873
|
+
const recoverableMissingAdmin = projection?.corruption === 'abandoned_without_matching_journal_or_admin_event'
|
|
874
|
+
&& projection.fence?.state === 'abandoned';
|
|
875
|
+
if (!projection || (projection.corruption && !recoverableMissingAdmin) || !projection.fence
|
|
876
|
+
|| projection.activation.generation_id !== input.generationId
|
|
877
|
+
|| (!['pending_closeout', 'recovery_required', 'closed'].includes(projection.fence.state)
|
|
878
|
+
&& projection.fence.state !== 'abandoned')) {
|
|
879
|
+
throw new Error(`ralplan_advisory_admin_abandon_invalid:${projection?.corruption ?? projection?.fence?.state ?? 'missing'}`);
|
|
880
|
+
}
|
|
881
|
+
if (projection.fence.state === 'abandoned' && projection.admin_event)
|
|
882
|
+
return projection;
|
|
883
|
+
const now = input.nowIso ?? new Date().toISOString();
|
|
884
|
+
const dir = generationDir(projection.activation.canonical_cwd, input.sessionId, input.generationId);
|
|
885
|
+
const alreadyAppended = projection.fence.state === 'abandoned';
|
|
886
|
+
const priorSequence = alreadyAppended ? projection.fence.sequence - 1 : projection.fence.sequence;
|
|
887
|
+
const priorFencePath = priorSequence === 0
|
|
888
|
+
? join(dir, 'fence.json')
|
|
889
|
+
: join(dir, `fence-event-${String(priorSequence).padStart(4, '0')}.json`);
|
|
890
|
+
const priorFenceBytes = await readFile(priorFencePath);
|
|
891
|
+
const priorJournalBytes = projection.journal ? await readFile(join(dir, 'closeout-journal.json')) : null;
|
|
892
|
+
const adminEvent = {
|
|
893
|
+
schema_version: 1, action: 'abandon', generation_id: input.generationId, session_id: input.sessionId,
|
|
894
|
+
root_thread_id: input.rootThreadId, turn_id: input.turnId,
|
|
895
|
+
prior_fence_sha256: sha256(priorFenceBytes),
|
|
896
|
+
...(priorJournalBytes ? { prior_journal_sha256: sha256(priorJournalBytes) } : {}),
|
|
897
|
+
created_at: now,
|
|
898
|
+
};
|
|
899
|
+
if (!alreadyAppended) {
|
|
900
|
+
await input.beforeMutation?.('admin_fence_event');
|
|
901
|
+
await appendFenceEvent(projection.activation.canonical_cwd, projection.fence, {
|
|
902
|
+
state: 'abandoned', closing_turn_id: projection.fence.closing_turn_id, iteration: projection.fence.iteration,
|
|
903
|
+
outcome: 'abandoned', integrity_status: 'unproven', updated_at: now,
|
|
904
|
+
});
|
|
905
|
+
await input.failpoint?.('after_fence_event');
|
|
906
|
+
projection = await readProjectionForGeneration(projection.activation.canonical_cwd, input.sessionId, input.generationId);
|
|
907
|
+
}
|
|
908
|
+
await input.beforeMutation?.('admin_event');
|
|
909
|
+
await writeExclusive(join(dir, 'admin-event-0001.json'), adminEvent);
|
|
910
|
+
await input.failpoint?.('after_admin_event');
|
|
911
|
+
return readProjectionForGeneration(projection.activation.canonical_cwd, input.sessionId, input.generationId);
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
//# sourceMappingURL=advisory.js.map
|