approval-md 0.1.0 → 0.3.0
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/README.md +629 -559
- package/SPEC.md +99 -24
- package/dist/src/adapters/agentmail.d.ts +426 -0
- package/dist/src/adapters/agentmail.js +2 -2
- package/dist/src/adapters/conformance.d.ts +149 -0
- package/dist/src/adapters/contract.d.ts +628 -0
- package/dist/src/adapters/contract.js +110 -16
- package/dist/src/adapters/contract.js.map +1 -1
- package/dist/src/adapters/email.d.ts +324 -0
- package/dist/src/adapters/env-passphrase.d.ts +93 -0
- package/dist/src/adapters/public.d.ts +11 -0
- package/dist/src/adapters/public.js +11 -0
- package/dist/src/adapters/public.js.map +1 -0
- package/dist/src/adapters/registry.d.ts +59 -0
- package/dist/src/adapters/registry.js +2 -1
- package/dist/src/adapters/registry.js.map +1 -1
- package/dist/src/adapters/smtp.d.ts +213 -0
- package/dist/src/adapters/vault-provider.d.ts +114 -0
- package/dist/src/adapters/vault-provider.js +3 -3
- package/dist/src/adapters/zzz.d.ts +66 -0
- package/dist/src/adapters/zzz.js +299 -0
- package/dist/src/adapters/zzz.js.map +1 -0
- package/dist/src/channels/batch.d.ts +109 -0
- package/dist/src/channels/cli.d.ts +193 -0
- package/dist/src/channels/conformance.d.ts +92 -0
- package/dist/src/channels/contract.d.ts +656 -0
- package/dist/src/channels/contract.js +200 -7
- package/dist/src/channels/contract.js.map +1 -1
- package/dist/src/channels/payload-view.d.ts +35 -0
- package/dist/src/channels/render-queue.d.ts +149 -0
- package/dist/src/channels/tagging.d.ts +196 -0
- package/dist/src/channels/telegram.d.ts +1944 -0
- package/dist/src/channels/telegram.js +218 -23
- package/dist/src/channels/telegram.js.map +1 -1
- package/dist/src/channels/web.d.ts +350 -0
- package/dist/src/channels/web.js +17 -0
- package/dist/src/channels/web.js.map +1 -1
- package/dist/src/cli/adapter.d.ts +90 -0
- package/dist/src/cli/adapter.js +25 -15
- package/dist/src/cli/adapter.js.map +1 -1
- package/dist/src/cli/amend.d.ts +59 -0
- package/dist/src/cli/amend.js +214 -30
- package/dist/src/cli/amend.js.map +1 -1
- package/dist/src/cli/args.d.ts +43 -0
- package/dist/src/cli/attest.d.ts +50 -0
- package/dist/src/cli/attest.js +134 -7
- package/dist/src/cli/attest.js.map +1 -1
- package/dist/src/cli/audit-card.d.ts +62 -0
- package/dist/src/cli/audit.d.ts +59 -0
- package/dist/src/cli/channel-telegram.d.ts +879 -0
- package/dist/src/cli/channel-telegram.js +311 -13
- package/dist/src/cli/channel-telegram.js.map +1 -1
- package/dist/src/cli/channel-web.d.ts +131 -0
- package/dist/src/cli/channel.d.ts +80 -0
- package/dist/src/cli/channel.js +9 -0
- package/dist/src/cli/channel.js.map +1 -1
- package/dist/src/cli/checkpoint-tap.d.ts +169 -0
- package/dist/src/cli/codex-bridge.d.ts +819 -0
- package/dist/src/cli/codex-bridge.js +1607 -0
- package/dist/src/cli/codex-bridge.js.map +1 -0
- package/dist/src/cli/codex.d.ts +2 -0
- package/dist/src/cli/codex.js +469 -0
- package/dist/src/cli/codex.js.map +1 -0
- package/dist/src/cli/coverage.d.ts +61 -0
- package/dist/src/cli/daemon.d.ts +120 -0
- package/dist/src/cli/daemon.js +4 -1
- package/dist/src/cli/daemon.js.map +1 -1
- package/dist/src/cli/doctor.d.ts +129 -0
- package/dist/src/cli/doctor.js +586 -17
- package/dist/src/cli/doctor.js.map +1 -1
- package/dist/src/cli/env.d.ts +65 -0
- package/dist/src/cli/execute.d.ts +202 -0
- package/dist/src/cli/execute.js +25 -2
- package/dist/src/cli/execute.js.map +1 -1
- package/dist/src/cli/exit-codes.d.ts +73 -0
- package/dist/src/cli/feedback.d.ts +60 -0
- package/dist/src/cli/gate-window.d.ts +40 -0
- package/dist/src/cli/gate.d.ts +68 -0
- package/dist/src/cli/git-scope.d.ts +190 -0
- package/dist/src/cli/gloss-attach.d.ts +85 -0
- package/dist/src/cli/gloss-codex-child.d.ts +9 -0
- package/dist/src/cli/gloss-codex.d.ts +24 -0
- package/dist/src/cli/gloss-options.d.ts +42 -0
- package/dist/src/cli/gloss.d.ts +265 -0
- package/dist/src/cli/help.d.ts +107 -0
- package/dist/src/cli/help.js +320 -93
- package/dist/src/cli/help.js.map +1 -1
- package/dist/src/cli/hook-codex.d.ts +126 -0
- package/dist/src/cli/hook-codex.js +226 -0
- package/dist/src/cli/hook-codex.js.map +1 -0
- package/dist/src/cli/hook.d.ts +787 -0
- package/dist/src/cli/hook.js +1235 -181
- package/dist/src/cli/hook.js.map +1 -1
- package/dist/src/cli/import.d.ts +35 -0
- package/dist/src/cli/import.js +1 -1
- package/dist/src/cli/import.js.map +1 -1
- package/dist/src/cli/init.d.ts +84 -0
- package/dist/src/cli/init.js +2 -2
- package/dist/src/cli/init.js.map +1 -1
- package/dist/src/cli/instructions.d.ts +23 -0
- package/dist/src/cli/journal.d.ts +41 -0
- package/dist/src/cli/log-advance.d.ts +287 -0
- package/dist/src/cli/log-advance.js +102 -11
- package/dist/src/cli/log-advance.js.map +1 -1
- package/dist/src/cli/log-anchor.d.ts +176 -0
- package/dist/src/cli/log-checkpoint.d.ts +22 -0
- package/dist/src/cli/log-sync.d.ts +243 -0
- package/dist/src/cli/log-verbs.d.ts +16 -0
- package/dist/src/cli/log-verbs.js +7 -1
- package/dist/src/cli/log-verbs.js.map +1 -1
- package/dist/src/cli/long-help.d.ts +70 -0
- package/dist/src/cli/main.d.ts +77 -0
- package/dist/src/cli/main.js +159 -7
- package/dist/src/cli/main.js.map +1 -1
- package/dist/src/cli/mcp.d.ts +52 -0
- package/dist/src/cli/paths.d.ts +56 -0
- package/dist/src/cli/payload.d.ts +58 -0
- package/dist/src/cli/policy-apply.d.ts +195 -0
- package/dist/src/cli/policy-apply.js +573 -0
- package/dist/src/cli/policy-apply.js.map +1 -0
- package/dist/src/cli/policy.d.ts +43 -0
- package/dist/src/cli/policy.js +14 -1
- package/dist/src/cli/policy.js.map +1 -1
- package/dist/src/cli/preflight.d.ts +501 -0
- package/dist/src/cli/preflight.js +689 -45
- package/dist/src/cli/preflight.js.map +1 -1
- package/dist/src/cli/progress.d.ts +78 -0
- package/dist/src/cli/prompt.d.ts +209 -0
- package/dist/src/cli/quickstart.d.ts +46 -0
- package/dist/src/cli/quickstart.js +297 -0
- package/dist/src/cli/quickstart.js.map +1 -0
- package/dist/src/cli/records.d.ts +34 -0
- package/dist/src/cli/render.d.ts +22 -0
- package/dist/src/cli/sandbox.d.ts +51 -0
- package/dist/src/cli/sandbox.js +17 -1
- package/dist/src/cli/sandbox.js.map +1 -1
- package/dist/src/cli/scaffold.d.ts +79 -0
- package/dist/src/cli/scaffold.js +1 -1
- package/dist/src/cli/setup-adapter.d.ts +137 -0
- package/dist/src/cli/setup-adapter.js +38 -4
- package/dist/src/cli/setup-adapter.js.map +1 -1
- package/dist/src/cli/setup-channel.d.ts +126 -0
- package/dist/src/cli/setup-channel.js +28 -1
- package/dist/src/cli/setup-channel.js.map +1 -1
- package/dist/src/cli/setup-checkpoint.d.ts +57 -0
- package/dist/src/cli/setup-common.d.ts +277 -0
- package/dist/src/cli/setup-common.js +3 -2
- package/dist/src/cli/setup-common.js.map +1 -1
- package/dist/src/cli/setup-flow.d.ts +287 -0
- package/dist/src/cli/setup-service.d.ts +96 -0
- package/dist/src/cli/setup.d.ts +204 -0
- package/dist/src/cli/setup.js +94 -2
- package/dist/src/cli/setup.js.map +1 -1
- package/dist/src/cli/style.d.ts +320 -0
- package/dist/src/cli/token.d.ts +39 -0
- package/dist/src/cli/up.d.ts +155 -0
- package/dist/src/cli/up.js +119 -53
- package/dist/src/cli/up.js.map +1 -1
- package/dist/src/cli/usage.d.ts +37 -0
- package/dist/src/cli/values.d.ts +40 -0
- package/dist/src/cli/values.js +3 -4
- package/dist/src/cli/values.js.map +1 -1
- package/dist/src/cli/vault.d.ts +59 -0
- package/dist/src/cli/vault.js +2 -2
- package/dist/src/cli/vault.js.map +1 -1
- package/dist/src/cli/verb-registry.d.ts +76 -0
- package/dist/src/cli/verb-registry.js +344 -11
- package/dist/src/cli/verb-registry.js.map +1 -1
- package/dist/src/cli/wordmark.d.ts +31 -0
- package/dist/src/cli/wordmark.js +2 -2
- package/dist/src/codex/broker.d.ts +229 -0
- package/dist/src/codex/broker.js +548 -0
- package/dist/src/codex/broker.js.map +1 -0
- package/dist/src/codex/doctor.d.ts +13 -0
- package/dist/src/codex/doctor.js +41 -0
- package/dist/src/codex/doctor.js.map +1 -0
- package/dist/src/codex/manifest.d.ts +49 -0
- package/dist/src/codex/manifest.js +103 -0
- package/dist/src/codex/manifest.js.map +1 -0
- package/dist/src/codex/runner.d.ts +178 -0
- package/dist/src/codex/runner.js +231 -0
- package/dist/src/codex/runner.js.map +1 -0
- package/dist/src/codex/serve.d.ts +56 -0
- package/dist/src/codex/serve.js +98 -0
- package/dist/src/codex/serve.js.map +1 -0
- package/dist/src/codex/templates.d.ts +41 -0
- package/dist/src/codex/templates.js +319 -0
- package/dist/src/codex/templates.js.map +1 -0
- package/dist/src/codex/trust.d.ts +19 -0
- package/dist/src/codex/trust.js +183 -0
- package/dist/src/codex/trust.js.map +1 -0
- package/dist/src/codex/workspace-commit.d.ts +219 -0
- package/dist/src/codex/workspace-commit.js +549 -0
- package/dist/src/codex/workspace-commit.js.map +1 -0
- package/dist/src/codex/workspace-plan.d.ts +131 -0
- package/dist/src/codex/workspace-plan.js +561 -0
- package/dist/src/codex/workspace-plan.js.map +1 -0
- package/dist/src/core/actor.d.ts +2 -0
- package/dist/src/core/actor.js +5 -0
- package/dist/src/core/actor.js.map +1 -0
- package/dist/src/core/advance-cycle.d.ts +221 -0
- package/dist/src/core/advance-cycle.js +66 -2
- package/dist/src/core/advance-cycle.js.map +1 -1
- package/dist/src/core/agents-md.d.ts +278 -0
- package/dist/src/core/agents-md.js +33 -31
- package/dist/src/core/agents-md.js.map +1 -1
- package/dist/src/core/apply-patch.d.ts +49 -0
- package/dist/src/core/apply-patch.js +266 -0
- package/dist/src/core/apply-patch.js.map +1 -0
- package/dist/src/core/attest.d.ts +635 -0
- package/dist/src/core/attest.js +326 -4
- package/dist/src/core/attest.js.map +1 -1
- package/dist/src/core/audit.d.ts +510 -0
- package/dist/src/core/audit.js +13 -0
- package/dist/src/core/audit.js.map +1 -1
- package/dist/src/core/budgets.d.ts +238 -0
- package/dist/src/core/channel-owner.d.ts +213 -0
- package/dist/src/core/channel-owner.js +358 -0
- package/dist/src/core/channel-owner.js.map +1 -0
- package/dist/src/core/checkpoint.d.ts +500 -0
- package/dist/src/core/child-env.d.ts +88 -0
- package/dist/src/core/clock.d.ts +52 -0
- package/dist/src/core/command-class.d.ts +697 -0
- package/dist/src/core/command-class.js +713 -25
- package/dist/src/core/command-class.js.map +1 -1
- package/dist/src/core/commit-guard.d.ts +272 -0
- package/dist/src/core/commit-guard.js +424 -0
- package/dist/src/core/commit-guard.js.map +1 -0
- package/dist/src/core/coverage-sources/adapter.d.ts +40 -0
- package/dist/src/core/coverage-sources/gh.d.ts +48 -0
- package/dist/src/core/coverage-sources/git.d.ts +101 -0
- package/dist/src/core/coverage.d.ts +217 -0
- package/dist/src/core/credential-spec.d.ts +72 -0
- package/dist/src/core/daemon-actor.d.ts +45 -0
- package/dist/src/core/daemon-actor.js +54 -0
- package/dist/src/core/daemon-actor.js.map +1 -0
- package/dist/src/core/dark-session.d.ts +432 -0
- package/dist/src/core/dark-session.js +266 -82
- package/dist/src/core/dark-session.js.map +1 -1
- package/dist/src/core/decision-refusal.d.ts +206 -0
- package/dist/src/core/decision-refusal.js +24 -2
- package/dist/src/core/decision-refusal.js.map +1 -1
- package/dist/src/core/env-file.d.ts +455 -0
- package/dist/src/core/env-file.js +60 -1
- package/dist/src/core/env-file.js.map +1 -1
- package/dist/src/core/execute.d.ts +871 -0
- package/dist/src/core/execute.js +59 -8
- package/dist/src/core/execute.js.map +1 -1
- package/dist/src/core/frontmatter.d.ts +78 -0
- package/dist/src/core/gate-window.d.ts +312 -0
- package/dist/src/core/gate.d.ts +1449 -0
- package/dist/src/core/gate.js +149 -14
- package/dist/src/core/gate.js.map +1 -1
- package/dist/src/core/gesture-refusal.d.ts +166 -0
- package/dist/src/core/gesture-refusal.js +188 -0
- package/dist/src/core/gesture-refusal.js.map +1 -0
- package/dist/src/core/git-run.d.ts +73 -0
- package/dist/src/core/harness-version.d.ts +157 -0
- package/dist/src/core/harness-version.js +4 -1
- package/dist/src/core/harness-version.js.map +1 -1
- package/dist/src/core/harness-wait.d.ts +55 -0
- package/dist/src/core/head-retry.d.ts +107 -0
- package/dist/src/core/instance.d.ts +310 -0
- package/dist/src/core/instance.js +113 -0
- package/dist/src/core/instance.js.map +1 -1
- package/dist/src/core/intake-limits.d.ts +247 -0
- package/dist/src/core/jcs.d.ts +52 -0
- package/dist/src/core/journal.d.ts +144 -0
- package/dist/src/core/live-draw.d.ts +436 -0
- package/dist/src/core/log-reconcile.d.ts +89 -0
- package/dist/src/core/log-subscribe.d.ts +36 -0
- package/dist/src/core/log-subscribe.js +162 -0
- package/dist/src/core/log-subscribe.js.map +1 -0
- package/dist/src/core/log.d.ts +316 -0
- package/dist/src/core/log.js.map +1 -1
- package/dist/src/core/loop.d.ts +274 -0
- package/dist/src/core/loop.js +11 -0
- package/dist/src/core/loop.js.map +1 -1
- package/dist/src/core/md-fence.d.ts +41 -0
- package/dist/src/core/money.d.ts +147 -0
- package/dist/src/core/payload-census.d.ts +74 -0
- package/dist/src/core/payload-store.d.ts +175 -0
- package/dist/src/core/payload.d.ts +71 -0
- package/dist/src/core/policy-diff.d.ts +292 -0
- package/dist/src/core/policy-diff.js +27 -4
- package/dist/src/core/policy-diff.js.map +1 -1
- package/dist/src/core/policy-expectations.d.ts +199 -0
- package/dist/src/core/policy-explain.d.ts +160 -0
- package/dist/src/core/policy-explain.js +63 -3
- package/dist/src/core/policy-explain.js.map +1 -1
- package/dist/src/core/policy-load.d.ts +567 -0
- package/dist/src/core/policy-load.js +36 -6
- package/dist/src/core/policy-load.js.map +1 -1
- package/dist/src/core/policy-match.d.ts +324 -0
- package/dist/src/core/policy-match.js +72 -9
- package/dist/src/core/policy-match.js.map +1 -1
- package/dist/src/core/policy-proposal.d.ts +317 -0
- package/dist/src/core/policy-proposal.js +102 -2
- package/dist/src/core/policy-proposal.js.map +1 -1
- package/dist/src/core/prompt-layout.d.ts +221 -0
- package/dist/src/core/protected-path-guard.d.ts +566 -0
- package/dist/src/core/protected-path-guard.js +848 -55
- package/dist/src/core/protected-path-guard.js.map +1 -1
- package/dist/src/core/question-preempted.d.ts +141 -0
- package/dist/src/core/question-preempted.js +152 -0
- package/dist/src/core/question-preempted.js.map +1 -0
- package/dist/src/core/read-scope.d.ts +172 -0
- package/dist/src/core/read-scope.js +252 -0
- package/dist/src/core/read-scope.js.map +1 -0
- package/dist/src/core/registration.d.ts +25 -0
- package/dist/src/core/reindex.d.ts +99 -0
- package/dist/src/core/sampler.d.ts +313 -0
- package/dist/src/core/sandbox.d.ts +371 -0
- package/dist/src/core/sandbox.js +190 -1
- package/dist/src/core/sandbox.js.map +1 -1
- package/dist/src/core/seal.d.ts +165 -0
- package/dist/src/core/sender-identity.d.ts +476 -0
- package/dist/src/core/sender-identity.js +572 -0
- package/dist/src/core/sender-identity.js.map +1 -0
- package/dist/src/core/shlex.d.ts +102 -0
- package/dist/src/core/shlex.js +159 -0
- package/dist/src/core/shlex.js.map +1 -0
- package/dist/src/core/state.d.ts +505 -0
- package/dist/src/core/task-file.d.ts +185 -0
- package/dist/src/core/telegram-config.d.ts +93 -0
- package/dist/src/core/token.d.ts +409 -0
- package/dist/src/core/token.js +21 -38
- package/dist/src/core/token.js.map +1 -1
- package/dist/src/core/validate.d.ts +138 -0
- package/dist/src/core/values.d.ts +147 -0
- package/dist/src/core/values.js +36 -1
- package/dist/src/core/values.js.map +1 -1
- package/dist/src/core/vault.d.ts +291 -0
- package/dist/src/core/verified-snapshot.d.ts +204 -0
- package/dist/src/core/verify.d.ts +336 -0
- package/dist/src/core/version.d.ts +8 -0
- package/dist/src/core/wysiwys.d.ts +370 -0
- package/dist/src/daemon/advance-child.d.ts +39 -0
- package/dist/src/daemon/advance.d.ts +476 -0
- package/dist/src/daemon/advance.js +25 -4
- package/dist/src/daemon/advance.js.map +1 -1
- package/dist/src/daemon/audit.d.ts +87 -0
- package/dist/src/daemon/daemon.d.ts +1180 -0
- package/dist/src/daemon/daemon.js +9 -0
- package/dist/src/daemon/daemon.js.map +1 -1
- package/dist/src/daemon/dark-session.d.ts +64 -0
- package/dist/src/daemon/draw-child.d.ts +36 -0
- package/dist/src/daemon/draw.d.ts +154 -0
- package/dist/src/daemon/git-evidence.d.ts +173 -0
- package/dist/src/daemon/git-evidence.js +1 -1
- package/dist/src/daemon/projection.d.ts +180 -0
- package/dist/src/daemon/prune.d.ts +207 -0
- package/dist/src/mcp/http.d.ts +113 -0
- package/dist/src/mcp/server.d.ts +265 -0
- package/dist/src/mcp/server.js +17 -1
- package/dist/src/mcp/server.js.map +1 -1
- package/docs/adapter-api.md +106 -0
- package/docs/cli-reference.md +1316 -63
- package/docs/codex-enforced-session.md +103 -0
- package/docs/codex-workspace-broker.md +118 -0
- package/package.json +14 -2
- package/schema/codex-instance.schema.json +82 -0
- package/schema/event.schema.json +539 -9
- package/schema/fixtures/codex-instance/invalid/unpinned-codex-version.json +40 -0
- package/schema/fixtures/codex-instance/valid/canonical.json +40 -0
- package/schema/fixtures/event/invalid/approval-granted-sender-hashed-false.json +20 -0
- package/schema/fixtures/event/invalid/approval-granted-sender-hashed-raw-id.json +20 -0
- package/schema/fixtures/event/invalid/audit-gesture-refused-human-actor.json +16 -0
- package/schema/fixtures/event/invalid/audit-gesture-refused-no-actor-no-sender.json +15 -0
- package/schema/fixtures/event/invalid/audit-gesture-refused-unknown-gesture.json +16 -0
- package/schema/fixtures/event/invalid/audit-question-preempted-agent-actor.json +16 -0
- package/schema/fixtures/event/invalid/audit-question-preempted-no-question-id.json +16 -0
- package/schema/fixtures/event/invalid/audit-question-preempted-unknown-source.json +15 -0
- package/schema/fixtures/event/invalid/gate-path-signed-off-absolute-path.json +14 -0
- package/schema/fixtures/event/invalid/gate-path-signed-off-agent-actor.json +14 -0
- package/schema/fixtures/event/invalid/gate-path-signed-off-missing-path.json +13 -0
- package/schema/fixtures/event/valid/approval-granted-sender-hashed.json +20 -0
- package/schema/fixtures/event/valid/audit-gesture-refused-review-note.json +21 -0
- package/schema/fixtures/event/valid/audit-gesture-refused-sender-key-unavailable.json +19 -0
- package/schema/fixtures/event/valid/audit-gesture-refused.json +19 -0
- package/schema/fixtures/event/valid/audit-question-preempted-no-verdict.json +16 -0
- package/schema/fixtures/event/valid/audit-question-preempted.json +20 -0
- package/schema/fixtures/event/valid/gate-path-signed-off.json +14 -0
- package/schema/fixtures/event/valid/harness-kind-claude-code.json +23 -0
- package/schema/fixtures/event/valid/harness-kind-codex.json +23 -0
- package/schema/fixtures/event/valid/harness-kind-cursor.json +23 -0
- package/schema/fixtures/event/valid/harness-kind-grok.json +23 -0
- package/schema/fixtures/event/valid/harness-kind-muse.json +23 -0
- package/schema/fixtures/policy/invalid/senders-half-keyed.json +20 -0
- package/schema/fixtures/policy/valid/canonical.json +1 -1
- package/schema/fixtures/policy/valid/senders-keyed.json +24 -0
- package/schema/fixtures/policy-md/valid/canonical.md +1 -1
- package/schema/fixtures/policy-md/valid/with-values.md +5 -7
- package/schema/fixtures/values/invalid/class-shaped.json +1 -1
- package/schema/fixtures/values/invalid/duplicate-entry.json +1 -1
- package/schema/fixtures/values/invalid/non-string-item.json +1 -1
- package/schema/fixtures/values/invalid/over-cap.json +1 -1
- package/schema/fixtures/values/invalid/unknown-key.json +1 -1
- package/schema/fixtures/values/invalid/version-float.json +1 -0
- package/schema/fixtures/values/invalid/version-integer.json +1 -0
- package/schema/fixtures/values/invalid/version-wrong-string.json +1 -0
- package/schema/fixtures/values/valid/empty-lists.json +2 -3
- package/schema/fixtures/values/valid/full.json +5 -7
- package/schema/fixtures/values/valid/minimal.json +1 -1
- package/schema/fixtures/values-md/invalid/schema-invalid.md +5 -3
- package/schema/fixtures/values-md/invalid/two-blocks.md +3 -3
- package/schema/fixtures/values-md/invalid/unterminated.md +2 -2
- package/schema/fixtures/values-md/invalid/version-1.md +69 -0
- package/schema/fixtures/values-md/invalid/version-unquoted.md +64 -0
- package/schema/fixtures/values-md/invalid/yaml-error.md +2 -2
- package/schema/fixtures/values-md/valid/absent.md +1 -1
- package/schema/fixtures/values-md/valid/with-values.md +5 -7
- package/schema/policy.schema.json +75 -3
- package/schema/values.schema.json +7 -11
- package/templates/codex/README.md +9 -0
- package/schema/fixtures/values/invalid/version-string.json +0 -1
package/dist/src/cli/doctor.js
CHANGED
|
@@ -55,15 +55,16 @@
|
|
|
55
55
|
*/
|
|
56
56
|
import { createServer } from "node:net";
|
|
57
57
|
import { closeSync, existsSync, openSync, readFileSync, readdirSync, statSync, unlinkSync, } from "node:fs";
|
|
58
|
-
import { dirname, isAbsolute, join, resolve as resolvePathSegments } from "node:path";
|
|
58
|
+
import { basename, dirname, isAbsolute, join, resolve as resolvePathSegments } from "node:path";
|
|
59
59
|
import { WEB_DEFAULT_PORT } from "../channels/web.js";
|
|
60
60
|
import { TELEGRAM_DEFAULT_API_BASE, telegramChatEnvFor, telegramTokenEnvFor, } from "../channels/telegram.js";
|
|
61
|
-
import { HUMAN_ACTOR_ENV, checkAttestation, findOrganAttestation, latestOrganAttestation, policyBytesHash, resolveHumanActor, } from "../core/attest.js";
|
|
62
|
-
import { isGateOrganPath } from "../core/command-class.js";
|
|
61
|
+
import { HUMAN_ACTOR_ENV, checkAttestation, findOrganAttestation, findPathSignOff, latestOrganAttestation, latestPathSignOff, policyBytesHash, resolveHumanActor, } from "../core/attest.js";
|
|
62
|
+
import { isGateOrganPath, POLICY_EDIT_SUBCLASS, protectedPathClass, } from "../core/command-class.js";
|
|
63
63
|
import { VALUES_INFO_STRING, loadValues } from "../core/values.js";
|
|
64
64
|
import { KEYSTORE_DEFERRED, NON_RESOLVING_RUNNER, envFilePathFor, resolveEnvironment, } from "../core/env-file.js";
|
|
65
65
|
import { readTaskFile } from "../core/frontmatter.js";
|
|
66
66
|
import { instanceFindings, instanceHomeFor, instanceIdFor } from "../core/instance.js";
|
|
67
|
+
import { describeOwners, otherOwnersOf, ownedBot } from "../core/channel-owner.js";
|
|
67
68
|
import { checkLogAnchor } from "./log-anchor.js";
|
|
68
69
|
import { checkLogCheckpoints, checkpointPolicyOf } from "../core/checkpoint.js";
|
|
69
70
|
import { payloadStoreCensus } from "../core/payload-census.js";
|
|
@@ -71,6 +72,7 @@ import { payloadStoreDirFor } from "../core/payload-store.js";
|
|
|
71
72
|
import { DEFAULT_TASKS_DIR, latestRegistration } from "../core/registration.js";
|
|
72
73
|
import { POLICY_FILENAMES, loadPolicy } from "../core/policy-load.js";
|
|
73
74
|
import { openObligations } from "../core/audit.js";
|
|
75
|
+
import { SENDER_CHANNELS, SENDER_KEY_ENV, mapsSendersFor, senderKeyFrom, senderMappingForms, } from "../core/sender-identity.js";
|
|
74
76
|
import { classSampling, resolveSampler } from "../core/sampler.js";
|
|
75
77
|
import { checkVault, passphraseEnvFor, passphraseFrom, vaultExists, vaultPathFor, } from "../core/vault.js";
|
|
76
78
|
import { admitSnapshot, logBytes, snapshotPathFor, snapshotSummary, } from "../core/verified-snapshot.js";
|
|
@@ -84,7 +86,7 @@ import { RESOLVE_DANGLING_COMMAND, lastAdvance, proveDanglingAdvances, } from ".
|
|
|
84
86
|
import { DEFAULT_DARK_WINDOW_MS, reportDarkSessions, } from "../core/dark-session.js";
|
|
85
87
|
import { HARNESS_BINARY, HARNESS_KINDS, installedHarnessVersion, isHarnessKind, readHarnessProvenance, } from "../core/harness-version.js";
|
|
86
88
|
import { git, repoPath, repoRoot } from "./git-scope.js";
|
|
87
|
-
import { ScanError, checkBuildFreshness, checkMainBehindOrigin, installationRoot, } from "./preflight.js";
|
|
89
|
+
import { ScanError, checkAttestedPolicyOnMain, checkBuildFreshness, checkMainBehindOrigin, installationRoot, } from "./preflight.js";
|
|
88
90
|
import { publishedState } from "./log-advance.js";
|
|
89
91
|
import { DOCTOR_HELP } from "./help.js";
|
|
90
92
|
import { DEFAULT_LOG_PATH, resolvePath } from "./paths.js";
|
|
@@ -332,7 +334,27 @@ function redact(text, token) {
|
|
|
332
334
|
* operator to set a variable their policy never mentions is the failure mode
|
|
333
335
|
* this parameter removes.
|
|
334
336
|
*/
|
|
335
|
-
|
|
337
|
+
/**
|
|
338
|
+
* Who owns the bot this row is about (APRV-390), as one clause.
|
|
339
|
+
*
|
|
340
|
+
* Read from the two ownership records and never from the network: doctor's
|
|
341
|
+
* `getMe` answers WHICH bot, and this answers WHOSE. An instance that has
|
|
342
|
+
* never started a listener has no record and gets no clause, because "nothing
|
|
343
|
+
* has claimed it yet" is a state and printing it on every row would make the
|
|
344
|
+
* real finding — two instances on one bot — harder to see.
|
|
345
|
+
*/
|
|
346
|
+
function ownershipClause(logPath, botId, apiBase) {
|
|
347
|
+
const mine = ownedBot(logPath, "telegram");
|
|
348
|
+
const others = otherOwnersOf(logPath, botId, apiBase);
|
|
349
|
+
if (others.length > 0) {
|
|
350
|
+
return `; ALSO claimed on this machine by ${describeOwners(others)}, which is two gates on one bot`;
|
|
351
|
+
}
|
|
352
|
+
if (mine !== null && mine.botId === botId) {
|
|
353
|
+
return `; owned by this instance (${mine.instanceHome}, instance ${mine.instanceId})`;
|
|
354
|
+
}
|
|
355
|
+
return "";
|
|
356
|
+
}
|
|
357
|
+
async function checkTelegram(apiBase, load, logPath) {
|
|
336
358
|
const tokenEnv = telegramTokenEnvFor(load);
|
|
337
359
|
const chatEnv = telegramChatEnvFor(load);
|
|
338
360
|
const token = process.env[tokenEnv] ?? "";
|
|
@@ -385,11 +407,23 @@ async function checkTelegram(apiBase, load) {
|
|
|
385
407
|
}
|
|
386
408
|
const result = (envelope["result"] ?? {});
|
|
387
409
|
const username = typeof result["username"] === "string" ? `@${result["username"]}` : "unnamed";
|
|
388
|
-
const
|
|
410
|
+
const botId = result["id"] === undefined ? "" : String(result["id"]);
|
|
411
|
+
const id = botId.length === 0 ? "unknown id" : `id ${botId}`;
|
|
412
|
+
// APRV-390. Two gates on one bot is a FAIL, not a note: it is the state in
|
|
413
|
+
// which an approval tap is answered by whichever listener asked first.
|
|
414
|
+
const owners = botId.length === 0 ? [] : otherOwnersOf(logPath, botId, base);
|
|
415
|
+
if (owners.length > 0) {
|
|
416
|
+
return {
|
|
417
|
+
check: "telegram",
|
|
418
|
+
status: "fail",
|
|
419
|
+
detail: `token valid: ${username} (${id}) via ${base}, chat ${chat} — but this bot is also claimed on this machine by ${describeOwners(owners)}. Both gates long-poll it, their getUpdates offsets acknowledge each other's updates, and an approval tap is answered by whichever listener asked first`,
|
|
420
|
+
fix: "approval setup channel telegram — give this instance its own bot from @BotFather; `approval channel telegram health` names the current owner of each",
|
|
421
|
+
};
|
|
422
|
+
}
|
|
389
423
|
return {
|
|
390
424
|
check: "telegram",
|
|
391
425
|
status: "pass",
|
|
392
|
-
detail: `token valid: ${username} (${id}) via ${base}, chat ${chat}; no message was sent and no update was consumed`,
|
|
426
|
+
detail: `token valid: ${username} (${id}) via ${base}, chat ${chat}${botId.length === 0 ? "" : ownershipClause(logPath, botId, base)}; no message was sent and no update was consumed`,
|
|
393
427
|
};
|
|
394
428
|
}
|
|
395
429
|
catch (cause) {
|
|
@@ -744,11 +778,19 @@ function checkValuesBlock(policyPath, policyFlagged, dir) {
|
|
|
744
778
|
detail: oneLine(`${result.message}, so there is no values block to read. The policy file's own absence is reported by the attestation row above.`),
|
|
745
779
|
};
|
|
746
780
|
}
|
|
781
|
+
// A block of the format APRV-336 replaced is a fixable block rather than a
|
|
782
|
+
// broken one, so its fix names the two edits instead of the load code.
|
|
783
|
+
const fix = result.code === "version-unsupported"
|
|
784
|
+
? `edit the block in ${result.source?.filename ?? "the policy file"}: fold \`wants:\` into \`like:\`, rename \`responds:\` to \`communication:\`, and set \`version: "0.2"\` with the quotes; then re-attest, since the attestation digests the whole file`
|
|
785
|
+
: `approval values --json — prints the same failure with its code (${result.code}); fix the block in ${result.source?.filename ?? "the policy file"} and re-attest, since the attestation digests the whole file`;
|
|
747
786
|
return {
|
|
748
787
|
check: "values-block",
|
|
749
788
|
status: "fail",
|
|
750
|
-
detail: oneLine(
|
|
751
|
-
|
|
789
|
+
detail: oneLine(
|
|
790
|
+
// The loader's own message may end in a full stop (the version refusals
|
|
791
|
+
// are whole sentences), and two of them in a row reads as a typo.
|
|
792
|
+
`a \`\`\`${VALUES_INFO_STRING} block is present and could not be read (${result.code}): ${result.message.replace(/\.$/u, "")}. Nothing about the policy changed — guidance is not enforcement — but the operator's stated values reach no agent until this parses.`),
|
|
793
|
+
fix,
|
|
752
794
|
};
|
|
753
795
|
}
|
|
754
796
|
if (!result.present) {
|
|
@@ -758,12 +800,160 @@ function checkValuesBlock(policyPath, policyFlagged, dir) {
|
|
|
758
800
|
detail: `${result.source.filename}: no approval-values block; the operator has declared no values here. That is a declaration rather than a gap, and \`approval values\` says so in those words.`,
|
|
759
801
|
};
|
|
760
802
|
}
|
|
761
|
-
const declared = ["love", "like", "dislike"
|
|
762
|
-
const
|
|
803
|
+
const declared = ["love", "like", "dislike"].filter((key) => result.values[key] !== undefined);
|
|
804
|
+
const communication = result.values.communication === undefined ? "" : ", communication";
|
|
763
805
|
return {
|
|
764
806
|
check: "values-block",
|
|
765
807
|
status: "pass",
|
|
766
|
-
detail: `${result.source.filename}: the values block parses and validates (version ${
|
|
808
|
+
detail: `${result.source.filename}: the values block parses and validates (version ${JSON.stringify(result.values.version)}; ${declared.length === 0 ? "no list" : declared.join(", ")}${communication}). It is guidance, so nothing here is enforced; read it with \`approval values\`.`,
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
// ---------------------------------------------------------------------------
|
|
812
|
+
// 7e. the deprecated bare `supervised` (APRV-335)
|
|
813
|
+
// ---------------------------------------------------------------------------
|
|
814
|
+
/**
|
|
815
|
+
* Which rules of this policy still write the deprecated bare `supervised`.
|
|
816
|
+
*
|
|
817
|
+
* SPEC.md §5.2 keeps the spelling parsing as `supervised-retro` so that no
|
|
818
|
+
* published policy breaks, and the loader records a note wherever it appears.
|
|
819
|
+
* A note is read by whoever happens to run a verb that prints notes; the
|
|
820
|
+
* deprecation needs a place an operator looks at the state of their repository
|
|
821
|
+
* on purpose, which is this report. SPEC.md §5.2 asks implementations for
|
|
822
|
+
* exactly that.
|
|
823
|
+
*
|
|
824
|
+
* It is never a `fail` and never a `warn`. The policy says what its author
|
|
825
|
+
* meant, every gate resolves it identically, and nothing on this machine is
|
|
826
|
+
* broken by a spelling: a red row here would be doctor going red over prose. A
|
|
827
|
+
* `skip` is the unreadable policy, whose own failure the attestation and
|
|
828
|
+
* `policy check` rows report; everything else is a `pass` that either names the
|
|
829
|
+
* rules to rewrite or says plainly that there are none.
|
|
830
|
+
*/
|
|
831
|
+
function checkAutonomyAlias(policyLoad) {
|
|
832
|
+
if (!policyLoad.ok) {
|
|
833
|
+
return {
|
|
834
|
+
check: "autonomy-alias",
|
|
835
|
+
status: "skip",
|
|
836
|
+
detail: `the policy did not load (${policyLoad.code}), so no rule could be read for the deprecated bare \`supervised\`. A failed load resolves every class to \`manual\` and names no level at all; the failure itself is reported by \`approval policy check\`.`,
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
// The same walk `aliasNotes` makes in the loader, and deliberately the same
|
|
840
|
+
// order: `defaults` first, then class patterns sorted, so the row and the
|
|
841
|
+
// load-time notes name the same places in the same sequence.
|
|
842
|
+
const where = [];
|
|
843
|
+
if (policyLoad.policy.defaults?.autonomy === "supervised")
|
|
844
|
+
where.push("defaults.autonomy");
|
|
845
|
+
for (const pattern of Object.keys(policyLoad.policy.classes ?? {}).sort()) {
|
|
846
|
+
if (policyLoad.policy.classes?.[pattern]?.autonomy === "supervised")
|
|
847
|
+
where.push(pattern);
|
|
848
|
+
}
|
|
849
|
+
if (where.length === 0) {
|
|
850
|
+
return {
|
|
851
|
+
check: "autonomy-alias",
|
|
852
|
+
status: "pass",
|
|
853
|
+
detail: `${policyLoad.source.filename}: no rule uses the deprecated bare \`supervised\`. Every supervised class says which bargain it means, \`supervised-retro\` or \`supervised-live\`, so nothing here changes when a future schema version drops the alias.`,
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
return {
|
|
857
|
+
check: "autonomy-alias",
|
|
858
|
+
status: "pass",
|
|
859
|
+
detail: oneLine(`deprecated bare \`supervised\` on ${String(where.length)} rule(s): ${where.join(", ")}. Each parses as \`supervised-retro\` and is enforced as one, so the policy means today what its author meant; the spelling is deprecated and a future version of the policy schema removes it (SPEC.md §5.2, APRV-335).`),
|
|
860
|
+
// FIX_COMMAND_PREFIXES: the repair is a hand edit of the policy file plus
|
|
861
|
+
// the re-attestation that edit costs, and `policy amend` is the one verb
|
|
862
|
+
// that owns both. The prose after it is the edit itself: write
|
|
863
|
+
// supervised-retro.
|
|
864
|
+
fix: "approval policy amend — write supervised-retro in place of the bare supervised on the rules above, and re-attest in the same step; the paste-ready draft for this repo is docs/proposals/approval-md-2026-09.md",
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* Which approvers can still be recognized by the channels they are listed for
|
|
869
|
+
* (APRV-324, `design/channel-sender-identity.md` §5 item 13).
|
|
870
|
+
*
|
|
871
|
+
* The row exists for one transition and says which side of it this deployment
|
|
872
|
+
* is on. Declaring the first `senders` entry for a channel turns enforcement on
|
|
873
|
+
* for that whole channel: from then on a decision arriving with an account the
|
|
874
|
+
* policy does not name is REFUSED rather than recorded under the listener's own
|
|
875
|
+
* identity. An operator who maps themselves and forgets a colleague has built a
|
|
876
|
+
* gate that will refuse that colleague's next tap, and the file reads as though
|
|
877
|
+
* both are approvers.
|
|
878
|
+
*
|
|
879
|
+
* Three answers, and the verdicts are chosen for what each one actually is:
|
|
880
|
+
*
|
|
881
|
+
* - **SKIP** when no approver declares a mapping. That is every installation
|
|
882
|
+
* before this key existed and most of them after, it is fully correct, and a
|
|
883
|
+
* red row for it would be a red row people learn to skip past.
|
|
884
|
+
* - **FAIL** for an approver listed for a mapped channel with no id of their
|
|
885
|
+
* own on it. Not a state: the policy says that person decides on that
|
|
886
|
+
* channel, and the runtime will not honour it.
|
|
887
|
+
* - **PASS** when every approver the policy can reach on a mapped channel is
|
|
888
|
+
* recognizable there.
|
|
889
|
+
*
|
|
890
|
+
* It reads the policy and nothing else — no log, no network, no credential —
|
|
891
|
+
* and it prints the id of nobody: a doctor row is read over a shoulder, the
|
|
892
|
+
* mapping is in a file the operator can open, and the row's job is to name who
|
|
893
|
+
* is missing rather than to recite everyone who is not.
|
|
894
|
+
*/
|
|
895
|
+
function checkSenderMapping(policyLoad, env = process.env) {
|
|
896
|
+
if (!policyLoad.ok) {
|
|
897
|
+
return {
|
|
898
|
+
check: "sender-mapping",
|
|
899
|
+
status: "skip",
|
|
900
|
+
detail: `the policy did not load (${policyLoad.code}), so no approver roster could be read. A failed load carries no sender mapping either, and a decision arriving with an authenticated sender is refused \`sender-unmapped\` until the file parses again — the \`attestation\` row above and \`approval policy check\` say what is wrong with it.`,
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
const approvers = policyLoad.policy.approvers;
|
|
904
|
+
const mapped = SENDER_CHANNELS.filter((channel) => mapsSendersFor(approvers, channel));
|
|
905
|
+
if (mapped.length === 0) {
|
|
906
|
+
return {
|
|
907
|
+
check: "sender-mapping",
|
|
908
|
+
status: "skip",
|
|
909
|
+
detail: `${policyLoad.source.filename}: no approver declares a \`senders\` block, so every decision is recorded against the identity the deciding process was launched with (\`--as\` / APPROVAL_HUMAN), exactly as it was before the mapping existed. Anyone who can reach a configured channel decides as that identity; map an approver's account id to record who actually tapped (SPEC.md §5.2, APRV-324).`,
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
// APRV-370. Which FORM each mapped channel is written in, and for a keyed one
|
|
913
|
+
// whether the key this process would compare with resolves at all. The row
|
|
914
|
+
// reports the form because an operator reading it cannot otherwise tell a
|
|
915
|
+
// gate that publishes its accounts from one that does not, and it reports the
|
|
916
|
+
// key because a keyed channel without one refuses every tap on it.
|
|
917
|
+
const key = senderKeyFrom(env);
|
|
918
|
+
const keyed = mapped.filter((channel) => senderMappingForms(approvers, channel).keyed);
|
|
919
|
+
const mixed = keyed.filter((channel) => senderMappingForms(approvers, channel).raw);
|
|
920
|
+
const forms = keyed.length === 0
|
|
921
|
+
? "raw account ids"
|
|
922
|
+
: mixed.length > 0
|
|
923
|
+
? `both forms (${mixed.join(", ")} carries raw ids beside keyed ones)`
|
|
924
|
+
: `the keyed form (${SENDER_KEY_ENV})`;
|
|
925
|
+
if (keyed.length > 0 && key === null) {
|
|
926
|
+
return {
|
|
927
|
+
check: "sender-mapping",
|
|
928
|
+
status: "fail",
|
|
929
|
+
detail: oneLine(`${policyLoad.source.filename} maps ${keyed.join(", ")} senders in the keyed form and ${SENDER_KEY_ENV} is unset or empty in this process, so no account on those channels can be resolved at all and EVERY decision there is refused \`sender-key-unavailable\`. Without the key the runtime can compute no digest, so it cannot tell a mapped account from an unmapped one and will not guess; there is no fallback to a raw comparison, by design.`),
|
|
930
|
+
fix: `approval setup sender-key mints and stores the key; \`eval "$(approval env)"\` establishes it in the shell that starts the listener, and the listener has to be restarted to pick it up. \`approval env --check\` says where the value lives and prints no values`,
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
const gaps = [];
|
|
934
|
+
for (const channel of mapped) {
|
|
935
|
+
for (const id of Object.keys(approvers ?? {}).sort()) {
|
|
936
|
+
const entry = approvers?.[id];
|
|
937
|
+
if (entry === undefined)
|
|
938
|
+
continue;
|
|
939
|
+
if (!entry.channels.includes(channel))
|
|
940
|
+
continue;
|
|
941
|
+
if (entry.senders?.[channel] === undefined)
|
|
942
|
+
gaps.push(`${id} on ${channel}`);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
if (gaps.length > 0) {
|
|
946
|
+
return {
|
|
947
|
+
check: "sender-mapping",
|
|
948
|
+
status: "fail",
|
|
949
|
+
detail: oneLine(`${policyLoad.source.filename} maps ${mapped.join(", ")} senders in ${forms}, and ${String(gaps.length)} approver(s) listed for a mapped channel carry no id there: ${gaps.join(", ")}. A decision from an account this file does not name is refused \`sender-unmapped\` and nothing is recorded, so the policy currently says those people may decide on a channel where their taps will not be honoured.`),
|
|
950
|
+
fix: "approval policy amend — give each approver above a `senders` entry for that channel, or drop the channel from their `channels` list if they are not meant to decide there; the mapping is attested policy, so the edit is inoperative until it is re-attested",
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
return {
|
|
954
|
+
check: "sender-mapping",
|
|
955
|
+
status: "pass",
|
|
956
|
+
detail: oneLine(`${policyLoad.source.filename} maps ${mapped.join(", ")} senders in ${forms}, and every approver listed for a mapped channel carries an id there.${keyed.length === 0 ? "" : ` ${SENDER_KEY_ENV} resolves, so the keyed ids can be compared.`} Each decision on those channels is recorded against the person the operator attested that account to; an account this file does not name is refused rather than attributed to the listener (SPEC.md §10.3, APRV-324, APRV-370).`),
|
|
767
957
|
};
|
|
768
958
|
}
|
|
769
959
|
// ---------------------------------------------------------------------------
|
|
@@ -1947,15 +2137,123 @@ function checkHarnessWiring(dir) {
|
|
|
1947
2137
|
detail: `WIRED on disk: ${path} registers \`approval hook\` for PreToolUse over ${GATED_TOOLS.join(", ")}. This is the file being present, NOT proof this session loaded it — the APRV-151 bypasses happened in worktrees carrying exactly this entry. The check that does not trust session wiring is the CI-side grant cross-check over the committed log, which asks whether the CHANGE was granted rather than whether the path ever was (APRV-202).`,
|
|
1948
2138
|
};
|
|
1949
2139
|
}
|
|
2140
|
+
/** The project-local Codex hook files doctor can observe without asking Codex. */
|
|
2141
|
+
const CODEX_HOOKS = join(".codex", "hooks.json");
|
|
2142
|
+
const CODEX_CONFIG = join(".codex", "config.toml");
|
|
2143
|
+
const CODEX_MATCHER = "Bash|apply_patch";
|
|
2144
|
+
const CODEX_HOOK_TIMEOUT_SECONDS = 600;
|
|
2145
|
+
function isDirectCodexHookCommand(command) {
|
|
2146
|
+
const match = command.match(/^(?:"([^"$`\\\r\n;&|<>]+)"|'([^'\\\r\n;&|<>]+)'|([^\s"'$`\\\r\n;&|<>]+)) hook codex --dir (?:"([^"$`\\\r\n;&|<>]+)"|'([^'\\\r\n;&|<>]+)'|([^\s"'$`\\\r\n;&|<>]+)) --as agent:codex --timeout 9m$/u);
|
|
2147
|
+
if (match === null)
|
|
2148
|
+
return false;
|
|
2149
|
+
const executable = match[1] ?? match[2] ?? match[3] ?? "";
|
|
2150
|
+
const primaryDir = match[4] ?? match[5] ?? match[6] ?? "";
|
|
2151
|
+
return isAbsolute(executable) && basename(executable) === "approval" && isAbsolute(primaryDir);
|
|
2152
|
+
}
|
|
2153
|
+
function codexEventConfigured(hooks, event) {
|
|
2154
|
+
if (typeof hooks !== "object" || hooks === null || Array.isArray(hooks))
|
|
2155
|
+
return false;
|
|
2156
|
+
const groups = hooks[event];
|
|
2157
|
+
if (!Array.isArray(groups))
|
|
2158
|
+
return false;
|
|
2159
|
+
return groups.some((group) => {
|
|
2160
|
+
if (typeof group !== "object" || group === null || Array.isArray(group))
|
|
2161
|
+
return false;
|
|
2162
|
+
const fields = group;
|
|
2163
|
+
if (fields["matcher"] !== CODEX_MATCHER || !Array.isArray(fields["hooks"]))
|
|
2164
|
+
return false;
|
|
2165
|
+
return fields["hooks"].some((handler) => {
|
|
2166
|
+
if (typeof handler !== "object" || handler === null || Array.isArray(handler))
|
|
2167
|
+
return false;
|
|
2168
|
+
const entry = handler;
|
|
2169
|
+
return (entry["type"] === "command" &&
|
|
2170
|
+
typeof entry["command"] === "string" &&
|
|
2171
|
+
isDirectCodexHookCommand(entry["command"]) &&
|
|
2172
|
+
entry["timeout"] === CODEX_HOOK_TIMEOUT_SECONDS &&
|
|
2173
|
+
(entry["async"] === undefined || entry["async"] === false));
|
|
2174
|
+
});
|
|
2175
|
+
});
|
|
2176
|
+
}
|
|
2177
|
+
/**
|
|
2178
|
+
* Report only project configuration visible on disk.
|
|
2179
|
+
*
|
|
2180
|
+
* Codex owns hook trust and runtime loading. Neither is inferable from a file,
|
|
2181
|
+
* and no historical log record proves what the current desktop session loaded.
|
|
2182
|
+
*/
|
|
2183
|
+
export function checkCodexHookWiring(dir) {
|
|
2184
|
+
const check = "codex-hook-wiring";
|
|
2185
|
+
const root = repoRoot(dir);
|
|
2186
|
+
const where = root ?? dir;
|
|
2187
|
+
const hooksPath = join(where, CODEX_HOOKS);
|
|
2188
|
+
const configPath = join(where, CODEX_CONFIG);
|
|
2189
|
+
const hasHooks = existsSync(hooksPath);
|
|
2190
|
+
const hasConfig = existsSync(configPath);
|
|
2191
|
+
if (!hasHooks && !hasConfig) {
|
|
2192
|
+
return {
|
|
2193
|
+
check,
|
|
2194
|
+
status: "skip",
|
|
2195
|
+
detail: `NOT CONFIGURED on disk: ${where} carries neither ${CODEX_HOOKS} nor ${CODEX_CONFIG}. Codex hook trust and observed execution are separate and remain unknown.`,
|
|
2196
|
+
};
|
|
2197
|
+
}
|
|
2198
|
+
if (!hasHooks) {
|
|
2199
|
+
return {
|
|
2200
|
+
check,
|
|
2201
|
+
status: "skip",
|
|
2202
|
+
detail: `${configPath} exists. Doctor does not interpret inline TOML hook tables, so Codex hook configuration, trust and observed execution are undetermined.`,
|
|
2203
|
+
fix: "approval hook codex --help — compare the documented PreToolUse and PostToolUse entries with .codex/config.toml",
|
|
2204
|
+
};
|
|
2205
|
+
}
|
|
2206
|
+
let parsed;
|
|
2207
|
+
try {
|
|
2208
|
+
parsed = JSON.parse(readFileSync(hooksPath, "utf8"));
|
|
2209
|
+
}
|
|
2210
|
+
catch (cause) {
|
|
2211
|
+
return {
|
|
2212
|
+
check,
|
|
2213
|
+
status: "fail",
|
|
2214
|
+
detail: `${hooksPath} cannot be read as JSON (${oneLine(detailOf(cause))}); configured wiring cannot be established, and trust or execution cannot be inferred.`,
|
|
2215
|
+
fix: "approval hook codex --help — compare and repair the project-local hook JSON after human review",
|
|
2216
|
+
};
|
|
2217
|
+
}
|
|
2218
|
+
const hooks = typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
|
|
2219
|
+
? parsed["hooks"]
|
|
2220
|
+
: null;
|
|
2221
|
+
const pre = codexEventConfigured(hooks, "PreToolUse");
|
|
2222
|
+
const post = codexEventConfigured(hooks, "PostToolUse");
|
|
2223
|
+
if (!pre || !post) {
|
|
2224
|
+
const missing = [!pre ? "PreToolUse" : null, !post ? "PostToolUse" : null]
|
|
2225
|
+
.filter((value) => value !== null)
|
|
2226
|
+
.join(" and ");
|
|
2227
|
+
return {
|
|
2228
|
+
check,
|
|
2229
|
+
status: "skip",
|
|
2230
|
+
detail: `${hooksPath} is present but does not match the expected APRV-313 profile for ${missing}: synchronous command hooks with matcher ${JSON.stringify(CODEX_MATCHER)}, command \`approval hook codex\`, and timeout ${String(CODEX_HOOK_TIMEOUT_SECONDS)} seconds. Other Codex hook configurations may be valid; this integration's coverage is undetermined. File presence proves neither trust nor observed execution.`,
|
|
2231
|
+
fix: "approval hook codex --help — install the documented pair only after human review",
|
|
2232
|
+
};
|
|
2233
|
+
}
|
|
2234
|
+
if (hasConfig) {
|
|
2235
|
+
return {
|
|
2236
|
+
check,
|
|
2237
|
+
status: "skip",
|
|
2238
|
+
detail: `CONFIGURED in ${hooksPath}: the required PreToolUse and PostToolUse entries are present. ${configPath} also exists and Codex merges hook sources; doctor does not interpret its TOML tables, so the effective configuration is not fully established. Trust and observed execution remain unknown.`,
|
|
2239
|
+
fix: "approval hook codex --help — compare .codex/config.toml with the reviewed hooks.json and keep one representation per layer",
|
|
2240
|
+
};
|
|
2241
|
+
}
|
|
2242
|
+
return {
|
|
2243
|
+
check,
|
|
2244
|
+
status: "pass",
|
|
2245
|
+
detail: `CONFIGURED on disk: ${hooksPath} carries synchronous PreToolUse and PostToolUse \`approval hook codex\` entries for ${CODEX_MATCHER}, each with a ${String(CODEX_HOOK_TIMEOUT_SECONDS)} second outer timeout. This does not establish Codex trust, loading, or observed execution; inspect and trust the exact hook with \`/hooks\`, then run the bounded smoke test.`,
|
|
2246
|
+
};
|
|
2247
|
+
}
|
|
1950
2248
|
// ---------------------------------------------------------------------------
|
|
1951
2249
|
// harness version provenance (APRV-227)
|
|
1952
2250
|
// ---------------------------------------------------------------------------
|
|
1953
2251
|
/** The Cursor counterpart of {@link CLAUDE_SETTINGS}. */
|
|
1954
2252
|
const CURSOR_HOOKS = join(".cursor", "hooks.json");
|
|
1955
2253
|
/** Where a harness hook registration can be written, one file per harness. */
|
|
1956
|
-
const HARNESS_SETTINGS = [CLAUDE_SETTINGS, CURSOR_HOOKS];
|
|
2254
|
+
const HARNESS_SETTINGS = [CLAUDE_SETTINGS, CURSOR_HOOKS, CODEX_HOOKS];
|
|
1957
2255
|
/** `approval hook <kind>` inside a command string, whichever file shape holds it. */
|
|
1958
|
-
const HOOK_COMMAND = /\bapproval
|
|
2256
|
+
const HOOK_COMMAND = /\bapproval["']?\s+hook\s+(claude-code|cursor|codex)\b/u;
|
|
1959
2257
|
/**
|
|
1960
2258
|
* Every harness this checkout registers an `approval hook` command for.
|
|
1961
2259
|
*
|
|
@@ -1968,7 +2266,7 @@ const HOOK_COMMAND = /\bapproval\s+hook\s+(claude-code|cursor)\b/u;
|
|
|
1968
2266
|
* searched. The row this feeds can only SKIP when the answer is empty, so a
|
|
1969
2267
|
* miss costs a skip and never a false red.
|
|
1970
2268
|
*/
|
|
1971
|
-
function registeredHarnesses(dir) {
|
|
2269
|
+
export function registeredHarnesses(dir) {
|
|
1972
2270
|
const found = new Set();
|
|
1973
2271
|
for (const relative of HARNESS_SETTINGS) {
|
|
1974
2272
|
const path = join(dir, relative);
|
|
@@ -1986,8 +2284,11 @@ function registeredHarnesses(dir) {
|
|
|
1986
2284
|
const node = stack.pop();
|
|
1987
2285
|
if (typeof node === "string") {
|
|
1988
2286
|
const match = HOOK_COMMAND.exec(node);
|
|
1989
|
-
if (match !== null &&
|
|
2287
|
+
if (match !== null &&
|
|
2288
|
+
isHarnessKind(match[1]) &&
|
|
2289
|
+
(match[1] !== "codex" || isDirectCodexHookCommand(node))) {
|
|
1990
2290
|
found.add(match[1]);
|
|
2291
|
+
}
|
|
1991
2292
|
continue;
|
|
1992
2293
|
}
|
|
1993
2294
|
if (Array.isArray(node)) {
|
|
@@ -2268,6 +2569,78 @@ function listGateOrgans(root) {
|
|
|
2268
2569
|
* configuration here, which is a state and not a fault, exactly as
|
|
2269
2570
|
* `harness-hook-wiring` treats the same absence.
|
|
2270
2571
|
*/
|
|
2572
|
+
// ---------------------------------------------------------------------------
|
|
2573
|
+
// 30. codex-auto-reviewer (APRV-378)
|
|
2574
|
+
// ---------------------------------------------------------------------------
|
|
2575
|
+
/**
|
|
2576
|
+
* How recent a pre-empted question has to be for this row to FAIL.
|
|
2577
|
+
*
|
|
2578
|
+
* The same twenty-four hours `dark-sessions` watches, and for the same reason:
|
|
2579
|
+
* a row that failed forever over a fact an operator has already dealt with is a
|
|
2580
|
+
* row nobody reads. An older record is not forgotten, it is reported in the
|
|
2581
|
+
* detail of a passing row, which is where a durable fact belongs once the thing
|
|
2582
|
+
* it asks for has been done.
|
|
2583
|
+
*/
|
|
2584
|
+
const AUTO_REVIEWER_WINDOW_MS = DEFAULT_DARK_WINDOW_MS;
|
|
2585
|
+
/**
|
|
2586
|
+
* Has something other than this gate answered a question this gate exists to
|
|
2587
|
+
* ask (APRV-378)?
|
|
2588
|
+
*
|
|
2589
|
+
* It reads `audit.question_preempted` and nothing else. There is no
|
|
2590
|
+
* configuration here to inspect: whether Codex's auto-reviewer runs is gated in
|
|
2591
|
+
* the harness's own configuration under a key this repository has no record of,
|
|
2592
|
+
* and a config read written against a guessed key finds nothing and reports a
|
|
2593
|
+
* GREEN row, which is the worst failure direction a health check has. So this
|
|
2594
|
+
* row reports what was OBSERVED, and the only durable record of an observation
|
|
2595
|
+
* is the one `approval codex bridge` appends when it stops.
|
|
2596
|
+
*
|
|
2597
|
+
* Reading this record is a diagnosis and not enforcement: the row authorizes
|
|
2598
|
+
* nothing and refuses nothing, and `tests/question-preempted.test.ts` pins that
|
|
2599
|
+
* no enforcement module reads the type.
|
|
2600
|
+
*
|
|
2601
|
+
* A row that says `pass` is saying the log holds no such record. It is NOT
|
|
2602
|
+
* saying the auto-reviewer is off: nothing on this machine can say that, which
|
|
2603
|
+
* is why the bridge probes rather than reads (APRV-364).
|
|
2604
|
+
*/
|
|
2605
|
+
function checkCodexAutoReviewer(records, verified) {
|
|
2606
|
+
const check = "codex-auto-reviewer";
|
|
2607
|
+
if (!verified) {
|
|
2608
|
+
return {
|
|
2609
|
+
check,
|
|
2610
|
+
status: "skip",
|
|
2611
|
+
detail: "the chain did not verify, so the records this row reads cannot be trusted; see the log check above",
|
|
2612
|
+
};
|
|
2613
|
+
}
|
|
2614
|
+
const seen = records.filter((record) => record.event === "audit.question_preempted");
|
|
2615
|
+
if (seen.length === 0) {
|
|
2616
|
+
return {
|
|
2617
|
+
check,
|
|
2618
|
+
status: "pass",
|
|
2619
|
+
detail: "no record of anything other than this gate answering a question it exists to ask. That is what the log holds, and it is not a claim that a harness auto-reviewer is off: only a bridge session's own probe can establish that, and only for the question it probed",
|
|
2620
|
+
};
|
|
2621
|
+
}
|
|
2622
|
+
const newest = seen[seen.length - 1];
|
|
2623
|
+
const payload = (newest.payload ?? {});
|
|
2624
|
+
const question = (payload["question"] ?? {});
|
|
2625
|
+
const source = typeof payload["source"] === "string" ? payload["source"] : "an unnamed source";
|
|
2626
|
+
const id = typeof question["id"] === "string" ? question["id"] : "(no id)";
|
|
2627
|
+
const verdict = typeof payload["verdict"] === "string" ? payload["verdict"] : "none stated";
|
|
2628
|
+
const recent = seen.filter((record) => Date.parse(record.ts) >= Date.now() - AUTO_REVIEWER_WINDOW_MS);
|
|
2629
|
+
const where = `${source} answered question ${id} (verdict: ${verdict}) at seq ${String(newest.seq)}`;
|
|
2630
|
+
if (recent.length === 0) {
|
|
2631
|
+
return {
|
|
2632
|
+
check,
|
|
2633
|
+
status: "pass",
|
|
2634
|
+
detail: `${String(seen.length)} record(s), none in the last ${String(Math.round(AUTO_REVIEWER_WINDOW_MS / 3_600_000))}h. The most recent: ${where}`,
|
|
2635
|
+
};
|
|
2636
|
+
}
|
|
2637
|
+
return {
|
|
2638
|
+
check,
|
|
2639
|
+
status: "fail",
|
|
2640
|
+
detail: `${String(recent.length)} of ${String(seen.length)} record(s) in the last ${String(Math.round(AUTO_REVIEWER_WINDOW_MS / 3_600_000))}h: ${where}. A session whose questions something else can answer is a session whose silence means nothing, and the bridge stopped on it`,
|
|
2641
|
+
fix: "approval doctor --verbose, then turn the harness's own auto-reviewer off in its configuration before running `approval codex bridge` again; this runtime cannot turn it off and cannot read whether it is on",
|
|
2642
|
+
};
|
|
2643
|
+
}
|
|
2271
2644
|
function checkGateOrgans(dir, records) {
|
|
2272
2645
|
const check = "gate-organs";
|
|
2273
2646
|
const root = repoRoot(dir) ?? dir;
|
|
@@ -2321,6 +2694,167 @@ function checkGateOrgans(dir, records) {
|
|
|
2321
2694
|
};
|
|
2322
2695
|
}
|
|
2323
2696
|
// ---------------------------------------------------------------------------
|
|
2697
|
+
// 31. pending-sign-off (APRV-338)
|
|
2698
|
+
// ---------------------------------------------------------------------------
|
|
2699
|
+
/**
|
|
2700
|
+
* The marker SPEC.md's amendment-provenance rule puts on unratified text.
|
|
2701
|
+
*
|
|
2702
|
+
* Matched case-insensitively on the words alone rather than on the whole
|
|
2703
|
+
* `(Amended APRV-n, pending sign-off.)` form, because the form is prose a human
|
|
2704
|
+
* writes and a row that missed a hand-typed variant would report a clean file
|
|
2705
|
+
* that is not one. Over-reporting here costs a line of output; under-reporting
|
|
2706
|
+
* costs the thing the marker exists for.
|
|
2707
|
+
*/
|
|
2708
|
+
const PENDING_SIGN_OFF_MARKER = /pending sign-off/giu;
|
|
2709
|
+
/**
|
|
2710
|
+
* Where to look for protected files carrying the marker.
|
|
2711
|
+
*
|
|
2712
|
+
* One level deep and enumerated, exactly as {@link ORGAN_SEARCH} is, and for
|
|
2713
|
+
* the same reason: `core/command-class.ts` decides what IS a protected path
|
|
2714
|
+
* (every candidate found here is put to it), so this list only says where to
|
|
2715
|
+
* look. A repository-wide walk would be a health check that reads the whole
|
|
2716
|
+
* working tree on every run, and the marker lives in governing documents, which
|
|
2717
|
+
* sit at the root or in a directory a policy named.
|
|
2718
|
+
*/
|
|
2719
|
+
const SIGN_OFF_SEARCH_DIRS = [".", join(".github", "workflows"), "docs", "design"];
|
|
2720
|
+
/** How many bytes of a candidate are read before it is skipped as not-prose. */
|
|
2721
|
+
const SIGN_OFF_MAX_BYTES = 4 * 1024 * 1024;
|
|
2722
|
+
/**
|
|
2723
|
+
* The protected files this checkout carries that doctor will read,
|
|
2724
|
+
* repository-relative and sorted.
|
|
2725
|
+
*
|
|
2726
|
+
* The enumerated directories above, plus every exact path the policy's
|
|
2727
|
+
* `protected_paths` names (a directory entry is enumerated one level deep like
|
|
2728
|
+
* the rest). A candidate is kept only when the classifier calls it `policy.edit`
|
|
2729
|
+
* or a `policy.edit.*` sub-class, which is the same set `approval policy attest
|
|
2730
|
+
* --path` will sign off: a row that listed a file the verb refuses would be
|
|
2731
|
+
* advice nobody can take.
|
|
2732
|
+
*/
|
|
2733
|
+
function listSignableFiles(root, extra) {
|
|
2734
|
+
const dirs = new Set(SIGN_OFF_SEARCH_DIRS);
|
|
2735
|
+
const exact = new Set();
|
|
2736
|
+
for (const raw of extra) {
|
|
2737
|
+
const entry = typeof raw === "string" ? raw : raw.path;
|
|
2738
|
+
if (typeof entry !== "string" || entry.length === 0)
|
|
2739
|
+
continue;
|
|
2740
|
+
if (entry.includes("..") || isAbsolute(entry))
|
|
2741
|
+
continue;
|
|
2742
|
+
if (/[/\\]$/u.test(entry))
|
|
2743
|
+
dirs.add(entry.replace(/[/\\]+$/u, ""));
|
|
2744
|
+
else
|
|
2745
|
+
exact.add(entry.split(/[/\\]+/u).join("/"));
|
|
2746
|
+
}
|
|
2747
|
+
const found = new Set();
|
|
2748
|
+
const keep = (relative) => {
|
|
2749
|
+
let isFile;
|
|
2750
|
+
try {
|
|
2751
|
+
const stats = statSync(join(root, relative));
|
|
2752
|
+
isFile = stats.isFile() && stats.size <= SIGN_OFF_MAX_BYTES;
|
|
2753
|
+
}
|
|
2754
|
+
catch {
|
|
2755
|
+
return;
|
|
2756
|
+
}
|
|
2757
|
+
if (!isFile)
|
|
2758
|
+
return;
|
|
2759
|
+
const routed = protectedPathClass(relative, extra);
|
|
2760
|
+
if (routed === "policy.edit" || (routed !== null && POLICY_EDIT_SUBCLASS.test(routed))) {
|
|
2761
|
+
found.add(relative);
|
|
2762
|
+
}
|
|
2763
|
+
};
|
|
2764
|
+
for (const dir of dirs) {
|
|
2765
|
+
let names;
|
|
2766
|
+
try {
|
|
2767
|
+
names = readdirSync(join(root, dir));
|
|
2768
|
+
}
|
|
2769
|
+
catch {
|
|
2770
|
+
continue;
|
|
2771
|
+
}
|
|
2772
|
+
const prefix = dir === "." ? "" : `${dir.split(/[/\\]+/u).join("/")}/`;
|
|
2773
|
+
for (const name of names.sort())
|
|
2774
|
+
keep(`${prefix}${name}`);
|
|
2775
|
+
}
|
|
2776
|
+
for (const path of exact)
|
|
2777
|
+
keep(path);
|
|
2778
|
+
return [...found].sort();
|
|
2779
|
+
}
|
|
2780
|
+
/**
|
|
2781
|
+
* Which protected files carry a pending-sign-off marker that no record
|
|
2782
|
+
* resolves (APRV-338)?
|
|
2783
|
+
*
|
|
2784
|
+
* SPEC.md's amendment-provenance rule says text that reached a protected file
|
|
2785
|
+
* without a grant carries `(Amended APRV-n, pending sign-off.)` and holds no
|
|
2786
|
+
* more authority than a proposal until a human ratifies it. Since APRV-338 the
|
|
2787
|
+
* ratification is a record, `gate.path.signed_off`, so the state is finally
|
|
2788
|
+
* answerable: a file whose CURRENT bytes carry a sign-off is ratified as it
|
|
2789
|
+
* stands, and one that carries the marker with no such record is a debt
|
|
2790
|
+
* somebody owes.
|
|
2791
|
+
*
|
|
2792
|
+
* **This row never moves the exit code**, for the reasons `gate-organs` does
|
|
2793
|
+
* not. Nothing on this machine is broken by unratified prose; what the row buys
|
|
2794
|
+
* is that the debt is visible at the terminal rather than discovered when a
|
|
2795
|
+
* pull request fails. And a marker is a human's own sentence about their own
|
|
2796
|
+
* text: a health check that failed on one would be the runtime grading prose.
|
|
2797
|
+
*/
|
|
2798
|
+
function checkPendingSignOff(dir, records, policyLoad) {
|
|
2799
|
+
const check = "pending-sign-off";
|
|
2800
|
+
const root = repoRoot(dir) ?? dir;
|
|
2801
|
+
// A policy that did not load contributes no entries, which narrows the set
|
|
2802
|
+
// doctor reads rather than widening it — the same direction every other
|
|
2803
|
+
// reader of `protected_paths` takes when it cannot have the list.
|
|
2804
|
+
const extra = policyLoad.ok ? (policyLoad.policy.protected_paths ?? []) : [];
|
|
2805
|
+
const files = listSignableFiles(root, extra);
|
|
2806
|
+
const pending = [];
|
|
2807
|
+
const ratified = [];
|
|
2808
|
+
const unreadable = [];
|
|
2809
|
+
for (const file of files) {
|
|
2810
|
+
let text;
|
|
2811
|
+
let sha256;
|
|
2812
|
+
try {
|
|
2813
|
+
const bytes = readFileSync(join(root, file));
|
|
2814
|
+
text = bytes.toString("utf8");
|
|
2815
|
+
sha256 = policyBytesHash(bytes);
|
|
2816
|
+
}
|
|
2817
|
+
catch (cause) {
|
|
2818
|
+
unreadable.push(`${file} (${detailOf(cause)})`);
|
|
2819
|
+
continue;
|
|
2820
|
+
}
|
|
2821
|
+
const markers = text.match(PENDING_SIGN_OFF_MARKER)?.length ?? 0;
|
|
2822
|
+
if (markers === 0)
|
|
2823
|
+
continue;
|
|
2824
|
+
const plural = markers === 1 ? "" : "s";
|
|
2825
|
+
if (findPathSignOff(records, file, sha256) !== null) {
|
|
2826
|
+
ratified.push(`${file} (${String(markers)} marker${plural}, signed off at these bytes)`);
|
|
2827
|
+
continue;
|
|
2828
|
+
}
|
|
2829
|
+
const previous = latestPathSignOff(records, file);
|
|
2830
|
+
pending.push(previous === null
|
|
2831
|
+
? `${file} (${String(markers)} marker${plural}, never signed off, live ${sha256.slice(0, 12)}…)`
|
|
2832
|
+
: `${file} (${String(markers)} marker${plural}, edited since seq ${previous.record.seq}: signed ${previous.sha256.slice(0, 12)}…, live ${sha256.slice(0, 12)}…)`);
|
|
2833
|
+
}
|
|
2834
|
+
if (pending.length === 0 && unreadable.length === 0) {
|
|
2835
|
+
return {
|
|
2836
|
+
check,
|
|
2837
|
+
status: "pass",
|
|
2838
|
+
detail: ratified.length === 0
|
|
2839
|
+
? `no protected file in ${root} carries a pending-sign-off marker (${String(files.length)} file(s) read)`
|
|
2840
|
+
: `every pending-sign-off marker in ${root} sits in a file a human has signed off at its current bytes: ${ratified.join("; ")}`,
|
|
2841
|
+
};
|
|
2842
|
+
}
|
|
2843
|
+
const parts = [];
|
|
2844
|
+
if (pending.length > 0)
|
|
2845
|
+
parts.push(`AWAITING SIGN-OFF: ${pending.join("; ")}`);
|
|
2846
|
+
if (unreadable.length > 0)
|
|
2847
|
+
parts.push(`unreadable: ${unreadable.join("; ")}`);
|
|
2848
|
+
const first = (pending[0] ?? "<path>").split(" ")[0] ?? "<path>";
|
|
2849
|
+
return {
|
|
2850
|
+
check,
|
|
2851
|
+
// Never a fail: see the note above.
|
|
2852
|
+
status: "skip",
|
|
2853
|
+
detail: `${parts.join(". ")}. Text carrying that marker holds no more authority than a proposal until a human ratifies it (SPEC.md §5.2), and the ratification is a gate.path.signed_off record over the file's exact bytes. A marker whose text was later granted through the gate should lose the suffix instead; signing off is for text that reached the file without a grant`,
|
|
2854
|
+
fix: `approval policy attest --path ${first} --as human:<id> — after READING the file as it now stands`,
|
|
2855
|
+
};
|
|
2856
|
+
}
|
|
2857
|
+
// ---------------------------------------------------------------------------
|
|
2324
2858
|
// 28. sealed-keys (APRV-285)
|
|
2325
2859
|
// ---------------------------------------------------------------------------
|
|
2326
2860
|
/** The exact line doctor tells an operator to add for the sealed-token key store. */
|
|
@@ -2556,7 +3090,7 @@ export function commandDoctor(argv, streams, cwd) {
|
|
|
2556
3090
|
checkIdentity(),
|
|
2557
3091
|
checkAttestationHealth(verified.records, policyPath),
|
|
2558
3092
|
checkLog(logPath, verified.result),
|
|
2559
|
-
await checkTelegram(apiBase, policyLoad),
|
|
3093
|
+
await checkTelegram(apiBase, policyLoad, logPath),
|
|
2560
3094
|
await checkWebPort(port ?? WEB_DEFAULT_PORT),
|
|
2561
3095
|
checkPayloadStore(logPath, verified.records),
|
|
2562
3096
|
// APRV-271: asks the running daemon for the one half of this answer that
|
|
@@ -2599,6 +3133,15 @@ export function commandDoctor(argv, streams, cwd) {
|
|
|
2599
3133
|
// network to be more accurate would be acting on its own account, so the
|
|
2600
3134
|
// answer is as fresh as the operator's last fetch and says so.
|
|
2601
3135
|
checkMainBehindOrigin(logPath, queuePath, root),
|
|
3136
|
+
// APRV-342: the interregnum between an attestation and its pull request
|
|
3137
|
+
// merging. `attestation` above asks whether the LOCAL policy is attested;
|
|
3138
|
+
// this asks whether the attested policy is on the remote, which is the
|
|
3139
|
+
// question a fresh checkout of main answers with `policy-not-attested`.
|
|
3140
|
+
checkAttestedPolicyOnMain({
|
|
3141
|
+
policyPath,
|
|
3142
|
+
records: verified.records,
|
|
3143
|
+
root: repoRoot(dirname(logPath)),
|
|
3144
|
+
}),
|
|
2602
3145
|
// APRV-227: appended, fourteenth time, same reason. The only row that
|
|
2603
3146
|
// asks a question about a binary OUTSIDE this repository, and it asks it
|
|
2604
3147
|
// the one way a log can: what the last record said the harness was,
|
|
@@ -2631,6 +3174,32 @@ export function commandDoctor(argv, streams, cwd) {
|
|
|
2631
3174
|
// `.approval/` is a directory people `git add` from, and the key store had
|
|
2632
3175
|
// nothing telling them it must not come along.
|
|
2633
3176
|
checkSealedKeys(logPath, dir),
|
|
3177
|
+
// APRV-313: appended, nineteenth time, same reason. Configuration on
|
|
3178
|
+
// disk is distinct from Codex trust, loading and observed execution.
|
|
3179
|
+
checkCodexHookWiring(dir),
|
|
3180
|
+
// APRV-335: appended, twentieth time, same reason. The health-report half
|
|
3181
|
+
// of the alias deprecation SPEC.md §5.2 now states: the loader's note is
|
|
3182
|
+
// seen by whoever runs a verb that prints notes, and this is the surface
|
|
3183
|
+
// an operator opens to read the state of their own repository.
|
|
3184
|
+
checkAutonomyAlias(policyLoad),
|
|
3185
|
+
// APRV-338: appended, twenty-first time, same reason. The terminal half
|
|
3186
|
+
// of the sign-off record: which protected files still carry SPEC.md's
|
|
3187
|
+
// pending-sign-off marker with no record ratifying their current bytes.
|
|
3188
|
+
// Informational and never a fail, exactly as gate-organs is — nothing on
|
|
3189
|
+
// this machine is broken by unratified prose, and a health check that
|
|
3190
|
+
// graded prose would be the runtime marking a human's homework.
|
|
3191
|
+
checkPendingSignOff(dir, verified.records, policyLoad),
|
|
3192
|
+
// APRV-324: appended, twenty-second time, same reason. The row design
|
|
3193
|
+
// §5 item 13 asks for, and the one an operator needs BEFORE the first
|
|
3194
|
+
// tap is refused: declaring a `senders` entry turns enforcement on for
|
|
3195
|
+
// that whole channel, so an approver left out of it is a person the
|
|
3196
|
+
// policy still lists and the gate will no longer honour.
|
|
3197
|
+
checkSenderMapping(policyLoad),
|
|
3198
|
+
// APRV-378: appended, twenty-third time, same reason. The terminal half
|
|
3199
|
+
// of `audit.question_preempted`: the bridge stops the session that saw
|
|
3200
|
+
// one, and this is where an operator finds out it happened without
|
|
3201
|
+
// reading the log by hand.
|
|
3202
|
+
checkCodexAutoReviewer(verified.records, verified.result.status === "clean"),
|
|
2634
3203
|
];
|
|
2635
3204
|
const ok = checks.every((entry) => entry.status !== "fail");
|
|
2636
3205
|
if (json)
|