approval-md 0.2.0 → 0.4.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 +85 -877
- package/SPEC.md +150 -19
- package/cli.js +66 -7
- package/dist/src/adapters/smtp.d.ts +18 -0
- package/dist/src/adapters/smtp.js +24 -3
- package/dist/src/adapters/smtp.js.map +1 -1
- package/dist/src/channels/contract.d.ts +53 -1
- package/dist/src/channels/contract.js +304 -7
- package/dist/src/channels/contract.js.map +1 -1
- package/dist/src/channels/relay-server.d.ts +90 -0
- package/dist/src/channels/relay-server.js +295 -0
- package/dist/src/channels/relay-server.js.map +1 -0
- package/dist/src/channels/relay.d.ts +273 -0
- package/dist/src/channels/relay.js +564 -0
- package/dist/src/channels/relay.js.map +1 -0
- package/dist/src/channels/tagging.js +6 -1
- package/dist/src/channels/tagging.js.map +1 -1
- package/dist/src/channels/telegram-webhook.d.ts +273 -0
- package/dist/src/channels/telegram-webhook.js +536 -0
- package/dist/src/channels/telegram-webhook.js.map +1 -0
- package/dist/src/channels/telegram.d.ts +394 -12
- package/dist/src/channels/telegram.js +621 -68
- package/dist/src/channels/telegram.js.map +1 -1
- package/dist/src/channels/web.d.ts +9 -0
- package/dist/src/channels/web.js +17 -0
- package/dist/src/channels/web.js.map +1 -1
- package/dist/src/cli/amend.js +564 -34
- package/dist/src/cli/amend.js.map +1 -1
- package/dist/src/cli/attest.d.ts +9 -0
- package/dist/src/cli/attest.js +177 -13
- package/dist/src/cli/attest.js.map +1 -1
- package/dist/src/cli/channel-relay.d.ts +108 -0
- package/dist/src/cli/channel-relay.js +334 -0
- package/dist/src/cli/channel-relay.js.map +1 -0
- package/dist/src/cli/channel-telegram-webhook.d.ts +273 -0
- package/dist/src/cli/channel-telegram-webhook.js +709 -0
- package/dist/src/cli/channel-telegram-webhook.js.map +1 -0
- package/dist/src/cli/channel-telegram.d.ts +347 -28
- package/dist/src/cli/channel-telegram.js +763 -28
- package/dist/src/cli/channel-telegram.js.map +1 -1
- package/dist/src/cli/channel.d.ts +10 -1
- package/dist/src/cli/channel.js +14 -1
- package/dist/src/cli/channel.js.map +1 -1
- 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 +1 -1
- package/dist/src/cli/codex.js +304 -7
- package/dist/src/cli/codex.js.map +1 -1
- package/dist/src/cli/daemon.js +41 -6
- package/dist/src/cli/daemon.js.map +1 -1
- package/dist/src/cli/doctor.d.ts +63 -0
- package/dist/src/cli/doctor.js +1322 -122
- package/dist/src/cli/doctor.js.map +1 -1
- package/dist/src/cli/execute.d.ts +17 -1
- package/dist/src/cli/execute.js +366 -22
- package/dist/src/cli/execute.js.map +1 -1
- package/dist/src/cli/exit-codes.d.ts +12 -0
- package/dist/src/cli/exit-codes.js +13 -0
- package/dist/src/cli/exit-codes.js.map +1 -1
- package/dist/src/cli/gate.d.ts +2 -0
- package/dist/src/cli/gate.js +233 -2
- package/dist/src/cli/gate.js.map +1 -1
- package/dist/src/cli/help.d.ts +14 -2
- package/dist/src/cli/help.js +395 -82
- package/dist/src/cli/help.js.map +1 -1
- package/dist/src/cli/hook-codex.d.ts +49 -1
- package/dist/src/cli/hook-codex.js +60 -1
- package/dist/src/cli/hook-codex.js.map +1 -1
- package/dist/src/cli/hook.d.ts +828 -4
- package/dist/src/cli/hook.js +2972 -142
- package/dist/src/cli/hook.js.map +1 -1
- package/dist/src/cli/import.js +1 -1
- package/dist/src/cli/import.js.map +1 -1
- package/dist/src/cli/init.js +16 -7
- package/dist/src/cli/init.js.map +1 -1
- package/dist/src/cli/instructions.js +24 -3
- package/dist/src/cli/instructions.js.map +1 -1
- package/dist/src/cli/log-anchor.d.ts +55 -0
- package/dist/src/cli/log-anchor.js +199 -60
- package/dist/src/cli/log-anchor.js.map +1 -1
- package/dist/src/cli/main.d.ts +0 -12
- package/dist/src/cli/main.js +56 -3
- package/dist/src/cli/main.js.map +1 -1
- package/dist/src/cli/muse.d.ts +2 -0
- package/dist/src/cli/muse.js +62 -0
- package/dist/src/cli/muse.js.map +1 -0
- package/dist/src/cli/payload.js +79 -1
- package/dist/src/cli/payload.js.map +1 -1
- 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.js +14 -1
- package/dist/src/cli/policy.js.map +1 -1
- package/dist/src/cli/preflight.d.ts +151 -13
- package/dist/src/cli/preflight.js +398 -41
- package/dist/src/cli/preflight.js.map +1 -1
- package/dist/src/cli/sandbox.js +17 -1
- package/dist/src/cli/sandbox.js.map +1 -1
- package/dist/src/cli/scaffold.d.ts +1 -1
- package/dist/src/cli/scaffold.js +7 -1
- package/dist/src/cli/scaffold.js.map +1 -1
- package/dist/src/cli/serve.d.ts +53 -0
- package/dist/src/cli/serve.js +270 -0
- package/dist/src/cli/serve.js.map +1 -0
- package/dist/src/cli/setup-channel.d.ts +9 -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-common.d.ts +3 -1
- package/dist/src/cli/setup-common.js +3 -2
- package/dist/src/cli/setup-common.js.map +1 -1
- package/dist/src/cli/setup.d.ts +2 -0
- package/dist/src/cli/setup.js +94 -2
- package/dist/src/cli/setup.js.map +1 -1
- package/dist/src/cli/up.js +134 -51
- package/dist/src/cli/up.js.map +1 -1
- package/dist/src/cli/values.js +3 -4
- package/dist/src/cli/values.js.map +1 -1
- package/dist/src/cli/verb-registry.d.ts +2 -0
- package/dist/src/cli/verb-registry.js +510 -47
- package/dist/src/cli/verb-registry.js.map +1 -1
- package/dist/src/cli/wordmark.d.ts +2 -2
- 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/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/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/core/advance-cycle.d.ts +51 -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 +20 -18
- package/dist/src/core/agents-md.js +33 -31
- package/dist/src/core/agents-md.js.map +1 -1
- package/dist/src/core/attest-bootstrap.d.ts +95 -0
- package/dist/src/core/attest-bootstrap.js +135 -0
- package/dist/src/core/attest-bootstrap.js.map +1 -0
- package/dist/src/core/attest.d.ts +234 -2
- package/dist/src/core/attest.js +342 -7
- package/dist/src/core/attest.js.map +1 -1
- package/dist/src/core/audit.d.ts +73 -3
- package/dist/src/core/audit.js +65 -5
- package/dist/src/core/audit.js.map +1 -1
- package/dist/src/core/channel-lease.d.ts +322 -0
- package/dist/src/core/channel-lease.js +663 -0
- package/dist/src/core/channel-lease.js.map +1 -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/command-class.d.ts +205 -0
- package/dist/src/core/command-class.js +1433 -28
- 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/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/daemon-host.d.ts +178 -0
- package/dist/src/core/daemon-host.js +228 -0
- package/dist/src/core/daemon-host.js.map +1 -0
- package/dist/src/core/daemon-identity.d.ts +238 -0
- package/dist/src/core/daemon-identity.js +252 -0
- package/dist/src/core/daemon-identity.js.map +1 -0
- package/dist/src/core/dark-session.d.ts +109 -8
- 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 +23 -2
- 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 +5 -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 +15 -2
- package/dist/src/core/execute.js +15 -2
- package/dist/src/core/execute.js.map +1 -1
- package/dist/src/core/gate.d.ts +392 -6
- package/dist/src/core/gate.js +852 -24
- 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/harness-version.d.ts +128 -4
- package/dist/src/core/harness-version.js +181 -12
- package/dist/src/core/harness-version.js.map +1 -1
- package/dist/src/core/harness-wait.d.ts +82 -3
- package/dist/src/core/harness-wait.js +89 -3
- package/dist/src/core/harness-wait.js.map +1 -1
- package/dist/src/core/instance.d.ts +59 -2
- package/dist/src/core/instance.js +113 -0
- package/dist/src/core/instance.js.map +1 -1
- package/dist/src/core/log-subscribe.d.ts +21 -0
- package/dist/src/core/log-subscribe.js +16 -6
- package/dist/src/core/log-subscribe.js.map +1 -1
- package/dist/src/core/log-write-layer.d.ts +55 -0
- package/dist/src/core/log-write-layer.js +91 -0
- package/dist/src/core/log-write-layer.js.map +1 -0
- package/dist/src/core/log.d.ts +89 -22
- package/dist/src/core/log.js +197 -19
- package/dist/src/core/log.js.map +1 -1
- package/dist/src/core/payload-census.d.ts +51 -0
- package/dist/src/core/payload-census.js +125 -2
- package/dist/src/core/payload-census.js.map +1 -1
- package/dist/src/core/payload-store.d.ts +31 -0
- package/dist/src/core/payload-store.js +113 -8
- package/dist/src/core/payload-store.js.map +1 -1
- package/dist/src/core/payload.d.ts +18 -3
- package/dist/src/core/payload.js +20 -4
- package/dist/src/core/payload.js.map +1 -1
- package/dist/src/core/policy-diff.d.ts +10 -0
- package/dist/src/core/policy-diff.js +10 -2
- package/dist/src/core/policy-diff.js.map +1 -1
- package/dist/src/core/policy-explain.d.ts +17 -1
- package/dist/src/core/policy-explain.js +48 -1
- package/dist/src/core/policy-explain.js.map +1 -1
- package/dist/src/core/policy-load.d.ts +65 -1
- package/dist/src/core/policy-load.js +21 -3
- package/dist/src/core/policy-load.js.map +1 -1
- package/dist/src/core/policy-match.d.ts +81 -0
- package/dist/src/core/policy-match.js +87 -0
- package/dist/src/core/policy-match.js.map +1 -1
- package/dist/src/core/policy-proposal.d.ts +75 -0
- package/dist/src/core/policy-proposal.js +110 -4
- package/dist/src/core/policy-proposal.js.map +1 -1
- package/dist/src/core/protected-path-guard.d.ts +117 -4
- package/dist/src/core/protected-path-guard.js +362 -48
- 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 +182 -0
- package/dist/src/core/read-scope.js +256 -0
- package/dist/src/core/read-scope.js.map +1 -0
- package/dist/src/core/run-payload.d.ts +160 -0
- package/dist/src/core/run-payload.js +304 -0
- package/dist/src/core/run-payload.js.map +1 -0
- package/dist/src/core/sandbox.d.ts +81 -0
- package/dist/src/core/sandbox.js +190 -1
- package/dist/src/core/sandbox.js.map +1 -1
- package/dist/src/core/sender-identity.d.ts +501 -0
- package/dist/src/core/sender-identity.js +609 -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 +52 -1
- package/dist/src/core/state.js +52 -6
- package/dist/src/core/state.js.map +1 -1
- package/dist/src/core/telegram-config.d.ts +79 -0
- package/dist/src/core/telegram-config.js +108 -0
- package/dist/src/core/telegram-config.js.map +1 -1
- package/dist/src/core/values.d.ts +18 -8
- package/dist/src/core/values.js +36 -1
- package/dist/src/core/values.js.map +1 -1
- package/dist/src/core/verify.d.ts +23 -1
- package/dist/src/core/verify.js +20 -2
- package/dist/src/core/verify.js.map +1 -1
- package/dist/src/daemon/advance.d.ts +10 -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 +80 -5
- package/dist/src/daemon/audit.js +126 -6
- package/dist/src/daemon/audit.js.map +1 -1
- package/dist/src/daemon/daemon.d.ts +174 -2
- package/dist/src/daemon/daemon.js +307 -7
- package/dist/src/daemon/daemon.js.map +1 -1
- package/dist/src/daemon/git-evidence.d.ts +2 -2
- package/dist/src/daemon/git-evidence.js +1 -1
- package/dist/src/daemon/projection.d.ts +13 -2
- package/dist/src/daemon/projection.js +39 -6
- package/dist/src/daemon/projection.js.map +1 -1
- package/dist/src/mcp/server.d.ts +39 -3
- package/dist/src/mcp/server.js +30 -3
- package/dist/src/mcp/server.js.map +1 -1
- package/dist/src/muse/server.d.ts +16 -0
- package/dist/src/muse/server.js +238 -0
- package/dist/src/muse/server.js.map +1 -0
- package/dist/src/serve/archive.d.ts +158 -0
- package/dist/src/serve/archive.js +354 -0
- package/dist/src/serve/archive.js.map +1 -0
- package/dist/src/serve/arguments.d.ts +93 -0
- package/dist/src/serve/arguments.js +228 -0
- package/dist/src/serve/arguments.js.map +1 -0
- package/dist/src/serve/credentials.d.ts +91 -0
- package/dist/src/serve/credentials.js +143 -0
- package/dist/src/serve/credentials.js.map +1 -0
- package/dist/src/serve/follow.d.ts +86 -0
- package/dist/src/serve/follow.js +101 -0
- package/dist/src/serve/follow.js.map +1 -0
- package/dist/src/serve/hook-thread.d.ts +134 -0
- package/dist/src/serve/hook-thread.js +333 -0
- package/dist/src/serve/hook-thread.js.map +1 -0
- package/dist/src/serve/hook-worker.d.ts +134 -0
- package/dist/src/serve/hook-worker.js +173 -0
- package/dist/src/serve/hook-worker.js.map +1 -0
- package/dist/src/serve/server.d.ts +412 -0
- package/dist/src/serve/server.js +1251 -0
- package/dist/src/serve/server.js.map +1 -0
- package/docs/README-extended.md +766 -0
- package/docs/cli-reference.md +2563 -73
- package/docs/codex-enforced-session.md +75 -2
- package/docs/codex-workspace-broker.md +118 -0
- package/package.json +6 -2
- package/schema/event.schema.json +595 -9
- 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/daemon-id-malformed.json +15 -0
- package/schema/fixtures/event/invalid/execution-started-truncated-policy-sha256.json +18 -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/invalid/harness-cap-at-margin.json +16 -0
- package/schema/fixtures/event/invalid/harness-cap-fractional.json +16 -0
- package/schema/fixtures/event/invalid/harness-cap-negative.json +16 -0
- package/schema/fixtures/event/invalid/harness-cap-string.json +16 -0
- package/schema/fixtures/event/invalid/harness-cap-without-harness-execution.json +16 -0
- package/schema/fixtures/event/invalid/harness-cap-zero.json +16 -0
- package/schema/fixtures/event/valid/approval-granted-sender-hashed.json +20 -0
- package/schema/fixtures/event/valid/approval-requested-harness-cap.json +18 -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/daemon-identity-declared.json +17 -0
- package/schema/fixtures/event/valid/daemon-identity.json +15 -0
- package/schema/fixtures/event/valid/execution-started-policy-sha256.json +18 -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-hermes.json +23 -0
- package/schema/fixtures/event/valid/harness-kind-muse.json +23 -0
- package/schema/fixtures/policy/invalid/daemons-malformed-id.json +10 -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/daemons-allowlist.json +19 -0
- package/schema/fixtures/policy/valid/senders-keyed.json +24 -0
- package/schema/fixtures/policy-md/invalid/schema-invalid-agent-may-request.md +21 -0
- package/schema/fixtures/policy-md/valid/agent-may-request.md +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 +100 -4
- package/schema/values.schema.json +7 -11
- package/schema/fixtures/values/invalid/version-string.json +0 -1
package/dist/src/cli/amend.js
CHANGED
|
@@ -59,12 +59,14 @@ import { createHash } from "node:crypto";
|
|
|
59
59
|
import { accessSync, constants, existsSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
60
60
|
import { tmpdir } from "node:os";
|
|
61
61
|
import { basename, dirname, isAbsolute, join, relative, resolve as resolvePathSegments, sep } from "node:path";
|
|
62
|
-
import { HUMAN_ACTOR_ENV, appendAttestation, checkAttestation,
|
|
62
|
+
import { HUMAN_ACTOR_ENV, appendAttestation, attestedPolicyPayloadHash, checkAttestation, policyBytesHash, resolveHumanActor, } from "../core/attest.js";
|
|
63
63
|
import { compareChains } from "../core/log-reconcile.js";
|
|
64
64
|
import { diffPolicies, renderDiff, SPEC_NAMESPACES } from "../core/policy-diff.js";
|
|
65
65
|
import { checkPolicyExpectations, describeFailure, describePinChange, diffPinSources, expectationsFor, DOGFOOD_SUITE_BUILT, DOGFOOD_SUITE_SOURCE, EXPECTATIONS_MODULE, } from "../core/policy-expectations.js";
|
|
66
66
|
import { loadPolicy, parseDuration, POLICY_FILENAMES, } from "../core/policy-load.js";
|
|
67
|
-
import {
|
|
67
|
+
import { payloadHash } from "../core/payload.js";
|
|
68
|
+
import { payloadPath, payloadStoreDirFor } from "../core/payload-store.js";
|
|
69
|
+
import { proposalPayloadValue, proposalState, proposeAttestation, } from "../core/policy-proposal.js";
|
|
68
70
|
import { readVerifiedRecords } from "../core/state.js";
|
|
69
71
|
import { boolFlag, parseFlags, stringFlag } from "./args.js";
|
|
70
72
|
import { EXIT_INTEGRITY, EXIT_IO, EXIT_OK, EXIT_TORN_TAIL, EXIT_USAGE, } from "./exit-codes.js";
|
|
@@ -83,6 +85,9 @@ const FLAGS = {
|
|
|
83
85
|
"--require-load": "boolean",
|
|
84
86
|
"--dry-run": "boolean",
|
|
85
87
|
"--commit": "boolean",
|
|
88
|
+
// APRV-341: the whole ceremony in one word. Implies `--commit` and forces the
|
|
89
|
+
// branch flow, so the amendment reaches a protected trunk the only way it can.
|
|
90
|
+
"--pr": "boolean",
|
|
86
91
|
"--no-publish": "boolean",
|
|
87
92
|
"--branch": "string",
|
|
88
93
|
"--direct": "boolean",
|
|
@@ -946,7 +951,11 @@ export function commandPolicyAmend(argv, streams, cwd) {
|
|
|
946
951
|
}
|
|
947
952
|
const dryRun = boolFlag(parsed.flags, "--dry-run");
|
|
948
953
|
const requireLoad = boolFlag(parsed.flags, "--require-load");
|
|
949
|
-
|
|
954
|
+
// APRV-341. `--pr` is `--commit` plus "and finish the job": the branch flow,
|
|
955
|
+
// whatever the protection probe thinks, because the operator has said which
|
|
956
|
+
// ceremony they want. `--commit` alone keeps letting the probe decide.
|
|
957
|
+
const wantPr = boolFlag(parsed.flags, "--pr");
|
|
958
|
+
const wantCommit = boolFlag(parsed.flags, "--commit") || wantPr;
|
|
950
959
|
// APRV-130: the ceremony publishes by default (push, and on a protected main
|
|
951
960
|
// branch + push + PR). `--no-publish` is the operator who wants it to stop at
|
|
952
961
|
// the commit, which is what `--commit` did before the publishing half existed.
|
|
@@ -960,6 +969,12 @@ export function commandPolicyAmend(argv, streams, cwd) {
|
|
|
960
969
|
if (branchFlag !== null && branchFlag.trim().length === 0) {
|
|
961
970
|
return usageError(streams, json, "--branch expects a branch name");
|
|
962
971
|
}
|
|
972
|
+
if (wantPr && forceDirect) {
|
|
973
|
+
return usageError(streams, json, "--pr and --direct ask for opposite ceremonies: --pr publishes through a branch and a pull request, --direct commits on the branch you are standing on. Pass one of them");
|
|
974
|
+
}
|
|
975
|
+
if (wantPr && noPublish) {
|
|
976
|
+
return usageError(streams, json, "--pr and --no-publish ask for opposite ceremonies: --pr opens the pull request, --no-publish stops at the commit. Pass one of them");
|
|
977
|
+
}
|
|
963
978
|
// Identity first, before a byte is read. Asking a human to read a diff and
|
|
964
979
|
// only then telling them their sign-off cannot be attributed wastes the one
|
|
965
980
|
// resource this system spends.
|
|
@@ -1006,13 +1021,54 @@ export function commandPolicyAmend(argv, streams, cwd) {
|
|
|
1006
1021
|
return refuse(streams, json, "io", policy.message, EXIT_IO);
|
|
1007
1022
|
const policyPath = policy.path;
|
|
1008
1023
|
let liveSha256;
|
|
1024
|
+
/**
|
|
1025
|
+
* The live bytes as text, for the payload file this ceremony will commit
|
|
1026
|
+
* (APRV-356).
|
|
1027
|
+
*
|
|
1028
|
+
* One read for both, on the rule `core/attest.ts` states for the same pair:
|
|
1029
|
+
* two reads of a file an operator may still be editing can disagree, and the
|
|
1030
|
+
* digest reported in the report has to be the digest of the bytes whose store
|
|
1031
|
+
* file the `git add` names.
|
|
1032
|
+
*/
|
|
1033
|
+
let liveText;
|
|
1009
1034
|
try {
|
|
1010
|
-
|
|
1035
|
+
const liveBytes = readFileSync(policyPath);
|
|
1036
|
+
liveSha256 = policyBytesHash(liveBytes);
|
|
1037
|
+
liveText = liveBytes.toString("utf8");
|
|
1011
1038
|
}
|
|
1012
1039
|
catch (cause) {
|
|
1013
1040
|
return refuse(streams, json, "io", `policy ${policyPath} could not be read: ${detail(cause)}`, EXIT_IO);
|
|
1014
1041
|
}
|
|
1015
1042
|
const logPath = resolvePath(stringFlag(parsed.flags, "--log"), DEFAULT_LOG_PATH, cwd);
|
|
1043
|
+
/**
|
|
1044
|
+
* The store file holding the attested policy text, absolute (APRV-356).
|
|
1045
|
+
*
|
|
1046
|
+
* The ceremony commit carries it beside the policy and the log, because a
|
|
1047
|
+
* committed log carrying a binding whose payload file was never committed is
|
|
1048
|
+
* a chain whose in-force bytes are unrecoverable to every reader of the
|
|
1049
|
+
* committed copy, which is the state this task exists to end. The CI
|
|
1050
|
+
* protected-path guard resolves payloads out of committed trees, so that copy
|
|
1051
|
+
* is the one it reads.
|
|
1052
|
+
*
|
|
1053
|
+
* Which file it is depends on which door the attestation comes through, and
|
|
1054
|
+
* both are addressable from the bytes on disk before anything is appended:
|
|
1055
|
+
*
|
|
1056
|
+
* - The HUMAN path appends the attestation here, and since APRV-356 that
|
|
1057
|
+
* record binds `{ text }` (`core/attest.ts`).
|
|
1058
|
+
* - The AGENT path proposes and waits for a tap. The phone's attestation
|
|
1059
|
+
* stores nothing, which APRV-356 left unchanged; what carries the text on
|
|
1060
|
+
* that chain is the `policy.proposed` this ceremony appends, binding
|
|
1061
|
+
* `{ policy_path, text }` (`core/policy-proposal.ts`).
|
|
1062
|
+
*
|
|
1063
|
+
* Addressed from `liveText` rather than read back from the appended record,
|
|
1064
|
+
* so it is a ceremony file from the start: `--dry-run` names it in the `git
|
|
1065
|
+
* add` it would run, and `--commit` counts it as its own rather than as a
|
|
1066
|
+
* stray in the index. An edit landing between here and the append moves the
|
|
1067
|
+
* hash, and the append refuses on the digest before this path is used.
|
|
1068
|
+
*/
|
|
1069
|
+
const payloadFile = payloadPath(payloadStoreDirFor(logPath), agentActor === null
|
|
1070
|
+
? attestedPolicyPayloadHash(liveText)
|
|
1071
|
+
: payloadHash(proposalPayloadValue(policyPath, liveText)));
|
|
1016
1072
|
// (a-pre) The thirty-three seconds of silence, ended (APRV-167).
|
|
1017
1073
|
//
|
|
1018
1074
|
// Everything from here to the `Policy` block below is work the operator could
|
|
@@ -1044,6 +1100,14 @@ export function commandPolicyAmend(argv, streams, cwd) {
|
|
|
1044
1100
|
: EXIT_IO;
|
|
1045
1101
|
return refuse(streams, json, read.code, read.message, exitCode);
|
|
1046
1102
|
}
|
|
1103
|
+
/**
|
|
1104
|
+
* The amendment branch's name, `--branch` or the seq-derived default.
|
|
1105
|
+
*
|
|
1106
|
+
* Declared HERE rather than beside the flow selection below (APRV-420): the
|
|
1107
|
+
* no-op path needs it too, because a re-run's whole job is to find the branch
|
|
1108
|
+
* a previous run left standing.
|
|
1109
|
+
*/
|
|
1110
|
+
const branchName = (seq) => branchFlag ?? `policy-amend-${seq}`;
|
|
1047
1111
|
const status = checkAttestation(read.records, policyPath);
|
|
1048
1112
|
const attested = status.status === "attested"
|
|
1049
1113
|
? { sha256: status.sha256, seq: status.seq }
|
|
@@ -1053,7 +1117,27 @@ export function commandPolicyAmend(argv, streams, cwd) {
|
|
|
1053
1117
|
// (a) Nothing to amend. A no-op ceremony is a SUCCESS, not an error: an
|
|
1054
1118
|
// operator (or a script) that runs `amend` on an already-attested policy has
|
|
1055
1119
|
// established exactly what they wanted to establish.
|
|
1120
|
+
//
|
|
1121
|
+
// APRV-420, one exception: an amendment branch already standing on origin.
|
|
1122
|
+
// The live policy does match its attestation, and saying only that leaves the
|
|
1123
|
+
// pull request carrying it open, unmergeable and unarmed, which is the state a
|
|
1124
|
+
// records advance landing first puts it in. So the re-run repairs it, or names
|
|
1125
|
+
// exactly what to run — never "nothing to amend" over a dirty amendment.
|
|
1056
1126
|
if (status.status === "attested") {
|
|
1127
|
+
const noopRoot = repoRoot(dirname(policyPath));
|
|
1128
|
+
const repair = wantCommit && noopRoot !== null && hasOrigin(noopRoot)
|
|
1129
|
+
? repairAmendBranch({
|
|
1130
|
+
root: noopRoot,
|
|
1131
|
+
policyPath,
|
|
1132
|
+
logPath,
|
|
1133
|
+
payloadFile,
|
|
1134
|
+
seq: status.seq,
|
|
1135
|
+
branch: branchName(String(status.seq)),
|
|
1136
|
+
// `--dry-run` and `--no-publish` mean the same thing here as
|
|
1137
|
+
// everywhere else in this verb: compute it, print it, run nothing.
|
|
1138
|
+
act: !dryRun && !noPublish,
|
|
1139
|
+
})
|
|
1140
|
+
: null;
|
|
1057
1141
|
if (json) {
|
|
1058
1142
|
emitReport(streams, {
|
|
1059
1143
|
policyPath,
|
|
@@ -1067,12 +1151,24 @@ export function commandPolicyAmend(argv, streams, cwd) {
|
|
|
1067
1151
|
noop: true,
|
|
1068
1152
|
dryRun,
|
|
1069
1153
|
aborted: false,
|
|
1154
|
+
repair,
|
|
1070
1155
|
});
|
|
1071
1156
|
}
|
|
1072
|
-
else {
|
|
1157
|
+
else if (repair === null) {
|
|
1073
1158
|
streams.out(`nothing to amend: ${relPath(policyPath, cwd)} already matches its attestation at seq ${status.seq} (sha256 ${shortHash(liveSha256)})\n`);
|
|
1074
1159
|
}
|
|
1075
|
-
|
|
1160
|
+
else {
|
|
1161
|
+
const headline = repair.state === "failed" ? st.glyph("fail") : st.glyph(repair.state === "owed" ? "point" : "ok");
|
|
1162
|
+
streams.out(`${headline} ${repair.branch}: ${repair.message}\n`);
|
|
1163
|
+
for (const step of repair.commands) {
|
|
1164
|
+
streams.err(` ${step.ok === null ? st.glyph("point") : step.ok ? st.glyph("ok") : st.glyph("fail")} ${step.command}\n`);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
// A repair that BROKE keeps the I/O exit, which is the split the rest of
|
|
1168
|
+
// this verb uses: the exit code speaks to scripts, the text to people.
|
|
1169
|
+
// `owed` is not a failure — it is `--dry-run` or `--no-publish` answering
|
|
1170
|
+
// the question they were asked — and `landed` and `repaired` are successes.
|
|
1171
|
+
return repair !== null && repair.state === "failed" ? EXIT_IO : EXIT_OK;
|
|
1076
1172
|
}
|
|
1077
1173
|
// (b) The baseline, and only a verifiable one. See the module header.
|
|
1078
1174
|
progress.phase("recovering the attested baseline and diffing it against the live policy");
|
|
@@ -1108,8 +1204,11 @@ export function commandPolicyAmend(argv, streams, cwd) {
|
|
|
1108
1204
|
const onProtectedDefault = probe.protection === "protected" &&
|
|
1109
1205
|
probe.currentBranch !== null &&
|
|
1110
1206
|
probe.currentBranch === probe.defaultBranch;
|
|
1111
|
-
|
|
1112
|
-
|
|
1207
|
+
// APRV-341: `--pr` is a third way into the branch flow, and it is the
|
|
1208
|
+
// operator's own instruction rather than an inference from a probe. A probe
|
|
1209
|
+
// that cannot reach GitHub answers `unknown`, which used to mean "commit in
|
|
1210
|
+
// place"; an operator who typed `--pr` has said what they want either way.
|
|
1211
|
+
const useBranch = branchFlag !== null || wantPr || (!forceDirect && onProtectedDefault);
|
|
1113
1212
|
// The direct flow's push is about to hit a protected branch. Say so before
|
|
1114
1213
|
// the human types it, rather than after GitHub says it.
|
|
1115
1214
|
const pushWarning = !useBranch && onProtectedDefault
|
|
@@ -1133,9 +1232,9 @@ export function commandPolicyAmend(argv, streams, cwd) {
|
|
|
1133
1232
|
*/
|
|
1134
1233
|
let pinsChange = null;
|
|
1135
1234
|
if (wantCommit && !dryRun) {
|
|
1136
|
-
const plan = planCommit(policyPath, logPath, useBranch ? { branch: branchFlag } : null);
|
|
1235
|
+
const plan = planCommit(policyPath, logPath, payloadFile, useBranch ? { branch: branchFlag } : null);
|
|
1137
1236
|
if (!plan.ok) {
|
|
1138
|
-
return refuse(streams, json,
|
|
1237
|
+
return refuse(streams, json, plan.code, plan.message, EXIT_USAGE);
|
|
1139
1238
|
}
|
|
1140
1239
|
commitPlan = plan.plan;
|
|
1141
1240
|
const prepared = prepareBase({ root: commitPlan.root, policyArg: commitPlan.policyArg, logPath, progress }, probe, attested?.sha256 ?? null);
|
|
@@ -1211,7 +1310,7 @@ export function commandPolicyAmend(argv, streams, cwd) {
|
|
|
1211
1310
|
pinsChange = pinsChangeIn(amendRoot, policyPath, "HEAD");
|
|
1212
1311
|
}
|
|
1213
1312
|
/**
|
|
1214
|
-
* The same command, with the
|
|
1313
|
+
* The same command, with the long absolute paths written the way the
|
|
1215
1314
|
* operator would type them.
|
|
1216
1315
|
*
|
|
1217
1316
|
* This is a HUMAN transform and nothing else: `--json`'s `git.commands` keeps
|
|
@@ -1224,7 +1323,9 @@ export function commandPolicyAmend(argv, streams, cwd) {
|
|
|
1224
1323
|
.split(policyPath)
|
|
1225
1324
|
.join(relPath(policyPath, cwd))
|
|
1226
1325
|
.split(logPath)
|
|
1227
|
-
.join(relPath(logPath, cwd))
|
|
1326
|
+
.join(relPath(logPath, cwd))
|
|
1327
|
+
.split(payloadFile)
|
|
1328
|
+
.join(relPath(payloadFile, cwd));
|
|
1228
1329
|
return pinsChange === null
|
|
1229
1330
|
? shortened
|
|
1230
1331
|
: shortened.split(pinsChange.path).join(relPath(pinsChange.path, cwd));
|
|
@@ -1242,8 +1343,13 @@ export function commandPolicyAmend(argv, streams, cwd) {
|
|
|
1242
1343
|
};
|
|
1243
1344
|
const summary = summarize(policyPath, diff, pinsChange);
|
|
1244
1345
|
// APRV-274: the pins file is a member of the `git add` exactly when it moved,
|
|
1245
|
-
// so a copied command lands the same three (or
|
|
1246
|
-
const ceremonyFiles = [
|
|
1346
|
+
// so a copied command lands the same three (or four) files the verb would.
|
|
1347
|
+
const ceremonyFiles = [
|
|
1348
|
+
policyPath,
|
|
1349
|
+
logPath,
|
|
1350
|
+
...(pinsChange === null ? [] : [pinsChange.path]),
|
|
1351
|
+
payloadFile,
|
|
1352
|
+
];
|
|
1247
1353
|
const commitCommands = (seq) => [
|
|
1248
1354
|
`git add ${ceremonyFiles.join(" ")}`,
|
|
1249
1355
|
`git commit -m ${JSON.stringify(`Policy: ${summary} (attested seq ${seq})`)}`,
|
|
@@ -1251,15 +1357,31 @@ export function commandPolicyAmend(argv, streams, cwd) {
|
|
|
1251
1357
|
const gitCommands = (seq) => {
|
|
1252
1358
|
// APRV-203: `--commit` runs none of these; it assembles the commit on the
|
|
1253
1359
|
// remote's tip without a checkout. These are the HAND procedure, and they
|
|
1254
|
-
// start where `--commit` starts:
|
|
1255
|
-
// on a local trunk that has fallen behind.
|
|
1360
|
+
// start where `--commit` starts: with the checkout brought current, so the
|
|
1361
|
+
// branch is not built on a local trunk that has fallen behind.
|
|
1362
|
+
//
|
|
1363
|
+
// APRV-360: and they never switch branches. The form printed before this
|
|
1364
|
+
// opened with `git checkout -b policy-amend-<seq> origin/main`, which is
|
|
1365
|
+
// the shape that forked the log on 2026-09-16 and that refused outright on
|
|
1366
|
+
// 2026-09-18, when the primary's main was fourteen commits behind and the
|
|
1367
|
+
// switch would have overwritten QUEUE.md, the working log and six
|
|
1368
|
+
// payloads. A branch switch in the primary is never the right answer: the
|
|
1369
|
+
// daemon is appending to the working log while the operator reads this.
|
|
1370
|
+
//
|
|
1371
|
+
// What replaces it is what the 2026-09-18 recovery actually did. `approval
|
|
1372
|
+
// log sync` brings the checkout current (and refuses `log-diverged` rather
|
|
1373
|
+
// than fast-forwarding over a fork), the commit is made where the operator
|
|
1374
|
+
// is standing, and the branch is created on the REMOTE by refspec, so it
|
|
1375
|
+
// exists without any local ref ever being checked out.
|
|
1256
1376
|
if (useBranch) {
|
|
1377
|
+
const branch = branchName(seq);
|
|
1378
|
+
const base = probe.defaultBranch === null ? "" : ` --base ${probe.defaultBranch}`;
|
|
1257
1379
|
return [
|
|
1258
|
-
"
|
|
1259
|
-
`git checkout -b ${branchName(seq)} origin/${probe.defaultBranch ?? "main"}`,
|
|
1380
|
+
"approval log sync",
|
|
1260
1381
|
...commitCommands(seq),
|
|
1261
|
-
`git push
|
|
1262
|
-
`gh pr create --title ${JSON.stringify(prTitle(summary, seq))} --body ${JSON.stringify(prBody(seq))}`,
|
|
1382
|
+
`git push origin HEAD:refs/heads/${branch}`,
|
|
1383
|
+
`gh pr create --title ${JSON.stringify(prTitle(summary, seq))} --body ${JSON.stringify(prBody(seq))} --head ${branch}${base}`,
|
|
1384
|
+
`gh pr merge ${branch} --auto --merge`,
|
|
1263
1385
|
];
|
|
1264
1386
|
}
|
|
1265
1387
|
return amendRoot === null
|
|
@@ -1506,6 +1628,7 @@ export function commandPolicyAmend(argv, streams, cwd) {
|
|
|
1506
1628
|
branch: null,
|
|
1507
1629
|
pushed: false,
|
|
1508
1630
|
prUrl: null,
|
|
1631
|
+
prUpdated: false,
|
|
1509
1632
|
autoMerge: "not-attempted",
|
|
1510
1633
|
steps: [],
|
|
1511
1634
|
stoppedAt: null,
|
|
@@ -1596,6 +1719,14 @@ export function commandPolicyAmend(argv, streams, cwd) {
|
|
|
1596
1719
|
const why = commandOutputLines(merge.stderr, merge.stdout)[0];
|
|
1597
1720
|
note(`${prLabel(publishing.prUrl)} is open — merge it with a MERGE COMMIT when CI is green. auto-merge was not armed${why === undefined ? "" : `: ${why}`}`);
|
|
1598
1721
|
};
|
|
1722
|
+
/**
|
|
1723
|
+
* Who publishes the log for this amendment (APRV-420).
|
|
1724
|
+
*
|
|
1725
|
+
* Settled at commit-assembly time against the base the commit is parented
|
|
1726
|
+
* on, and reported, so the pull request's file list is never a surprise. The
|
|
1727
|
+
* default is the behaviour that predates the task: this commit carries it.
|
|
1728
|
+
*/
|
|
1729
|
+
let carriage = { carry: true, reason: null, publisher: null };
|
|
1599
1730
|
if (commitPlan !== null && commitBase !== null) {
|
|
1600
1731
|
// APRV-203. The commit is ASSEMBLED, never checked out: a scratch index is
|
|
1601
1732
|
// filled from the remote's tree, the two ceremony files are laid over it
|
|
@@ -1605,6 +1736,10 @@ export function commandPolicyAmend(argv, streams, cwd) {
|
|
|
1605
1736
|
const message = `Policy: ${summary} (attested seq ${seq})`;
|
|
1606
1737
|
/** `origin/main abc123`, or `HEAD abc123` where there is no remote. */
|
|
1607
1738
|
const baseLabel = `${commitBase.remote === null ? "HEAD" : `${commitBase.remote}/${commitBase.branch}`} ${commitBase.sha.slice(0, 12)}`;
|
|
1739
|
+
// APRV-420: who publishes the log. Decided here, from the base this commit
|
|
1740
|
+
// is about to be parented on, so the answer is about the tree the commit
|
|
1741
|
+
// will actually sit against.
|
|
1742
|
+
carriage = decideLogCarriage(commitPlan.root, commitBase, logPath);
|
|
1608
1743
|
progress.phase(`building the amendment commit on ${baseLabel} (nothing is checked out)`);
|
|
1609
1744
|
const built = commitOnBase(commitPlan.root, {
|
|
1610
1745
|
base: commitBase.sha,
|
|
@@ -1613,12 +1748,23 @@ export function commandPolicyAmend(argv, streams, cwd) {
|
|
|
1613
1748
|
// keeps this ceremony from reverting a pins edit somebody else landed.
|
|
1614
1749
|
paths: [
|
|
1615
1750
|
commitPlan.policyArg,
|
|
1616
|
-
|
|
1751
|
+
// APRV-420: the log joins the commit only when this amendment is the
|
|
1752
|
+
// thing publishing it. When a records advance is, two commits carrying
|
|
1753
|
+
// the same appended records is a conflict on `events.jsonl` whichever
|
|
1754
|
+
// lands first, and the repair is a hand merge of the log.
|
|
1755
|
+
...(carriage.carry ? [commitPlan.logArg] : []),
|
|
1617
1756
|
...(pinsChange === null ? [] : [pinsChange.arg]),
|
|
1757
|
+
// APRV-356: the attested policy text, so the commit carrying the
|
|
1758
|
+
// binding carries the bytes it binds. Without it the in-force policy is
|
|
1759
|
+
// unrecoverable for every reader of the committed copy, the CI guard
|
|
1760
|
+
// included.
|
|
1761
|
+
commitPlan.payloadArg,
|
|
1618
1762
|
],
|
|
1619
1763
|
message,
|
|
1620
1764
|
});
|
|
1621
1765
|
progress.done();
|
|
1766
|
+
if (carriage.reason !== null)
|
|
1767
|
+
note(`${st.glyph("ok")} ${carriage.reason}`);
|
|
1622
1768
|
if (!built.ok) {
|
|
1623
1769
|
return gitFailed(built.step, built.quote.join(" | "), `the attestation was appended at seq ${seq}, but ${built.message}; the checkout is untouched, so run the printed commands by hand`, commands);
|
|
1624
1770
|
}
|
|
@@ -1669,6 +1815,47 @@ export function commandPolicyAmend(argv, streams, cwd) {
|
|
|
1669
1815
|
: `${commitSha.slice(0, 12)} on ${baseLabel} (held at ${anchor}; your checkout was not moved)`;
|
|
1670
1816
|
/** `gh pr create …` as the operator would type it, for both flows. */
|
|
1671
1817
|
const prCreateCommand = (head) => `gh pr create --title ${JSON.stringify(prTitle(summary, String(seq)))} --body ${JSON.stringify(prBody(String(seq)))} --head ${head}${probe.defaultBranch === null ? "" : ` --base ${probe.defaultBranch}`}`;
|
|
1818
|
+
/**
|
|
1819
|
+
* The open pull request for `head`, or `null` when there is none (APRV-341).
|
|
1820
|
+
*
|
|
1821
|
+
* Asked before `gh pr create`, because a second run of the ceremony onto a
|
|
1822
|
+
* branch that already carries one is the ordinary shape of "the ceremony
|
|
1823
|
+
* stopped half-way and I ran it again": `gh pr create` answers that with a
|
|
1824
|
+
* failure, and a failure there used to end the verb holding an attestation
|
|
1825
|
+
* it had already appended. A `gh` that cannot answer is `null`, which falls
|
|
1826
|
+
* through to `create` — the path that was there before this existed.
|
|
1827
|
+
*/
|
|
1828
|
+
const openPrFor = (head) => {
|
|
1829
|
+
const listed = gh(["pr", "list", "--head", head, "--state", "open", "--json", "url"], commitPlan.root);
|
|
1830
|
+
if (!listed.ok)
|
|
1831
|
+
return null;
|
|
1832
|
+
const text = listed.stdout.trim();
|
|
1833
|
+
if (text.length === 0)
|
|
1834
|
+
return null;
|
|
1835
|
+
let rows;
|
|
1836
|
+
try {
|
|
1837
|
+
rows = JSON.parse(text);
|
|
1838
|
+
}
|
|
1839
|
+
catch {
|
|
1840
|
+
return null;
|
|
1841
|
+
}
|
|
1842
|
+
if (!Array.isArray(rows))
|
|
1843
|
+
return null;
|
|
1844
|
+
const first = rows[0];
|
|
1845
|
+
if (typeof first !== "object" || first === null)
|
|
1846
|
+
return null;
|
|
1847
|
+
const url = first.url;
|
|
1848
|
+
return typeof url === "string" && url.length > 0 ? url : null;
|
|
1849
|
+
};
|
|
1850
|
+
const prEditArgs = (head) => [
|
|
1851
|
+
"pr",
|
|
1852
|
+
"edit",
|
|
1853
|
+
head,
|
|
1854
|
+
"--title",
|
|
1855
|
+
prTitle(summary, String(seq)),
|
|
1856
|
+
"--body",
|
|
1857
|
+
prBody(String(seq)),
|
|
1858
|
+
];
|
|
1672
1859
|
const prCreateArgs = (head) => [
|
|
1673
1860
|
"pr",
|
|
1674
1861
|
"create",
|
|
@@ -1714,11 +1901,22 @@ export function commandPolicyAmend(argv, streams, cwd) {
|
|
|
1714
1901
|
publishing.pushed = true;
|
|
1715
1902
|
output = `${output}\n${`${push.stdout}${push.stderr}`.trim()}`.trim();
|
|
1716
1903
|
if (ghAvailable(commitPlan.root)) {
|
|
1717
|
-
|
|
1718
|
-
|
|
1904
|
+
// APRV-341: open, or update the one that is already open for this
|
|
1905
|
+
// branch. A ceremony re-run onto a branch a pull request already stands
|
|
1906
|
+
// on is not an error to report, it is the state to bring up to date.
|
|
1907
|
+
const standing = openPrFor(branch);
|
|
1908
|
+
const pr = standing === null
|
|
1909
|
+
? gh(prCreateArgs(branch), commitPlan.root)
|
|
1910
|
+
: gh(prEditArgs(branch), commitPlan.root);
|
|
1911
|
+
publishing.steps.push({
|
|
1912
|
+
command: standing === null ? prCreateCommand(branch) : `gh pr edit ${branch} --title … --body …`,
|
|
1913
|
+
ok: pr.ok,
|
|
1914
|
+
});
|
|
1719
1915
|
if (!pr.ok) {
|
|
1720
1916
|
const ghFailure = pr.stderr.trim() || pr.stdout.trim() || "gh did not run";
|
|
1721
|
-
return stalled("pr-failed",
|
|
1917
|
+
return stalled("pr-failed", standing === null
|
|
1918
|
+
? "`gh pr create` failed; the branch is already on origin"
|
|
1919
|
+
: "`gh pr edit` failed; the branch and its pull request are already on origin", pr, `the attestation was appended at seq ${seq}, committed on ${branch} and pushed, but \`gh pr create\` failed: ${ghFailure}; open the pull request by hand and merge it with a merge commit`, [
|
|
1722
1920
|
`attestation appended at seq ${seq}: it is in the log, on disk`,
|
|
1723
1921
|
`committed on ${branch} and PUSHED: origin has the branch`,
|
|
1724
1922
|
"no pull request: origin's default branch still carries the previous policy",
|
|
@@ -1727,8 +1925,12 @@ export function commandPolicyAmend(argv, streams, cwd) {
|
|
|
1727
1925
|
{ command: `gh pr merge ${branch} --merge`, note: "or merge it in the web UI" },
|
|
1728
1926
|
], 0, [MERGE_COMMIT_LINE]);
|
|
1729
1927
|
}
|
|
1730
|
-
|
|
1928
|
+
// `gh pr edit` prints the pull request's URL too, but the one this
|
|
1929
|
+
// ceremony already knows is the one it asked for; `create` has nothing
|
|
1930
|
+
// but its own output. Either way `null` is an honest answer.
|
|
1931
|
+
prUrl = standing ?? lastUrl(pr.stdout);
|
|
1731
1932
|
publishing.prUrl = prUrl;
|
|
1933
|
+
publishing.prUpdated = standing !== null;
|
|
1732
1934
|
publishing.complete = true;
|
|
1733
1935
|
// APRV-130: the ceremony offers to finish the last step too.
|
|
1734
1936
|
armAutoMerge(commitPlan.root, branch);
|
|
@@ -1865,7 +2067,10 @@ export function commandPolicyAmend(argv, streams, cwd) {
|
|
|
1865
2067
|
// APRV-274: the headline names what the commit actually carries. A reader
|
|
1866
2068
|
// who is told "the policy and the log" and finds a third file in the diff
|
|
1867
2069
|
// has been told something false about the one commit that must not lie.
|
|
1868
|
-
|
|
2070
|
+
// APRV-356 added the fourth: the store file holding the attested bytes.
|
|
2071
|
+
const carried = pinsChange === null
|
|
2072
|
+
? "the policy, the log and the attested policy text"
|
|
2073
|
+
: "the policy, the log, the pins and the attested policy text";
|
|
1869
2074
|
const done = [
|
|
1870
2075
|
branch === null
|
|
1871
2076
|
? `${st.glyph("ok")} committed ${carried} together:`
|
|
@@ -1958,6 +2163,8 @@ export function commandPolicyAmend(argv, streams, cwd) {
|
|
|
1958
2163
|
* across two commits and a hand-run cherry-pick.
|
|
1959
2164
|
*/
|
|
1960
2165
|
function planCommit(policyPath, logPath,
|
|
2166
|
+
/** The store file holding the attested policy text, absolute (APRV-356). */
|
|
2167
|
+
payloadFile,
|
|
1961
2168
|
/**
|
|
1962
2169
|
* The branch flow's preconditions, checked here for the same reason: an
|
|
1963
2170
|
* `origin` that does not exist, or a branch name already taken, would fail
|
|
@@ -1971,11 +2178,16 @@ branchFlow) {
|
|
|
1971
2178
|
if (root === null) {
|
|
1972
2179
|
return {
|
|
1973
2180
|
ok: false,
|
|
2181
|
+
code: "commit-preconditions",
|
|
1974
2182
|
message: `--commit needs a git repository and ${policyPath} is not inside one; nothing was attested`,
|
|
1975
2183
|
};
|
|
1976
2184
|
}
|
|
1977
2185
|
const policyArg = repoPath(root, policyPath);
|
|
1978
2186
|
const logArg = repoPath(root, logPath);
|
|
2187
|
+
// APRV-356. The payload store is a sibling of the log directory, so a log
|
|
2188
|
+
// inside the repository puts this inside it too; the check below covers both
|
|
2189
|
+
// with one sentence rather than naming a path no operator chose.
|
|
2190
|
+
const payloadArg = repoPath(root, payloadFile);
|
|
1979
2191
|
// APRV-274. `null` where these pins do not govern this policy, and where the
|
|
1980
2192
|
// repository simply has no pins module: in both cases the ceremony's file set
|
|
1981
2193
|
// is the two it always was.
|
|
@@ -1985,32 +2197,59 @@ branchFlow) {
|
|
|
1985
2197
|
if (policyArg.startsWith("../") || logArg.startsWith("../")) {
|
|
1986
2198
|
return {
|
|
1987
2199
|
ok: false,
|
|
2200
|
+
code: "commit-preconditions",
|
|
1988
2201
|
message: `--commit needs the policy (${policyPath}) and the log (${logPath}) inside the same repository (${root}); nothing was attested`,
|
|
1989
2202
|
};
|
|
1990
2203
|
}
|
|
1991
2204
|
const status = git(["status", "--porcelain"], root);
|
|
1992
2205
|
if (!status.ok) {
|
|
1993
|
-
return {
|
|
2206
|
+
return {
|
|
2207
|
+
ok: false,
|
|
2208
|
+
code: "commit-preconditions",
|
|
2209
|
+
message: `--commit could not read git status: ${status.stderr.trim()}`,
|
|
2210
|
+
};
|
|
1994
2211
|
}
|
|
2212
|
+
const ceremonyFiles = [policyArg, logArg, payloadArg, ...(pinsArg === null ? [] : [pinsArg])];
|
|
2213
|
+
const carried = pinsArg === null
|
|
2214
|
+
? "the policy, the log and the attested policy text"
|
|
2215
|
+
: `the policy, the log, the attested policy text and ${pinsArg}`;
|
|
1995
2216
|
const strays = [];
|
|
2217
|
+
/** Ceremony files staged in one state and left in another (APRV-341). */
|
|
2218
|
+
const split = [];
|
|
1996
2219
|
for (const line of status.stdout.split("\n")) {
|
|
1997
2220
|
if (line.trim().length === 0)
|
|
1998
2221
|
continue;
|
|
1999
2222
|
const index = line[0] ?? " ";
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2223
|
+
const worktree = line[1] ?? " ";
|
|
2224
|
+
const path = line.slice(3).trim();
|
|
2225
|
+
const ceremony = ceremonyFiles.includes(path);
|
|
2226
|
+
// A ceremony file the operator staged and then edited again. The commit is
|
|
2227
|
+
// assembled from the working tree, so the bytes it would carry are not the
|
|
2228
|
+
// bytes `git diff --cached` shows, and an amendment is the one commit that
|
|
2229
|
+
// may not be a surprise to the person who signed it.
|
|
2230
|
+
if (ceremony && index !== " " && index !== "?" && worktree === "M")
|
|
2231
|
+
split.push(path);
|
|
2232
|
+
// Only the INDEX column matters for everything else: an unstaged or
|
|
2233
|
+
// untracked file elsewhere is not going into this commit (the scratch index
|
|
2234
|
+
// lays exactly the ceremony paths over the remote's tree), and refusing
|
|
2235
|
+
// over one would make the verb unusable in any working repository.
|
|
2003
2236
|
if (index === " " || index === "?")
|
|
2004
2237
|
continue;
|
|
2005
|
-
|
|
2006
|
-
if (path === policyArg || path === logArg || path === pinsArg)
|
|
2238
|
+
if (ceremony)
|
|
2007
2239
|
continue;
|
|
2008
2240
|
strays.push(path);
|
|
2009
2241
|
}
|
|
2242
|
+
if (split.length > 0) {
|
|
2243
|
+
return {
|
|
2244
|
+
ok: false,
|
|
2245
|
+
code: "dirty-tree",
|
|
2246
|
+
message: `--commit refuses: ${split.join(", ")} ${split.length === 1 ? "is" : "are"} staged in one state and modified again in the working tree. The amendment commit is assembled from the WORKING TREE, so it would carry bytes your \`git diff --cached\` does not show, and the one commit that may not surprise the person who signed it is this one. Stage the file as it stands (\`git add ${split.join(" ")}\`) or unstage it, then run this again. Nothing was attested`,
|
|
2247
|
+
};
|
|
2248
|
+
}
|
|
2010
2249
|
if (strays.length > 0) {
|
|
2011
|
-
const carried = pinsArg === null ? "the policy and the log" : `the policy, the log and ${pinsArg}`;
|
|
2012
2250
|
return {
|
|
2013
2251
|
ok: false,
|
|
2252
|
+
code: "staged-unrelated",
|
|
2014
2253
|
message: `--commit refuses: the index carries ${strays.length} staged change(s) beyond ${carried} (${strays.join(", ")}). The amendment commit carries EXACTLY those files, so that "this commit is the amendment" stays true. Unstage them, or drop --commit and run the printed commands yourself. Nothing was attested`,
|
|
2015
2254
|
};
|
|
2016
2255
|
}
|
|
@@ -2019,6 +2258,7 @@ branchFlow) {
|
|
|
2019
2258
|
if (!remote.ok) {
|
|
2020
2259
|
return {
|
|
2021
2260
|
ok: false,
|
|
2261
|
+
code: "commit-preconditions",
|
|
2022
2262
|
message: `--commit on a branch needs an "origin" remote to push to, and ${root} has none (${remote.stderr.trim()}); pass --direct to commit in place, or add the remote. Nothing was attested`,
|
|
2023
2263
|
};
|
|
2024
2264
|
}
|
|
@@ -2027,12 +2267,13 @@ branchFlow) {
|
|
|
2027
2267
|
if (exists.ok) {
|
|
2028
2268
|
return {
|
|
2029
2269
|
ok: false,
|
|
2270
|
+
code: "commit-preconditions",
|
|
2030
2271
|
message: `--branch ${branchFlow.branch} already exists in ${root}; the amendment branch is created fresh so it carries exactly one commit. Pick another name. Nothing was attested`,
|
|
2031
2272
|
};
|
|
2032
2273
|
}
|
|
2033
2274
|
}
|
|
2034
2275
|
}
|
|
2035
|
-
return { ok: true, plan: { root, policyArg, logArg, pinsArg } };
|
|
2276
|
+
return { ok: true, plan: { root, policyArg, logArg, pinsArg, payloadArg } };
|
|
2036
2277
|
}
|
|
2037
2278
|
/**
|
|
2038
2279
|
* Fetch the remote and establish that this ceremony may be based on it (APRV-203).
|
|
@@ -2137,6 +2378,292 @@ function readLogText(path) {
|
|
|
2137
2378
|
return "";
|
|
2138
2379
|
}
|
|
2139
2380
|
}
|
|
2381
|
+
/**
|
|
2382
|
+
* The open records-advance branch on `origin`, or `null`.
|
|
2383
|
+
*
|
|
2384
|
+
* `git ls-remote` rather than `gh`, deliberately: this is a question about refs
|
|
2385
|
+
* and it must answer the same way on a box with no GitHub CLI, no token and no
|
|
2386
|
+
* network beyond the remote itself. `approval log advance` names its branch
|
|
2387
|
+
* `records-log-<YYYY-MM-DD>` and falls back to `records-log-<date>-<n>`
|
|
2388
|
+
* (`cli/log-advance.ts`), and merged branches are deleted by the reconcile
|
|
2389
|
+
* script, so a branch matching that pattern on origin is a live advance.
|
|
2390
|
+
*/
|
|
2391
|
+
function recordsPublisher(root) {
|
|
2392
|
+
const listed = git(["ls-remote", "--heads", "origin", "records-log-*"], root);
|
|
2393
|
+
if (!listed.ok)
|
|
2394
|
+
return null;
|
|
2395
|
+
for (const line of listed.stdout.split("\n")) {
|
|
2396
|
+
const [sha, ref] = line.trim().split(/\s+/u);
|
|
2397
|
+
if (ref === undefined || sha === undefined)
|
|
2398
|
+
continue;
|
|
2399
|
+
const name = ref.replace(/^refs\/heads\//u, "");
|
|
2400
|
+
if (name.startsWith("records-log-"))
|
|
2401
|
+
return { name, sha };
|
|
2402
|
+
}
|
|
2403
|
+
return null;
|
|
2404
|
+
}
|
|
2405
|
+
/**
|
|
2406
|
+
* Whether the records branch at `sha` already carries every record of the
|
|
2407
|
+
* working log, this attestation included.
|
|
2408
|
+
*
|
|
2409
|
+
* A branch pushed BEFORE the attestation was appended does not, and that is
|
|
2410
|
+
* the ordinary order (the cadence advance ran, then the human signed), so
|
|
2411
|
+
* saying "this attestation included" of it would be a claim the committed
|
|
2412
|
+
* log contradicts. What publishes the attestation then is the NEXT advance,
|
|
2413
|
+
* and the pull request's protected-path guard holds the policy change until
|
|
2414
|
+
* a records branch or main carries the record. `null` when the branch's log
|
|
2415
|
+
* could not be fetched or read, which the caller reports as unknown rather
|
|
2416
|
+
* than as either answer.
|
|
2417
|
+
*/
|
|
2418
|
+
function publisherCarriesLog(root, sha, logArg, logPath) {
|
|
2419
|
+
const fetched = git(["fetch", "--quiet", "origin", sha], root);
|
|
2420
|
+
if (!fetched.ok)
|
|
2421
|
+
return null;
|
|
2422
|
+
const branchLog = showBlob(root, sha, logArg);
|
|
2423
|
+
if (branchLog === null)
|
|
2424
|
+
return null;
|
|
2425
|
+
const compared = compareChains({ label: "the working log", text: readLogText(logPath) }, { label: `records branch ${sha.slice(0, 12)}:${logArg}`, text: branchLog.toString("utf8") });
|
|
2426
|
+
if (!compared.ok)
|
|
2427
|
+
return null;
|
|
2428
|
+
return compared.drift.relation === "equal" || compared.drift.relation === "behind";
|
|
2429
|
+
}
|
|
2430
|
+
/**
|
|
2431
|
+
* Decide who publishes the log for this amendment. See the block above.
|
|
2432
|
+
*
|
|
2433
|
+
* Fails toward CARRYING it: every answer this function cannot establish (a log
|
|
2434
|
+
* that does not verify, a blob it could not read, a remote it could not list)
|
|
2435
|
+
* leaves the amendment holding the log, which is the behaviour that predates
|
|
2436
|
+
* this task. A dirty pull request is a nuisance; an attestation that reaches
|
|
2437
|
+
* `main` in nobody's commit is a policy in force that the committed log does
|
|
2438
|
+
* not record, and between those two there is no contest.
|
|
2439
|
+
*/
|
|
2440
|
+
function decideLogCarriage(root, base, logPath) {
|
|
2441
|
+
const logArg = repoPath(root, logPath);
|
|
2442
|
+
const baseLabel = base.remote === null ? base.branch : `${base.remote}/${base.branch}`;
|
|
2443
|
+
const baseLog = showBlob(root, base.sha, logArg);
|
|
2444
|
+
const compared = compareChains({ label: "the working log", text: readLogText(logPath) }, { label: `${baseLabel}:${logArg}`, text: baseLog === null ? "" : baseLog.toString("utf8") });
|
|
2445
|
+
if (compared.ok && (compared.drift.relation === "equal" || compared.drift.relation === "behind")) {
|
|
2446
|
+
return {
|
|
2447
|
+
carry: false,
|
|
2448
|
+
reason: `${baseLabel} already carries every record this attestation added, so this amendment does not touch the log`,
|
|
2449
|
+
publisher: null,
|
|
2450
|
+
};
|
|
2451
|
+
}
|
|
2452
|
+
const publisher = recordsPublisher(root);
|
|
2453
|
+
if (publisher !== null) {
|
|
2454
|
+
const carries = publisherCarriesLog(root, publisher.sha, logArg, logPath);
|
|
2455
|
+
return {
|
|
2456
|
+
carry: false,
|
|
2457
|
+
reason: carries === true
|
|
2458
|
+
? `the records advance on ${publisher.name} publishes the whole log, this attestation included, so this amendment carries only the policy and cannot conflict with it`
|
|
2459
|
+
: `a records advance is live on ${publisher.name}${carries === null ? " (its log could not be read from here)" : ", pushed before this attestation was appended"}, so this amendment carries only the policy and cannot conflict with it; the attestation record lands with the next advance, and the pull request's protected-path check holds the policy change until a records branch or main carries it`,
|
|
2460
|
+
publisher: publisher.name,
|
|
2461
|
+
};
|
|
2462
|
+
}
|
|
2463
|
+
return { carry: true, reason: null, publisher: null };
|
|
2464
|
+
}
|
|
2465
|
+
/**
|
|
2466
|
+
* Repair the amendment pull request a previous run left standing (APRV-420).
|
|
2467
|
+
*
|
|
2468
|
+
* The second run of a ceremony that already attested reaches "nothing to
|
|
2469
|
+
* amend", which is true and useless: the live policy does match its
|
|
2470
|
+
* attestation, and the pull request carrying that attestation to the trunk is
|
|
2471
|
+
* open, unmergeable and unarmed. On 2026-09-21 the repair was a hand merge of
|
|
2472
|
+
* `events.jsonl` in a throwaway worktree, which is the work every idiom here
|
|
2473
|
+
* exists to remove.
|
|
2474
|
+
*
|
|
2475
|
+
* So the re-run rebuilds. The trunk's log is a superset of the amendment's by
|
|
2476
|
+
* construction — every record the amendment appended reaches the trunk through
|
|
2477
|
+
* a records advance as well — so a commit built fresh on the current trunk,
|
|
2478
|
+
* carrying only what the trunk still LACKS, is the merge without a merge.
|
|
2479
|
+
*
|
|
2480
|
+
* Two things this does not do. It does not check anything out: the rebuild is
|
|
2481
|
+
* `commitOnBase`'s scratch index, exactly as the first run's was, so a live
|
|
2482
|
+
* appender never has the log moved underneath it. And it does not touch a
|
|
2483
|
+
* branch it cannot account for: the remote tip must be a single commit on top
|
|
2484
|
+
* of an ancestor of the base, which is the shape this ceremony creates and
|
|
2485
|
+
* nothing else. Anything else is reported with its commands rather than
|
|
2486
|
+
* force-pushed over.
|
|
2487
|
+
*
|
|
2488
|
+
* Returns `null` when there is nothing standing, which leaves the caller's
|
|
2489
|
+
* "nothing to amend" exactly as it was.
|
|
2490
|
+
*/
|
|
2491
|
+
function repairAmendBranch(input) {
|
|
2492
|
+
const { root, policyPath, logPath, payloadFile, seq, branch, act } = input;
|
|
2493
|
+
const listed = git(["ls-remote", "--heads", "origin", branch], root);
|
|
2494
|
+
const tipLine = listed.ok
|
|
2495
|
+
? listed.stdout.split("\n").find((line) => line.trim().length > 0)
|
|
2496
|
+
: undefined;
|
|
2497
|
+
if (tipLine === undefined)
|
|
2498
|
+
return null;
|
|
2499
|
+
const commands = [];
|
|
2500
|
+
const stop = (state, message, extra = []) => {
|
|
2501
|
+
for (const command of extra)
|
|
2502
|
+
commands.push({ command, ok: null });
|
|
2503
|
+
return {
|
|
2504
|
+
branch,
|
|
2505
|
+
state,
|
|
2506
|
+
base: null,
|
|
2507
|
+
carried: [],
|
|
2508
|
+
commit: null,
|
|
2509
|
+
pushed: false,
|
|
2510
|
+
autoMerge: "not-attempted",
|
|
2511
|
+
commands,
|
|
2512
|
+
message,
|
|
2513
|
+
};
|
|
2514
|
+
};
|
|
2515
|
+
const probe = probeProtection(root);
|
|
2516
|
+
const baseBranch = probe.defaultBranch ?? probe.currentBranch;
|
|
2517
|
+
if (baseBranch === null) {
|
|
2518
|
+
return stop("failed", `${branch} is on origin and no branch could be resolved to rebuild it on (${probe.reason})`);
|
|
2519
|
+
}
|
|
2520
|
+
commands.push({ command: `git fetch origin ${baseBranch}`, ok: null });
|
|
2521
|
+
const fetched = fetchBase(root, "origin", baseBranch);
|
|
2522
|
+
if (!fetched.ok) {
|
|
2523
|
+
commands[commands.length - 1] = { command: `git fetch origin ${baseBranch}`, ok: false };
|
|
2524
|
+
return stop("failed", `${branch} is on origin and ${fetched.message}`);
|
|
2525
|
+
}
|
|
2526
|
+
commands[commands.length - 1] = { command: `git fetch origin ${baseBranch}`, ok: true };
|
|
2527
|
+
const base = { remote: "origin", branch: baseBranch, sha: fetched.sha };
|
|
2528
|
+
const baseLabel = `origin/${baseBranch}`;
|
|
2529
|
+
// Fetched rather than read off `ls-remote`, because the count below needs the
|
|
2530
|
+
// tip to be an OBJECT in this repository: a commit somebody else pushed to
|
|
2531
|
+
// the branch has never been near this checkout, and `rev-list` over a sha it
|
|
2532
|
+
// does not have answers nothing at all.
|
|
2533
|
+
commands.push({ command: `git fetch origin ${branch}`, ok: null });
|
|
2534
|
+
const fetchedTip = fetchBase(root, "origin", branch);
|
|
2535
|
+
if (!fetchedTip.ok) {
|
|
2536
|
+
commands[commands.length - 1] = { command: `git fetch origin ${branch}`, ok: false };
|
|
2537
|
+
return stop("failed", `${branch} is on origin and ${fetchedTip.message}`);
|
|
2538
|
+
}
|
|
2539
|
+
commands[commands.length - 1] = { command: `git fetch origin ${branch}`, ok: true };
|
|
2540
|
+
const tipSha = fetchedTip.sha;
|
|
2541
|
+
// What the base still lacks. Each answer is read from the base's own tree,
|
|
2542
|
+
// so a record or a payload a records advance already landed is not carried
|
|
2543
|
+
// twice, which is the whole cause of the conflict this repairs.
|
|
2544
|
+
const policyArg = repoPath(root, policyPath);
|
|
2545
|
+
const payloadArg = repoPath(root, payloadFile);
|
|
2546
|
+
const carried = [];
|
|
2547
|
+
const reasons = [];
|
|
2548
|
+
const basePolicy = showBlob(root, base.sha, policyArg);
|
|
2549
|
+
const livePolicy = ((() => {
|
|
2550
|
+
try {
|
|
2551
|
+
return readFileSync(policyPath);
|
|
2552
|
+
}
|
|
2553
|
+
catch {
|
|
2554
|
+
return null;
|
|
2555
|
+
}
|
|
2556
|
+
})());
|
|
2557
|
+
if (livePolicy === null) {
|
|
2558
|
+
return stop("failed", `${policyPath} could not be read, so the rebuild has no policy bytes to carry`);
|
|
2559
|
+
}
|
|
2560
|
+
if (basePolicy === null || !basePolicy.equals(livePolicy)) {
|
|
2561
|
+
carried.push(policyArg);
|
|
2562
|
+
reasons.push(`the policy bytes (${baseLabel} does not carry them)`);
|
|
2563
|
+
}
|
|
2564
|
+
if (showBlob(root, base.sha, payloadArg) === null) {
|
|
2565
|
+
carried.push(payloadArg);
|
|
2566
|
+
reasons.push("the attested policy text");
|
|
2567
|
+
}
|
|
2568
|
+
const carriage = decideLogCarriage(root, base, logPath);
|
|
2569
|
+
if (carriage.carry) {
|
|
2570
|
+
carried.push(repoPath(root, logPath));
|
|
2571
|
+
reasons.push("the log, which nothing else is publishing");
|
|
2572
|
+
}
|
|
2573
|
+
else {
|
|
2574
|
+
reasons.push(`NOT the log: ${carriage.reason ?? "it is published elsewhere"}`);
|
|
2575
|
+
}
|
|
2576
|
+
const pins = pinsChangeIn(root, policyPath, base.sha);
|
|
2577
|
+
if (pins !== null) {
|
|
2578
|
+
carried.push(pins.arg);
|
|
2579
|
+
reasons.push(pins.arg);
|
|
2580
|
+
}
|
|
2581
|
+
if (carried.length === 0) {
|
|
2582
|
+
return stop("landed", `the amendment at seq ${seq} is already on ${baseLabel} in full: the policy, its attested text and its records are all there, so ${branch} has nothing left to carry. Close the pull request`, [`gh pr close ${branch} --delete-branch`]);
|
|
2583
|
+
}
|
|
2584
|
+
// Only now that a rebuild is actually owed does the branch's SHAPE matter,
|
|
2585
|
+
// and it has to be the one shape this ceremony makes: one commit, on a parent
|
|
2586
|
+
// the base already contains. `rev-list --count base..tip` counts the commits
|
|
2587
|
+
// the tip has that the base does not, so anything above one is work this verb
|
|
2588
|
+
// did not put there and a force-push would lose it. Zero is not checked here,
|
|
2589
|
+
// because a branch the base already contains has nothing left to carry and
|
|
2590
|
+
// left through the `landed` exit above.
|
|
2591
|
+
const countRun = git(["rev-list", "--count", `${base.sha}..${tipSha}`], root);
|
|
2592
|
+
const count = countRun.ok ? Number(countRun.stdout.trim()) : Number.NaN;
|
|
2593
|
+
if (!Number.isInteger(count) || count > 1) {
|
|
2594
|
+
return stop("failed", `${branch} on origin carries ${Number.isInteger(count) ? String(count) : "an unknown number of"} commits that ${baseLabel} does not. The amendment ceremony builds exactly one, so this branch holds work this verb did not put there and it will not be rebuilt over. Rebase or merge it by hand`, [`git log --oneline ${base.sha.slice(0, 12)}..${tipSha.slice(0, 12)}`]);
|
|
2595
|
+
}
|
|
2596
|
+
const subject = git(["log", "-1", "--format=%s", tipSha], root);
|
|
2597
|
+
const message = subject.ok && subject.stdout.trim().length > 0
|
|
2598
|
+
? subject.stdout.trim()
|
|
2599
|
+
: `Policy: amend ${policyArg} (attested seq ${seq})`;
|
|
2600
|
+
const rebuild = `git push origin +<rebuilt>:refs/heads/${branch}`;
|
|
2601
|
+
const built = commitOnBase(root, { base: base.sha, paths: carried, message });
|
|
2602
|
+
if (!built.ok) {
|
|
2603
|
+
commands.push({ command: rebuild, ok: null });
|
|
2604
|
+
return stop("failed", `${branch} could not be rebuilt on ${baseLabel}: ${built.message}`);
|
|
2605
|
+
}
|
|
2606
|
+
if (built.unchanged) {
|
|
2607
|
+
return stop("landed", `the amendment at seq ${seq} is already on ${baseLabel}: a commit carrying ${carried.join(", ")} over ${baseLabel} is the same tree. Close the pull request`, [`gh pr close ${branch} --delete-branch`]);
|
|
2608
|
+
}
|
|
2609
|
+
const commitSha = built.sha;
|
|
2610
|
+
const carriedNote = `rebuilt ${commitSha.slice(0, 12)} on ${baseLabel} ${base.sha.slice(0, 12)} carrying ${reasons.join(", ")}`;
|
|
2611
|
+
const push = `git push origin +${commitSha.slice(0, 12)}:refs/heads/${branch}`;
|
|
2612
|
+
if (!act) {
|
|
2613
|
+
commands.push({ command: push, ok: null });
|
|
2614
|
+
commands.push({ command: `gh pr merge ${branch} --merge --auto`, ok: null });
|
|
2615
|
+
return {
|
|
2616
|
+
branch,
|
|
2617
|
+
state: "owed",
|
|
2618
|
+
base: base.sha,
|
|
2619
|
+
carried,
|
|
2620
|
+
commit: commitSha,
|
|
2621
|
+
pushed: false,
|
|
2622
|
+
autoMerge: "not-attempted",
|
|
2623
|
+
commands,
|
|
2624
|
+
message: `${carriedNote}; nothing was pushed, so the pull request is unchanged. Run the commands above to finish it`,
|
|
2625
|
+
};
|
|
2626
|
+
}
|
|
2627
|
+
// Force, and only here: the branch carries exactly the one commit this
|
|
2628
|
+
// ceremony put on it (checked above), and the whole repair is replacing that
|
|
2629
|
+
// commit with the same amendment parented on a base that has moved.
|
|
2630
|
+
const pushed = git(["push", "origin", `+${commitSha}:refs/heads/${branch}`], root);
|
|
2631
|
+
commands.push({ command: push, ok: pushed.ok });
|
|
2632
|
+
if (!pushed.ok) {
|
|
2633
|
+
commands.push({ command: `gh pr merge ${branch} --merge --auto`, ok: null });
|
|
2634
|
+
return {
|
|
2635
|
+
branch,
|
|
2636
|
+
state: "failed",
|
|
2637
|
+
base: base.sha,
|
|
2638
|
+
carried,
|
|
2639
|
+
commit: commitSha,
|
|
2640
|
+
pushed: false,
|
|
2641
|
+
autoMerge: "not-attempted",
|
|
2642
|
+
commands,
|
|
2643
|
+
message: `${carriedNote}, and \`${push}\` was REJECTED: ${pushFailureText(pushed)}. The commit exists locally; push it and re-arm the merge`,
|
|
2644
|
+
};
|
|
2645
|
+
}
|
|
2646
|
+
let autoMerge = "not-attempted";
|
|
2647
|
+
if (ghAvailable(root)) {
|
|
2648
|
+
const armed = gh(["pr", "merge", branch, "--merge", "--auto"], root);
|
|
2649
|
+
commands.push({ command: `gh pr merge ${branch} --merge --auto`, ok: armed.ok });
|
|
2650
|
+
autoMerge = armed.ok ? "armed" : "refused";
|
|
2651
|
+
}
|
|
2652
|
+
else {
|
|
2653
|
+
commands.push({ command: `gh pr merge ${branch} --merge --auto`, ok: null });
|
|
2654
|
+
}
|
|
2655
|
+
return {
|
|
2656
|
+
branch,
|
|
2657
|
+
state: "repaired",
|
|
2658
|
+
base: base.sha,
|
|
2659
|
+
carried,
|
|
2660
|
+
commit: commitSha,
|
|
2661
|
+
pushed: true,
|
|
2662
|
+
autoMerge,
|
|
2663
|
+
commands,
|
|
2664
|
+
message: `${carriedNote}, and force-updated ${branch} on origin, so the pull request is mergeable again${autoMerge === "armed" ? " and armed" : ""}`,
|
|
2665
|
+
};
|
|
2666
|
+
}
|
|
2140
2667
|
function loadSummary(load) {
|
|
2141
2668
|
return load.ok
|
|
2142
2669
|
? { ok: true, code: null, message: null }
|
|
@@ -2166,6 +2693,9 @@ function emitReport(streams, report) {
|
|
|
2166
2693
|
// APRV-109, additive and always present: `null` says the attestation was
|
|
2167
2694
|
// performed at this terminal, an object says it was collected as a tap.
|
|
2168
2695
|
proposal: report.proposal ?? null,
|
|
2696
|
+
// APRV-420, additive and always present: `null` says this run had an
|
|
2697
|
+
// amendment to make, or found no standing branch to repair.
|
|
2698
|
+
repair: report.repair ?? null,
|
|
2169
2699
|
})}\n`);
|
|
2170
2700
|
}
|
|
2171
2701
|
//# sourceMappingURL=amend.js.map
|