@deksden-com/dd-flow-cli 0.9.0-beta.16 → 0.9.0-beta.161
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/CHANGELOG.md +965 -0
- package/README.md +139 -10
- package/dist/build-info.json +8 -8
- package/dist/cli/argument-values.js +38 -0
- package/dist/cli/command-inputs.js +417 -0
- package/dist/cli/command-metadata.js +303 -0
- package/dist/cli/command-option-descriptions.js +192 -0
- package/dist/cli/completion-cli.js +70 -0
- package/dist/cli/completion.js +57 -0
- package/dist/cli/delivery.js +108 -0
- package/dist/cli/diagnostic-invocation.js +62 -0
- package/dist/cli/diagnostic.js +131 -0
- package/dist/cli/error-output.js +62 -0
- package/dist/cli/executor-onboarding.js +76 -0
- package/dist/cli/flows.js +29 -0
- package/dist/cli/harnesses.js +84 -0
- package/dist/cli/help.js +521 -19
- package/dist/cli/hook-ingress.js +89 -0
- package/dist/cli/input-preparation.js +193 -0
- package/dist/cli/interactive-invocation.js +33 -0
- package/dist/cli/interactive.js +114 -0
- package/dist/cli/memory-fix-invocation.js +52 -0
- package/dist/cli/memory-fix.js +100 -0
- package/dist/cli/memory-flow-admission.js +54 -0
- package/dist/cli/memory-init-invocation.js +85 -0
- package/dist/cli/memory-init.js +250 -0
- package/dist/cli/memory-storage-admission.js +54 -0
- package/dist/cli/memory-storage-qualification.js +105 -0
- package/dist/cli/memory-storage-transition.js +123 -0
- package/dist/cli/memory-upgrade.js +189 -0
- package/dist/cli/models.js +39 -0
- package/dist/cli/native-hook-ingress.js +94 -0
- package/dist/cli/native-hook-preparation.js +82 -0
- package/dist/cli/operator-profiles.js +61 -0
- package/dist/cli/output-options.js +35 -0
- package/dist/cli/response-file.js +72 -0
- package/dist/cli/run-cli.js +2020 -555
- package/dist/cli/runtime-maintenance-diagnostics.js +46 -0
- package/dist/cli/runtime-maintenance-ingress.js +52 -0
- package/dist/cli/runtime-process.js +153 -0
- package/dist/cli/setup.js +150 -0
- package/dist/cli.js +103 -2
- package/dist/domain/delivery-flows.js +8 -0
- package/dist/domain/diagnostic-aspects.js +103 -0
- package/dist/domain/diagnostic-flows.js +8 -0
- package/dist/domain/distill-flow.js +20 -0
- package/dist/domain/interactive-flow.js +1 -0
- package/dist/domain/memory-fix-flow.js +2 -0
- package/dist/domain/review-fix-flow.js +2 -0
- package/dist/domain/source-snapshot-flows.js +4 -0
- package/dist/domain/stage-catalog.js +1 -1
- package/dist/domain/stage-handler-identities.js +28 -0
- package/dist/harness-runtime/DD-ZCODE.md +112 -0
- package/dist/harness-runtime/OPENCODE-LICENSE.txt +24 -0
- package/dist/harness-runtime/bin/dd-agy.mjs +71 -0
- package/dist/harness-runtime/bin/dd-codex.mjs +27 -0
- package/dist/harness-runtime/bin/dd-droid.mjs +34 -0
- package/dist/harness-runtime/bin/dd-grok.mjs +50 -0
- package/dist/harness-runtime/bin/dd-opencode.mjs +22 -0
- package/dist/harness-runtime/bin/dd-zcode.mjs +95 -0
- package/dist/harness-runtime/lib/adapter-timeouts.d.mts +5 -0
- package/dist/harness-runtime/lib/adapter-timeouts.mjs +34 -0
- package/dist/harness-runtime/lib/agy-error-boundary.mjs +87 -0
- package/dist/harness-runtime/lib/agy-hooks.d.mts +1 -0
- package/dist/harness-runtime/lib/agy-hooks.mjs +9 -0
- package/dist/harness-runtime/lib/agy-native-inspection.mjs +225 -0
- package/dist/harness-runtime/lib/agy-native-inventory.mjs +19 -0
- package/dist/harness-runtime/lib/agy-native_pb.js +117 -0
- package/dist/harness-runtime/lib/agy-terminal-envelope.mjs +74 -0
- package/dist/harness-runtime/lib/agy-transcript-stream.mjs +54 -0
- package/dist/harness-runtime/lib/daemon-observation.d.mts +3 -0
- package/dist/harness-runtime/lib/daemon-observation.mjs +171 -0
- package/dist/harness-runtime/lib/daemon-operations.d.mts +2 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +512 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +7 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +1362 -0
- package/dist/harness-runtime/lib/dd-agy.mjs +105 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +271 -0
- package/dist/harness-runtime/lib/dd-codex.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-codex.mjs +986 -0
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +155 -0
- package/dist/harness-runtime/lib/dd-droid.mjs +739 -0
- package/dist/harness-runtime/lib/dd-grok-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +667 -0
- package/dist/harness-runtime/lib/dd-grok.mjs +182 -0
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +772 -0
- package/dist/harness-runtime/lib/dd-opencode.mjs +411 -0
- package/dist/harness-runtime/lib/dd-zcode-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +845 -0
- package/dist/harness-runtime/lib/dd-zcode.mjs +1239 -0
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +16 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +179 -0
- package/dist/harness-runtime/lib/delivery-command.mjs +84 -0
- package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
- package/dist/harness-runtime/lib/driver-recovery.mjs +121 -0
- package/dist/harness-runtime/lib/droid-observation.mjs +70 -0
- package/dist/harness-runtime/lib/entity-references.d.mts +2 -0
- package/dist/harness-runtime/lib/entity-references.mjs +9 -0
- package/dist/harness-runtime/lib/lease-renewal.d.mts +8 -0
- package/dist/harness-runtime/lib/lease-renewal.mjs +37 -0
- package/dist/harness-runtime/lib/lifecycle-command.d.mts +20 -0
- package/dist/harness-runtime/lib/lifecycle-command.mjs +273 -0
- package/dist/harness-runtime/lib/lifecycle-contract.d.mts +11 -0
- package/dist/harness-runtime/lib/lifecycle-contract.mjs +20 -0
- package/dist/harness-runtime/lib/managed-daemon.d.mts +16 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +698 -0
- package/dist/harness-runtime/lib/managed-flow-env.mjs +15 -0
- package/dist/harness-runtime/lib/model-observations.mjs +113 -0
- package/dist/harness-runtime/lib/native-children.d.mts +34 -0
- package/dist/harness-runtime/lib/native-children.mjs +265 -0
- package/dist/harness-runtime/lib/native-hook-command.d.mts +8 -0
- package/dist/harness-runtime/lib/native-hook-command.mjs +306 -0
- package/dist/harness-runtime/lib/native-request-proof.d.mts +2 -0
- package/dist/harness-runtime/lib/native-request-proof.mjs +65 -0
- package/dist/harness-runtime/lib/native-streams.mjs +38 -0
- package/dist/harness-runtime/lib/observation-clock.d.mts +18 -0
- package/dist/harness-runtime/lib/observation-clock.mjs +95 -0
- package/dist/harness-runtime/lib/opencode-plugin.mjs +112 -0
- package/dist/harness-runtime/lib/operation-context.mjs +5 -0
- package/dist/harness-runtime/lib/operation-errors.d.mts +16 -0
- package/dist/harness-runtime/lib/operation-errors.mjs +140 -0
- package/dist/harness-runtime/lib/operation-progress.d.mts +7 -0
- package/dist/harness-runtime/lib/operation-progress.mjs +118 -0
- package/dist/harness-runtime/lib/process-json.d.mts +5 -0
- package/dist/harness-runtime/lib/process-json.mjs +200 -0
- package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
- package/dist/harness-runtime/lib/provider-overload-policy.d.mts +23 -0
- package/dist/harness-runtime/lib/provider-overload-policy.mjs +242 -0
- package/dist/harness-runtime/lib/runner-events.mjs +202 -0
- package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
- package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
- package/dist/harness-runtime/lib/setup-progress.d.mts +9 -0
- package/dist/harness-runtime/lib/setup-progress.mjs +53 -0
- package/dist/harness-runtime/lib/tool-observations.d.mts +21 -0
- package/dist/harness-runtime/lib/tool-observations.mjs +238 -0
- package/dist/harness-runtime/lib/zcode-native-hook-ownership.mjs +109 -0
- package/dist/harness-runtime/lib/zcode-usage.mjs +55 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-1y4a697q.js +81 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-2ny4vxct.js +92 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-6mptkzbc.js +0 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-7x67cf1q.js +52 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-9rqn6x4v.js +16 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-9vdz0zwn.js +1661 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-ecs5ftka.js +1466 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-frbwqjmf.js +145 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-gne7h7mq.js +50 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-gx9wnkzr.js +105 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-m2eywc8w.js +0 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-nwcpakzx.js +12 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-q4g3afnw.js +623 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-qav6mfdz.js +0 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-sxt88q69.js +12 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-t1xa31vm.js +39 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-ve4aws36.js +103 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-vmytr4q8.js +4 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-vt9jbch8.js +36 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-vyas3vgt.js +0 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-wjyeq25y.js +30 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-xgnj2w4v.js +20 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-y2etg5bs.js +9 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-ydhz1zrh.js +238 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-z6qdwcsf.js +62 -0
- package/dist/harness-runtime/vendor/opencode-client/contract.d.ts +1 -0
- package/dist/harness-runtime/vendor/opencode-client/contract.js +12 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/api/api.d.ts +2806 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/api/api.js +0 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/api.d.ts +4 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/api.js +0 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/client.d.ts +2530 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/client.js +15 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/generated/client-error.d.ts +7 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/generated/client-error.js +6 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/generated/client.d.ts +2529 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/generated/client.js +8 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/generated/index.d.ts +2 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/generated/index.js +12 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/index.d.ts +35 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/index.js +75 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/rpc.d.ts +20 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/rpc.js +11 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/service.d.ts +90 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/service.js +187 -0
- package/dist/harness-runtime/vendor/opencode-client/package.json +84 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/api.d.ts +21 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/api.js +0 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/client.d.ts +252 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/client.js +14 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/generated/client-error.d.ts +8 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/generated/client-error.js +6 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/generated/client.d.ts +259 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/generated/client.js +7 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/generated/index.d.ts +3 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/generated/index.js +79 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/generated/types.d.ts +8932 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/generated/types.js +70 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/index.d.ts +6 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/index.js +83 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/rpc.d.ts +32 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/rpc.js +7 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/service.d.ts +24 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/service.js +187 -0
- package/dist/harness-runtime/vendor/opencode-client/pty-handoff.d.ts +12 -0
- package/dist/harness-runtime/vendor/opencode-client/pty-handoff.js +14 -0
- package/dist/harness-runtime/vendor/opencode-client/rpc-runtime.d.ts +20 -0
- package/dist/harness-runtime/vendor/opencode-client/rpc-runtime.js +14 -0
- package/dist/harness-runtime/vendor/opencode-client/service-contender.d.ts +37 -0
- package/dist/harness-runtime/vendor/opencode-client/service-contender.js +10 -0
- package/dist/harness-runtime/vendor/opencode-client/service-probe.d.ts +70 -0
- package/dist/harness-runtime/vendor/opencode-client/service-probe.js +16 -0
- package/dist/harness-runtime/vendor/opencode-client/service-timing.d.ts +12 -0
- package/dist/harness-runtime/vendor/opencode-client/service-timing.js +10 -0
- package/dist/harness-runtime/vendor/opencode-client/service-version.d.ts +2 -0
- package/dist/harness-runtime/vendor/opencode-client/service-version.js +6 -0
- package/dist/harness-runtime/vendor/opencode-client/service.d.ts +52 -0
- package/dist/harness-runtime/vendor/opencode-client/service.js +1 -0
- package/dist/harness-runtime/vendor/opencode-client/shared-events.d.ts +11 -0
- package/dist/harness-runtime/vendor/opencode-client/shared-events.js +8 -0
- package/dist/harness-runtime/vendor/opencode-client/solid/connection.d.ts +37 -0
- package/dist/harness-runtime/vendor/opencode-client/solid/connection.js +10 -0
- package/dist/harness-runtime/vendor/opencode-client/solid/data.d.ts +211 -0
- package/dist/harness-runtime/vendor/opencode-client/solid/data.js +18 -0
- package/dist/harness-runtime/vendor/opencode-client/solid/index.d.ts +3 -0
- package/dist/harness-runtime/vendor/opencode-client/solid/index.js +32 -0
- package/dist/harness-runtime/vendor/opencode-client/solid/pty.d.ts +22 -0
- package/dist/harness-runtime/vendor/opencode-client/solid/pty.js +8 -0
- package/dist/harness-runtime/vendor/opencode-plugin.mjs +3 -0
- package/dist/protocol/local-files.js +18 -9
- package/dist/runtime/context.js +8 -4
- package/dist/schemas/agent-profile.schema.json +1 -1
- package/dist/schemas/code-review-decision.schema.json +9 -0
- package/dist/schemas/code-review-result.schema.json +1 -1
- package/dist/schemas/code-verification.schema.json +7 -5
- package/dist/schemas/code-work-batch.schema.json +2 -2
- package/dist/schemas/code-work-result.schema.json +2 -1
- package/dist/schemas/delivery-stage-result.schema.json +286 -0
- package/dist/schemas/diagnostic-stage-result.schema.json +354 -0
- package/dist/schemas/diagnostic-work-result.schema.json +296 -0
- package/dist/schemas/discovery-result.schema.json +184 -0
- package/dist/schemas/distill-stage-result.schema.json +667 -0
- package/dist/schemas/distill-work-result.schema.json +428 -0
- package/dist/schemas/execution-bundle.schema.json +215 -0
- package/dist/schemas/execution-settings.schema.json +162 -0
- package/dist/schemas/flow-descriptor.schema.json +54 -0
- package/dist/schemas/flow-run.schema.json +29 -0
- package/dist/schemas/harness-catalog.schema.json +80 -0
- package/dist/schemas/interactive-stage-result.schema.json +47 -0
- package/dist/schemas/interactive-work-result.schema.json +63 -0
- package/dist/schemas/mb-fix-stage-result.schema.json +93 -0
- package/dist/schemas/mb-init-compose-result.schema.json +457 -0
- package/dist/schemas/mb-init-discover-result.schema.json +500 -0
- package/dist/schemas/mb-init-finish-result.schema.json +422 -0
- package/dist/schemas/mb-init-preflight-result.schema.json +367 -0
- package/dist/schemas/mb-init-review-result.schema.json +396 -0
- package/dist/schemas/mb-init-verify-result.schema.json +391 -0
- package/dist/schemas/mb-upgrade-stage-result.schema.json +863 -0
- package/dist/schemas/memory-bootstrap-receipt-v2.schema.json +67 -0
- package/dist/schemas/memory-bootstrap-receipt.schema.json +354 -0
- package/dist/schemas/memory-composition-work-result.schema.json +38 -0
- package/dist/schemas/memory-repair-work-result.schema.json +56 -0
- package/dist/schemas/memory-review-correction-plan.schema.json +30 -0
- package/dist/schemas/memory-review-work-result.schema.json +43 -0
- package/dist/schemas/model-catalog.schema.json +153 -0
- package/dist/schemas/plan-review-decision.schema.json +12 -3
- package/dist/schemas/plan-review-result.schema.json +2 -2
- package/dist/schemas/profile-settings.schema.json +122 -0
- package/dist/schemas/project-flow-pack-manifest.schema.json +2 -2
- package/dist/schemas/release-impact.schema.json +3 -3
- package/dist/schemas/release-manifest.schema.json +291 -0
- package/dist/schemas/review-fix-stage-result.schema.json +210 -0
- package/dist/schemas/run-control-receipt.schema.json +119 -0
- package/dist/schemas/run-control-request.schema.json +36 -0
- package/dist/schemas/run-engine-binding.schema.json +1 -1
- package/dist/schemas/stage-start-response.schema.json +4 -0
- package/dist/schemas/vnext-protocol-plan.schema.json +3 -3
- package/dist/services/accepted-intent.js +139 -0
- package/dist/services/assigned-lifecycle.js +382 -0
- package/dist/services/branch-context.js +3 -3
- package/dist/services/canon.js +54 -15
- package/dist/services/check-environment.js +21 -0
- package/dist/services/check-runner-evidence.js +344 -0
- package/dist/services/check-runner-proof.js +373 -0
- package/dist/services/cleanup.js +133 -40
- package/dist/services/cli-operation-classifier.js +32 -64
- package/dist/services/cli-upgrade-allowlist.js +27 -0
- package/dist/services/code-checks.js +1426 -121
- package/dist/services/codex-capacity.js +367 -0
- package/dist/services/codex-hook-delivery.js +41 -0
- package/dist/services/command-context.js +80 -0
- package/dist/services/compatibility-preflight.js +16 -3
- package/dist/services/config.js +11 -8
- package/dist/services/continuation-outcome.js +25 -0
- package/dist/services/controller-child-capacity.js +71 -0
- package/dist/services/controller-child-observer.js +121 -0
- package/dist/services/controller-child-repair.js +117 -0
- package/dist/services/controller-execution.js +97 -0
- package/dist/services/controller-fanout.js +277 -0
- package/dist/services/controller-memory-task.js +45 -0
- package/dist/services/controller-memory-worker.js +51 -0
- package/dist/services/controller-result-authority.js +127 -0
- package/dist/services/controller-work-failure.js +198 -0
- package/dist/services/dashboard.js +145 -58
- package/dist/services/delivery-access.js +123 -0
- package/dist/services/delivery-approval.js +68 -0
- package/dist/services/delivery-artifact.js +86 -0
- package/dist/services/delivery-checks.js +178 -0
- package/dist/services/delivery-execution.js +181 -0
- package/dist/services/delivery-intents.js +212 -0
- package/dist/services/delivery-operation.js +81 -0
- package/dist/services/delivery-policy.js +91 -0
- package/dist/services/delivery-procedures.js +43 -0
- package/dist/services/delivery-request.js +99 -0
- package/dist/services/delivery-result.js +144 -0
- package/dist/services/delivery-stage-commands.js +26 -0
- package/dist/services/delivery-stage-entry.js +56 -0
- package/dist/services/delivery-stage-handlers.js +135 -0
- package/dist/services/delivery-stage-results.js +94 -0
- package/dist/services/descriptor-coordinator-session.js +37 -0
- package/dist/services/descriptor-root-work.js +53 -0
- package/dist/services/descriptor-run-settlement.js +107 -0
- package/dist/services/descriptor-run-start.js +495 -0
- package/dist/services/descriptor-stage-coordinator.js +318 -0
- package/dist/services/descriptor-stage-settlement.js +268 -0
- package/dist/services/descriptor-stage-start.js +175 -0
- package/dist/services/diagnostic-evidence.js +73 -0
- package/dist/services/diagnostic-repair-intake.js +123 -0
- package/dist/services/diagnostic-repair-source-intake.js +102 -0
- package/dist/services/diagnostic-report-publication.js +72 -0
- package/dist/services/diagnostic-report.js +97 -0
- package/dist/services/diagnostic-run-bundle.js +113 -0
- package/dist/services/diagnostic-snapshot.js +159 -0
- package/dist/services/diagnostic-stage-handlers.js +109 -0
- package/dist/services/diagnostic-stage-results.js +158 -0
- package/dist/services/diagnostic-start.js +134 -0
- package/dist/services/diagnostic-work-batch.js +34 -0
- package/dist/services/diagnostic-work-collection.js +97 -0
- package/dist/services/diagnostic-work-registration.js +101 -0
- package/dist/services/diagnostic-work-results.js +93 -0
- package/dist/services/discovery/agy-discovery.js +2 -0
- package/dist/services/discovery/codex-discovery.js +2 -0
- package/dist/services/discovery/droid-discovery.js +2 -0
- package/dist/services/discovery/grok-discovery.js +2 -0
- package/dist/services/discovery/opencode-discovery.js +2 -0
- package/dist/services/discovery/static.js +31 -0
- package/dist/services/discovery/zcode-discovery.js +2 -0
- package/dist/services/distill-document-publication.js +280 -0
- package/dist/services/distill-promotion.js +197 -0
- package/dist/services/distill-publication-custody.js +162 -0
- package/dist/services/distill-stage-handlers.js +249 -0
- package/dist/services/distill-stage-results.js +105 -0
- package/dist/services/distill-user-inputs.js +4 -0
- package/dist/services/distill-work-batch.js +42 -0
- package/dist/services/distill-work-collection.js +74 -0
- package/dist/services/distill-work-registration.js +93 -0
- package/dist/services/distill-work-results.js +98 -0
- package/dist/services/engines.js +354 -163
- package/dist/services/eval-snapshots.js +1326 -150
- package/dist/services/execution-bundles.js +275 -0
- package/dist/services/execution-policy.js +185 -0
- package/dist/services/executor-onboarding.js +56 -0
- package/dist/services/external-work-launch.js +316 -0
- package/dist/services/flow-catalog.js +54 -0
- package/dist/services/flow-descriptors.js +82 -0
- package/dist/services/frozen-engine.js +404 -0
- package/dist/services/generic-execution.js +165 -0
- package/dist/services/harness-adapter.js +342 -35
- package/dist/services/harness-config.js +2 -0
- package/dist/services/harness-discovery.js +29 -0
- package/dist/services/harness-doctor.js +75 -0
- package/dist/services/harness-probe-effects.js +51 -0
- package/dist/services/harness-version-probe.js +100 -0
- package/dist/services/hook-memory-routing.js +188 -0
- package/dist/services/hook-recovery-guard.js +14 -0
- package/dist/services/hook-session-observation.js +24 -0
- package/dist/services/hooks.js +813 -327
- package/dist/services/hybrid-contracts.js +479 -0
- package/dist/services/hybrid-digest.js +14 -0
- package/dist/services/ids.js +29 -1
- package/dist/services/installation-lock.js +95 -0
- package/dist/services/installed-flow-source.js +261 -0
- package/dist/services/interactive-attached-session.js +215 -0
- package/dist/services/interactive-baseline-custody.js +54 -0
- package/dist/services/interactive-before-versions.js +148 -0
- package/dist/services/interactive-bundle-data.js +85 -0
- package/dist/services/interactive-checks.js +179 -0
- package/dist/services/interactive-execution.js +33 -0
- package/dist/services/interactive-finish-integration.js +236 -0
- package/dist/services/interactive-git-baseline.js +230 -0
- package/dist/services/interactive-git-dispatch.js +278 -0
- package/dist/services/interactive-input-delivery.js +82 -0
- package/dist/services/interactive-mutation-scope.js +231 -0
- package/dist/services/interactive-request.js +287 -0
- package/dist/services/interactive-run-bundle.js +109 -0
- package/dist/services/interactive-stage-entry.js +41 -0
- package/dist/services/interactive-stage-handlers.js +289 -0
- package/dist/services/interactive-stage-observation.js +136 -0
- package/dist/services/interactive-stage-results.js +111 -0
- package/dist/services/interactive-user-inputs.js +142 -0
- package/dist/services/interactive-work-batch.js +43 -0
- package/dist/services/interactive-work-collection.js +119 -0
- package/dist/services/interactive-work-completion.js +180 -0
- package/dist/services/interactive-work-context.js +249 -0
- package/dist/services/interactive-work-history.js +112 -0
- package/dist/services/interactive-work-registration.js +124 -0
- package/dist/services/interactive-work-results.js +115 -0
- package/dist/services/interactive-work-start.js +155 -0
- package/dist/services/lanes.js +78 -65
- package/dist/services/lifecycle-command.js +1 -288
- package/dist/services/lifecycle-contract.js +1 -0
- package/dist/services/lifecycle-invocations.js +2549 -0
- package/dist/services/managed-daemon-binding.js +129 -0
- package/dist/services/managed-lease-monitor.js +133 -0
- package/dist/services/managed-processes.js +449 -69
- package/dist/services/memory-accepted-composition.js +78 -0
- package/dist/services/memory-baseline-preservation.js +65 -0
- package/dist/services/memory-completed-correction-wave.js +328 -0
- package/dist/services/memory-compose-handler.js +380 -0
- package/dist/services/memory-composition-acceptance.js +375 -0
- package/dist/services/memory-composition-graph.js +165 -0
- package/dist/services/memory-composition-registration.js +161 -0
- package/dist/services/memory-composition-work-custody.js +156 -0
- package/dist/services/memory-composition-work-results.js +108 -0
- package/dist/services/memory-corrected-review-acceptance.js +129 -0
- package/dist/services/memory-corrected-review-batch.js +37 -0
- package/dist/services/memory-corrected-review-findings.js +108 -0
- package/dist/services/memory-corrected-review-inputs.js +215 -0
- package/dist/services/memory-corrected-review-projection.js +78 -0
- package/dist/services/memory-corrected-review-receipt.js +178 -0
- package/dist/services/memory-corrected-review-registration.js +192 -0
- package/dist/services/memory-correction-check-receipt.js +235 -0
- package/dist/services/memory-correction-input-owner.js +6 -0
- package/dist/services/memory-custody-snapshot.js +36 -0
- package/dist/services/memory-discover-handler.js +104 -0
- package/dist/services/memory-finish-evidence.js +457 -0
- package/dist/services/memory-finish-handler.js +229 -0
- package/dist/services/memory-finish-integration.js +491 -0
- package/dist/services/memory-finish-readiness.js +169 -0
- package/dist/services/memory-fix-finish-integration.js +129 -0
- package/dist/services/memory-fix-git-dispatch.js +139 -0
- package/dist/services/memory-fix-intake-custody.js +11 -0
- package/dist/services/memory-fix-intake-settlement.js +127 -0
- package/dist/services/memory-fix-publication.js +196 -0
- package/dist/services/memory-fix-repair-admission.js +116 -0
- package/dist/services/memory-fix-request.js +141 -0
- package/dist/services/memory-fix-run-bundle.js +83 -0
- package/dist/services/memory-fix-scope.js +78 -0
- package/dist/services/memory-fix-sources.js +85 -0
- package/dist/services/memory-fix-stage-entry.js +24 -0
- package/dist/services/memory-fix-stage-handlers.js +282 -0
- package/dist/services/memory-fix-stage-results.js +106 -0
- package/dist/services/memory-fix-workspace.js +127 -0
- package/dist/services/memory-flow-user-inputs.js +88 -0
- package/dist/services/memory-git-integration.js +220 -0
- package/dist/services/memory-historical-corrected-review.js +212 -0
- package/dist/services/memory-historical-original-review.js +143 -0
- package/dist/services/memory-historical-repair-registration.js +105 -0
- package/dist/services/memory-historical-second-review.js +147 -0
- package/dist/services/memory-init-bootstrap-readiness.js +210 -0
- package/dist/services/memory-init-contexts.js +181 -0
- package/dist/services/memory-init-controller-start.js +119 -0
- package/dist/services/memory-init-engine-release.js +223 -0
- package/dist/services/memory-init-git-policy.js +165 -0
- package/dist/services/memory-init-lint-capability.js +203 -0
- package/dist/services/memory-init-native-capacity.js +309 -0
- package/dist/services/memory-init-preflight.js +334 -0
- package/dist/services/memory-init-project-bootstrap.js +599 -0
- package/dist/services/memory-init-request-candidate.js +213 -0
- package/dist/services/memory-init-request-input.js +82 -0
- package/dist/services/memory-init-resource-storage.js +125 -0
- package/dist/services/memory-init-run-allocation.js +81 -0
- package/dist/services/memory-init-run-bundle.js +135 -0
- package/dist/services/memory-init-start.js +181 -0
- package/dist/services/memory-init-startup-custody.js +80 -0
- package/dist/services/memory-init-startup-input.js +240 -0
- package/dist/services/memory-init-storage.js +91 -0
- package/dist/services/memory-init-stores.js +359 -0
- package/dist/services/memory-init-worktrunk-capability.js +174 -0
- package/dist/services/memory-layer-publication.js +611 -0
- package/dist/services/memory-layer.js +225 -0
- package/dist/services/memory-lint-config.js +194 -0
- package/dist/services/memory-native-review-copy.js +127 -0
- package/dist/services/memory-normal-flow-entry.js +87 -0
- package/dist/services/memory-ordinary-defaults.js +129 -0
- package/dist/services/memory-preflight-bootstrap-custody.js +115 -0
- package/dist/services/memory-preflight-handler.js +274 -0
- package/dist/services/memory-repair-current-view.js +299 -0
- package/dist/services/memory-repair-document-delta.js +489 -0
- package/dist/services/memory-repair-history.js +496 -0
- package/dist/services/memory-repair-physical-binding.js +194 -0
- package/dist/services/memory-repair-publication-files.js +152 -0
- package/dist/services/memory-repair-publication-intent.js +27 -0
- package/dist/services/memory-repair-publication-plan.js +162 -0
- package/dist/services/memory-repair-publication-source.js +225 -0
- package/dist/services/memory-repair-publication.js +795 -0
- package/dist/services/memory-repair-registration.js +240 -0
- package/dist/services/memory-repair-work-baseline.js +415 -0
- package/dist/services/memory-repair-work-completion.js +117 -0
- package/dist/services/memory-repair-work-custody.js +73 -0
- package/dist/services/memory-repair-work-results.js +108 -0
- package/dist/services/memory-repair-workspace.js +261 -0
- package/dist/services/memory-repair-writer-copy.js +736 -0
- package/dist/services/memory-result-binding.js +239 -0
- package/dist/services/memory-retained-review-authority.js +138 -0
- package/dist/services/memory-retained-review-results.js +70 -0
- package/dist/services/memory-retained-second-baseline.js +102 -0
- package/dist/services/memory-retained-second-repairs.js +162 -0
- package/dist/services/memory-review-acceptance.js +214 -0
- package/dist/services/memory-review-correction-plan.js +206 -0
- package/dist/services/memory-review-handler.js +585 -0
- package/dist/services/memory-review-inputs.js +246 -0
- package/dist/services/memory-review-limits.js +10 -0
- package/dist/services/memory-review-receipt.js +180 -0
- package/dist/services/memory-review-registration.js +117 -0
- package/dist/services/memory-review-work-custody.js +126 -0
- package/dist/services/memory-review-work-input.js +91 -0
- package/dist/services/memory-review-work-results.js +95 -0
- package/dist/services/memory-second-corrected-review-inputs.js +167 -0
- package/dist/services/memory-second-correction-plan.js +77 -0
- package/dist/services/memory-second-repair-baseline.js +91 -0
- package/dist/services/memory-second-repair-launch.js +614 -0
- package/dist/services/memory-second-repair-registration.js +88 -0
- package/dist/services/memory-second-repair-source.js +187 -0
- package/dist/services/memory-second-review-acceptance.js +115 -0
- package/dist/services/memory-source-observations.js +117 -0
- package/dist/services/memory-stage-dispatch.js +65 -0
- package/dist/services/memory-stage-results.js +290 -0
- package/dist/services/memory-tool-probe.js +25 -0
- package/dist/services/memory-upgrade-analysis-handler.js +243 -0
- package/dist/services/memory-upgrade-bootstrap-readiness.js +251 -0
- package/dist/services/memory-upgrade-evidence.js +100 -0
- package/dist/services/memory-upgrade-inventory.js +442 -0
- package/dist/services/memory-upgrade-publication-custody.js +73 -0
- package/dist/services/memory-upgrade-publication.js +413 -0
- package/dist/services/memory-upgrade-selection.js +430 -0
- package/dist/services/memory-upgrade-start.js +60 -0
- package/dist/services/memory-upgrade-storage-readiness.js +340 -0
- package/dist/services/memory-verification-acceptance.js +156 -0
- package/dist/services/memory-verification-checks.js +317 -0
- package/dist/services/memory-verification-receipt.js +160 -0
- package/dist/services/memory-verify-handler.js +137 -0
- package/dist/services/memory-work-result-json.js +12 -0
- package/dist/services/memory-work-result-preparation.js +64 -0
- package/dist/services/merge-queue.js +228 -149
- package/dist/services/merge-server.js +163 -35
- package/dist/services/migrations.js +164 -114
- package/dist/services/model-catalog.js +109 -0
- package/dist/services/model-discovery.js +46 -0
- package/dist/services/native-daemon-history.js +50 -0
- package/dist/services/native-session-control.js +39 -0
- package/dist/services/operator-profiles.js +236 -0
- package/dist/services/physical-startup.js +45 -0
- package/dist/services/plan-runtime.js +26 -11
- package/dist/services/plans.js +28 -25
- package/dist/services/project-flow-source.js +195 -0
- package/dist/services/project-git-integration.js +92 -0
- package/dist/services/project-registry.js +97 -0
- package/dist/services/project-summary.js +1 -1
- package/dist/services/projects.js +17 -87
- package/dist/services/prompt-delivery.js +71 -0
- package/dist/services/prompts.js +28 -17
- package/dist/services/protocols.js +163 -87
- package/dist/services/provider-capacity.js +463 -0
- package/dist/services/recovery-observation-budget.js +121 -0
- package/dist/services/recovery-snapshot-database.js +588 -0
- package/dist/services/release-archive-producer.js +52 -0
- package/dist/services/release-archive.js +143 -0
- package/dist/services/release-cache.js +150 -0
- package/dist/services/release-compatibility.js +18 -0
- package/dist/services/release-download.js +67 -0
- package/dist/services/release-envelope.js +104 -0
- package/dist/services/release-manifest.js +45 -0
- package/dist/services/release-payload.js +114 -0
- package/dist/services/release-producer.js +63 -0
- package/dist/services/release-provenance.js +160 -0
- package/dist/services/release-registry.js +66 -0
- package/dist/services/release-source.js +194 -0
- package/dist/services/repair-intents.js +29 -0
- package/dist/services/repair-receipt.js +13 -0
- package/dist/services/retained-native-hook.js +274 -0
- package/dist/services/review-copy.js +165 -0
- package/dist/services/review-deferral.js +23 -0
- package/dist/services/review-fix-handoff.js +82 -0
- package/dist/services/review-fix-intake-custody.js +17 -0
- package/dist/services/review-fix-stage-entry.js +43 -0
- package/dist/services/review-fix-stage-handlers.js +176 -0
- package/dist/services/review-fix-stage-results.js +86 -0
- package/dist/services/run-control-receipt.js +56 -0
- package/dist/services/run-control-worker.js +469 -0
- package/dist/services/run-control.js +1109 -0
- package/dist/services/run-controller-adapter.js +434 -0
- package/dist/services/run-controller-capture.js +186 -0
- package/dist/services/run-controller-process.js +226 -0
- package/dist/services/run-controller-recovery.js +335 -0
- package/dist/services/run-controller-state.js +106 -0
- package/dist/services/run-controller.js +1717 -0
- package/dist/services/run-engine-bindings.js +1 -1
- package/dist/services/run-fork.js +147 -0
- package/dist/services/run-health.js +149 -0
- package/dist/services/run-observations.js +152 -0
- package/dist/services/run-projection.js +22 -64
- package/dist/services/run-recovery-guard.js +107 -0
- package/dist/services/run-recovery-runtime.js +112 -0
- package/dist/services/run-recovery.js +485 -0
- package/dist/services/run-start-custody.js +189 -0
- package/dist/services/run-timeline.js +59 -0
- package/dist/services/runs.js +996 -271
- package/dist/services/runtime-budget.js +446 -0
- package/dist/services/runtime-command.js +43 -0
- package/dist/services/runtime-scope-capture.js +52 -0
- package/dist/services/runtime-scope-control.js +468 -0
- package/dist/services/runtime-scope-resume.js +605 -0
- package/dist/services/runtime-scope-stop.js +211 -0
- package/dist/services/runtime-scope-worker.js +314 -0
- package/dist/services/runtime-service.js +19 -14
- package/dist/services/schema-validation.js +10 -8
- package/dist/services/scope-writer-cleanup.js +27 -0
- package/dist/services/sessions.js +37 -91
- package/dist/services/setup.js +427 -0
- package/dist/services/stage-blocker.js +17 -7
- package/dist/services/stage-context.js +240 -18
- package/dist/services/stage-handlers.js +141 -0
- package/dist/services/stage-lifecycle.js +156 -119
- package/dist/services/stage-pause.js +222 -94
- package/dist/services/stage-report-renderer.js +230 -6
- package/dist/services/stage-work-graph.js +52 -0
- package/dist/services/technical-notes.js +31 -0
- package/dist/services/tooling.js +111 -44
- package/dist/services/usage.js +49 -101
- package/dist/services/verification-matrix.js +393 -0
- package/dist/services/version-status.js +1 -1
- package/dist/services/vnext-code-review.js +302 -91
- package/dist/services/vnext-code.js +325 -251
- package/dist/services/vnext-contracts.js +10 -1
- package/dist/services/vnext-execution-profile.js +80 -5
- package/dist/services/vnext-fanout.js +399 -20
- package/dist/services/vnext-merge.js +336 -114
- package/dist/services/vnext-plan-review.js +288 -133
- package/dist/services/vnext-plan.js +257 -110
- package/dist/services/vnext-protocolize.js +534 -96
- package/dist/services/vnext-specify.js +131 -84
- package/dist/services/vnext-workspace-policy.js +7 -2
- package/dist/services/work-registry.js +1950 -215
- package/dist/services/work-retry-context.js +55 -0
- package/dist/services/workspace-bootstrap.js +53 -15
- package/dist/services/workspace-files.js +85 -0
- package/dist/services/worktrees.js +108 -48
- package/dist/shared/entity-references.js +1 -0
- package/dist/shared/errors.js +27 -0
- package/dist/storage/agent-output.js +26 -0
- package/dist/storage/data-preservation.js +65 -0
- package/dist/storage/database.js +931 -40
- package/dist/storage/foreign-key-transaction.js +39 -0
- package/dist/storage/immutable-input.js +45 -0
- package/dist/storage/import-guard.js +159 -0
- package/dist/storage/memory-upgrade-schema-bootstrap.js +38 -0
- package/dist/storage/native-cleanup.js +52 -0
- package/dist/storage/paired-writer-migration.js +424 -0
- package/dist/storage/paths.js +17 -4
- package/dist/storage/projection-lock.js +73 -0
- package/dist/storage/work-references.js +23 -0
- package/dist/storage/writer-contract.js +96 -0
- package/dist/storage/writer-migration.js +346 -0
- package/dist/storage/writer-restore.js +217 -0
- package/dist/verification-components.json +384 -0
- package/package.json +52 -13
- package/tools/create-canon-release.mjs +33 -0
- package/tools/probe-codex-history.mjs +37 -0
- package/tools/produce-hybrid-release.mjs +41 -0
- package/tools/repair-paused-run-status.mjs +84 -0
- package/tools/verification-jobs.mjs +342 -0
package/dist/services/hooks.js
CHANGED
|
@@ -2,18 +2,30 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import { parse, stringify } from "smol-toml";
|
|
5
|
+
import { parse, stringify, TomlError } from "smol-toml";
|
|
6
6
|
import { AppError } from "../shared/errors.js";
|
|
7
|
+
import { entityReferenceVariants } from "../shared/entity-references.js";
|
|
7
8
|
import { parseJsonObject } from "../shared/json.js";
|
|
8
|
-
import { ensureDir, resolveProjectRoot } from "../storage/paths.js";
|
|
9
|
+
import { canonicalPath, ensureDir, resolveProjectRoot } from "../storage/paths.js";
|
|
10
|
+
import { resolveWorkReference } from "../storage/work-references.js";
|
|
9
11
|
import { appendAudit } from "./audit.js";
|
|
12
|
+
import { memoryRepairAssignedHookCwd, memoryNativeReviewAssignedHookCwd } from "./hook-memory-routing.js";
|
|
13
|
+
import { prepareTextFile, prepareJsonText, prepareError } from "../cli/input-preparation.js";
|
|
14
|
+
import { nativeHookNonParticipation } from "../cli/native-hook-preparation.js";
|
|
10
15
|
import { nativeSessionIdentity, storageSessionId } from "./session-identity.js";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
16
|
+
import { assertRecoveryHookCurrent } from "./hook-recovery-guard.js";
|
|
17
|
+
import { observeLifecycleInvocation, observedLifecycleInvocation, managedNativeInvocationScope } from "./lifecycle-invocations.js";
|
|
18
|
+
import { commandHasOption, commandOption, commandPosition, shellSuffixOffset, parseLifecycleCommand, lifecycleCommandFromPayload, continuationCommand } from "./lifecycle-command.js";
|
|
19
|
+
import { registerProject, requireProjectByRoot } from "./project-registry.js";
|
|
20
|
+
import { activeFlowSessionsForProject, recordFlowSessionObservation } from "./hook-session-observation.js";
|
|
14
21
|
const defaultProfileName = "default";
|
|
15
22
|
const maxSanitizedSummaryLength = 1600;
|
|
16
|
-
|
|
23
|
+
function lifecycleObservationResult(receipt) {
|
|
24
|
+
return { observed: receipt.disposition === "admitted", duplicate: receipt.duplicate,
|
|
25
|
+
event_key: receipt.authoritativeEventKey, current_event_key: receipt.currentEventKey,
|
|
26
|
+
authoritative_event_key: receipt.authoritativeEventKey, receipt_disposition: receipt.disposition };
|
|
27
|
+
}
|
|
28
|
+
export const codexHomeSharedEntries = [
|
|
17
29
|
"auth.json",
|
|
18
30
|
"auth.bk",
|
|
19
31
|
"sessions",
|
|
@@ -50,22 +62,28 @@ export function planCodexHome(context, input) {
|
|
|
50
62
|
config_path: paths.configPath,
|
|
51
63
|
hooks_path: paths.hooksPath,
|
|
52
64
|
owned_files: ["config.toml", "hooks.json", ".dd-flow-home.json", "log/", ".tmp/", "tmp/"],
|
|
53
|
-
shared_entries:
|
|
65
|
+
shared_entries: codexHomeSharedEntries,
|
|
54
66
|
exists: fs.existsSync(paths.targetHome),
|
|
55
67
|
codex_env: { CODEX_HOME: paths.targetHome }
|
|
56
68
|
};
|
|
57
69
|
}
|
|
58
|
-
export function
|
|
70
|
+
export function prepareCodexHome(context, input) {
|
|
59
71
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
60
72
|
const profile = resolveProfileName(input.profile);
|
|
61
73
|
const paths = resolveHomePaths(context, project, profile, input);
|
|
74
|
+
if (canonicalPath(paths.sourceHome, false) === canonicalPath(paths.targetHome, false))
|
|
75
|
+
throw new AppError("validation", "Managed Codex home must differ from its source home", 2);
|
|
76
|
+
const config = readTomlIfExists(path.join(paths.sourceHome, "config.toml"));
|
|
77
|
+
return { project, profile, paths, config };
|
|
78
|
+
}
|
|
79
|
+
export function initCodexHome(context, input, prepared = prepareCodexHome(context, input)) {
|
|
80
|
+
const { project, profile, paths, config } = prepared;
|
|
62
81
|
const now = context.now();
|
|
63
82
|
ensureDir(paths.targetHome);
|
|
64
83
|
ensureDir(path.join(paths.targetHome, "log"));
|
|
65
84
|
ensureDir(path.join(paths.targetHome, ".tmp"));
|
|
66
85
|
ensureDir(path.join(paths.targetHome, "tmp"));
|
|
67
86
|
const linked = createSharedLinks(paths.sourceHome, paths.targetHome);
|
|
68
|
-
const config = readTomlIfExists(path.join(paths.sourceHome, "config.toml"));
|
|
69
87
|
fs.writeFileSync(paths.configPath, stringify(withManagedConfig(config, paths.sourceHome, paths.targetHome)));
|
|
70
88
|
writeJsonFile(paths.hooksPath, expectedHooksConfig(project.root));
|
|
71
89
|
writeJsonFile(path.join(paths.targetHome, ".dd-flow-home.json"), {
|
|
@@ -136,7 +154,7 @@ export function printCodexHomeEnv(context, input) {
|
|
|
136
154
|
command_prefix: `CODEX_HOME=${JSON.stringify(record.target_home)}`
|
|
137
155
|
};
|
|
138
156
|
}
|
|
139
|
-
export function
|
|
157
|
+
export function prepareCodexHomeRemoval(context, input) {
|
|
140
158
|
if (!["keep-shared", "remove-owned"].includes(input.mode)) {
|
|
141
159
|
throw new AppError("validation", "--mode must be keep-shared or remove-owned", 2);
|
|
142
160
|
}
|
|
@@ -144,9 +162,30 @@ export function removeCodexHome(context, input) {
|
|
|
144
162
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
145
163
|
const profile = resolveProfileName(input.profile);
|
|
146
164
|
const record = homeProfile(context, project.id, profile);
|
|
165
|
+
if (record && record.status !== "removed" && fs.existsSync(record.target_home)) {
|
|
166
|
+
let marker;
|
|
167
|
+
try {
|
|
168
|
+
const value = JSON.parse(fs.readFileSync(path.join(record.target_home, ".dd-flow-home.json"), "utf8"));
|
|
169
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
170
|
+
throw new Error("Invalid marker");
|
|
171
|
+
marker = value;
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
throw new AppError("runtime_artifact_invalid", "Managed Codex home ownership marker is missing or invalid; refusing removal", 1, { target_home: record.target_home });
|
|
175
|
+
}
|
|
176
|
+
if (marker.managed_by !== "dd-flow-cli" || marker.project_id !== project.id || marker.profile !== profile || marker.target_home !== record.target_home || marker.source_home !== record.source_home || canonicalPath(record.target_home, false) === canonicalPath(record.source_home, false)) {
|
|
177
|
+
throw new AppError("runtime_artifact_invalid", "Managed Codex home ownership does not match its registration; refusing removal", 1, { target_home: record.target_home });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return { project, profile, record, mode };
|
|
181
|
+
}
|
|
182
|
+
export function removeCodexHome(context, input) {
|
|
183
|
+
const { project, profile, record, mode } = prepareCodexHomeRemoval(context, input);
|
|
147
184
|
if (!record) {
|
|
148
185
|
return { ok: true, removed: false, profile, status: "not_initialized" };
|
|
149
186
|
}
|
|
187
|
+
if (record.status === "removed")
|
|
188
|
+
return { ok: true, removed: false, profile, status: "already_removed" };
|
|
150
189
|
const removedEntries = [];
|
|
151
190
|
for (const relative of ["config.toml", "hooks.json", ".dd-flow-home.json", "log", ".tmp", "tmp"]) {
|
|
152
191
|
const target = path.join(record.target_home, relative);
|
|
@@ -207,7 +246,7 @@ export function getCodexHooksStatus(context, input) {
|
|
|
207
246
|
recorded
|
|
208
247
|
};
|
|
209
248
|
}
|
|
210
|
-
export function
|
|
249
|
+
export function prepareCodexHooks(context, input) {
|
|
211
250
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
212
251
|
const target = resolveHookTarget(input.target);
|
|
213
252
|
const location = requireHookLocation(context, project, target, resolveProfileName(input.profile));
|
|
@@ -217,9 +256,16 @@ export function installCodexHooks(context, input) {
|
|
|
217
256
|
hooks_path: location.hooksPath
|
|
218
257
|
});
|
|
219
258
|
}
|
|
220
|
-
const
|
|
221
|
-
const
|
|
259
|
+
const text = fs.existsSync(location.hooksPath) ? prepareTextFile(location.hooksPath, "hooks configuration").text : null;
|
|
260
|
+
const existing = text === null ? {} : prepareJsonText(text, { file: location.hooksPath }).value;
|
|
261
|
+
if (!existing || typeof existing !== "object" || Array.isArray(existing) || (existing.hooks !== undefined && (!existing.hooks || typeof existing.hooks !== "object" || Array.isArray(existing.hooks) || Object.values(existing.hooks).some(entries => !Array.isArray(entries)))))
|
|
262
|
+
return prepareError(new AppError("validation", "Hooks configuration requires an object with event arrays", 2), { file: location.hooksPath });
|
|
263
|
+
return { project, target, location, existing, text };
|
|
264
|
+
}
|
|
265
|
+
export function installCodexHooks(context, input, prepared = prepareCodexHooks(context, input)) {
|
|
266
|
+
const { project, target, location, existing, text } = prepared;
|
|
222
267
|
const nextConfig = mergeHooksConfig(existing, project.root);
|
|
268
|
+
const backup = target === "default" && text !== null ? backupFile(location.hooksPath, context.now(), text) : null;
|
|
223
269
|
ensureDir(path.dirname(location.hooksPath));
|
|
224
270
|
writeJsonFile(location.hooksPath, nextConfig);
|
|
225
271
|
recordHookInstallation(context, project.id, installationScope(target, location.profile, location.hooksPath), location.hooksPath, true, "none");
|
|
@@ -238,20 +284,11 @@ export function installCodexHooks(context, input) {
|
|
|
238
284
|
expected_hash: hashObject(expectedHooksConfig(project.root))
|
|
239
285
|
};
|
|
240
286
|
}
|
|
241
|
-
export function removeCodexHooks(context, input) {
|
|
242
|
-
const project
|
|
243
|
-
const target = resolveHookTarget(input.target);
|
|
244
|
-
const location = requireHookLocation(context, project, target, resolveProfileName(input.profile));
|
|
245
|
-
if (target === "default" && !input.yes) {
|
|
246
|
-
throw new AppError("confirmation_required", "Direct default Codex home mutation requires --yes", 1, {
|
|
247
|
-
target,
|
|
248
|
-
hooks_path: location.hooksPath
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
const existing = readJsonIfExists(location.hooksPath) ?? {};
|
|
287
|
+
export function removeCodexHooks(context, input, prepared = prepareCodexHooks(context, input)) {
|
|
288
|
+
const { project, target, location, existing, text } = prepared;
|
|
252
289
|
const hadManaged = hasManagedHooks(existing, project.root);
|
|
253
|
-
const backup = target === "default" && fs.existsSync(location.hooksPath) ? backupFile(location.hooksPath, context.now()) : null;
|
|
254
290
|
const nextConfig = removeManagedHooks(existing, project.root);
|
|
291
|
+
const backup = target === "default" && text !== null ? backupFile(location.hooksPath, context.now(), text) : null;
|
|
255
292
|
ensureDir(path.dirname(location.hooksPath));
|
|
256
293
|
writeJsonFile(location.hooksPath, nextConfig);
|
|
257
294
|
recordHookInstallation(context, project.id, installationScope(target, location.profile, location.hooksPath), location.hooksPath, false, "not_installed");
|
|
@@ -274,102 +311,163 @@ export function handleCodexHook(context, input) {
|
|
|
274
311
|
// therefore the only valid identity for a fresh Work.
|
|
275
312
|
const rawChildProviderSessionId = stringValue(payload.agent_id) ?? null;
|
|
276
313
|
const providerSessionId = rawChildProviderSessionId ?? rootProviderSessionId;
|
|
277
|
-
const parentProviderSessionId = rawChildProviderSessionId ? codexNativeParentId(payload, rawChildProviderSessionId) : null;
|
|
278
314
|
const turnId = stringValue(payload.turn_id);
|
|
279
315
|
const toolName = stringValue(payload.tool_name) ?? toolNameFromPayload(payload);
|
|
280
316
|
const command = lifecycleCommandFromPayload(payload);
|
|
281
317
|
if (!command)
|
|
282
318
|
return { ok: true, observed: false, reason: "non_bash_tool" };
|
|
283
319
|
const lifecycle = lifecycleFacts(command);
|
|
284
|
-
if (!lifecycle)
|
|
320
|
+
if (!lifecycle && !continuationCommand(command))
|
|
285
321
|
return { ok: true, observed: false, reason: "event_not_participating", event: eventName };
|
|
322
|
+
const parentProviderSessionId = rawChildProviderSessionId
|
|
323
|
+
? context.codexHookProof?.sessionId === rawChildProviderSessionId
|
|
324
|
+
? context.codexHookProof.parentSessionId
|
|
325
|
+
: codexNativeParentId(payload, rawChildProviderSessionId)
|
|
326
|
+
: null;
|
|
286
327
|
// Resume legitimately receives an answer through stdin. It is matched by
|
|
287
328
|
// immutable lifecycle arguments below; never reject or rewrite that pipe.
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
permissionDecision: "deny",
|
|
298
|
-
permissionDecisionReason: `dd-flow lifecycle commands must be standalone. Retry in this same Session: ${lifecycle.invocation.command}`
|
|
299
|
-
}
|
|
300
|
-
};
|
|
329
|
+
const bootstrapStageStart = lifecycle?.bootstrapStageStart;
|
|
330
|
+
const invocationId = lifecycle ? commandOption(lifecycle.invocation, "invocation-id") : undefined;
|
|
331
|
+
if (invocationId || context.env.DD_FLOW_DAEMON_ID) {
|
|
332
|
+
const proof = context.codexHookProof;
|
|
333
|
+
if (!proof || proof.daemonId !== context.env.DD_FLOW_DAEMON_ID || proof.rootSessionId !== rootProviderSessionId
|
|
334
|
+
|| proof.sessionId !== providerSessionId || proof.parentSessionId !== parentProviderSessionId
|
|
335
|
+
|| proof.toolCallId !== payload.tool_use_id || proof.command !== command) {
|
|
336
|
+
throw new AppError("native_hook_unproven", "Managed Codex admission requires its active native hook delivery, not a manual handler call", 1, { ...(invocationId ? { invocation_id: invocationId } : {}), effect: "no_effect" });
|
|
337
|
+
}
|
|
301
338
|
}
|
|
302
|
-
const
|
|
303
|
-
const matchKey = commandProjectRoot ? lifecycleMatchKey(lifecycle, commandProjectRoot) : null;
|
|
339
|
+
const nativeProjectRoot = context.codexHookProof?.projectRoot ?? input.projectRoot;
|
|
304
340
|
// A bootstrap stage is the first stateful command for a new materialized project.
|
|
305
341
|
// Its PreToolUse event arrives before stage start can register that project.
|
|
306
|
-
|
|
307
|
-
|
|
342
|
+
const bootstrapRoot = nativeProjectRoot ?? stringValue(payload.cwd);
|
|
343
|
+
if (bootstrapStageStart && bootstrapRoot) {
|
|
344
|
+
registerProject(context, { root: bootstrapRoot });
|
|
308
345
|
}
|
|
309
|
-
const project = projectForHook(context,
|
|
310
|
-
if (!project)
|
|
346
|
+
const project = projectForHook(context, nativeProjectRoot, stringValue(payload.cwd));
|
|
347
|
+
if (!project) {
|
|
348
|
+
if (invocationId)
|
|
349
|
+
throw new AppError("project_not_registered", "Managed lifecycle invocation has no registered project", 1, { project_root: nativeProjectRoot ?? null });
|
|
311
350
|
return { ok: true, observed: false, reason: "unrelated_cwd" };
|
|
351
|
+
}
|
|
352
|
+
let matchKey = lifecycle ? lifecycleMatchKey(lifecycle, project.root) : null;
|
|
312
353
|
// Codex retains the root session_id for a thread-spawned child, while the
|
|
313
354
|
// payload transcript belongs to the current agent_id. Keep lineage separate
|
|
314
355
|
// from ownership: otherwise a child overwrites the root transcript binding.
|
|
315
|
-
const
|
|
316
|
-
const binding = bindingOwnerId ? upsertSessionBindingFromPayload(context, project, bindingOwnerId, payload) : undefined;
|
|
356
|
+
const eventKey = hookEventKey(payload, eventName, toolName, command);
|
|
317
357
|
const storageId = providerSessionId ? storageSessionId(nativeSessionIdentity("codex-desktop", providerSessionId)) : null;
|
|
318
358
|
const parentSessionId = parentProviderSessionId ? storageSessionId(nativeSessionIdentity("codex-desktop", parentProviderSessionId)) : null;
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
protocolId,
|
|
333
|
-
harness: "codex-desktop",
|
|
334
|
-
providerSessionId: providerSessionId ?? null,
|
|
335
|
-
parentSessionId,
|
|
336
|
-
sessionId: effectiveSessionId,
|
|
337
|
-
agentId: rawChildProviderSessionId ?? null,
|
|
338
|
-
turnId: turnId ?? null,
|
|
339
|
-
eventName,
|
|
340
|
-
toolName: toolName ?? null,
|
|
341
|
-
status: "observed",
|
|
342
|
-
payload,
|
|
343
|
-
eventKey,
|
|
344
|
-
matchKey,
|
|
345
|
-
transcriptPath: binding?.transcript_path ?? null,
|
|
346
|
-
cwd: stringValue(payload.cwd) ?? null
|
|
347
|
-
});
|
|
348
|
-
if (effectiveSessionId) {
|
|
349
|
-
recordFlowSessionObservation(context, {
|
|
359
|
+
const eventCwd = stringValue(payload.cwd) ?? project.root;
|
|
360
|
+
assertHookEventReplay(context, { projectId: project.id, eventKey, harness: "codex-desktop", providerSessionId: providerSessionId ?? null, parentSessionId, daemonId: stringValue(context.env.DD_FLOW_DAEMON_ID) ?? null, sessionId: storageId, turnId: turnId ?? null, eventName, toolName: toolName ?? null, matchKey, cwd: eventCwd });
|
|
361
|
+
const bindingOwnerId = providerSessionId ?? rootProviderSessionId;
|
|
362
|
+
// Resolve legacy transcript discovery before acquiring the receipt write lock.
|
|
363
|
+
const transcriptPath = stringValue(payload.transcript_path)
|
|
364
|
+
?? (bindingOwnerId ? sessionBinding(context, project.id, bindingOwnerId)?.transcript_path ?? locateCodexTranscript(bindingOwnerId, context.env.CODEX_HOME) : null);
|
|
365
|
+
const persist = (scope) => {
|
|
366
|
+
if (scope)
|
|
367
|
+
matchKey = lifecycleMatchKey(lifecycleFacts(scope.command), project.root, scope);
|
|
368
|
+
const binding = bindingOwnerId ? upsertSessionBindingFromPayload(context, project, bindingOwnerId, payload, transcriptPath) : undefined;
|
|
369
|
+
const effectiveSessionId = storageId;
|
|
370
|
+
const protocolId = binding?.protocol_id ?? null;
|
|
371
|
+
const inserted = recordHookEvent(context, {
|
|
350
372
|
projectId: project.id,
|
|
351
|
-
sessionId: effectiveSessionId,
|
|
352
|
-
runId: observedSession?.run_id ?? null,
|
|
353
373
|
protocolId,
|
|
354
|
-
|
|
374
|
+
harness: "codex-desktop",
|
|
375
|
+
providerSessionId: providerSessionId ?? null,
|
|
376
|
+
parentSessionId,
|
|
377
|
+
// Pinned by the owning app-server process, never derived from current RUN
|
|
378
|
+
// state or an agent-supplied shell command/payload.
|
|
379
|
+
daemonId: stringValue(context.env.DD_FLOW_DAEMON_ID) ?? null,
|
|
380
|
+
sessionId: effectiveSessionId,
|
|
381
|
+
agentId: rawChildProviderSessionId ?? null,
|
|
382
|
+
turnId: turnId ?? null,
|
|
383
|
+
eventName,
|
|
355
384
|
toolName: toolName ?? null,
|
|
356
|
-
|
|
385
|
+
status: "observed",
|
|
386
|
+
payload,
|
|
387
|
+
eventKey,
|
|
388
|
+
matchKey,
|
|
389
|
+
transcriptPath: binding?.transcript_path ?? null,
|
|
390
|
+
cwd: stringValue(payload.cwd) ?? project.root
|
|
357
391
|
});
|
|
358
|
-
|
|
359
|
-
|
|
392
|
+
if (effectiveSessionId) {
|
|
393
|
+
recordFlowSessionObservation(context, {
|
|
394
|
+
projectId: project.id,
|
|
395
|
+
sessionId: effectiveSessionId,
|
|
396
|
+
protocolId,
|
|
397
|
+
cwd: eventCwd,
|
|
398
|
+
toolName: toolName ?? null,
|
|
399
|
+
eventKey
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
return { inserted, protocolId };
|
|
403
|
+
};
|
|
404
|
+
const common = {
|
|
360
405
|
ok: true,
|
|
361
|
-
observed: inserted,
|
|
362
|
-
duplicate: !inserted,
|
|
363
406
|
event_key: eventKey,
|
|
364
407
|
session: providerSessionId ? nativeSessionIdentity("codex-desktop", providerSessionId) : null,
|
|
365
408
|
...(parentProviderSessionId ? { parent_session: nativeSessionIdentity("codex-desktop", parentProviderSessionId) } : {}),
|
|
366
|
-
protocol_id:
|
|
367
|
-
|
|
409
|
+
protocol_id: null
|
|
410
|
+
};
|
|
411
|
+
if (!lifecycle) {
|
|
412
|
+
if (providerSessionId)
|
|
413
|
+
assignedLifecycleCwd(context, project.root, null, providerSessionId, command, stringValue(payload.cwd));
|
|
414
|
+
const persisted = context.db.writeTransaction(() => persist());
|
|
415
|
+
return { ...common, observed: persisted.inserted, duplicate: !persisted.inserted };
|
|
416
|
+
}
|
|
417
|
+
if (invocationId || context.env.DD_FLOW_DAEMON_ID) {
|
|
418
|
+
const daemonId = stringValue(context.env.DD_FLOW_DAEMON_ID);
|
|
419
|
+
const toolCallId = stringValue(payload.tool_use_id) ?? stringValue(payload.event_id) ?? stringValue(payload.delivery_id) ?? stringValue(payload.id);
|
|
420
|
+
if (!daemonId || !rootProviderSessionId || !providerSessionId || !toolCallId)
|
|
421
|
+
throw new AppError("codex_identity_missing", "Managed Codex lifecycle hook has no complete native invocation identity", 1, { invocation_id: invocationId });
|
|
422
|
+
let executionCwd;
|
|
423
|
+
try {
|
|
424
|
+
executionCwd = assignedLifecycleCwd(context, project.root, lifecycle, providerSessionId, command, stringValue(payload.cwd), { daemonId, rootSessionId: rootProviderSessionId, sessionId: providerSessionId, parentSessionId: parentProviderSessionId, toolCallId }, "codex-desktop");
|
|
425
|
+
}
|
|
426
|
+
catch (error) {
|
|
427
|
+
// A proven external Work is not its Stage coordinator. Deny the tool
|
|
428
|
+
// normally; a policy refusal is not a broken native hook or a receipt.
|
|
429
|
+
if (!(error instanceof AppError) || error.code !== "work_stage_boundary")
|
|
430
|
+
throw error;
|
|
431
|
+
return { ...common, observed: false, reason: error.code, hookSpecificOutput: { hookEventName: "PreToolUse",
|
|
432
|
+
permissionDecision: "deny", permissionDecisionReason: error.message } };
|
|
433
|
+
}
|
|
434
|
+
const receipt = observeLifecycleInvocation(context, {
|
|
435
|
+
currentEventKey: eventKey,
|
|
436
|
+
...(invocationId ? { id: invocationId } : {}),
|
|
437
|
+
command,
|
|
438
|
+
harness: "codex-desktop",
|
|
439
|
+
projectId: project.id,
|
|
440
|
+
...(executionCwd ? { executionCwd } : {}),
|
|
441
|
+
identity: { daemonId, rootSessionId: rootProviderSessionId, sessionId: providerSessionId, parentSessionId: parentProviderSessionId, toolCallId },
|
|
442
|
+
recordReceipt: scope => { persist(scope); return eventKey; }
|
|
443
|
+
});
|
|
444
|
+
const stored = context.db.get("SELECT protocol_id FROM hook_events WHERE project_id = ? AND event_key = ?", [project.id, receipt.eventKey]);
|
|
445
|
+
const admittedInvocationId = receipt.invocationId ?? invocationId;
|
|
446
|
+
if (admittedInvocationId)
|
|
447
|
+
return { ...common, protocol_id: stored?.protocol_id ?? null, ...lifecycleObservationResult(receipt), invocation_id: admittedInvocationId };
|
|
448
|
+
return {
|
|
449
|
+
...common,
|
|
450
|
+
protocol_id: stored?.protocol_id ?? null,
|
|
451
|
+
...lifecycleObservationResult(receipt),
|
|
452
|
+
hookSpecificOutput: {
|
|
453
|
+
hookEventName: "PreToolUse",
|
|
454
|
+
permissionDecision: "allow",
|
|
455
|
+
updatedInput: { ...objectRecord(payload.tool_input), command: commandWithHookEvent(command, eventKey) }
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
const persisted = persist();
|
|
460
|
+
return {
|
|
461
|
+
...common,
|
|
462
|
+
protocol_id: persisted.protocolId,
|
|
463
|
+
observed: persisted.inserted,
|
|
464
|
+
duplicate: !persisted.inserted,
|
|
465
|
+
...(providerSessionId
|
|
368
466
|
? {
|
|
369
467
|
hookSpecificOutput: {
|
|
370
468
|
hookEventName: "PreToolUse",
|
|
371
469
|
permissionDecision: "allow",
|
|
372
|
-
updatedInput: { command: commandWithHookEvent(command, eventKey) }
|
|
470
|
+
updatedInput: { ...objectRecord(payload.tool_input), command: commandWithHookEvent(command, eventKey) }
|
|
373
471
|
}
|
|
374
472
|
}
|
|
375
473
|
: {})
|
|
@@ -381,7 +479,24 @@ function codexNativeParentId(payload, childId) {
|
|
|
381
479
|
if (!transcript || !path.isAbsolute(transcript))
|
|
382
480
|
return null;
|
|
383
481
|
try {
|
|
384
|
-
const
|
|
482
|
+
const descriptor = fs.openSync(transcript, "r");
|
|
483
|
+
const chunks = [];
|
|
484
|
+
try {
|
|
485
|
+
for (;;) {
|
|
486
|
+
const buffer = Buffer.alloc(4096);
|
|
487
|
+
const length = fs.readSync(descriptor, buffer, 0, buffer.length, null);
|
|
488
|
+
if (!length)
|
|
489
|
+
break;
|
|
490
|
+
const newline = buffer.subarray(0, length).indexOf(10);
|
|
491
|
+
chunks.push(buffer.subarray(0, newline < 0 ? length : newline));
|
|
492
|
+
if (newline >= 0)
|
|
493
|
+
break;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
finally {
|
|
497
|
+
fs.closeSync(descriptor);
|
|
498
|
+
}
|
|
499
|
+
const firstLine = Buffer.concat(chunks).toString("utf8");
|
|
385
500
|
if (!firstLine)
|
|
386
501
|
return null;
|
|
387
502
|
const record = JSON.parse(firstLine);
|
|
@@ -393,120 +508,153 @@ function codexNativeParentId(payload, childId) {
|
|
|
393
508
|
return null;
|
|
394
509
|
}
|
|
395
510
|
}
|
|
396
|
-
/**
|
|
511
|
+
/** Qualified ACP admission binds public commands to an exact issued assignment.
|
|
512
|
+
* Its provenance is an observation, never a synchronous PreToolUse assertion. */
|
|
397
513
|
export function handleZcodeEvent(context, input) {
|
|
514
|
+
const unrelated = nativeHookNonParticipation(["zcode", "event"], input.stdin);
|
|
515
|
+
if (unrelated)
|
|
516
|
+
return unrelated;
|
|
398
517
|
const notification = parseJsonObject(input.stdin || "{}", "zcode ACP notification");
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
const params = objectRecord(notification.params);
|
|
402
|
-
const update = objectRecord(params.update);
|
|
403
|
-
if (update.sessionUpdate !== "tool_call")
|
|
518
|
+
const params = objectRecord(notification.params), update = objectRecord(params.update);
|
|
519
|
+
if (notification.method !== "session/update" || update.sessionUpdate !== "tool_call")
|
|
404
520
|
return { ok: true, observed: false, reason: "event_not_participating" };
|
|
405
|
-
const
|
|
406
|
-
const
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
const rawInput = objectRecord(update.rawInput);
|
|
521
|
+
const rawInput = objectRecord(update.rawInput), meta = objectRecord(update._meta), native = objectRecord(meta.zcodeRuntime);
|
|
522
|
+
const toolName = stringValue(objectRecord(meta.claudeCode).toolName);
|
|
523
|
+
if (toolName !== "Bash" && toolName !== "bash" && toolName !== "execute")
|
|
524
|
+
return { ok: true, observed: false, reason: "non_bash_tool" };
|
|
410
525
|
const command = stringValue(rawInput.command) ?? stringValue(rawInput.cmd);
|
|
411
|
-
|
|
526
|
+
const parsed = command ? parseLifecycleCommand(command) : { kind: "none", participation: "none" };
|
|
527
|
+
if (parsed.participation === "none" || parsed.participation === "unproven")
|
|
528
|
+
return { ok: true, observed: false, reason: "acp_evidence_only" };
|
|
529
|
+
if (parsed.kind === "none" && !continuationCommand(command))
|
|
530
|
+
throw new AppError("lifecycle_shell_syntax_invalid", "Known lifecycle shell execution is unsupported", 1, { reason: parsed.reason, effect: "unknown", recoverable: false });
|
|
531
|
+
const controlled = objectRecord(objectRecord(notification._meta).ddZcode);
|
|
532
|
+
const providerSessionId = stringValue(native.childSessionId) ?? stringValue(controlled.rootProviderSessionId);
|
|
533
|
+
return recordZcodeLifecycle(context, { projectRoot: input.projectRoot, stdin: JSON.stringify({
|
|
534
|
+
session_id: providerSessionId, tool_use_id: update.toolCallId, tool_name: toolName, tool_input: rawInput,
|
|
535
|
+
agent_id: native.agentId, _meta: { ddZcode: controlled }
|
|
536
|
+
}) }, parsed.kind !== "none" ? commandOption(parsed.invocation, "invocation-id") : undefined);
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Accept the synchronous native ZCode PreToolUse callback. Invocation-ID
|
|
540
|
+
* commands instead rendezvous with their ACP event while the CLI awaits
|
|
541
|
+
* admission; the native hook must not create a competing receipt for them.
|
|
542
|
+
*/
|
|
543
|
+
export function handleZcodeHook(context, input) {
|
|
544
|
+
const unrelated = nativeHookNonParticipation(["zcode", "hook"], input.stdin);
|
|
545
|
+
if (unrelated)
|
|
546
|
+
return unrelated;
|
|
547
|
+
return recordZcodeLifecycle(context, input, undefined, true);
|
|
548
|
+
}
|
|
549
|
+
function recordZcodeLifecycle(context, input, invocationId, nativePreTool = false) {
|
|
550
|
+
const hook = parseJsonObject(input.stdin || "{}", "ZCode hook event");
|
|
551
|
+
const eventName = stringValue(hook.hook_event_name) ?? stringValue(hook.hookEventName);
|
|
552
|
+
if (eventName && eventName !== "PreToolUse" && eventName !== "pre_tool_use")
|
|
553
|
+
return { ok: true, observed: false, reason: "event_not_participating" };
|
|
554
|
+
const toolName = stringValue(hook.tool_name) ?? stringValue(hook.toolName);
|
|
555
|
+
const rawInput = objectRecord(hook.tool_input ?? hook.toolInput);
|
|
556
|
+
const command = stringValue(rawInput.command) ?? stringValue(rawInput.cmd) ?? stringValue(hook.command);
|
|
557
|
+
if (!command || (toolName && toolName !== "Bash" && toolName !== "bash" && toolName !== "execute"))
|
|
412
558
|
return { ok: true, observed: false, reason: "non_bash_tool" };
|
|
413
|
-
}
|
|
414
559
|
const lifecycle = lifecycleFacts(command);
|
|
415
|
-
if (!lifecycle)
|
|
560
|
+
if (!lifecycle && !continuationCommand(command))
|
|
416
561
|
return { ok: true, observed: false, reason: "event_not_participating" };
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
}
|
|
562
|
+
// ZCode can execute background Bash without emitting the ACP tool_call that admits an issued CLI invocation.
|
|
563
|
+
if (nativePreTool && lifecycle && rawInput.run_in_background === true) {
|
|
564
|
+
const reason = "Run dd-flow lifecycle commands in the foreground; ZCode background Bash does not provide the required admission receipt.";
|
|
565
|
+
return { continue: false, reason, hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: reason } };
|
|
421
566
|
}
|
|
422
|
-
|
|
567
|
+
if (!invocationId && lifecycle && commandOption(lifecycle.invocation, "invocation-id"))
|
|
568
|
+
return { ok: true, observed: false, reason: "cli_invocation_owns_admission" };
|
|
423
569
|
const expectedRoot = resolveProjectRoot(input.projectRoot);
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
570
|
+
const meta = objectRecord(hook._meta);
|
|
571
|
+
const ddZcode = objectRecord(meta.ddZcode);
|
|
572
|
+
const providerSessionId = stringValue(hook.session_id) ?? stringValue(hook.sessionId);
|
|
573
|
+
const rootProviderSessionId = stringValue(ddZcode.rootProviderSessionId);
|
|
574
|
+
const parentProviderSessionId = stringValue(ddZcode.parentProviderSessionId);
|
|
575
|
+
const daemonId = stringValue(ddZcode.daemonId);
|
|
576
|
+
if (!providerSessionId || !rootProviderSessionId || !daemonId) {
|
|
577
|
+
throw new AppError("zcode_identity_missing", "ZCode native hook has no controlled Session identity", 1);
|
|
429
578
|
}
|
|
430
|
-
|
|
431
|
-
|
|
579
|
+
if (providerSessionId === rootProviderSessionId ? parentProviderSessionId : !parentProviderSessionId) {
|
|
580
|
+
throw new AppError("zcode_identity_missing", "ZCode native hook has inconsistent Session ancestry", 1);
|
|
581
|
+
}
|
|
582
|
+
const toolUseId = stringValue(hook.tool_use_id) ?? stringValue(hook.toolUseId) ?? stringValue(hook.event_id) ?? stringValue(hook.eventId);
|
|
583
|
+
if (!toolUseId)
|
|
584
|
+
throw new AppError("zcode_hook_event_missing", "ZCode native hook has no tool-use ID", 1);
|
|
585
|
+
const eventKey = crypto.createHash("sha256").update(JSON.stringify(["zcode-acp", daemonId, providerSessionId, toolUseId])).digest("hex");
|
|
586
|
+
const rewritten = invocationId ? command : commandWithHookEvent(command, eventKey);
|
|
587
|
+
if (lifecycle?.bootstrapStageStart)
|
|
432
588
|
registerProject(context, { root: expectedRoot });
|
|
433
589
|
const project = requireProjectByRoot(context, expectedRoot);
|
|
434
|
-
const envelopeMeta = objectRecord(notification._meta);
|
|
435
|
-
const ddZcode = objectRecord(envelopeMeta.ddZcode);
|
|
436
|
-
const observedProfile = objectRecord(ddZcode.observedProfile);
|
|
437
|
-
const rootProviderSessionId = stringValue(ddZcode.rootProviderSessionId) ?? stringValue(params.sessionId);
|
|
438
|
-
const daemonId = stringValue(ddZcode.daemonId);
|
|
439
|
-
if (!rootProviderSessionId)
|
|
440
|
-
throw new AppError("zcode_identity_missing", "ZCode ACP event has no root provider Session ID", 1);
|
|
441
|
-
const childProviderSessionId = stringValue(runtime.childSessionId);
|
|
442
|
-
const providerSessionId = childProviderSessionId ?? rootProviderSessionId;
|
|
443
590
|
const sessionId = storageSessionId(nativeSessionIdentity("zcode-acp", providerSessionId));
|
|
444
|
-
const parentSessionId =
|
|
445
|
-
const
|
|
446
|
-
const
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
model: stringValue(observedProfile.model),
|
|
465
|
-
reasoning: stringValue(observedProfile.reasoning),
|
|
466
|
-
mode: stringValue(observedProfile.mode),
|
|
467
|
-
agent_type: stringValue(runtime.source)
|
|
591
|
+
const parentSessionId = parentProviderSessionId ? storageSessionId(nativeSessionIdentity("zcode-acp", parentProviderSessionId)) : null;
|
|
592
|
+
const observedProfile = objectRecord(ddZcode.observedProfile);
|
|
593
|
+
const observedCwd = stringValue(hook.cwd) && path.isAbsolute(String(hook.cwd)) ? resolveProjectRoot(String(hook.cwd)) : null;
|
|
594
|
+
let matchKey = lifecycle ? lifecycleMatchKey(lifecycle, expectedRoot) : null;
|
|
595
|
+
const eventNameForReceipt = nativePreTool ? "PreToolUse" : "ToolCallObserved";
|
|
596
|
+
const persist = (scope) => {
|
|
597
|
+
if (scope)
|
|
598
|
+
matchKey = lifecycleMatchKey(lifecycleFacts(scope.command), expectedRoot, scope);
|
|
599
|
+
assertHookEventReplay(context, { projectId: project.id, eventKey, harness: "zcode-acp", providerSessionId, parentSessionId, daemonId, sessionId, turnId: null, eventName: eventNameForReceipt, toolName: toolName ?? "Bash", matchKey, cwd: observedCwd });
|
|
600
|
+
const payload = { ...hook, session_id: providerSessionId, cwd: observedCwd, tool_name: toolName ?? "Bash", tool_input: rawInput,
|
|
601
|
+
command, provider: stringValue(observedProfile.provider), model: stringValue(observedProfile.model), reasoning: stringValue(observedProfile.reasoning), mode: stringValue(observedProfile.mode) };
|
|
602
|
+
const inserted = recordHookEvent(context, {
|
|
603
|
+
projectId: project.id, protocolId: null, harness: "zcode-acp", providerSessionId, parentSessionId,
|
|
604
|
+
daemonId, sessionId, agentId: stringValue(hook.agent_id) ?? stringValue(hook.agentId) ?? null, turnId: null,
|
|
605
|
+
eventName: eventNameForReceipt, toolName: toolName ?? "Bash", status: "observed", payload, eventKey, matchKey,
|
|
606
|
+
transcriptPath: stringValue(hook.transcript_path) ?? null, cwd: observedCwd
|
|
607
|
+
});
|
|
608
|
+
recordFlowSessionObservation(context, { projectId: project.id, sessionId, cwd: observedCwd, toolName: toolName ?? "Bash", eventKey });
|
|
609
|
+
const result = { ok: true, observed: inserted, duplicate: !inserted, event_key: eventKey, harness: "zcode-acp", session: nativeSessionIdentity("zcode-acp", providerSessionId), ...(parentProviderSessionId ? { parent_session: nativeSessionIdentity("zcode-acp", parentProviderSessionId) } : {}), daemon_id: daemonId };
|
|
610
|
+
return { ...result, hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { ...rawInput, command: rewritten } } };
|
|
468
611
|
};
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
612
|
+
if (!lifecycle) {
|
|
613
|
+
assignedLifecycleCwd(context, expectedRoot, null, providerSessionId, command, observedCwd ?? undefined);
|
|
614
|
+
return context.db.writeTransaction(() => persist());
|
|
615
|
+
}
|
|
616
|
+
let persistedResult;
|
|
617
|
+
const executionCwd = assignedLifecycleCwd(context, expectedRoot, lifecycle, providerSessionId, command, observedCwd ?? undefined, { daemonId, rootSessionId: rootProviderSessionId, sessionId: providerSessionId, parentSessionId: parentProviderSessionId ?? null, toolCallId: toolUseId }, "zcode-acp");
|
|
618
|
+
const receipt = observeLifecycleInvocation(context, {
|
|
619
|
+
currentEventKey: eventKey,
|
|
620
|
+
...(invocationId ? { id: invocationId } : {}), command,
|
|
472
621
|
harness: "zcode-acp",
|
|
473
|
-
providerSessionId,
|
|
474
|
-
parentSessionId,
|
|
475
|
-
daemonId: daemonId ?? null,
|
|
476
|
-
sessionId,
|
|
477
|
-
agentId: agentId ?? null,
|
|
478
|
-
turnId: null,
|
|
479
|
-
eventName: "PreToolUse",
|
|
480
|
-
toolName: toolName ?? "Bash",
|
|
481
|
-
status: "observed",
|
|
482
|
-
payload,
|
|
483
|
-
eventKey,
|
|
484
|
-
matchKey,
|
|
485
|
-
transcriptPath: null,
|
|
486
|
-
cwd: expectedRoot
|
|
487
|
-
});
|
|
488
|
-
recordFlowSessionObservation(context, {
|
|
489
622
|
projectId: project.id,
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
cwd: expectedRoot,
|
|
494
|
-
toolName: toolName ?? "Bash",
|
|
495
|
-
eventKey
|
|
623
|
+
...(executionCwd ? { executionCwd } : {}),
|
|
624
|
+
identity: { daemonId, rootSessionId: rootProviderSessionId, sessionId: providerSessionId, parentSessionId: parentProviderSessionId ?? null, toolCallId: toolUseId },
|
|
625
|
+
recordReceipt: scope => { persistedResult = persist(scope); return eventKey; }
|
|
496
626
|
});
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
627
|
+
if (receipt.disposition === "replay") {
|
|
628
|
+
// Keep first admission immutable. A second transport is independently
|
|
629
|
+
// retained corroboration, never another claim or an invented ACP cwd.
|
|
630
|
+
const retained = context.db.get("SELECT event_name, match_key, harness, provider_session_id, parent_session_id, daemon_id FROM hook_events WHERE project_id = ? AND event_key = ?", [project.id, receipt.eventKey]);
|
|
631
|
+
if (retained && retained.event_name !== eventNameForReceipt) {
|
|
632
|
+
if (retained.harness !== "zcode-acp" || retained.provider_session_id !== providerSessionId || retained.parent_session_id !== parentSessionId || retained.daemon_id !== daemonId) {
|
|
633
|
+
throw new AppError("hook_event_replay_mismatch", "Corroborating transport has different native ownership", 1, { event_key: receipt.eventKey });
|
|
634
|
+
}
|
|
635
|
+
context.db.writeTransaction(() => recordHookEvent(context, { projectId: project.id, protocolId: null, harness: "zcode-acp", providerSessionId, parentSessionId,
|
|
636
|
+
daemonId, sessionId, agentId: stringValue(hook.agent_id) ?? null, turnId: null, eventName: eventNameForReceipt, toolName: toolName ?? "Bash", status: "corroborating",
|
|
637
|
+
payload: { ...hook, command, corroborates_event_key: receipt.eventKey, admission_transport: nativePreTool ? "native_pre_tool" : "acp_observation" },
|
|
638
|
+
eventKey: `${receipt.eventKey}:${eventNameForReceipt}`, matchKey: null, transcriptPath: stringValue(hook.transcript_path) ?? null, cwd: observedCwd }));
|
|
639
|
+
}
|
|
640
|
+
else if (retained) {
|
|
641
|
+
assertHookEventReplay(context, { projectId: project.id, eventKey: receipt.eventKey, harness: "zcode-acp", providerSessionId, parentSessionId, daemonId,
|
|
642
|
+
sessionId, turnId: null, eventName: eventNameForReceipt, toolName: toolName ?? "Bash", matchKey: retained.match_key, cwd: observedCwd });
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
const admittedInvocationId = receipt.invocationId ?? invocationId;
|
|
646
|
+
if (admittedInvocationId)
|
|
647
|
+
return { ok: true, ...lifecycleObservationResult(receipt), invocation_id: admittedInvocationId };
|
|
648
|
+
// Without an issued managed attempt this is still ordinary hook evidence.
|
|
649
|
+
// Preserve persist()'s replay result instead of overwriting it with the
|
|
650
|
+
// admission layer's non-authoritative duplicate flag.
|
|
651
|
+
return persistedResult ?? persist();
|
|
507
652
|
}
|
|
508
653
|
/** Convert a Grok Build PreToolUse hook into a trusted lifecycle receipt. */
|
|
509
654
|
export function handleGrokEvent(context, input) {
|
|
655
|
+
const unrelated = nativeHookNonParticipation(["grok", "event"], input.stdin);
|
|
656
|
+
if (unrelated)
|
|
657
|
+
return unrelated;
|
|
510
658
|
const hook = parseJsonObject(input.stdin || "{}", "Grok Build hook event");
|
|
511
659
|
const eventName = stringValue(hook.hook_event_name) ?? stringValue(hook.hookEventName);
|
|
512
660
|
if (eventName && eventName !== "PreToolUse" && eventName !== "pre_tool_use") {
|
|
@@ -519,27 +667,18 @@ export function handleGrokEvent(context, input) {
|
|
|
519
667
|
return { ok: true, observed: false, reason: "non_bash_tool" };
|
|
520
668
|
}
|
|
521
669
|
const lifecycle = lifecycleFacts(command);
|
|
522
|
-
if (!lifecycle)
|
|
670
|
+
if (!lifecycle && !continuationCommand(command))
|
|
523
671
|
return { ok: true, observed: false, reason: "event_not_participating" };
|
|
524
|
-
|
|
525
|
-
throw new AppError("compound_lifecycle_command", "dd-flow lifecycle commands must be a standalone Grok Build tool call", 1, {
|
|
526
|
-
standalone_command: lifecycle.invocation.command
|
|
527
|
-
});
|
|
528
|
-
}
|
|
529
|
-
const { flowPayload, stageStart, stageResume, workStart, bootstrapStageStart, commandProjectRoot } = lifecycle;
|
|
672
|
+
const bootstrapStageStart = lifecycle?.bootstrapStageStart;
|
|
530
673
|
const expectedRoot = resolveProjectRoot(input.projectRoot);
|
|
531
|
-
|
|
532
|
-
throw new AppError("project_mismatch", "Grok Build lifecycle command project root does not match the controlled workspace", 1, {
|
|
533
|
-
expected: expectedRoot, actual: commandProjectRoot
|
|
534
|
-
});
|
|
535
|
-
}
|
|
536
|
-
const matchKey = lifecycleMatchKey(lifecycle, expectedRoot);
|
|
674
|
+
let matchKey = lifecycle ? lifecycleMatchKey(lifecycle, expectedRoot) : null;
|
|
537
675
|
if (bootstrapStageStart)
|
|
538
676
|
registerProject(context, { root: expectedRoot });
|
|
539
677
|
const project = requireProjectByRoot(context, expectedRoot);
|
|
540
678
|
const meta = objectRecord(hook._meta);
|
|
541
679
|
const ddGrok = objectRecord(meta.ddGrok);
|
|
542
680
|
const observedProfile = objectRecord(ddGrok.observedProfile);
|
|
681
|
+
const observedCwd = stringValue(hook.cwd) && path.isAbsolute(String(hook.cwd)) ? resolveProjectRoot(String(hook.cwd)) : null;
|
|
543
682
|
const rootProviderSessionId = stringValue(ddGrok.rootProviderSessionId);
|
|
544
683
|
const providerSessionId = stringValue(hook.session_id) ?? stringValue(hook.sessionId);
|
|
545
684
|
if (!rootProviderSessionId || !providerSessionId)
|
|
@@ -549,41 +688,183 @@ export function handleGrokEvent(context, input) {
|
|
|
549
688
|
const parentSessionId = isChild ? storageSessionId(nativeSessionIdentity("grok-acp", stringValue(ddGrok.parentProviderSessionId) ?? rootProviderSessionId)) : null;
|
|
550
689
|
const daemonId = stringValue(ddGrok.daemonId);
|
|
551
690
|
const agentId = stringValue(hook.agent_id) ?? stringValue(hook.agentId);
|
|
552
|
-
const observedSession = flowPayload ? bindObservedFlowSession(context, project, {
|
|
553
|
-
...flowPayload, harness: "grok-acp", provider_session_id: providerSessionId, agent_id: agentId ?? null, parent_session_id: parentSessionId
|
|
554
|
-
}, sessionId) : undefined;
|
|
555
691
|
const eventKey = stringValue(hook.event_id) ?? stringValue(hook.eventId) ?? crypto.createHash("sha256").update(JSON.stringify({
|
|
556
|
-
harness: "grok-acp", providerSessionId,
|
|
692
|
+
harness: "grok-acp", providerSessionId, toolUseId: hook.tool_use_id ?? hook.toolUseId ?? null,
|
|
693
|
+
promptId: hook.prompt_id ?? hook.promptId ?? null, command, turn: hook.turn_id ?? hook.turnId ?? null
|
|
557
694
|
})).digest("hex");
|
|
558
|
-
const
|
|
559
|
-
|
|
560
|
-
|
|
695
|
+
const persist = (scope) => {
|
|
696
|
+
if (scope)
|
|
697
|
+
matchKey = lifecycleMatchKey(lifecycleFacts(scope.command), expectedRoot, scope);
|
|
698
|
+
assertHookEventReplay(context, { projectId: project.id, eventKey, harness: "grok-acp", providerSessionId, parentSessionId, daemonId: daemonId ?? null, sessionId, turnId: stringValue(hook.turn_id) ?? stringValue(hook.turnId) ?? null, eventName: "PreToolUse", toolName: toolName ?? "Bash", matchKey, cwd: observedCwd });
|
|
699
|
+
const payload = {
|
|
700
|
+
...hook, session_id: providerSessionId, agent_id: agentId, cwd: observedCwd, tool_name: toolName ?? "Bash", tool_input: rawInput,
|
|
701
|
+
command, provider: stringValue(observedProfile.provider), model: stringValue(observedProfile.model), reasoning: stringValue(observedProfile.reasoning), mode: stringValue(observedProfile.mode), agent_type: stringValue(hook.subagent_type) ?? stringValue(hook.subagentType)
|
|
702
|
+
};
|
|
703
|
+
const inserted = recordHookEvent(context, {
|
|
704
|
+
projectId: project.id, protocolId: null, harness: "grok-acp", providerSessionId, parentSessionId,
|
|
705
|
+
daemonId: daemonId ?? null, sessionId, agentId: agentId ?? null, turnId: stringValue(hook.turn_id) ?? stringValue(hook.turnId) ?? null,
|
|
706
|
+
eventName: "PreToolUse", toolName: toolName ?? "Bash", status: "observed", payload, eventKey, matchKey, transcriptPath: null, cwd: observedCwd
|
|
707
|
+
});
|
|
708
|
+
recordFlowSessionObservation(context, { projectId: project.id, sessionId, cwd: observedCwd, toolName: toolName ?? "Bash", eventKey });
|
|
709
|
+
return inserted;
|
|
561
710
|
};
|
|
562
|
-
const
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
711
|
+
const result = (inserted) => ({ ok: true, observed: inserted, duplicate: !inserted, event_key: eventKey, session: nativeSessionIdentity("grok-acp", providerSessionId),
|
|
712
|
+
...(isChild ? { parent_session: nativeSessionIdentity("grok-acp", stringValue(ddGrok.parentProviderSessionId) ?? rootProviderSessionId) } : {}), daemon_id: daemonId ?? null });
|
|
713
|
+
if (!lifecycle) {
|
|
714
|
+
assignedLifecycleCwd(context, expectedRoot, null, providerSessionId, command, observedCwd ?? undefined);
|
|
715
|
+
return { ...result(context.db.writeTransaction(() => persist())), hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { ...rawInput, command: commandWithHookEvent(command, eventKey) } } };
|
|
716
|
+
}
|
|
717
|
+
let inserted = false;
|
|
718
|
+
const executionCwd = assignedLifecycleCwd(context, expectedRoot, lifecycle, providerSessionId, command, observedCwd ?? undefined, { daemonId: daemonId ?? "", rootSessionId: rootProviderSessionId, sessionId: providerSessionId, parentSessionId, toolCallId: eventKey }, "grok-acp");
|
|
719
|
+
const receipt = observeLifecycleInvocation(context, {
|
|
720
|
+
currentEventKey: eventKey,
|
|
721
|
+
command, harness: "grok-acp", projectId: project.id,
|
|
722
|
+
...(executionCwd ? { executionCwd } : {}),
|
|
723
|
+
identity: { daemonId: daemonId ?? "", rootSessionId: rootProviderSessionId, sessionId: providerSessionId, parentSessionId, toolCallId: eventKey },
|
|
724
|
+
recordReceipt: scope => { inserted = persist(scope); return eventKey; }
|
|
570
725
|
});
|
|
571
|
-
|
|
572
|
-
...(isChild ? { parent_session: nativeSessionIdentity("grok-acp", stringValue(ddGrok.parentProviderSessionId) ?? rootProviderSessionId) } : {}), daemon_id: daemonId ?? null };
|
|
573
|
-
return (flowPayload || stageStart || stageResume || workStart)
|
|
574
|
-
? { ...result, hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { ...(rawInput ?? {}), command: commandWithHookEvent(command, eventKey) } } }
|
|
575
|
-
: result;
|
|
726
|
+
return { ...result(inserted), ...lifecycleObservationResult(receipt), ...(receipt.invocationId ? { invocation_id: receipt.invocationId } : {}), hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { ...rawInput, command: receipt.eventKey ? commandWithHookEvent(command, receipt.eventKey) : command } } };
|
|
576
727
|
}
|
|
577
728
|
/** Convert a controlled OpenCode tool hook into the shared lifecycle receipt. */
|
|
578
729
|
export function handleOpenCodeEvent(context, input) {
|
|
730
|
+
const unrelated = nativeHookNonParticipation(["opencode", "event"], input.stdin);
|
|
731
|
+
if (unrelated)
|
|
732
|
+
return unrelated;
|
|
579
733
|
const event = parseJsonObject(input.stdin || "{}", "OpenCode tool event");
|
|
580
|
-
const agy = event.source_harness === "antigravity-cli";
|
|
581
|
-
const harness = agy ? "antigravity-cli" : "opencode-server";
|
|
582
734
|
if (event.schema_id !== "dd-flow/opencode-tool-event@1")
|
|
583
735
|
throw new AppError("unsupported_opencode_event_schema", "Unsupported OpenCode event schema", 1);
|
|
736
|
+
return handleControlledToolEvent(context, input, event, event.source_harness === "antigravity-cli" ? "antigravity-cli" : "opencode-server");
|
|
737
|
+
}
|
|
738
|
+
/** Accept adapter-observed Factory identity, never a model-supplied Session ID. */
|
|
739
|
+
export function handleDroidEvent(context, input) {
|
|
740
|
+
const unrelated = nativeHookNonParticipation(["droid", "event"], input.stdin);
|
|
741
|
+
if (unrelated)
|
|
742
|
+
return unrelated;
|
|
743
|
+
const event = parseJsonObject(input.stdin || "{}", "Droid tool event");
|
|
744
|
+
if (event.schema_id !== "dd-flow/droid-tool-event@1")
|
|
745
|
+
throw new AppError("unsupported_droid_event_schema", "Unsupported Droid event schema", 1);
|
|
746
|
+
return handleControlledToolEvent(context, input, event, "droid-cli");
|
|
747
|
+
}
|
|
748
|
+
/** Stable project identity is not the native Session's execution workspace. */
|
|
749
|
+
function requiresAssignedLifecycleCwd(facts) {
|
|
750
|
+
// Settlement and user interaction address the retained RUN explicitly;
|
|
751
|
+
// their tools/checks receive owned paths, not the shell's working directory.
|
|
752
|
+
return !facts || !["stage_finish", "stage_pause", "stage_resume", "work_finish", "work_fail"].includes(facts.invocation.operation);
|
|
753
|
+
}
|
|
754
|
+
function assignedLifecycleCwd(context, projectRoot, facts, providerSessionId, command, observedCwd, identity, harness) {
|
|
755
|
+
const auxiliary = continuationCommand(command);
|
|
756
|
+
if ((!facts && !auxiliary) || facts?.analysis.supported === false)
|
|
757
|
+
return undefined;
|
|
758
|
+
const proven = identity && harness ? managedNativeInvocationScope(context, projectRoot, identity, harness) : null;
|
|
759
|
+
if (proven && (facts?.invocation.operation.startsWith("stage_") || facts?.invocation.operation.startsWith("delivery_")) && !facts.bootstrapStageStart
|
|
760
|
+
&& context.db.get("SELECT 1 FROM sqlite_master WHERE type='table' AND name='work_launches'")) {
|
|
761
|
+
const worker = context.db.get("SELECT w.work_id FROM works w JOIN work_launches l ON l.work_id=w.work_id JOIN work_sessions ws ON ws.work_id=w.work_id JOIN sessions s ON s.session_id=ws.session_id AND s.project_id=w.project_id JOIN projects p ON p.id=w.project_id WHERE p.root=? AND w.run_id=? AND s.harness=? AND s.provider_session_id=? AND l.status IN ('launching','start_requested') LIMIT 1", [proven.projectRoot, proven.runId, harness, providerSessionId]);
|
|
762
|
+
if (worker)
|
|
763
|
+
throw new AppError("work_stage_boundary", "This external Work cannot issue coordinator Stage or delivery commands. Complete only its assigned Work; after successful work finish, end this Turn. The coordinator continues the Stage.", 1, { phase: "prepare", effect: "no_effect", recoverable: false });
|
|
764
|
+
}
|
|
765
|
+
const constraint = proven ?? (identity ? { projectRoot, daemonId: identity.daemonId, rootSessionId: identity.rootSessionId } : undefined);
|
|
766
|
+
const issuedRunId = facts && !facts.runId && !facts.workId && !facts.bootstrapStageStart && constraint ? observedLifecycleInvocation(context, command, constraint, identity ? { identity } : undefined)?.scope.runId : null;
|
|
767
|
+
if (facts?.invocation.operation.startsWith("stage_") && !facts.bootstrapStageStart && !facts.runId && !issuedRunId)
|
|
768
|
+
throw new AppError("invocation_assignment_missing", "Short lifecycle call has no unique issued RUN assignment; its workspace cannot be inferred", 1, { phase: "prepare", effect: "no_effect", recoverable: false });
|
|
769
|
+
if (!requiresAssignedLifecycleCwd(facts))
|
|
770
|
+
return undefined;
|
|
771
|
+
const expected = controlledLifecycleCwd(context, projectRoot, facts, providerSessionId, auxiliary, issuedRunId, observedCwd);
|
|
772
|
+
// Unknown CLI targets have no assigned workspace. Recording their native
|
|
773
|
+
// observation must not turn the stable identity root into cwd authority.
|
|
774
|
+
if (!expected)
|
|
775
|
+
return undefined;
|
|
776
|
+
const route = facts?.invocation.expectedCwd ?? auxiliary?.expectedCwd;
|
|
777
|
+
if (route && resolveProjectRoot(route) !== expected)
|
|
778
|
+
throw new AppError("invocation_directory_mismatch", "Literal lifecycle route differs from the assigned workspace", 1, { expected, actual: route, actual_provenance: "shell_literal_route", phase: "prepare", effect: "no_effect", recoverable: false });
|
|
779
|
+
if (!route && observedCwd && resolveProjectRoot(observedCwd) !== expected)
|
|
780
|
+
throw new AppError("invocation_directory_mismatch", "Native lifecycle directory differs from the assigned workspace", 1, { expected, actual: observedCwd, actual_provenance: "native_tool_cwd", phase: "prepare", effect: "no_effect", recoverable: false });
|
|
781
|
+
// ACP without native cwd still binds assigned authority; CLI confirms the
|
|
782
|
+
// physical process directory, rather than inventing native observation.
|
|
783
|
+
return expected;
|
|
784
|
+
}
|
|
785
|
+
function controlledLifecycleCwd(context, projectRoot, facts, providerSessionId, auxiliary, issuedRunId, observedCwd) {
|
|
786
|
+
if (facts?.bootstrapStageStart)
|
|
787
|
+
return projectRoot;
|
|
788
|
+
const project = context.db.get("SELECT id FROM projects WHERE root = ?", [projectRoot]);
|
|
789
|
+
if (!project)
|
|
790
|
+
return undefined;
|
|
791
|
+
if (facts?.invocation.operation === "merge_apply" || facts?.invocation.operation === "merge_repair") {
|
|
792
|
+
const requestId = commandPosition(facts.invocation, 0);
|
|
793
|
+
const request = requestId && context.db.get("SELECT target_workspace FROM merge_requests WHERE project_id = ? AND merge_request_id = ?", [project.id, requestId]);
|
|
794
|
+
if (request)
|
|
795
|
+
return resolveProjectRoot(request.target_workspace);
|
|
796
|
+
}
|
|
797
|
+
let work;
|
|
798
|
+
if (facts?.workId) {
|
|
799
|
+
try {
|
|
800
|
+
work = resolveWorkReference(context, facts.workId, { projectId: project.id });
|
|
801
|
+
}
|
|
802
|
+
catch (error) {
|
|
803
|
+
if (!(error instanceof AppError) || !["not_found", "ambiguous_work_alias"].includes(error.code))
|
|
804
|
+
throw error;
|
|
805
|
+
}
|
|
806
|
+
if (!work)
|
|
807
|
+
return undefined;
|
|
808
|
+
}
|
|
809
|
+
const reference = work?.run_id ?? facts?.runId ?? auxiliary?.args.options.get("run")?.[0] ?? auxiliary?.args.positional[2] ?? issuedRunId;
|
|
810
|
+
if (!reference)
|
|
811
|
+
return undefined;
|
|
812
|
+
const run = context.db.get("SELECT id, run_root, workspace_root FROM runs WHERE project_id = ? AND (id = ? OR short_id = ?)", [project.id, reference, reference]);
|
|
813
|
+
if (!run)
|
|
814
|
+
return undefined; // Hook ingress does not validate a mistyped CLI target.
|
|
815
|
+
if (work) {
|
|
816
|
+
const repair = memoryRepairAssignedHookCwd(context, { projectRoot, projectId: project.id, runId: run.id,
|
|
817
|
+
workId: work.work_id, providerSessionId, ...(observedCwd ? { observedCwd: resolveProjectRoot(observedCwd) } : {}),
|
|
818
|
+
...(facts?.invocation.expectedCwd ? { literalExecutionCwd: resolveProjectRoot(facts.invocation.expectedCwd) } : {}) });
|
|
819
|
+
if (repair)
|
|
820
|
+
return resolveProjectRoot(repair.root);
|
|
821
|
+
const nativeReview = memoryNativeReviewAssignedHookCwd(context, { projectId: project.id, runId: run.id, workId: work.work_id, providerSessionId });
|
|
822
|
+
if (nativeReview)
|
|
823
|
+
return resolveProjectRoot(nativeReview);
|
|
824
|
+
// PROTOCOLIZE provisions the feature checkout after its coordinator
|
|
825
|
+
// Session has launched in the stable project root. Its transition Work
|
|
826
|
+
// must keep that physical route until the Stage is complete. Child Works
|
|
827
|
+
// share stage membership, not the root coordinator's execution directory.
|
|
828
|
+
if (work.parent_work_id === null && context.db.get("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = 'stage_work_memberships'")
|
|
829
|
+
&& context.db.get("SELECT 1 FROM stage_work_memberships WHERE project_id = ? AND run_id = ? AND work_id = ? AND stage IN ('specify','protocolize') LIMIT 1", [project.id, run.id, work.work_id]))
|
|
830
|
+
return projectRoot;
|
|
831
|
+
const merge = context.db.get("SELECT target_workspace FROM merge_requests WHERE executor_work_id = ? AND run_id = ?", [work.work_id, run.id]);
|
|
832
|
+
if (merge)
|
|
833
|
+
return resolveProjectRoot(merge.target_workspace);
|
|
834
|
+
if (context.db.get("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = 'work_launches'")) {
|
|
835
|
+
const launch = context.db.get("SELECT provider_session_id, profile_json FROM work_launches WHERE work_id = ? AND status IN ('launching', 'start_requested')", [work.work_id]);
|
|
836
|
+
if (launch?.provider_session_id === providerSessionId && run.run_root) {
|
|
837
|
+
const copy = JSON.parse(launch.profile_json).review_copy?.root;
|
|
838
|
+
if (copy) {
|
|
839
|
+
const target = path.resolve(run.run_root, copy);
|
|
840
|
+
const relative = path.relative(run.run_root, target);
|
|
841
|
+
if (relative === ".." || relative.startsWith(`..${path.sep}`))
|
|
842
|
+
throw new AppError("read_only_workspace_unbound", "Review copy escapes its owning RUN", 1);
|
|
843
|
+
return resolveProjectRoot(target);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
if (facts?.stageName === "specify" || facts?.stageName === "protocolize")
|
|
849
|
+
return projectRoot;
|
|
850
|
+
if (facts?.stageName === "merge" || facts?.invocation.operation === "merge_apply" || facts?.invocation.operation === "merge_repair") {
|
|
851
|
+
const pending = context.db.all("SELECT target_workspace FROM merge_requests WHERE project_id = ? AND run_id = ? AND status IN ('dispatching','active','action_required') ORDER BY updated_at DESC", [project.id, run.id]);
|
|
852
|
+
const targets = [...new Set(pending.map(item => resolveProjectRoot(item.target_workspace)))];
|
|
853
|
+
if (targets.length > 1)
|
|
854
|
+
throw new AppError("merge_workspace_ambiguous", "Current MERGE requests disagree on the native execution workspace", 1, { run_id: run.id, target_workspaces: targets });
|
|
855
|
+
if (targets.length === 1)
|
|
856
|
+
return targets[0];
|
|
857
|
+
}
|
|
858
|
+
return resolveProjectRoot(run.workspace_root);
|
|
859
|
+
}
|
|
860
|
+
function handleControlledToolEvent(context, input, event, harness) {
|
|
861
|
+
const agy = harness === "antigravity-cli";
|
|
862
|
+
const droid = harness === "droid-cli";
|
|
863
|
+
const label = droid ? "Droid" : agy ? "Antigravity" : "OpenCode";
|
|
864
|
+
const errorPrefix = droid ? "droid" : agy ? "agy" : "opencode";
|
|
584
865
|
const phase = stringValue(event.phase);
|
|
585
866
|
if (phase !== "before" && phase !== "after")
|
|
586
|
-
throw new AppError(
|
|
867
|
+
throw new AppError(`unsupported_${errorPrefix}_event_schema`, `${label} event phase must be before or after`, 1);
|
|
587
868
|
const session = objectRecord(event.session);
|
|
588
869
|
const profile = objectRecord(event.profile);
|
|
589
870
|
const rawInput = objectRecord(event.input);
|
|
@@ -591,16 +872,16 @@ export function handleOpenCodeEvent(context, input) {
|
|
|
591
872
|
const nativeParentId = stringValue(session.parent_provider_session_id);
|
|
592
873
|
const daemonId = stringValue(event.daemon_id);
|
|
593
874
|
const toolCallId = stringValue(event.tool_call_id);
|
|
594
|
-
const eventId = stringValue(event.event_id) ?? toolCallId;
|
|
875
|
+
const eventId = stringValue(event.event_id) ?? (droid ? undefined : toolCallId);
|
|
595
876
|
const toolName = stringValue(event.tool);
|
|
596
877
|
const directory = stringValue(session.directory);
|
|
597
|
-
if (!providerSessionId || !daemonId || !toolCallId || !eventId || !toolName || !directory || !path.isAbsolute(directory)) {
|
|
598
|
-
throw new AppError(
|
|
878
|
+
if (!providerSessionId || !daemonId || (!droid && !toolCallId) || !eventId || !toolName || !directory || !path.isAbsolute(directory)) {
|
|
879
|
+
throw new AppError(`${errorPrefix}_session_identity_invalid`, `${label} event is missing trusted physical identity`, 1);
|
|
599
880
|
}
|
|
600
881
|
const expectedRoot = resolveProjectRoot(input.projectRoot);
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
882
|
+
const observedCwd = resolveProjectRoot(directory);
|
|
883
|
+
if (droid && nativeParentId === providerSessionId)
|
|
884
|
+
throw new AppError("droid_session_identity_invalid", "A Droid Session cannot parent itself", 1);
|
|
604
885
|
const sessionId = storageSessionId(nativeSessionIdentity(harness, providerSessionId));
|
|
605
886
|
const parentSessionId = nativeParentId ? storageSessionId(nativeSessionIdentity(harness, nativeParentId)) : null;
|
|
606
887
|
const command = stringValue(rawInput.command) ?? stringValue(rawInput.cmd) ?? stringValue(rawInput.CommandLine);
|
|
@@ -610,67 +891,131 @@ export function handleOpenCodeEvent(context, input) {
|
|
|
610
891
|
const inserted = recordHookEvent(context, {
|
|
611
892
|
projectId: project.id, protocolId: null, harness, providerSessionId, parentSessionId,
|
|
612
893
|
daemonId, sessionId, agentId: stringValue(session.agent_id) ?? null, turnId: stringValue(event.message_id) ?? null,
|
|
613
|
-
eventName: "PostToolUse", toolName, status: "observed", payload: { session_id: providerSessionId, cwd:
|
|
614
|
-
eventKey: baseKey, matchKey: null, transcriptPath: null, cwd:
|
|
894
|
+
eventName: "PostToolUse", toolName, status: "observed", payload: { session_id: providerSessionId, cwd: observedCwd, tool_name: toolName, status: objectRecord(event.outcome).status },
|
|
895
|
+
eventKey: baseKey, matchKey: null, transcriptPath: droid ? stringValue(event.transcript_path) ?? null : null, cwd: observedCwd
|
|
615
896
|
});
|
|
616
897
|
return { ok: true, observed: inserted, duplicate: !inserted, event_key: baseKey, phase, harness, session: nativeSessionIdentity(harness, providerSessionId), ...(nativeParentId ? { parent_session: nativeSessionIdentity(harness, nativeParentId) } : {}) };
|
|
617
898
|
}
|
|
618
|
-
if (!command || !["bash", "Bash", "run_command", "run_terminal_command", "RunTerminalCommand"].includes(toolName)) {
|
|
899
|
+
if (!command || !(droid ? ["Execute"] : ["bash", "Bash", "shell", "run_command", "run_terminal_command", "RunTerminalCommand"]).includes(toolName)) {
|
|
619
900
|
return { ok: true, observed: false, reason: command ? "non_bash_tool" : "event_not_participating" };
|
|
620
901
|
}
|
|
621
902
|
const lifecycle = lifecycleFacts(command);
|
|
622
|
-
|
|
903
|
+
const auxiliary = continuationCommand(command);
|
|
904
|
+
if (!lifecycle && !auxiliary)
|
|
623
905
|
return { ok: true, observed: false, reason: "event_not_participating" };
|
|
624
|
-
|
|
625
|
-
|
|
906
|
+
// Syntax refusal precedes assignment/cwd lookup: an unsupported AST cannot
|
|
907
|
+
// supply a route or execution authority, including a short public Stage call.
|
|
908
|
+
const shellRefusal = lifecycle?.analysis.supported === false;
|
|
909
|
+
const proof = context.nativeHookProof;
|
|
910
|
+
if (proof && (proof.harness !== harness || proof.daemonId !== daemonId || proof.sessionId !== providerSessionId || proof.toolCallId !== toolCallId))
|
|
911
|
+
throw new AppError("native_hook_unproven", "Verified native Work callback differs from its physical identity", 1);
|
|
912
|
+
const nativeIdentity = { daemonId, rootSessionId: proof?.rootSessionId ?? nativeParentId ?? providerSessionId, sessionId: providerSessionId, parentSessionId: nativeParentId ?? null, toolCallId: toolCallId };
|
|
913
|
+
const provenScope = lifecycle && !shellRefusal ? managedNativeInvocationScope(context, expectedRoot, nativeIdentity, harness) : null;
|
|
914
|
+
const issuedRunId = lifecycle && !lifecycle.runId && !lifecycle.workId && !lifecycle.bootstrapStageStart
|
|
915
|
+
? observedLifecycleInvocation(context, command, provenScope ?? { projectRoot: expectedRoot, daemonId, rootSessionId: nativeIdentity.rootSessionId }, { identity: nativeIdentity })?.scope.runId : null;
|
|
916
|
+
if (!shellRefusal && lifecycle && lifecycle.invocation.operation.startsWith("stage_") && !lifecycle.bootstrapStageStart && !lifecycle.runId && !issuedRunId) {
|
|
917
|
+
throw new AppError("invocation_assignment_missing", "Short lifecycle call has no unique issued RUN assignment; its workspace cannot be inferred", 1, { effect: "no_effect", recoverable: false });
|
|
918
|
+
}
|
|
919
|
+
// These controlled platforms additionally qualify their native launch
|
|
920
|
+
// route against the stable project root when no CLI target is assigned.
|
|
921
|
+
const workspaceBound = requiresAssignedLifecycleCwd(lifecycle);
|
|
922
|
+
const expectedCwd = shellRefusal || !workspaceBound ? observedCwd : controlledLifecycleCwd(context, expectedRoot, lifecycle, providerSessionId, auxiliary, issuedRunId, observedCwd) ?? expectedRoot;
|
|
923
|
+
const executionRoute = shellRefusal || !workspaceBound ? undefined : lifecycle?.invocation.expectedCwd ?? auxiliary?.expectedCwd;
|
|
924
|
+
if (executionRoute && resolveProjectRoot(executionRoute) !== expectedCwd) {
|
|
925
|
+
throw new AppError(`${errorPrefix}_directory_mismatch`, "Literal shell cd route does not match the assigned lifecycle workspace", 1, { expected: expectedCwd, actual: executionRoute, actual_provenance: "shell_literal_route", effect: "no_effect", recoverable: false });
|
|
626
926
|
}
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
throw new AppError("project_mismatch", "OpenCode lifecycle command project root does not match the controlled workspace", 1, { expected: expectedRoot, actual: commandProjectRoot });
|
|
927
|
+
if (workspaceBound && !executionRoute && observedCwd !== expectedCwd) {
|
|
928
|
+
throw new AppError(`${errorPrefix}_directory_mismatch`, `${label} ${agy ? "launch route" : "Session directory"} does not match the assigned execution workspace`, 1, { expected: expectedCwd, actual: observedCwd, effect: "no_effect", recoverable: false, ...(agy ? { actual_provenance: stringValue(event.cwd_provenance) ?? "unknown" } : {}) });
|
|
630
929
|
}
|
|
631
|
-
const
|
|
930
|
+
const bootstrapStageStart = !shellRefusal && lifecycle?.bootstrapStageStart;
|
|
931
|
+
let matchKey = lifecycle && !shellRefusal ? lifecycleMatchKey(lifecycle, expectedRoot) : null;
|
|
632
932
|
if (bootstrapStageStart)
|
|
633
933
|
registerProject(context, { root: expectedRoot });
|
|
634
934
|
const project = requireProjectByRoot(context, expectedRoot);
|
|
935
|
+
if (droid) {
|
|
936
|
+
const existing = context.db.get("SELECT harness, provider_session_id, parent_session_id, daemon_id, match_key, tool_name FROM hook_events WHERE project_id = ? AND event_key = ?", [project.id, baseKey]);
|
|
937
|
+
if (existing && (existing.harness !== harness || existing.provider_session_id !== providerSessionId || existing.parent_session_id !== parentSessionId || existing.daemon_id !== daemonId || existing.match_key !== matchKey || existing.tool_name !== toolName)) {
|
|
938
|
+
throw new AppError("droid_event_replay_mismatch", "Droid event ID was already observed with different lifecycle identity", 1);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
635
941
|
const agentId = stringValue(session.agent_id);
|
|
636
|
-
const
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
942
|
+
const persist = (scope) => {
|
|
943
|
+
if (scope)
|
|
944
|
+
matchKey = lifecycleMatchKey(lifecycleFacts(scope.command), expectedRoot, scope);
|
|
945
|
+
assertHookEventReplay(context, { projectId: project.id, eventKey: baseKey, harness, providerSessionId, parentSessionId, daemonId, sessionId, turnId: stringValue(event.message_id) ?? null, eventName: "PreToolUse", toolName, matchKey, cwd: observedCwd });
|
|
946
|
+
const payload = {
|
|
947
|
+
session_id: providerSessionId, agent_id: agentId, cwd: observedCwd, tool_name: toolName, tool_input: rawInput, command,
|
|
948
|
+
...(agy ? { cwd_provenance: stringValue(event.cwd_provenance) ?? "unknown" } : {}),
|
|
949
|
+
...(executionRoute ? { lifecycle_execution_cwd: expectedCwd, lifecycle_execution_cwd_provenance: "literal_cd_intent_requires_cli_confirmation" } : {}),
|
|
950
|
+
provider: stringValue(profile.provider), model: stringValue(profile.model), reasoning: stringValue(droid ? profile.reasoning : profile.variant),
|
|
951
|
+
mode: stringValue(droid ? profile.mode : profile.agent), agent_type: stringValue(droid ? profile.agent_type : profile.agent)
|
|
952
|
+
};
|
|
953
|
+
const inserted = recordHookEvent(context, {
|
|
954
|
+
projectId: project.id, protocolId: null, harness, providerSessionId, parentSessionId,
|
|
955
|
+
daemonId, sessionId, agentId: agentId ?? null, turnId: stringValue(event.message_id) ?? null, eventName: "PreToolUse", toolName,
|
|
956
|
+
status: "observed", payload, eventKey: baseKey, matchKey, transcriptPath: droid ? stringValue(event.transcript_path) ?? null : null, cwd: observedCwd
|
|
957
|
+
});
|
|
958
|
+
recordFlowSessionObservation(context, { projectId: project.id, sessionId, cwd: observedCwd, toolName, eventKey: baseKey });
|
|
959
|
+
return inserted;
|
|
646
960
|
};
|
|
647
|
-
const
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
961
|
+
const result = (inserted) => ({ ok: true, observed: inserted, duplicate: !inserted, event_key: baseKey, phase, harness, session: nativeSessionIdentity(harness, providerSessionId), ...(nativeParentId ? { parent_session: nativeSessionIdentity(harness, nativeParentId) } : {}), daemon_id: daemonId });
|
|
962
|
+
if (lifecycle) {
|
|
963
|
+
let inserted = false;
|
|
964
|
+
const receipt = observeLifecycleInvocation(context, {
|
|
965
|
+
currentEventKey: baseKey,
|
|
966
|
+
command, harness, projectId: project.id, ...(!shellRefusal && workspaceBound ? { executionCwd: expectedCwd } : {}), identity: nativeIdentity,
|
|
967
|
+
recordReceipt: scope => { inserted = persist(scope); return baseKey; }
|
|
968
|
+
});
|
|
969
|
+
const admitted = { ...result(inserted), ...lifecycleObservationResult(receipt), ...(receipt.invocationId ? { invocation_id: receipt.invocationId } : {}) };
|
|
970
|
+
return agy || shellRefusal ? admitted : { ...admitted, hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { ...rawInput, command: receipt.eventKey ? commandWithHookEvent(command, receipt.eventKey) : command } } };
|
|
971
|
+
}
|
|
972
|
+
const inserted = context.db.writeTransaction(() => persist());
|
|
973
|
+
return agy ? result(inserted) : { ...result(inserted), hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: { ...rawInput, command: commandWithHookEvent(command, baseKey) } } };
|
|
655
974
|
}
|
|
656
975
|
/** Convert an Antigravity CLI tool hook into the shared lifecycle receipt. */
|
|
657
976
|
export function handleAgyEvent(context, input) {
|
|
977
|
+
const unrelated = nativeHookNonParticipation(["agy", "event"], input.stdin);
|
|
978
|
+
if (unrelated)
|
|
979
|
+
return unrelated;
|
|
658
980
|
const event = parseJsonObject(input.stdin || "{}", "Antigravity tool event");
|
|
659
981
|
if (event.schema_id !== "dd-flow/agy-tool-event@1")
|
|
660
982
|
throw new AppError("unsupported_agy_event_schema", "Unsupported Antigravity event schema", 1);
|
|
661
|
-
|
|
662
|
-
|
|
983
|
+
// workspacePaths lists available workspaces; its first element is not a
|
|
984
|
+
// native selected-cwd observation. The daemon's spawn cwd is a separate,
|
|
985
|
+
// launch-bound route, and the list only corroborates its membership.
|
|
986
|
+
const workspacePaths = Array.isArray(event.workspace_paths)
|
|
987
|
+
? event.workspace_paths.filter((value) => typeof value === "string" && path.isAbsolute(value)) : [];
|
|
988
|
+
const launchCwd = stringValue(event.launch_cwd);
|
|
989
|
+
const qualifiedLaunchCwd = launchCwd && path.isAbsolute(launchCwd) ? launchCwd : undefined;
|
|
990
|
+
const directory = qualifiedLaunchCwd ?? (workspacePaths.length === 1 ? workspacePaths[0] : undefined);
|
|
991
|
+
if (!directory && workspacePaths.length > 1)
|
|
992
|
+
throw new AppError("agy_workspace_route_unproven", "Antigravity did not identify the active route among multiple workspaces", 1, { workspace_paths: workspacePaths });
|
|
993
|
+
if (directory && qualifiedLaunchCwd && workspacePaths.length
|
|
994
|
+
&& !workspacePaths.some(value => resolveProjectRoot(value) === resolveProjectRoot(directory))) {
|
|
995
|
+
throw new AppError("agy_workspace_route_unproven", "Antigravity launch cwd is absent from the native workspace list", 1, { launch_cwd: directory, workspace_paths: workspacePaths });
|
|
996
|
+
}
|
|
663
997
|
const providerSessionId = stringValue(event.conversation_id);
|
|
664
998
|
const daemonId = stringValue(event.daemon_id);
|
|
665
999
|
const eventId = stringValue(event.event_id);
|
|
666
1000
|
const toolName = stringValue(event.tool);
|
|
667
1001
|
if (!directory || !providerSessionId || !daemonId || !eventId || !toolName)
|
|
668
1002
|
throw new AppError("agy_session_identity_invalid", "Antigravity event is missing trusted physical identity", 1);
|
|
1003
|
+
const rawInput = objectRecord(event.input);
|
|
1004
|
+
const command = stringValue(rawInput.command) ?? stringValue(rawInput.cmd) ?? stringValue(rawInput.CommandLine);
|
|
1005
|
+
if (command && (lifecycleFacts(command) || continuationCommand(command)) && !qualifiedLaunchCwd) {
|
|
1006
|
+
throw new AppError("agy_workspace_route_unproven", "Managed Antigravity call has no launch-bound execution cwd", 1, { phase: "prepare", effect: "no_effect", recoverable: false, workspace_paths: workspacePaths });
|
|
1007
|
+
}
|
|
1008
|
+
const invalidStep = !Number.isSafeInteger(event.step_index) || Number(event.step_index) < 0;
|
|
1009
|
+
const invalidExecution = event.execution_num !== undefined && (!Number.isSafeInteger(event.execution_num) || Number(event.execution_num) < 0);
|
|
1010
|
+
if (event.phase === "before" && command && (lifecycleFacts(command) || continuationCommand(command)) && (invalidStep || invalidExecution)) {
|
|
1011
|
+
throw new AppError("agy_native_call_identity_missing", "Participating Antigravity lifecycle calls require a native step identity and a valid execution identity when provided", 1, { phase: "prepare", effect: "no_effect", recoverable: false });
|
|
1012
|
+
}
|
|
669
1013
|
const translated = {
|
|
670
1014
|
schema_id: "dd-flow/opencode-tool-event@1", source_harness: "antigravity-cli", phase: event.phase, event_id: eventId,
|
|
671
1015
|
daemon_id: daemonId, tool_call_id: eventId, tool: toolName, input: event.input,
|
|
672
1016
|
message_id: event.step_index === undefined ? null : String(event.step_index),
|
|
673
1017
|
session: { provider_session_id: providerSessionId, parent_provider_session_id: event.parent_conversation_id, directory },
|
|
1018
|
+
cwd_provenance: qualifiedLaunchCwd ? "launch_bound" : "workspace_paths_singleton",
|
|
674
1019
|
profile: { provider: objectRecord(event.profile).provider, model: objectRecord(event.profile).model, variant: objectRecord(event.profile).reasoning, agent: objectRecord(event.profile).mode },
|
|
675
1020
|
outcome: { status: stringValue(event.error) ? "error" : "completed" }
|
|
676
1021
|
};
|
|
@@ -698,15 +1043,25 @@ export function isolatedDdFlowHomeFromHookStdin(stdin) {
|
|
|
698
1043
|
* hook contract intentionally narrow: unwrap only that whole-command form,
|
|
699
1044
|
* then apply the same lifecycle parser to its actual shell payload.
|
|
700
1045
|
*/
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
1046
|
+
/**
|
|
1047
|
+
* Adds the receipt to the command that the shell will execute. Heredoc input
|
|
1048
|
+
* belongs to that command too, but an option appended after its delimiter is a
|
|
1049
|
+
* second shell command rather than an argument, so insert it before `<<`.
|
|
1050
|
+
*/
|
|
1051
|
+
export function commandWithHookEvent(command, eventKey) {
|
|
706
1052
|
const analysis = parseLifecycleCommand(command);
|
|
707
|
-
if (analysis.kind
|
|
1053
|
+
if (analysis.kind === "none" || analysis.supported === false)
|
|
1054
|
+
return command;
|
|
1055
|
+
if (commandHasOption(analysis.invocation, "hook-event-id")) {
|
|
708
1056
|
return command;
|
|
709
|
-
|
|
1057
|
+
}
|
|
1058
|
+
const option = `--hook-event-id '${eventKey.replaceAll("'", "'\\''")}'`;
|
|
1059
|
+
if (analysis.invocation.rewriteSafe) {
|
|
1060
|
+
const index = analysis.invocation.sourceRange?.end ?? shellSuffixOffset(command);
|
|
1061
|
+
const suffix = command.slice(index);
|
|
1062
|
+
return `${command.slice(0, index)} ${option}${suffix ? /^\s/.test(suffix) ? suffix : ` ${suffix}` : ""}`;
|
|
1063
|
+
}
|
|
1064
|
+
return command;
|
|
710
1065
|
}
|
|
711
1066
|
/**
|
|
712
1067
|
* Recognise the small shell surface used by generated dd-flow lifecycle
|
|
@@ -723,29 +1078,47 @@ export function analyzeLifecycleShellCommand(command, includeSessionRegister = f
|
|
|
723
1078
|
}
|
|
724
1079
|
function lifecycleFacts(command) {
|
|
725
1080
|
const analysis = parseLifecycleCommand(command);
|
|
1081
|
+
if (analysis.kind === "none" && analysis.unresolved && ["lifecycle", "auxiliary"].includes(analysis.participation))
|
|
1082
|
+
throw new AppError("lifecycle_shell_syntax_invalid", "Known lifecycle execution is unsupported; use a literal issued command", 1, { phase: "prepare", effect: "no_effect", recoverable: true, disposition: "pre_cli_shell_refusal", reason: analysis.reason });
|
|
726
1083
|
if (analysis.kind === "none")
|
|
727
1084
|
return null;
|
|
728
1085
|
const invocation = analysis.invocation;
|
|
729
1086
|
const stageStart = invocation.operation === "stage_start";
|
|
1087
|
+
const stageFinish = invocation.operation === "stage_finish";
|
|
1088
|
+
const stagePause = invocation.operation === "stage_pause";
|
|
730
1089
|
const stageResume = invocation.operation === "stage_resume";
|
|
731
1090
|
const workStart = invocation.operation === "work_start";
|
|
1091
|
+
const workFinish = invocation.operation === "work_finish";
|
|
1092
|
+
const workFail = invocation.operation === "work_fail";
|
|
1093
|
+
const delivery = invocation.operation.startsWith("delivery_");
|
|
732
1094
|
return {
|
|
733
1095
|
analysis,
|
|
734
1096
|
invocation,
|
|
735
|
-
flowPayload: flowSessionPayloadFromRegisterCommand(invocation),
|
|
736
1097
|
stageStart,
|
|
1098
|
+
stageFinish,
|
|
1099
|
+
stagePause,
|
|
737
1100
|
stageResume,
|
|
738
|
-
workStart,
|
|
1101
|
+
workStart, workFinish, workFail,
|
|
739
1102
|
bootstrapStageStart: stageStart && commandHasOption(invocation, "bootstrap"),
|
|
740
1103
|
commandProjectRoot: commandOption(invocation, "project-root"),
|
|
741
|
-
stageName: stageStart || stageResume ? commandOption(invocation, "stage") : undefined,
|
|
742
|
-
runId: stageStart || stageResume ? commandPosition(invocation, 0) : undefined,
|
|
743
|
-
workId: stageResume ? commandOption(invocation, "work") : workStart ? commandPosition(invocation, 0) : undefined,
|
|
1104
|
+
stageName: stageStart || stageFinish || stagePause || stageResume || delivery ? commandOption(invocation, "stage") : undefined,
|
|
1105
|
+
runId: stageStart || stageFinish || stagePause || stageResume || delivery ? commandPosition(invocation, 0) : undefined,
|
|
1106
|
+
workId: stagePause || stageResume ? commandOption(invocation, "work") : workStart || workFinish || workFail ? commandPosition(invocation, 0) : undefined,
|
|
744
1107
|
contextSha256: commandOption(invocation, "context-sha256")
|
|
745
1108
|
};
|
|
746
1109
|
}
|
|
747
|
-
function lifecycleMatchKey(facts, projectRoot) {
|
|
748
|
-
|
|
1110
|
+
function lifecycleMatchKey(facts, projectRoot, scope) {
|
|
1111
|
+
// Observe the actual argument without validating it. A mistyped project
|
|
1112
|
+
// must not leave a receipt matching a later, corrected non-rewriting call.
|
|
1113
|
+
if (!scope && facts.commandProjectRoot && canonicalPath(facts.commandProjectRoot, false) !== canonicalPath(projectRoot, false))
|
|
1114
|
+
return null;
|
|
1115
|
+
const { invocation, bootstrapStageStart, stageStart, stageFinish, stagePause, stageResume, workStart, workFinish, workFail, stageName, runId, workId, contextSha256 } = facts;
|
|
1116
|
+
const effectiveRoot = scope?.projectRoot ?? projectRoot;
|
|
1117
|
+
const effectiveRunId = scope?.runId ?? runId;
|
|
1118
|
+
if (invocation.operation === "recovery_accept") {
|
|
1119
|
+
const run = commandPosition(invocation, 0), recovery = commandOption(invocation, "recovery-id");
|
|
1120
|
+
return (scope?.runId ?? run) && recovery ? recoveryAcceptMatchKey((scope?.runId ?? run), recovery, effectiveRoot) : null;
|
|
1121
|
+
}
|
|
749
1122
|
if (bootstrapStageStart)
|
|
750
1123
|
return bootstrapMatchKey({
|
|
751
1124
|
projectRoot,
|
|
@@ -753,14 +1126,100 @@ function lifecycleMatchKey(facts, projectRoot) {
|
|
|
753
1126
|
intakeMode: contextSha256 ? "context" : commandHasOption(invocation, "intake-stdin") ? "stdin" : "file",
|
|
754
1127
|
...(contextSha256 ? { contextSha256 } : {})
|
|
755
1128
|
});
|
|
756
|
-
if (stageStart &&
|
|
757
|
-
return stageStartMatchKey(
|
|
758
|
-
if (
|
|
759
|
-
return
|
|
1129
|
+
if (stageStart && effectiveRunId && stageName)
|
|
1130
|
+
return stageStartMatchKey(effectiveRunId, stageName, effectiveRoot, contextSha256);
|
|
1131
|
+
if (stageFinish && effectiveRunId && stageName)
|
|
1132
|
+
return stageLifecycleMatchKey("stage_finish", effectiveRunId, stageName, effectiveRoot);
|
|
1133
|
+
if (stagePause && effectiveRunId && stageName && workId)
|
|
1134
|
+
return stageLifecycleMatchKey("stage_pause", effectiveRunId, stageName, effectiveRoot, workId);
|
|
1135
|
+
if (stageResume && effectiveRunId && stageName && workId)
|
|
1136
|
+
return stageResumeMatchKey(effectiveRunId, stageName, workId, effectiveRoot);
|
|
760
1137
|
if (workStart && workId)
|
|
761
|
-
return workStartMatchKey(workId,
|
|
1138
|
+
return workStartMatchKey(workId, effectiveRoot, commandOption(invocation, "recovery-id"));
|
|
1139
|
+
if (workFinish && workId)
|
|
1140
|
+
return workLifecycleMatchKey("work_finish", workId, effectiveRoot);
|
|
1141
|
+
if (workFail && workId)
|
|
1142
|
+
return workLifecycleMatchKey("work_fail", workId, effectiveRoot);
|
|
762
1143
|
return null;
|
|
763
1144
|
}
|
|
1145
|
+
/** Observe exact immutable command attribution before reading model-supplied
|
|
1146
|
+
* result files. This does not claim or authorize the lifecycle mutation. */
|
|
1147
|
+
export function observeCommandHook(context, command) {
|
|
1148
|
+
const facts = lifecycleFacts(command);
|
|
1149
|
+
if (!facts || facts.analysis.kind !== "standalone" || !facts.commandProjectRoot)
|
|
1150
|
+
return;
|
|
1151
|
+
const project = context.db.get("SELECT id FROM projects WHERE root = ?", [resolveProjectRoot(facts.commandProjectRoot)]);
|
|
1152
|
+
if (!project)
|
|
1153
|
+
return; // Bootstrap has not registered a project yet.
|
|
1154
|
+
const matchKey = lifecycleMatchKey(facts, facts.commandProjectRoot);
|
|
1155
|
+
if (!matchKey)
|
|
1156
|
+
return;
|
|
1157
|
+
let key = commandOption(facts.invocation, "hook-event-id");
|
|
1158
|
+
if (!key && commandOption(facts.invocation, "invocation-id"))
|
|
1159
|
+
return;
|
|
1160
|
+
if (!key) {
|
|
1161
|
+
try {
|
|
1162
|
+
key = findRecentMatchingHookEvent(context, { projectId: project.id, matchKey, errorCode: "trusted_session_binding_required", operation: facts.invocation.operation }).eventKey;
|
|
1163
|
+
}
|
|
1164
|
+
catch {
|
|
1165
|
+
return;
|
|
1166
|
+
} // Dispatch still owns its mandatory admission error.
|
|
1167
|
+
}
|
|
1168
|
+
const event = context.db.get("SELECT id FROM hook_events WHERE project_id = ? AND event_key = ? AND match_key = ? AND status = 'observed'", [project.id, key, matchKey]);
|
|
1169
|
+
if (event)
|
|
1170
|
+
context.observeLifecycleHook?.(project.id, event.id);
|
|
1171
|
+
}
|
|
1172
|
+
export function stageLifecycleMatchKey(operation, runId, stage, projectRoot, workId) {
|
|
1173
|
+
return crypto.createHash("sha256").update(JSON.stringify({ operation, run_id: runId, stage, project_root: resolveProjectRoot(projectRoot), ...(workId ? { work_id: workId } : {}) })).digest("hex");
|
|
1174
|
+
}
|
|
1175
|
+
export function recoveryAcceptMatchKey(runId, recoveryId, projectRoot) {
|
|
1176
|
+
return crypto.createHash("sha256").update(JSON.stringify({ operation: "recovery_accept", run_id: runId, recovery_id: recoveryId, project_root: resolveProjectRoot(projectRoot) })).digest("hex");
|
|
1177
|
+
}
|
|
1178
|
+
export function workLifecycleMatchKey(operation, workId, projectRoot) {
|
|
1179
|
+
return crypto.createHash("sha256").update(JSON.stringify({ operation, work_id: workId, project_root: resolveProjectRoot(projectRoot) })).digest("hex");
|
|
1180
|
+
}
|
|
1181
|
+
function workLifecycleMatchKeys(operation, workId, projectRoot) {
|
|
1182
|
+
return entityReferenceVariants(workId).map((reference) => workLifecycleMatchKey(operation, reference, projectRoot));
|
|
1183
|
+
}
|
|
1184
|
+
function stageLifecycleMatchKeys(operation, runId, stage, projectRoot, workId) {
|
|
1185
|
+
const runReferences = entityReferenceVariants(runId);
|
|
1186
|
+
const workReferences = workId ? entityReferenceVariants(workId) : [undefined];
|
|
1187
|
+
return [...new Set(runReferences.flatMap((runReference) => workReferences.map((workReference) => stageLifecycleMatchKey(operation, runReference, stage, projectRoot, workReference))))];
|
|
1188
|
+
}
|
|
1189
|
+
/** Claims a Work terminal command only after its exact hook receipt has proved
|
|
1190
|
+
* the physical Session that issued it. Work code performs the owner check. */
|
|
1191
|
+
export function claimWorkLifecycleHookEvent(context, input) {
|
|
1192
|
+
const matchKeys = workLifecycleMatchKeys(input.operation, input.workId, input.projectRoot);
|
|
1193
|
+
const eventKey = input.eventKey ?? findRecentMatchingHookEvent(context, {
|
|
1194
|
+
projectId: input.projectId, matchKey: matchKeys, errorCode: "trusted_session_binding_required", operation: input.operation.replace("_", " ")
|
|
1195
|
+
}).eventKey;
|
|
1196
|
+
const event = context.db.get("SELECT id, session_id, match_key, status FROM hook_events WHERE project_id = ? AND event_key = ?", [input.projectId, eventKey]);
|
|
1197
|
+
if (!event?.session_id || event.status !== "observed" || !event.match_key || !matchKeys.includes(event.match_key)) {
|
|
1198
|
+
const reason = !event ? "event_missing" : !event.session_id ? "identity_missing" : event.status !== "observed" ? "event_consumed" : "target_mismatch";
|
|
1199
|
+
throw new AppError("trusted_session_binding_required", `${input.operation.replace("_", " ")} requires one fresh matching PreToolUse hook event`, 1, { work_id: input.workId, event_key: eventKey, reason, observed_status: event?.status ?? null });
|
|
1200
|
+
}
|
|
1201
|
+
const claimed = context.db.run("UPDATE hook_events SET status = 'claimed', claimed_at = ? WHERE id = ? AND status = 'observed'", [context.now(), event.id]);
|
|
1202
|
+
if (claimed.changes !== 1)
|
|
1203
|
+
throw new AppError("trusted_session_binding_required", "matching Work lifecycle hook event was already claimed", 1, { work_id: input.workId });
|
|
1204
|
+
return hookSessionIdentity(context, input.projectId, eventKey);
|
|
1205
|
+
}
|
|
1206
|
+
/** Claims a lifecycle command only after its exact hook receipt has proved the
|
|
1207
|
+
* physical Session that issued it. Stage code performs the Work-owner check. */
|
|
1208
|
+
export function claimStageLifecycleHookEvent(context, input) {
|
|
1209
|
+
const matchKeys = stageLifecycleMatchKeys(input.operation, input.runId, input.stage, input.projectRoot, input.workId);
|
|
1210
|
+
const eventKey = input.eventKey ?? findRecentMatchingHookEvent(context, {
|
|
1211
|
+
projectId: input.projectId, matchKey: matchKeys, errorCode: "trusted_session_binding_required", operation: input.operation.replace("_", " ")
|
|
1212
|
+
}).eventKey;
|
|
1213
|
+
const event = context.db.get("SELECT id, session_id, match_key, status FROM hook_events WHERE project_id = ? AND event_key = ?", [input.projectId, eventKey]);
|
|
1214
|
+
if (!event?.session_id || event.status !== "observed" || !event.match_key || !matchKeys.includes(event.match_key)) {
|
|
1215
|
+
const reason = !event ? "event_missing" : !event.session_id ? "identity_missing" : event.status !== "observed" ? "event_consumed" : "target_mismatch";
|
|
1216
|
+
throw new AppError("trusted_session_binding_required", `${input.operation.replace("_", " ")} requires one fresh matching PreToolUse hook event`, 1, { run_id: input.runId, stage: input.stage, event_key: eventKey, reason, observed_status: event?.status ?? null });
|
|
1217
|
+
}
|
|
1218
|
+
const claimed = context.db.run("UPDATE hook_events SET status = 'claimed', claimed_at = ? WHERE id = ? AND status = 'observed'", [context.now(), event.id]);
|
|
1219
|
+
if (claimed.changes !== 1)
|
|
1220
|
+
throw new AppError("trusted_session_binding_required", "matching lifecycle hook event was already claimed", 1, { run_id: input.runId, stage: input.stage });
|
|
1221
|
+
return hookSessionIdentity(context, input.projectId, eventKey);
|
|
1222
|
+
}
|
|
764
1223
|
/** Trusted session identity is created by PreToolUse, never supplied by an agent. */
|
|
765
1224
|
export function sessionIdForHookEvent(context, projectId, eventKey) {
|
|
766
1225
|
const event = context.db.get("SELECT session_id FROM hook_events WHERE project_id = ? AND event_key = ?", [projectId, eventKey]);
|
|
@@ -776,6 +1235,7 @@ export function hookSessionIdentity(context, projectId, eventKey) {
|
|
|
776
1235
|
WHERE he.project_id = ? AND he.event_key = ?`, [projectId, eventKey]);
|
|
777
1236
|
if (!event?.session_id)
|
|
778
1237
|
throw new AppError("hook_event_not_found", "stage start requires a trusted PreToolUse hook event", 1, { event_key: eventKey });
|
|
1238
|
+
context.observeLifecycleHook?.(projectId, event.id);
|
|
779
1239
|
return {
|
|
780
1240
|
hookEventId: event.id,
|
|
781
1241
|
harness: event.harness,
|
|
@@ -812,21 +1272,30 @@ export function claimRecentMatchingHookEvent(context, input) {
|
|
|
812
1272
|
/** Finds the trusted event that a stage- or Work-specific claimant will atomically claim. */
|
|
813
1273
|
export function findRecentMatchingHookEvent(context, input) {
|
|
814
1274
|
const freshAfter = new Date(Date.parse(context.now()) - 60_000).toISOString();
|
|
815
|
-
const
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
1275
|
+
const matchKeys = Array.isArray(input.matchKey) ? [...new Set(input.matchKey)] : [input.matchKey];
|
|
1276
|
+
const placeholders = matchKeys.map(() => "?").join(", ");
|
|
1277
|
+
const find = () => context.db.all(`SELECT id, event_key FROM hook_events
|
|
1278
|
+
WHERE project_id = ? AND match_key IN (${placeholders}) AND status = 'observed' AND created_at >= ?
|
|
1279
|
+
AND NOT (COALESCE(json_extract(outcome_json, '$.effect'), '') = 'no_effect'
|
|
1280
|
+
AND COALESCE(json_extract(outcome_json, '$.disposition'), '') IN ('correctable', 'fatal'))
|
|
1281
|
+
AND harness IN ('codex-desktop', 'antigravity-cli')
|
|
1282
|
+
AND (? IS NULL OR session_id = ?)
|
|
1283
|
+
ORDER BY id DESC LIMIT 2`, [input.projectId, ...matchKeys, freshAfter, input.sessionId ?? null, input.sessionId ?? null]);
|
|
1284
|
+
let events = find();
|
|
1285
|
+
// Only the qualified non-rewriting transports may correlate the original
|
|
1286
|
+
// command: Codex Desktop and Antigravity. New harnesses fail closed by default.
|
|
821
1287
|
const deadline = Date.now() + 250;
|
|
822
|
-
while (
|
|
1288
|
+
while (events.length === 0 && Date.now() < deadline) {
|
|
823
1289
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
|
|
824
|
-
|
|
1290
|
+
events = find();
|
|
825
1291
|
}
|
|
826
|
-
if (!
|
|
827
|
-
throw new AppError(input.errorCode, `${input.operation} requires
|
|
1292
|
+
if (events.length === 0 || !events[0]?.event_key) {
|
|
1293
|
+
throw new AppError(input.errorCode, `${input.operation} requires a trusted lifecycle receipt. Check native hook delivery; managed commands are admitted from committed native identity, not from model-supplied invocation IDs or updatedInput.`, 1, { reason: "event_missing" });
|
|
828
1294
|
}
|
|
829
|
-
|
|
1295
|
+
if (events.length > 1) {
|
|
1296
|
+
throw new AppError("ambiguous_lifecycle_receipt", `${input.operation} has more than one fresh matching lifecycle event; stop and let the controller reconcile native delivery`, 1, { reason: "multiple_live_events", match_keys: matchKeys, event_keys: events.map((event) => event.event_key) });
|
|
1297
|
+
}
|
|
1298
|
+
return { id: events[0].id, eventKey: events[0].event_key };
|
|
830
1299
|
}
|
|
831
1300
|
/** Fallback for Desktop paths that execute the original command after a hook rewrite. */
|
|
832
1301
|
export function claimBootstrapHookEvent(context, input) {
|
|
@@ -849,9 +1318,9 @@ export function bootstrapMatchKey(input) {
|
|
|
849
1318
|
})).digest("hex");
|
|
850
1319
|
}
|
|
851
1320
|
export function claimWorkStartHookEvent(context, input) {
|
|
852
|
-
const expected =
|
|
1321
|
+
const expected = entityReferenceVariants(input.workId).map((reference) => workStartMatchKey(reference, input.projectRoot, input.recoveryId));
|
|
853
1322
|
const event = context.db.get("SELECT id, session_id, agent_id, turn_id, transcript_path, model, agent_type, match_key, status FROM hook_events WHERE project_id = ? AND event_key = ?", [input.projectId, input.eventKey]);
|
|
854
|
-
if (!event?.session_id || event.status !== "observed" || !event.match_key || !expected.
|
|
1323
|
+
if (!event?.session_id || event.status !== "observed" || !event.match_key || !expected.includes(event.match_key)) {
|
|
855
1324
|
throw new AppError("trusted_work_launch_required", "work start requires one fresh matching PreToolUse event", 1, { work_id: input.workId, event_key: input.eventKey });
|
|
856
1325
|
}
|
|
857
1326
|
const claimed = context.db.run("UPDATE hook_events SET status = 'claimed', claimed_at = ? WHERE id = ? AND status = 'observed'", [context.now(), event.id]);
|
|
@@ -859,15 +1328,16 @@ export function claimWorkStartHookEvent(context, input) {
|
|
|
859
1328
|
throw new AppError("trusted_work_launch_required", "matching work-start hook event was already claimed", 1, { work_id: input.workId });
|
|
860
1329
|
return hookSessionIdentity(context, input.projectId, input.eventKey);
|
|
861
1330
|
}
|
|
862
|
-
function shortWorkReference(id) { return /^WRK-\d{3,}(?:-|$)/.exec(id)?.[0]?.replace(/-$/, "") ?? id; }
|
|
863
1331
|
/** Claims the exact non-bootstrap stage entry which starts a coordinator Work. */
|
|
864
1332
|
export function assertStageStartHookEvent(context, input) {
|
|
865
|
-
const expected =
|
|
1333
|
+
const expected = entityReferenceVariants(input.runId).map((reference) => stageStartMatchKey(reference, input.stage, input.projectRoot, input.contextSha256));
|
|
866
1334
|
const event = context.db.get("SELECT session_id, match_key, status FROM hook_events WHERE project_id = ? AND event_key = ?", [input.projectId, input.eventKey]);
|
|
867
|
-
if (!event?.session_id || event.status !== "observed" || event.match_key
|
|
868
|
-
throw new AppError("trusted_stage_launch_required", "stage start requires one fresh matching PreToolUse event", 1, { run_id: input.runId, stage: input.stage, event_key: input.eventKey,
|
|
1335
|
+
if (!event?.session_id || event.status !== "observed" || !event.match_key || !expected.includes(event.match_key)) {
|
|
1336
|
+
throw new AppError("trusted_stage_launch_required", "stage start requires one fresh matching PreToolUse event", 1, { run_id: input.runId, stage: input.stage, event_key: input.eventKey, expected_match_keys: expected, observed_match_key: event?.match_key ?? null, observed_status: event?.status ?? null });
|
|
869
1337
|
}
|
|
870
|
-
|
|
1338
|
+
const identity = hookSessionIdentity(context, input.projectId, input.eventKey);
|
|
1339
|
+
assertRecoveryHookCurrent(context, input.projectId, input.runId, identity);
|
|
1340
|
+
return identity;
|
|
871
1341
|
}
|
|
872
1342
|
export function claimStageStartHookEvent(context, input) {
|
|
873
1343
|
assertStageStartHookEvent(context, input);
|
|
@@ -897,24 +1367,25 @@ export function stageResumeMatchKey(runId, stage, workId, projectRoot) {
|
|
|
897
1367
|
}
|
|
898
1368
|
/** Claims the resume command observed in the Session that received the user answer. */
|
|
899
1369
|
export function claimStageResumeHookEvent(context, input) {
|
|
900
|
-
const
|
|
901
|
-
const
|
|
902
|
-
const event = input.eventKey
|
|
903
|
-
|
|
904
|
-
: context.db.get("SELECT id, event_key, session_id, match_key, status FROM hook_events WHERE project_id = ? AND session_id = ? AND match_key = ? AND status = 'observed' AND created_at >= ? ORDER BY id DESC LIMIT 1", [input.projectId, input.sessionId ?? "", matchKey, freshAfter]);
|
|
905
|
-
if (!event?.event_key || !event.session_id || event.match_key !== matchKey || event.status !== "observed") {
|
|
1370
|
+
const matchKeys = entityReferenceVariants(input.runId).flatMap((runReference) => entityReferenceVariants(input.workId).map((workReference) => stageResumeMatchKey(runReference, input.stage, workReference, input.projectRoot)));
|
|
1371
|
+
const eventKey = input.eventKey ?? findRecentMatchingHookEvent(context, { projectId: input.projectId, matchKey: matchKeys, sessionId: input.sessionId ?? "", errorCode: "trusted_stage_resume_required", operation: "stage resume" }).eventKey;
|
|
1372
|
+
const event = context.db.get("SELECT id, event_key, session_id, match_key, status FROM hook_events WHERE project_id = ? AND event_key = ?", [input.projectId, eventKey]);
|
|
1373
|
+
if (!event?.event_key || !event.session_id || !event.match_key || !matchKeys.includes(event.match_key) || event.status !== "observed") {
|
|
906
1374
|
throw new AppError("trusted_stage_resume_required", "stage resume requires one fresh matching PreToolUse event", 1, { run_id: input.runId, stage: input.stage, work_id: input.workId });
|
|
907
1375
|
}
|
|
1376
|
+
const identity = hookSessionIdentity(context, input.projectId, event.event_key);
|
|
1377
|
+
assertRecoveryHookCurrent(context, input.projectId, input.runId, identity);
|
|
908
1378
|
const claimed = context.db.run("UPDATE hook_events SET status = 'claimed', claimed_at = ? WHERE id = ? AND status = 'observed'", [context.now(), event.id]);
|
|
909
1379
|
if (claimed.changes !== 1)
|
|
910
1380
|
throw new AppError("trusted_stage_resume_required", "matching stage-resume hook event was already claimed", 1, { run_id: input.runId, stage: input.stage });
|
|
911
|
-
return
|
|
1381
|
+
return identity;
|
|
912
1382
|
}
|
|
913
|
-
export function workStartMatchKey(workId, projectRoot) {
|
|
1383
|
+
export function workStartMatchKey(workId, projectRoot, recoveryId) {
|
|
914
1384
|
return crypto.createHash("sha256").update(JSON.stringify({
|
|
915
1385
|
operation: "work_start",
|
|
916
1386
|
work_id: workId,
|
|
917
|
-
project_root: resolveProjectRoot(projectRoot)
|
|
1387
|
+
project_root: resolveProjectRoot(projectRoot),
|
|
1388
|
+
...(recoveryId ? { recovery_id: recoveryId } : {})
|
|
918
1389
|
})).digest("hex");
|
|
919
1390
|
}
|
|
920
1391
|
export function hookStatusForProject(context, projectId) {
|
|
@@ -954,6 +1425,8 @@ export function codexHookEventsForProject(context, projectId) {
|
|
|
954
1425
|
FROM hook_events WHERE project_id = ? ORDER BY created_at DESC, id DESC LIMIT 20`, [projectId]);
|
|
955
1426
|
}
|
|
956
1427
|
function resolveProfileName(profile) {
|
|
1428
|
+
if (profile !== undefined && (typeof profile !== "string" || !profile.trim()))
|
|
1429
|
+
throw new AppError("validation", "--profile must be nonempty text", 2);
|
|
957
1430
|
return sanitizeProfile(profile ?? defaultProfileName);
|
|
958
1431
|
}
|
|
959
1432
|
function sanitizeProfile(value) {
|
|
@@ -981,11 +1454,12 @@ function readTomlIfExists(file) {
|
|
|
981
1454
|
if (!fs.existsSync(file)) {
|
|
982
1455
|
return {};
|
|
983
1456
|
}
|
|
1457
|
+
const { text } = prepareTextFile(file, "Codex config");
|
|
984
1458
|
try {
|
|
985
|
-
return parse(
|
|
1459
|
+
return parse(text);
|
|
986
1460
|
}
|
|
987
1461
|
catch (error) {
|
|
988
|
-
throw new AppError("validation", `Invalid TOML in ${file}
|
|
1462
|
+
throw new AppError("validation", `Invalid TOML in ${file}`, 2, { file, ...(error instanceof TomlError ? { line: error.line, column: error.column } : {}) });
|
|
989
1463
|
}
|
|
990
1464
|
}
|
|
991
1465
|
function withManagedConfig(config, sourceHome, targetHome) {
|
|
@@ -1004,7 +1478,7 @@ function objectTable(value) {
|
|
|
1004
1478
|
function createSharedLinks(sourceHome, targetHome) {
|
|
1005
1479
|
const linked = [];
|
|
1006
1480
|
const skipped = [];
|
|
1007
|
-
for (const entry of
|
|
1481
|
+
for (const entry of codexHomeSharedEntries) {
|
|
1008
1482
|
const source = path.join(sourceHome, entry);
|
|
1009
1483
|
const target = path.join(targetHome, entry);
|
|
1010
1484
|
if (!fs.existsSync(source)) {
|
|
@@ -1027,7 +1501,7 @@ function createSharedLinks(sourceHome, targetHome) {
|
|
|
1027
1501
|
return { linked, skipped };
|
|
1028
1502
|
}
|
|
1029
1503
|
function removeSharedSymlinks(targetHome) {
|
|
1030
|
-
for (const entry of
|
|
1504
|
+
for (const entry of codexHomeSharedEntries) {
|
|
1031
1505
|
const target = path.join(targetHome, entry);
|
|
1032
1506
|
if (fs.existsSync(target) && fs.lstatSync(target).isSymbolicLink()) {
|
|
1033
1507
|
fs.rmSync(target);
|
|
@@ -1040,7 +1514,7 @@ function removeDirIfEmpty(dir) {
|
|
|
1040
1514
|
}
|
|
1041
1515
|
}
|
|
1042
1516
|
function sharedEntryStatus(sourceHome, targetHome) {
|
|
1043
|
-
return
|
|
1517
|
+
return codexHomeSharedEntries.map((entry) => {
|
|
1044
1518
|
const source = path.join(sourceHome, entry);
|
|
1045
1519
|
const target = path.join(targetHome, entry);
|
|
1046
1520
|
return {
|
|
@@ -1061,11 +1535,16 @@ function hookEntry(matcher, command, statusMessage) {
|
|
|
1061
1535
|
return { matcher, hooks: [commandHook(command, statusMessage)] };
|
|
1062
1536
|
}
|
|
1063
1537
|
function commandHook(command, statusMessage) {
|
|
1064
|
-
|
|
1538
|
+
// Hook admission is synchronous (the CLI must see a committed receipt), but
|
|
1539
|
+
// Admission keeps its own shared 20s deadline. Leave separate headroom for
|
|
1540
|
+
// the shell, cold CLI imports and bounded subprocess cleanup: the host must
|
|
1541
|
+
// not kill the hook before it can report its typed admission failure.
|
|
1542
|
+
return { type: "command", command, timeout: 60, statusMessage };
|
|
1065
1543
|
}
|
|
1066
1544
|
function hookCommand(event) {
|
|
1067
|
-
const
|
|
1068
|
-
|
|
1545
|
+
const args = `codex hook handle --event ${event} --json`;
|
|
1546
|
+
const handler = `if [ -n "\${DD_FLOW_BIN:-}" ]; then "$DD_FLOW_BIN" ${args}; else dd-flow ${args}; fi`;
|
|
1547
|
+
return `/bin/bash -c 'export PATH="\${DD_FLOW_HOME:+\${DD_FLOW_HOME}/bin:}\${PATH}:\${HOME}/Library/pnpm:/opt/homebrew/bin:/usr/local/bin"; log_dir="\${DD_FLOW_HOME:-\${HOME}/.dd-flow}/logs"; mkdir -p "$log_dir"; log="$log_dir/codex-hook.log"; printf "%s event=${event} phase=start pid=%s\\n" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$$" >>"$log"; ${handler} 2> >(tee -a "$log" >&2); status=$?; printf "%s event=${event} phase=finish pid=%s exit=%s\\n" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$$" "$status" >>"$log"; exit "$status"'`;
|
|
1069
1548
|
}
|
|
1070
1549
|
function resolveHookTarget(target) {
|
|
1071
1550
|
if (!target || target === "isolated") {
|
|
@@ -1240,11 +1719,10 @@ function recordHookInstallation(context, projectId, scope, configPath, installed
|
|
|
1240
1719
|
function installationScope(target, profile, hooksPath) {
|
|
1241
1720
|
return target === "isolated" ? `isolated:${profile}` : `default:${path.dirname(hooksPath)}`;
|
|
1242
1721
|
}
|
|
1243
|
-
function upsertSessionBindingFromPayload(context, project, sessionId, payload) {
|
|
1722
|
+
function upsertSessionBindingFromPayload(context, project, sessionId, payload, transcriptPath) {
|
|
1244
1723
|
const existing = sessionBinding(context, project.id, sessionId);
|
|
1245
1724
|
const now = context.now();
|
|
1246
1725
|
const cwd = stringValue(payload.cwd) ?? existing?.cwd ?? null;
|
|
1247
|
-
const transcriptPath = stringValue(payload.transcript_path) ?? existing?.transcript_path ?? locateCodexTranscript(sessionId, context.env.CODEX_HOME);
|
|
1248
1726
|
context.db.run(`INSERT INTO codex_session_bindings
|
|
1249
1727
|
(session_id, project_id, protocol_id, handshake_id, cwd, transcript_path, status, created_at, updated_at)
|
|
1250
1728
|
VALUES (?, ?, NULL, NULL, ?, ?, 'active', ?, ?)
|
|
@@ -1298,12 +1776,11 @@ function sessionBinding(context, projectId, sessionId) {
|
|
|
1298
1776
|
return context.db.get("SELECT * FROM codex_session_bindings WHERE project_id = ? AND session_id = ?", [projectId, sessionId]);
|
|
1299
1777
|
}
|
|
1300
1778
|
function recordHookEvent(context, input) {
|
|
1301
|
-
|
|
1302
|
-
if (existing)
|
|
1779
|
+
if (assertHookEventReplay(context, input))
|
|
1303
1780
|
return false;
|
|
1304
1781
|
context.db.run(`INSERT INTO hook_events
|
|
1305
|
-
|
|
1306
|
-
|
|
1782
|
+
(project_id, protocol_id, harness, provider_session_id, parent_session_id, daemon_id, session_id, agent_id, turn_id, transcript_path, provider, model, reasoning, mode, agent_type, cwd, event_key, match_key, event_name, tool_name, status, sanitized_summary, created_at)
|
|
1783
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
1307
1784
|
input.projectId,
|
|
1308
1785
|
input.protocolId,
|
|
1309
1786
|
input.harness,
|
|
@@ -1330,6 +1807,23 @@ function recordHookEvent(context, input) {
|
|
|
1330
1807
|
]);
|
|
1331
1808
|
return true;
|
|
1332
1809
|
}
|
|
1810
|
+
/** A duplicate delivery is harmless only when all durable identity facts match. */
|
|
1811
|
+
function assertHookEventReplay(context, input) {
|
|
1812
|
+
const existing = context.db.get(`SELECT harness, provider_session_id, parent_session_id, daemon_id, session_id, turn_id, event_name, tool_name, match_key, cwd
|
|
1813
|
+
FROM hook_events WHERE project_id = ? AND event_key = ?`, [input.projectId, input.eventKey]);
|
|
1814
|
+
if (existing) {
|
|
1815
|
+
const observed = {
|
|
1816
|
+
harness: input.harness, provider_session_id: input.providerSessionId, parent_session_id: input.parentSessionId,
|
|
1817
|
+
daemon_id: input.daemonId ?? null, session_id: input.sessionId, turn_id: input.turnId,
|
|
1818
|
+
event_name: input.eventName, tool_name: input.toolName, match_key: input.matchKey, cwd: input.cwd
|
|
1819
|
+
};
|
|
1820
|
+
if (Object.entries(observed).some(([key, value]) => existing[key] !== value)) {
|
|
1821
|
+
throw new AppError("hook_event_replay_mismatch", "Lifecycle event ID was already observed with different immutable identity", 1, { event_key: input.eventKey });
|
|
1822
|
+
}
|
|
1823
|
+
return true;
|
|
1824
|
+
}
|
|
1825
|
+
return false;
|
|
1826
|
+
}
|
|
1333
1827
|
function projectForHook(context, explicitRoot, cwd) {
|
|
1334
1828
|
if (explicitRoot)
|
|
1335
1829
|
return requireProjectByRoot(context, resolveProjectRoot(explicitRoot));
|
|
@@ -1356,14 +1850,6 @@ function hookEventKey(payload, eventName, toolName, command) {
|
|
|
1356
1850
|
command
|
|
1357
1851
|
})).digest("hex");
|
|
1358
1852
|
}
|
|
1359
|
-
function commandFromPayload(payload) {
|
|
1360
|
-
const direct = stringValue(payload.command);
|
|
1361
|
-
if (direct) {
|
|
1362
|
-
return direct;
|
|
1363
|
-
}
|
|
1364
|
-
const toolInput = objectRecord(payload.tool_input);
|
|
1365
|
-
return stringValue(toolInput.command) ?? stringValue(toolInput.cmd);
|
|
1366
|
-
}
|
|
1367
1853
|
function toolNameFromPayload(payload) {
|
|
1368
1854
|
const toolInput = objectRecord(payload.tool_input);
|
|
1369
1855
|
return stringValue(toolInput.name);
|
|
@@ -1406,10 +1892,10 @@ function writeJsonFile(file, value) {
|
|
|
1406
1892
|
ensureDir(path.dirname(file));
|
|
1407
1893
|
fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
|
|
1408
1894
|
}
|
|
1409
|
-
function backupFile(file, now) {
|
|
1895
|
+
function backupFile(file, now, text) {
|
|
1410
1896
|
const suffix = now.replace(/[:.]/g, "-");
|
|
1411
1897
|
const backup = `${file}.${suffix}.bak`;
|
|
1412
|
-
fs.
|
|
1898
|
+
fs.writeFileSync(backup, text, { mode: 0o600 });
|
|
1413
1899
|
return backup;
|
|
1414
1900
|
}
|
|
1415
1901
|
function hashObject(value) {
|