approval-md 0.0.1 → 0.2.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/LICENSE +176 -0
- package/NOTICE +5 -0
- package/README.md +940 -4
- package/SPEC.md +476 -34
- package/cli.js +29 -3
- package/dist/src/adapters/agentmail.d.ts +426 -0
- package/dist/src/adapters/agentmail.js +1200 -0
- package/dist/src/adapters/agentmail.js.map +1 -0
- package/dist/src/adapters/conformance.d.ts +149 -0
- package/dist/src/adapters/conformance.js +461 -0
- package/dist/src/adapters/conformance.js.map +1 -0
- package/dist/src/adapters/contract.d.ts +628 -0
- package/dist/src/adapters/contract.js +1035 -0
- package/dist/src/adapters/contract.js.map +1 -0
- package/dist/src/adapters/email.d.ts +324 -0
- package/dist/src/adapters/email.js +749 -0
- package/dist/src/adapters/email.js.map +1 -0
- package/dist/src/adapters/env-passphrase.d.ts +93 -0
- package/dist/src/adapters/env-passphrase.js +132 -0
- package/dist/src/adapters/env-passphrase.js.map +1 -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 +77 -0
- package/dist/src/adapters/registry.js.map +1 -0
- package/dist/src/adapters/smtp.d.ts +213 -0
- package/dist/src/adapters/smtp.js +499 -0
- package/dist/src/adapters/smtp.js.map +1 -0
- package/dist/src/adapters/vault-provider.d.ts +114 -0
- package/dist/src/adapters/vault-provider.js +161 -0
- package/dist/src/adapters/vault-provider.js.map +1 -0
- 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/batch.js +121 -0
- package/dist/src/channels/batch.js.map +1 -0
- package/dist/src/channels/cli.d.ts +193 -0
- package/dist/src/channels/cli.js +468 -0
- package/dist/src/channels/cli.js.map +1 -0
- package/dist/src/channels/conformance.d.ts +92 -0
- package/dist/src/channels/conformance.js +445 -0
- package/dist/src/channels/conformance.js.map +1 -0
- package/dist/src/channels/contract.d.ts +623 -0
- package/dist/src/channels/contract.js +494 -0
- package/dist/src/channels/contract.js.map +1 -0
- package/dist/src/channels/payload-view.d.ts +35 -0
- package/dist/src/channels/payload-view.js +43 -0
- package/dist/src/channels/payload-view.js.map +1 -0
- package/dist/src/channels/render-queue.d.ts +149 -0
- package/dist/src/channels/render-queue.js +564 -0
- package/dist/src/channels/render-queue.js.map +1 -0
- package/dist/src/channels/tagging.d.ts +196 -0
- package/dist/src/channels/tagging.js +723 -0
- package/dist/src/channels/tagging.js.map +1 -0
- package/dist/src/channels/telegram.d.ts +1832 -0
- package/dist/src/channels/telegram.js +3190 -0
- package/dist/src/channels/telegram.js.map +1 -0
- package/dist/src/channels/web.d.ts +341 -0
- package/dist/src/channels/web.js +903 -0
- package/dist/src/channels/web.js.map +1 -0
- package/dist/src/cli/adapter.d.ts +90 -0
- package/dist/src/cli/adapter.js +288 -0
- package/dist/src/cli/adapter.js.map +1 -0
- package/dist/src/cli/amend.d.ts +59 -0
- package/dist/src/cli/amend.js +2171 -0
- package/dist/src/cli/amend.js.map +1 -0
- package/dist/src/cli/args.d.ts +43 -0
- package/dist/src/cli/args.js +86 -0
- package/dist/src/cli/args.js.map +1 -0
- package/dist/src/cli/attest.d.ts +41 -0
- package/dist/src/cli/attest.js +307 -0
- package/dist/src/cli/attest.js.map +1 -0
- package/dist/src/cli/audit-card.d.ts +62 -0
- package/dist/src/cli/audit-card.js +201 -0
- package/dist/src/cli/audit-card.js.map +1 -0
- package/dist/src/cli/audit.d.ts +59 -0
- package/dist/src/cli/audit.js +460 -0
- package/dist/src/cli/audit.js.map +1 -0
- package/dist/src/cli/channel-telegram.d.ts +806 -0
- package/dist/src/cli/channel-telegram.js +2063 -0
- package/dist/src/cli/channel-telegram.js.map +1 -0
- package/dist/src/cli/channel-web.d.ts +131 -0
- package/dist/src/cli/channel-web.js +357 -0
- package/dist/src/cli/channel-web.js.map +1 -0
- package/dist/src/cli/channel.d.ts +71 -0
- package/dist/src/cli/channel.js +438 -0
- package/dist/src/cli/channel.js.map +1 -0
- package/dist/src/cli/checkpoint-tap.d.ts +169 -0
- package/dist/src/cli/checkpoint-tap.js +238 -0
- package/dist/src/cli/checkpoint-tap.js.map +1 -0
- package/dist/src/cli/codex.d.ts +2 -0
- package/dist/src/cli/codex.js +172 -0
- package/dist/src/cli/codex.js.map +1 -0
- package/dist/src/cli/coverage.d.ts +61 -0
- package/dist/src/cli/coverage.js +343 -0
- package/dist/src/cli/coverage.js.map +1 -0
- package/dist/src/cli/daemon.d.ts +120 -0
- package/dist/src/cli/daemon.js +631 -0
- package/dist/src/cli/daemon.js.map +1 -0
- package/dist/src/cli/doctor.d.ts +129 -0
- package/dist/src/cli/doctor.js +2762 -0
- package/dist/src/cli/doctor.js.map +1 -0
- package/dist/src/cli/env.d.ts +65 -0
- package/dist/src/cli/env.js +302 -0
- package/dist/src/cli/env.js.map +1 -0
- package/dist/src/cli/execute.d.ts +202 -0
- package/dist/src/cli/execute.js +1682 -0
- package/dist/src/cli/execute.js.map +1 -0
- package/dist/src/cli/exit-codes.d.ts +73 -0
- package/dist/src/cli/exit-codes.js +82 -0
- package/dist/src/cli/exit-codes.js.map +1 -0
- package/dist/src/cli/feedback.d.ts +60 -0
- package/dist/src/cli/feedback.js +205 -0
- package/dist/src/cli/feedback.js.map +1 -0
- package/dist/src/cli/gate-window.d.ts +40 -0
- package/dist/src/cli/gate-window.js +294 -0
- package/dist/src/cli/gate-window.js.map +1 -0
- package/dist/src/cli/gate.d.ts +68 -0
- package/dist/src/cli/gate.js +557 -0
- package/dist/src/cli/gate.js.map +1 -0
- package/dist/src/cli/git-scope.d.ts +190 -0
- package/dist/src/cli/git-scope.js +295 -0
- package/dist/src/cli/git-scope.js.map +1 -0
- package/dist/src/cli/gloss-attach.d.ts +85 -0
- package/dist/src/cli/gloss-attach.js +107 -0
- package/dist/src/cli/gloss-attach.js.map +1 -0
- package/dist/src/cli/gloss-codex-child.d.ts +9 -0
- package/dist/src/cli/gloss-codex-child.js +149 -0
- package/dist/src/cli/gloss-codex-child.js.map +1 -0
- package/dist/src/cli/gloss-codex.d.ts +24 -0
- package/dist/src/cli/gloss-codex.js +255 -0
- package/dist/src/cli/gloss-codex.js.map +1 -0
- package/dist/src/cli/gloss-options.d.ts +42 -0
- package/dist/src/cli/gloss-options.js +79 -0
- package/dist/src/cli/gloss-options.js.map +1 -0
- package/dist/src/cli/gloss.d.ts +265 -0
- package/dist/src/cli/gloss.js +362 -0
- package/dist/src/cli/gloss.js.map +1 -0
- package/dist/src/cli/help.d.ts +103 -0
- package/dist/src/cli/help.js +2339 -0
- package/dist/src/cli/help.js.map +1 -0
- package/dist/src/cli/hook-codex.d.ts +78 -0
- package/dist/src/cli/hook-codex.js +167 -0
- package/dist/src/cli/hook-codex.js.map +1 -0
- package/dist/src/cli/hook.d.ts +331 -0
- package/dist/src/cli/hook.js +2849 -0
- package/dist/src/cli/hook.js.map +1 -0
- package/dist/src/cli/import.d.ts +35 -0
- package/dist/src/cli/import.js +175 -0
- package/dist/src/cli/import.js.map +1 -0
- package/dist/src/cli/init.d.ts +84 -0
- package/dist/src/cli/init.js +336 -0
- package/dist/src/cli/init.js.map +1 -0
- package/dist/src/cli/instructions.d.ts +23 -0
- package/dist/src/cli/instructions.js +262 -0
- package/dist/src/cli/instructions.js.map +1 -0
- package/dist/src/cli/journal.d.ts +41 -0
- package/dist/src/cli/journal.js +238 -0
- package/dist/src/cli/journal.js.map +1 -0
- package/dist/src/cli/log-advance.d.ts +287 -0
- package/dist/src/cli/log-advance.js +840 -0
- package/dist/src/cli/log-advance.js.map +1 -0
- package/dist/src/cli/log-anchor.d.ts +176 -0
- package/dist/src/cli/log-anchor.js +387 -0
- package/dist/src/cli/log-anchor.js.map +1 -0
- package/dist/src/cli/log-checkpoint.d.ts +22 -0
- package/dist/src/cli/log-checkpoint.js +128 -0
- package/dist/src/cli/log-checkpoint.js.map +1 -0
- package/dist/src/cli/log-sync.d.ts +243 -0
- package/dist/src/cli/log-sync.js +849 -0
- package/dist/src/cli/log-sync.js.map +1 -0
- package/dist/src/cli/log-verbs.d.ts +16 -0
- package/dist/src/cli/log-verbs.js +360 -0
- package/dist/src/cli/log-verbs.js.map +1 -0
- package/dist/src/cli/long-help.d.ts +70 -0
- package/dist/src/cli/long-help.js +148 -0
- package/dist/src/cli/long-help.js.map +1 -0
- package/dist/src/cli/main.d.ts +77 -0
- package/dist/src/cli/main.js +1206 -0
- package/dist/src/cli/main.js.map +1 -0
- package/dist/src/cli/mcp.d.ts +52 -0
- package/dist/src/cli/mcp.js +306 -0
- package/dist/src/cli/mcp.js.map +1 -0
- package/dist/src/cli/paths.d.ts +56 -0
- package/dist/src/cli/paths.js +79 -0
- package/dist/src/cli/paths.js.map +1 -0
- package/dist/src/cli/payload.d.ts +58 -0
- package/dist/src/cli/payload.js +253 -0
- package/dist/src/cli/payload.js.map +1 -0
- package/dist/src/cli/policy.d.ts +43 -0
- package/dist/src/cli/policy.js +229 -0
- package/dist/src/cli/policy.js.map +1 -0
- package/dist/src/cli/preflight.d.ts +363 -0
- package/dist/src/cli/preflight.js +1175 -0
- package/dist/src/cli/preflight.js.map +1 -0
- package/dist/src/cli/progress.d.ts +78 -0
- package/dist/src/cli/progress.js +112 -0
- package/dist/src/cli/progress.js.map +1 -0
- package/dist/src/cli/prompt.d.ts +209 -0
- package/dist/src/cli/prompt.js +312 -0
- package/dist/src/cli/prompt.js.map +1 -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/records.js +66 -0
- package/dist/src/cli/records.js.map +1 -0
- package/dist/src/cli/render.d.ts +22 -0
- package/dist/src/cli/render.js +132 -0
- package/dist/src/cli/render.js.map +1 -0
- package/dist/src/cli/sandbox.d.ts +51 -0
- package/dist/src/cli/sandbox.js +150 -0
- package/dist/src/cli/sandbox.js.map +1 -0
- package/dist/src/cli/scaffold.d.ts +79 -0
- package/dist/src/cli/scaffold.js +137 -0
- package/dist/src/cli/scaffold.js.map +1 -0
- package/dist/src/cli/setup-adapter.d.ts +137 -0
- package/dist/src/cli/setup-adapter.js +509 -0
- package/dist/src/cli/setup-adapter.js.map +1 -0
- package/dist/src/cli/setup-channel.d.ts +117 -0
- package/dist/src/cli/setup-channel.js +635 -0
- package/dist/src/cli/setup-channel.js.map +1 -0
- package/dist/src/cli/setup-checkpoint.d.ts +57 -0
- package/dist/src/cli/setup-checkpoint.js +196 -0
- package/dist/src/cli/setup-checkpoint.js.map +1 -0
- package/dist/src/cli/setup-common.d.ts +275 -0
- package/dist/src/cli/setup-common.js +376 -0
- package/dist/src/cli/setup-common.js.map +1 -0
- package/dist/src/cli/setup-flow.d.ts +287 -0
- package/dist/src/cli/setup-flow.js +476 -0
- package/dist/src/cli/setup-flow.js.map +1 -0
- package/dist/src/cli/setup-service.d.ts +96 -0
- package/dist/src/cli/setup-service.js +308 -0
- package/dist/src/cli/setup-service.js.map +1 -0
- package/dist/src/cli/setup.d.ts +202 -0
- package/dist/src/cli/setup.js +473 -0
- package/dist/src/cli/setup.js.map +1 -0
- package/dist/src/cli/style.d.ts +320 -0
- package/dist/src/cli/style.js +469 -0
- package/dist/src/cli/style.js.map +1 -0
- package/dist/src/cli/token.d.ts +39 -0
- package/dist/src/cli/token.js +274 -0
- package/dist/src/cli/token.js.map +1 -0
- package/dist/src/cli/up.d.ts +155 -0
- package/dist/src/cli/up.js +849 -0
- package/dist/src/cli/up.js.map +1 -0
- package/dist/src/cli/usage.d.ts +37 -0
- package/dist/src/cli/usage.js +91 -0
- package/dist/src/cli/usage.js.map +1 -0
- package/dist/src/cli/values.d.ts +40 -0
- package/dist/src/cli/values.js +189 -0
- package/dist/src/cli/values.js.map +1 -0
- package/dist/src/cli/vault.d.ts +59 -0
- package/dist/src/cli/vault.js +362 -0
- package/dist/src/cli/vault.js.map +1 -0
- package/dist/src/cli/verb-registry.d.ts +76 -0
- package/dist/src/cli/verb-registry.js +2341 -0
- package/dist/src/cli/verb-registry.js.map +1 -0
- package/dist/src/cli/wordmark.d.ts +31 -0
- package/dist/src/cli/wordmark.js +52 -0
- package/dist/src/cli/wordmark.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/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-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 +170 -0
- package/dist/src/core/advance-cycle.js +200 -0
- package/dist/src/core/advance-cycle.js.map +1 -0
- package/dist/src/core/agents-md.d.ts +276 -0
- package/dist/src/core/agents-md.js +747 -0
- package/dist/src/core/agents-md.js.map +1 -0
- 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 +420 -0
- package/dist/src/core/attest.js +589 -0
- package/dist/src/core/attest.js.map +1 -0
- package/dist/src/core/audit.d.ts +492 -0
- package/dist/src/core/audit.js +882 -0
- package/dist/src/core/audit.js.map +1 -0
- package/dist/src/core/budgets.d.ts +238 -0
- package/dist/src/core/budgets.js +449 -0
- package/dist/src/core/budgets.js.map +1 -0
- package/dist/src/core/checkpoint.d.ts +500 -0
- package/dist/src/core/checkpoint.js +738 -0
- package/dist/src/core/checkpoint.js.map +1 -0
- package/dist/src/core/child-env.d.ts +88 -0
- package/dist/src/core/child-env.js +86 -0
- package/dist/src/core/child-env.js.map +1 -0
- package/dist/src/core/clock.d.ts +52 -0
- package/dist/src/core/clock.js +43 -0
- package/dist/src/core/clock.js.map +1 -0
- package/dist/src/core/command-class.d.ts +543 -0
- package/dist/src/core/command-class.js +2356 -0
- package/dist/src/core/command-class.js.map +1 -0
- package/dist/src/core/coverage-sources/adapter.d.ts +40 -0
- package/dist/src/core/coverage-sources/adapter.js +71 -0
- package/dist/src/core/coverage-sources/adapter.js.map +1 -0
- package/dist/src/core/coverage-sources/gh.d.ts +48 -0
- package/dist/src/core/coverage-sources/gh.js +136 -0
- package/dist/src/core/coverage-sources/gh.js.map +1 -0
- package/dist/src/core/coverage-sources/git.d.ts +101 -0
- package/dist/src/core/coverage-sources/git.js +269 -0
- package/dist/src/core/coverage-sources/git.js.map +1 -0
- package/dist/src/core/coverage.d.ts +217 -0
- package/dist/src/core/coverage.js +337 -0
- package/dist/src/core/coverage.js.map +1 -0
- package/dist/src/core/credential-spec.d.ts +72 -0
- package/dist/src/core/credential-spec.js +23 -0
- package/dist/src/core/credential-spec.js.map +1 -0
- package/dist/src/core/dark-session.d.ts +331 -0
- package/dist/src/core/dark-session.js +714 -0
- package/dist/src/core/dark-session.js.map +1 -0
- package/dist/src/core/decision-refusal.d.ts +185 -0
- package/dist/src/core/decision-refusal.js +265 -0
- package/dist/src/core/decision-refusal.js.map +1 -0
- package/dist/src/core/env-file.d.ts +450 -0
- package/dist/src/core/env-file.js +837 -0
- package/dist/src/core/env-file.js.map +1 -0
- package/dist/src/core/execute.d.ts +858 -0
- package/dist/src/core/execute.js +1271 -0
- package/dist/src/core/execute.js.map +1 -0
- package/dist/src/core/frontmatter.d.ts +78 -0
- package/dist/src/core/frontmatter.js +100 -0
- package/dist/src/core/frontmatter.js.map +1 -0
- package/dist/src/core/gate-window.d.ts +312 -0
- package/dist/src/core/gate-window.js +506 -0
- package/dist/src/core/gate-window.js.map +1 -0
- package/dist/src/core/gate.d.ts +1364 -0
- package/dist/src/core/gate.js +3002 -0
- package/dist/src/core/gate.js.map +1 -0
- package/dist/src/core/git-run.d.ts +73 -0
- package/dist/src/core/git-run.js +93 -0
- package/dist/src/core/git-run.js.map +1 -0
- package/dist/src/core/harness-version.d.ts +157 -0
- package/dist/src/core/harness-version.js +211 -0
- package/dist/src/core/harness-version.js.map +1 -0
- package/dist/src/core/harness-wait.d.ts +55 -0
- package/dist/src/core/harness-wait.js +58 -0
- package/dist/src/core/harness-wait.js.map +1 -0
- package/dist/src/core/head-retry.d.ts +107 -0
- package/dist/src/core/head-retry.js +121 -0
- package/dist/src/core/head-retry.js.map +1 -0
- package/dist/src/core/instance.d.ts +253 -0
- package/dist/src/core/instance.js +319 -0
- package/dist/src/core/instance.js.map +1 -0
- package/dist/src/core/intake-limits.d.ts +247 -0
- package/dist/src/core/intake-limits.js +350 -0
- package/dist/src/core/intake-limits.js.map +1 -0
- package/dist/src/core/jcs.d.ts +52 -0
- package/dist/src/core/jcs.js +132 -0
- package/dist/src/core/jcs.js.map +1 -0
- package/dist/src/core/journal.d.ts +144 -0
- package/dist/src/core/journal.js +200 -0
- package/dist/src/core/journal.js.map +1 -0
- package/dist/src/core/live-draw.d.ts +436 -0
- package/dist/src/core/live-draw.js +703 -0
- package/dist/src/core/live-draw.js.map +1 -0
- package/dist/src/core/log-reconcile.d.ts +89 -0
- package/dist/src/core/log-reconcile.js +136 -0
- package/dist/src/core/log-reconcile.js.map +1 -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 +278 -0
- package/dist/src/core/log.js +546 -0
- package/dist/src/core/log.js.map +1 -0
- package/dist/src/core/loop.d.ts +274 -0
- package/dist/src/core/loop.js +487 -0
- package/dist/src/core/loop.js.map +1 -0
- package/dist/src/core/md-fence.d.ts +41 -0
- package/dist/src/core/md-fence.js +74 -0
- package/dist/src/core/md-fence.js.map +1 -0
- package/dist/src/core/money.d.ts +147 -0
- package/dist/src/core/money.js +195 -0
- package/dist/src/core/money.js.map +1 -0
- package/dist/src/core/payload-census.d.ts +74 -0
- package/dist/src/core/payload-census.js +146 -0
- package/dist/src/core/payload-census.js.map +1 -0
- package/dist/src/core/payload-store.d.ts +175 -0
- package/dist/src/core/payload-store.js +340 -0
- package/dist/src/core/payload-store.js.map +1 -0
- package/dist/src/core/payload.d.ts +71 -0
- package/dist/src/core/payload.js +80 -0
- package/dist/src/core/payload.js.map +1 -0
- package/dist/src/core/policy-diff.d.ts +292 -0
- package/dist/src/core/policy-diff.js +588 -0
- package/dist/src/core/policy-diff.js.map +1 -0
- package/dist/src/core/policy-expectations.d.ts +199 -0
- package/dist/src/core/policy-expectations.js +394 -0
- package/dist/src/core/policy-expectations.js.map +1 -0
- package/dist/src/core/policy-explain.d.ts +150 -0
- package/dist/src/core/policy-explain.js +258 -0
- package/dist/src/core/policy-explain.js.map +1 -0
- package/dist/src/core/policy-load.d.ts +527 -0
- package/dist/src/core/policy-load.js +536 -0
- package/dist/src/core/policy-load.js.map +1 -0
- package/dist/src/core/policy-match.d.ts +281 -0
- package/dist/src/core/policy-match.js +478 -0
- package/dist/src/core/policy-match.js.map +1 -0
- package/dist/src/core/policy-proposal.d.ts +265 -0
- package/dist/src/core/policy-proposal.js +458 -0
- package/dist/src/core/policy-proposal.js.map +1 -0
- package/dist/src/core/prompt-layout.d.ts +221 -0
- package/dist/src/core/prompt-layout.js +422 -0
- package/dist/src/core/prompt-layout.js.map +1 -0
- package/dist/src/core/protected-path-guard.d.ts +453 -0
- package/dist/src/core/protected-path-guard.js +1566 -0
- package/dist/src/core/protected-path-guard.js.map +1 -0
- package/dist/src/core/registration.d.ts +25 -0
- package/dist/src/core/registration.js +39 -0
- package/dist/src/core/registration.js.map +1 -0
- package/dist/src/core/reindex.d.ts +99 -0
- package/dist/src/core/reindex.js +336 -0
- package/dist/src/core/reindex.js.map +1 -0
- package/dist/src/core/sampler.d.ts +313 -0
- package/dist/src/core/sampler.js +388 -0
- package/dist/src/core/sampler.js.map +1 -0
- package/dist/src/core/sandbox.d.ts +290 -0
- package/dist/src/core/sandbox.js +424 -0
- package/dist/src/core/sandbox.js.map +1 -0
- package/dist/src/core/seal.d.ts +165 -0
- package/dist/src/core/seal.js +290 -0
- package/dist/src/core/seal.js.map +1 -0
- package/dist/src/core/state.d.ts +505 -0
- package/dist/src/core/state.js +1009 -0
- package/dist/src/core/state.js.map +1 -0
- package/dist/src/core/task-file.d.ts +185 -0
- package/dist/src/core/task-file.js +464 -0
- package/dist/src/core/task-file.js.map +1 -0
- package/dist/src/core/telegram-config.d.ts +93 -0
- package/dist/src/core/telegram-config.js +114 -0
- package/dist/src/core/telegram-config.js.map +1 -0
- package/dist/src/core/token.d.ts +409 -0
- package/dist/src/core/token.js +561 -0
- package/dist/src/core/token.js.map +1 -0
- package/dist/src/core/validate.d.ts +138 -0
- package/dist/src/core/validate.js +0 -0
- package/dist/src/core/validate.js.map +1 -0
- package/dist/src/core/values.d.ts +137 -0
- package/dist/src/core/values.js +153 -0
- package/dist/src/core/values.js.map +1 -0
- package/dist/src/core/vault.d.ts +291 -0
- package/dist/src/core/vault.js +612 -0
- package/dist/src/core/vault.js.map +1 -0
- package/dist/src/core/verified-snapshot.d.ts +204 -0
- package/dist/src/core/verified-snapshot.js +506 -0
- package/dist/src/core/verified-snapshot.js.map +1 -0
- package/dist/src/core/verify.d.ts +336 -0
- package/dist/src/core/verify.js +549 -0
- package/dist/src/core/verify.js.map +1 -0
- package/dist/src/core/version.d.ts +8 -0
- package/dist/src/core/version.js +9 -0
- package/dist/src/core/version.js.map +1 -0
- package/dist/src/core/wysiwys.d.ts +370 -0
- package/dist/src/core/wysiwys.js +728 -0
- package/dist/src/core/wysiwys.js.map +1 -0
- package/dist/src/daemon/advance-child.d.ts +39 -0
- package/dist/src/daemon/advance-child.js +78 -0
- package/dist/src/daemon/advance-child.js.map +1 -0
- package/dist/src/daemon/advance.d.ts +466 -0
- package/dist/src/daemon/advance.js +849 -0
- package/dist/src/daemon/advance.js.map +1 -0
- package/dist/src/daemon/audit.d.ts +87 -0
- package/dist/src/daemon/audit.js +90 -0
- package/dist/src/daemon/audit.js.map +1 -0
- package/dist/src/daemon/daemon.d.ts +1180 -0
- package/dist/src/daemon/daemon.js +1988 -0
- package/dist/src/daemon/daemon.js.map +1 -0
- package/dist/src/daemon/dark-session.d.ts +64 -0
- package/dist/src/daemon/dark-session.js +119 -0
- package/dist/src/daemon/dark-session.js.map +1 -0
- package/dist/src/daemon/draw-child.d.ts +36 -0
- package/dist/src/daemon/draw-child.js +132 -0
- package/dist/src/daemon/draw-child.js.map +1 -0
- package/dist/src/daemon/draw.d.ts +154 -0
- package/dist/src/daemon/draw.js +458 -0
- package/dist/src/daemon/draw.js.map +1 -0
- package/dist/src/daemon/git-evidence.d.ts +173 -0
- package/dist/src/daemon/git-evidence.js +345 -0
- package/dist/src/daemon/git-evidence.js.map +1 -0
- package/dist/src/daemon/projection.d.ts +180 -0
- package/dist/src/daemon/projection.js +233 -0
- package/dist/src/daemon/projection.js.map +1 -0
- package/dist/src/daemon/prune.d.ts +207 -0
- package/dist/src/daemon/prune.js +376 -0
- package/dist/src/daemon/prune.js.map +1 -0
- package/dist/src/mcp/http.d.ts +113 -0
- package/dist/src/mcp/http.js +343 -0
- package/dist/src/mcp/http.js.map +1 -0
- package/dist/src/mcp/server.d.ts +265 -0
- package/dist/src/mcp/server.js +602 -0
- package/dist/src/mcp/server.js.map +1 -0
- package/docs/adapter-api.md +106 -0
- package/docs/cli-reference.md +5716 -0
- package/docs/codex-enforced-session.md +30 -0
- package/package.json +53 -4
- package/schema/.gitkeep +0 -0
- package/schema/LICENSE +117 -0
- package/schema/codex-instance.schema.json +82 -0
- package/schema/envelope.schema.json +137 -0
- package/schema/event.schema.json +1811 -0
- 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/envelope/invalid/action-missing-idempotency-key.json +15 -0
- package/schema/fixtures/envelope/invalid/action-unknown-class-format.json +14 -0
- package/schema/fixtures/envelope/invalid/confidence-out-of-range.json +11 -0
- package/schema/fixtures/envelope/invalid/est-cost-bare-number.json +14 -0
- package/schema/fixtures/envelope/invalid/est-cost-noncanonical-string.json +14 -0
- package/schema/fixtures/envelope/invalid/malformed-assignee.json +10 -0
- package/schema/fixtures/envelope/invalid/malformed-created-by.json +7 -0
- package/schema/fixtures/envelope/invalid/malformed-max-latency.json +11 -0
- package/schema/fixtures/envelope/invalid/malformed-payload-hash.json +14 -0
- package/schema/fixtures/envelope/invalid/max-cost-bare-number.json +11 -0
- package/schema/fixtures/envelope/invalid/missing-origin.json +3 -0
- package/schema/fixtures/envelope/invalid/negative-est-cost.json +14 -0
- package/schema/fixtures/envelope/invalid/unknown-state.json +7 -0
- package/schema/fixtures/envelope/invalid/unknown-top-level-field.json +8 -0
- package/schema/fixtures/envelope/valid/action-payload-hash.json +17 -0
- package/schema/fixtures/envelope/valid/actions-without-budget.json +18 -0
- package/schema/fixtures/envelope/valid/canonical.json +25 -0
- package/schema/fixtures/envelope/valid/minimal.json +7 -0
- package/schema/fixtures/envelope/valid/multi-action-executed.json +30 -0
- package/schema/fixtures/envelope/valid/record-write-stage.json +22 -0
- package/schema/fixtures/event/invalid/approval-granted-agent-actor.json +15 -0
- package/schema/fixtures/event/invalid/approval-granted-empty-batch-delivery-id.json +17 -0
- package/schema/fixtures/event/invalid/approval-granted-fifth-reaction.json +16 -0
- package/schema/fixtures/event/invalid/approval-granted-missing-actor.json +14 -0
- package/schema/fixtures/event/invalid/approval-requested-missing-action-key.json +14 -0
- package/schema/fixtures/event/invalid/approval-withdrawn-agent-policy-drift.json +15 -0
- package/schema/fixtures/event/invalid/approval-withdrawn-missing-reason.json +15 -0
- package/schema/fixtures/event/invalid/approval-withdrawn-system-actor.json +15 -0
- package/schema/fixtures/event/invalid/audit-decision-refused-human-actor.json +17 -0
- package/schema/fixtures/event/invalid/audit-decision-refused-missing-code.json +16 -0
- package/schema/fixtures/event/invalid/audit-reviewed-agent-actor.json +15 -0
- package/schema/fixtures/event/invalid/audit-reviewed-loved-no-note.json +16 -0
- package/schema/fixtures/event/invalid/audit-reviewed-system-actor.json +15 -0
- package/schema/fixtures/event/invalid/bad-actor-prefix.json +15 -0
- package/schema/fixtures/event/invalid/est-cost-bare-number.json +17 -0
- package/schema/fixtures/event/invalid/execution-completed-fabricated-exit-code.json +16 -0
- package/schema/fixtures/event/invalid/execution-completed-provider-ref-empty-id.json +18 -0
- package/schema/fixtures/event/invalid/execution-completed-provider-ref-extra-field.json +19 -0
- package/schema/fixtures/event/invalid/execution-completed-provider-ref-id-not-string.json +18 -0
- package/schema/fixtures/event/invalid/execution-completed-provider-ref-missing-adapter.json +17 -0
- package/schema/fixtures/event/invalid/execution-failed-open-reported-by.json +16 -0
- package/schema/fixtures/event/invalid/execution-indeterminate-open-reason.json +14 -0
- package/schema/fixtures/event/invalid/execution-reconciled-agent-actor.json +17 -0
- package/schema/fixtures/event/invalid/execution-started-negative-env-stripped.json +16 -0
- package/schema/fixtures/event/invalid/gate-bypassed-missing-opened-seq.json +15 -0
- package/schema/fixtures/event/invalid/gate-closed-non-integer-opened-seq.json +13 -0
- package/schema/fixtures/event/invalid/gate-opened-agent-actor.json +16 -0
- package/schema/fixtures/event/invalid/gate-organ-attested-absolute-path.json +14 -0
- package/schema/fixtures/event/invalid/gate-organ-attested-agent-actor.json +14 -0
- package/schema/fixtures/event/invalid/gate-organ-attested-missing-organ-path.json +13 -0
- package/schema/fixtures/event/invalid/harness-unknown-kind.json +18 -0
- package/schema/fixtures/event/invalid/harness-version-multiline.json +16 -0
- package/schema/fixtures/event/invalid/log-checkpoint-agent-actor.json +17 -0
- package/schema/fixtures/event/invalid/log-checkpoint-missing-signature.json +16 -0
- package/schema/fixtures/event/invalid/log-checkpoint-short-signed-hash.json +17 -0
- package/schema/fixtures/event/invalid/log-checkpoint-unknown-signature-alg.json +17 -0
- package/schema/fixtures/event/invalid/malformed-ts.json +15 -0
- package/schema/fixtures/event/invalid/missing-alg.json +14 -0
- package/schema/fixtures/event/invalid/missing-hash.json +14 -0
- package/schema/fixtures/event/invalid/non-integer-seq.json +15 -0
- package/schema/fixtures/event/invalid/payload-pruned-human-actor.json +14 -0
- package/schema/fixtures/event/invalid/payload-pruned-missing-hash.json +14 -0
- package/schema/fixtures/event/invalid/policy-declined-agent-actor.json +15 -0
- package/schema/fixtures/event/invalid/policy-proposed-missing-diff.json +19 -0
- package/schema/fixtures/event/invalid/policy-proposed-system-actor.json +26 -0
- package/schema/fixtures/event/invalid/short-hash.json +15 -0
- package/schema/fixtures/event/invalid/unknown-alg.json +15 -0
- package/schema/fixtures/event/invalid/unknown-event-type.json +15 -0
- package/schema/fixtures/event/invalid/unknown-top-level-field.json +16 -0
- package/schema/fixtures/event/valid/approval-expired.json +15 -0
- package/schema/fixtures/event/valid/approval-granted-batch.json +19 -0
- package/schema/fixtures/event/valid/approval-granted-reaction.json +16 -0
- package/schema/fixtures/event/valid/approval-granted.json +15 -0
- package/schema/fixtures/event/valid/approval-rejected.json +15 -0
- package/schema/fixtures/event/valid/approval-requested.json +19 -0
- package/schema/fixtures/event/valid/approval-revoked.json +15 -0
- package/schema/fixtures/event/valid/approval-withdrawn-policy-drift.json +17 -0
- package/schema/fixtures/event/valid/approval-withdrawn.json +16 -0
- package/schema/fixtures/event/valid/audit-decision-refused.json +20 -0
- package/schema/fixtures/event/valid/audit-reviewed-reaction.json +17 -0
- package/schema/fixtures/event/valid/audit-reviewed.json +15 -0
- package/schema/fixtures/event/valid/audit-sampled.json +14 -0
- package/schema/fixtures/event/valid/budget-exceeded.json +21 -0
- package/schema/fixtures/event/valid/envelope-drift.json +16 -0
- package/schema/fixtures/event/valid/execution-completed-harness-report.json +16 -0
- package/schema/fixtures/event/valid/execution-completed-provider-ref.json +18 -0
- package/schema/fixtures/event/valid/execution-completed.json +15 -0
- package/schema/fixtures/event/valid/execution-failed-harness-report.json +16 -0
- package/schema/fixtures/event/valid/execution-failed.json +15 -0
- package/schema/fixtures/event/valid/execution-indeterminate.json +15 -0
- package/schema/fixtures/event/valid/execution-reconciled.json +17 -0
- package/schema/fixtures/event/valid/execution-started-env-stripped.json +17 -0
- package/schema/fixtures/event/valid/execution-started.json +14 -0
- package/schema/fixtures/event/valid/gate-bypassed-harness-version.json +18 -0
- package/schema/fixtures/event/valid/gate-bypassed.json +19 -0
- package/schema/fixtures/event/valid/gate-closed.json +14 -0
- package/schema/fixtures/event/valid/gate-opened.json +16 -0
- package/schema/fixtures/event/valid/gate-organ-attested.json +14 -0
- package/schema/fixtures/event/valid/genesis-null-prev.json +14 -0
- package/schema/fixtures/event/valid/log-checkpoint.json +17 -0
- package/schema/fixtures/event/valid/payload-pruned-orphan.json +13 -0
- package/schema/fixtures/event/valid/payload-pruned.json +17 -0
- package/schema/fixtures/event/valid/policy-declined.json +16 -0
- package/schema/fixtures/event/valid/policy-proposed.json +35 -0
- package/schema/fixtures/event/valid/policy-updated.json +14 -0
- package/schema/fixtures/event/valid/reconciliation-required.json +18 -0
- package/schema/fixtures/event/valid/reconciliation-satisfied.json +17 -0
- package/schema/fixtures/event/valid/route-accepted.json +15 -0
- package/schema/fixtures/event/valid/route-proposed.json +16 -0
- package/schema/fixtures/event/valid/spec-example.json +15 -0
- package/schema/fixtures/event/valid/task-registered-harness-version.json +23 -0
- package/schema/fixtures/event/valid/task-registered.json +14 -0
- package/schema/fixtures/hash/known-answer-pre-121.json +74 -0
- package/schema/fixtures/hash/known-answer.json +74 -0
- package/schema/fixtures/policy/invalid/bad-approval-ttl.json +7 -0
- package/schema/fixtures/policy/invalid/bad-web-port.json +4 -0
- package/schema/fixtures/policy/invalid/checkpoint-key-not-base64.json +6 -0
- package/schema/fixtures/policy/invalid/class-rule-missing-autonomy.json +9 -0
- package/schema/fixtures/policy/invalid/empty-class-key.json +6 -0
- package/schema/fixtures/policy/invalid/live-rate-on-human-only.json +7 -0
- package/schema/fixtures/policy/invalid/malformed-class-key.json +6 -0
- package/schema/fixtures/policy/invalid/missing-version.json +8 -0
- package/schema/fixtures/policy/invalid/negative-limit.json +9 -0
- package/schema/fixtures/policy/invalid/non-numeric-limit.json +9 -0
- package/schema/fixtures/policy/invalid/non-positive-max-pending.json +9 -0
- package/schema/fixtures/policy/invalid/on-expiry-grant.json +8 -0
- package/schema/fixtures/policy/invalid/payload-retention-bare-number.json +4 -0
- package/schema/fixtures/policy/invalid/payload-retention-compound.json +4 -0
- package/schema/fixtures/policy/invalid/payload-retention-fractional.json +4 -0
- package/schema/fixtures/policy/invalid/payload-retention-zero.json +4 -0
- package/schema/fixtures/policy/invalid/protected-paths-escape.json +4 -0
- package/schema/fixtures/policy/invalid/protected-paths-glob.json +4 -0
- package/schema/fixtures/policy/invalid/retro-rate-on-human-only.json +7 -0
- package/schema/fixtures/policy/invalid/retro-rate-on-manual.json +7 -0
- package/schema/fixtures/policy/invalid/retro-rate-zero.json +7 -0
- package/schema/fixtures/policy/invalid/sample-rate-too-high.json +5 -0
- package/schema/fixtures/policy/invalid/sampling-secret-env-empty.json +7 -0
- package/schema/fixtures/policy/invalid/sampling-secret-env-not-string.json +6 -0
- package/schema/fixtures/policy/invalid/skew-tolerance-compound.json +6 -0
- package/schema/fixtures/policy/invalid/unknown-autonomy.json +7 -0
- package/schema/fixtures/policy/invalid/unknown-class-rule-key.json +6 -0
- package/schema/fixtures/policy/invalid/unknown-top-level-key.json +7 -0
- package/schema/fixtures/policy/invalid/vault-passphrase-env-empty.json +6 -0
- package/schema/fixtures/policy/invalid/vault-passphrase-literal.json +6 -0
- package/schema/fixtures/policy/invalid/version-not-string.json +4 -0
- package/schema/fixtures/policy/valid/canonical.json +47 -0
- package/schema/fixtures/policy/valid/checkpoint-keys.json +18 -0
- package/schema/fixtures/policy/valid/class-approvers-limits.json +25 -0
- package/schema/fixtures/policy/valid/class-retro-rate.json +17 -0
- package/schema/fixtures/policy/valid/global-budgets.json +19 -0
- package/schema/fixtures/policy/valid/human-only.json +9 -0
- package/schema/fixtures/policy/valid/minimal.json +6 -0
- package/schema/fixtures/policy/valid/protected-paths.json +10 -0
- package/schema/fixtures/policy/valid/record-namespace.json +13 -0
- package/schema/fixtures/policy/valid/request-volume-limits.json +26 -0
- package/schema/fixtures/policy/valid/retention-and-sampling-secret.json +16 -0
- package/schema/fixtures/policy/valid/skew-tolerance.json +15 -0
- package/schema/fixtures/policy/valid/vault-passphrase-env.json +14 -0
- package/schema/fixtures/policy/valid/wildcards.json +15 -0
- package/schema/fixtures/policy-md/invalid/alias-bomb.md +15 -0
- package/schema/fixtures/policy-md/invalid/no-fence.md +7 -0
- package/schema/fixtures/policy-md/invalid/protected-route-not-a-subclass.md +16 -0
- package/schema/fixtures/policy-md/invalid/schema-invalid-autonomy.md +16 -0
- package/schema/fixtures/policy-md/invalid/schema-invalid-read-proof.md +17 -0
- package/schema/fixtures/policy-md/invalid/two-fences.md +19 -0
- package/schema/fixtures/policy-md/invalid/unclosed-fence.md +11 -0
- package/schema/fixtures/policy-md/invalid/wrong-info-string.md +11 -0
- package/schema/fixtures/policy-md/invalid/yaml-syntax-error.md +13 -0
- package/schema/fixtures/policy-md/precedence/both/APPROVAL.md +7 -0
- package/schema/fixtures/policy-md/precedence/both/APPROVALS.md +7 -0
- package/schema/fixtures/policy-md/precedence/fallback-only/APPROVALS.md +7 -0
- package/schema/fixtures/policy-md/valid/canonical.md +50 -0
- package/schema/fixtures/policy-md/valid/daemon-read-proof.md +18 -0
- package/schema/fixtures/policy-md/valid/minimal.md +3 -0
- package/schema/fixtures/policy-md/valid/prose-lookalikes.md +54 -0
- package/schema/fixtures/policy-md/valid/routed-protected-paths.md +49 -0
- package/schema/fixtures/policy-md/valid/with-values.md +79 -0
- package/schema/fixtures/sample-record/invalid/bad-date-time.json +4 -0
- package/schema/fixtures/sample-record/invalid/missing-required-field.json +3 -0
- package/schema/fixtures/sample-record/invalid/unknown-top-level-field.json +5 -0
- package/schema/fixtures/sample-record/invalid/wrong-type.json +4 -0
- package/schema/fixtures/sample-record/valid/minimal.json +4 -0
- package/schema/fixtures/sample-record/valid/with-note.json +5 -0
- package/schema/fixtures/values/invalid/class-shaped.json +9 -0
- package/schema/fixtures/values/invalid/duplicate-entry.json +4 -0
- package/schema/fixtures/values/invalid/non-string-item.json +4 -0
- package/schema/fixtures/values/invalid/over-cap.json +26 -0
- package/schema/fixtures/values/invalid/unknown-key.json +5 -0
- package/schema/fixtures/values/invalid/version-string.json +1 -0
- package/schema/fixtures/values/valid/empty-lists.json +7 -0
- package/schema/fixtures/values/valid/full.json +20 -0
- package/schema/fixtures/values/valid/minimal.json +1 -0
- package/schema/fixtures/values-md/invalid/schema-invalid.md +62 -0
- package/schema/fixtures/values-md/invalid/two-blocks.md +69 -0
- package/schema/fixtures/values-md/invalid/unterminated.md +61 -0
- package/schema/fixtures/values-md/invalid/yaml-error.md +63 -0
- package/schema/fixtures/values-md/valid/absent.md +50 -0
- package/schema/fixtures/values-md/valid/with-values.md +79 -0
- package/schema/policy.schema.json +501 -0
- package/schema/sample-record.schema.json +26 -0
- package/schema/values.schema.json +55 -0
- package/templates/codex/README.md +9 -0
|
@@ -0,0 +1,1175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The startup preflight (APRV-215): is the code about to run the code that is
|
|
3
|
+
* on origin, and is the build behind it the one the sources describe?
|
|
4
|
+
*
|
|
5
|
+
* ## The four manual steps this replaces
|
|
6
|
+
*
|
|
7
|
+
* Deploying the APRV-212 fix in the primary checkout took `git fetch`, a
|
|
8
|
+
* judgment call about whether the ten upstream commits touched
|
|
9
|
+
* `.approval/log/events.jsonl` while the working log was dirty, `git pull
|
|
10
|
+
* --ff-only`, and `npm run build`. Three of those are typing; the second is the
|
|
11
|
+
* one a human cannot make from `git status` alone, because `git status` does not
|
|
12
|
+
* say what the *upstream range* changed. Carter's standing view is that manual
|
|
13
|
+
* git steps for the human are a defect rather than a runbook, so the judgment
|
|
14
|
+
* moves here and the typing moves with it.
|
|
15
|
+
*
|
|
16
|
+
* ## What it is allowed to do
|
|
17
|
+
*
|
|
18
|
+
* Read git, and at most three writes: a `--ff-only` merge, `npm run build`,
|
|
19
|
+
* and, when the merge refused over an untracked file under `backlog/tasks/`
|
|
20
|
+
* that main already contains, clearing that file out of the way (APRV-300, and
|
|
21
|
+
* the rules it obeys are in {@link reconcileUntrackedTaskFiles}).
|
|
22
|
+
*
|
|
23
|
+
* It never resets, never stashes, never checks anything out, and never touches
|
|
24
|
+
* the working log. That list is not conservatism for its own sake — it is fork 2
|
|
25
|
+
* of 2026-08-20 (APRV-104's notes, and the reason `approval log sync` exists at
|
|
26
|
+
* all): a working `events.jsonl` rewound through git underneath a live appender
|
|
27
|
+
* is two chains where there was one. `--ff-only` cannot rewind a file that
|
|
28
|
+
* upstream did not change, and when upstream DID change it while the working
|
|
29
|
+
* copy is dirty, this module refuses and names `approval log sync`, which is the
|
|
30
|
+
* verb that knows how to do it safely.
|
|
31
|
+
*
|
|
32
|
+
* ## Refusals, not repairs
|
|
33
|
+
*
|
|
34
|
+
* Four codes, each firing for exactly one condition. The first three are
|
|
35
|
+
* evaluated in this order, before anything is written:
|
|
36
|
+
*
|
|
37
|
+
* - `up-preflight-behind-ahead` — `origin/<branch>..HEAD` is non-empty. Local
|
|
38
|
+
* commits exist that the remote does not have. A fast-forward is not the
|
|
39
|
+
* operation for that state, and guessing which side to keep is a decision.
|
|
40
|
+
* - `up-preflight-log-diverged` — the upstream range changes the working log or
|
|
41
|
+
* the queue projection, and the working copy has uncommitted changes to them.
|
|
42
|
+
* This is the case the human could not judge by eye, and it is `approval log
|
|
43
|
+
* sync`'s whole subject.
|
|
44
|
+
* - `up-preflight-dirty-protected` — some OTHER path the upstream range changes
|
|
45
|
+
* is locally modified, so `git merge --ff-only` would refuse to overwrite it.
|
|
46
|
+
* Named separately because the repair is different: look at the edit and
|
|
47
|
+
* decide, or start on the current build with `--no-preflight`.
|
|
48
|
+
*
|
|
49
|
+
* The fourth is answered after the merge has already refused, and only for the
|
|
50
|
+
* one path shape where two checkouts routinely author one file (APRV-300):
|
|
51
|
+
*
|
|
52
|
+
* - `up-preflight-task-file-conflict` — an untracked file under
|
|
53
|
+
* `backlog/tasks/` stopped the fast-forward, and it holds lines the incoming
|
|
54
|
+
* copy does not. See {@link reconcileUntrackedTaskFiles} for what happens
|
|
55
|
+
* when it holds none, and why that is a claim rather than an assumption.
|
|
56
|
+
*
|
|
57
|
+
* `git reset --hard` appears in none of them, and never will: it is the command
|
|
58
|
+
* that turns "your checkout is confusing" into "your work is gone".
|
|
59
|
+
*
|
|
60
|
+
* A fourth refusal reports a write that failed rather than a judgment:
|
|
61
|
+
* `up-preflight-failed` carries the step and, for the build, the exit code
|
|
62
|
+
* `npm run build` came back with. The runtime does not start after it, because the whole point
|
|
63
|
+
* of the rebuild is that a daemon and a hook running compiled-away code is the
|
|
64
|
+
* defect being fixed (APRV-301); `--no-build` is how an operator says they
|
|
65
|
+
* meant to run the stale build anyway.
|
|
66
|
+
*
|
|
67
|
+
* ## A fetch that fails is weather
|
|
68
|
+
*
|
|
69
|
+
* A laptop on a train has no origin to compare against. That is not a reason to
|
|
70
|
+
* refuse to run a gate — the log is local, the policy is local, and the human is
|
|
71
|
+
* holding the phone. A failed fetch is reported as a warning and the runtime
|
|
72
|
+
* starts on the build it has, saying so.
|
|
73
|
+
*
|
|
74
|
+
* ## Two callers, one judgment
|
|
75
|
+
*
|
|
76
|
+
* `approval up` (and therefore `approval daemon run --with-channels`) acts on
|
|
77
|
+
* it; `approval doctor`'s `main-behind-origin` row reports it. Doctor passes
|
|
78
|
+
* `fetch: false`, because a report is not allowed to make a network call the
|
|
79
|
+
* operator did not ask for, and says out loud that its answer is only as fresh
|
|
80
|
+
* as the last fetch.
|
|
81
|
+
*/
|
|
82
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, } from "node:fs";
|
|
83
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
84
|
+
import { basename, dirname, join, resolve as resolvePathSegments } from "node:path";
|
|
85
|
+
import { fileURLToPath } from "node:url";
|
|
86
|
+
import { EXIT_IO, EXIT_OK } from "./exit-codes.js";
|
|
87
|
+
import { currentBranch, failureText, fetchBase, git, repoPath, repoRoot, showBlob, } from "./git-scope.js";
|
|
88
|
+
import { runbook, style } from "./style.js";
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// Build freshness (moved here from cli/doctor.ts, APRV-215)
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
/**
|
|
93
|
+
* The installation root: the directory holding `cli.js`, `src/`, `dist/`.
|
|
94
|
+
*
|
|
95
|
+
* Derived from this module's own location rather than from `cwd`, because the
|
|
96
|
+
* question is "is the code I am running stale", and the answer must not change
|
|
97
|
+
* when the operator runs the CLI from somewhere else. Compiled, this file is
|
|
98
|
+
* `<root>/dist/src/cli/preflight.js`, hence three levels up.
|
|
99
|
+
*/
|
|
100
|
+
export function installationRoot() {
|
|
101
|
+
return resolvePathSegments(dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
|
|
102
|
+
}
|
|
103
|
+
/** Thrown out of the source walk so a real I/O denial can become exit 4. */
|
|
104
|
+
export class ScanError extends Error {
|
|
105
|
+
}
|
|
106
|
+
function detailOf(cause) {
|
|
107
|
+
return cause instanceof Error ? cause.message : String(cause);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Newest mtime under `dir`, or `null` when `dir` does not exist.
|
|
111
|
+
*
|
|
112
|
+
* ENOENT anywhere in the walk is "not there", which is an answer. Anything else
|
|
113
|
+
* — a permission bit, a vanished mount — is the caller failing to look, and is
|
|
114
|
+
* raised so it can report an I/O error rather than quietly reporting a build as
|
|
115
|
+
* fresh because half the tree was invisible.
|
|
116
|
+
*/
|
|
117
|
+
function newestMtime(path) {
|
|
118
|
+
let stats;
|
|
119
|
+
try {
|
|
120
|
+
stats = statSync(path);
|
|
121
|
+
}
|
|
122
|
+
catch (cause) {
|
|
123
|
+
if (cause.code === "ENOENT")
|
|
124
|
+
return null;
|
|
125
|
+
throw new ScanError(`${path} could not be stat'd: ${detailOf(cause)}`);
|
|
126
|
+
}
|
|
127
|
+
if (!stats.isDirectory())
|
|
128
|
+
return stats.mtimeMs;
|
|
129
|
+
let newest = stats.mtimeMs;
|
|
130
|
+
let entries;
|
|
131
|
+
try {
|
|
132
|
+
entries = readdirSync(path, { withFileTypes: true });
|
|
133
|
+
}
|
|
134
|
+
catch (cause) {
|
|
135
|
+
if (cause.code === "ENOENT")
|
|
136
|
+
return newest;
|
|
137
|
+
throw new ScanError(`${path} could not be listed: ${detailOf(cause)}`);
|
|
138
|
+
}
|
|
139
|
+
for (const entry of entries) {
|
|
140
|
+
const child = newestMtime(join(path, entry.name));
|
|
141
|
+
if (child !== null && child > newest)
|
|
142
|
+
newest = child;
|
|
143
|
+
}
|
|
144
|
+
return newest;
|
|
145
|
+
}
|
|
146
|
+
/** The four paths a freshness question is asked about. One definition. */
|
|
147
|
+
function installationPaths(root) {
|
|
148
|
+
return {
|
|
149
|
+
loader: join(root, "cli.js"),
|
|
150
|
+
marker: join(root, "dist", "src", "cli", "main.js"),
|
|
151
|
+
sources: join(root, "src"),
|
|
152
|
+
tsconfig: join(root, "tsconfig.json"),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Date one installation: the single measurement both freshness answers read.
|
|
157
|
+
*
|
|
158
|
+
* {@link checkBuildFreshness} (doctor's row) and {@link distStale} (the boolean
|
|
159
|
+
* the preflight rebuilds on) interpret this differently, because a missing bin
|
|
160
|
+
* loader is a fault to report and not a reason to compile anything. What they
|
|
161
|
+
* must never do is disagree about WHAT WAS MEASURED, which is what a second
|
|
162
|
+
* copy of these four paths would eventually cause: a source added to one list
|
|
163
|
+
* and not the other means doctor calling a build stale that `approval up` had
|
|
164
|
+
* just declared fresh, or worse, the other way round (APRV-301).
|
|
165
|
+
*
|
|
166
|
+
* Raises {@link ScanError} the way {@link newestMtime} does. Doctor turns that
|
|
167
|
+
* into an I/O error; the preflight turns it into "cannot tell".
|
|
168
|
+
*/
|
|
169
|
+
function dateInstallation(root) {
|
|
170
|
+
const { loader, marker, sources, tsconfig } = installationPaths(root);
|
|
171
|
+
const sourceMtime = newestMtime(sources);
|
|
172
|
+
return {
|
|
173
|
+
loaderMtime: newestMtime(loader),
|
|
174
|
+
markerMtime: newestMtime(marker),
|
|
175
|
+
sourceMtime,
|
|
176
|
+
newestSource: sourceMtime === null ? null : Math.max(sourceMtime, newestMtime(tsconfig) ?? 0),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Is the built CLI at least as new as the sources it was built from?
|
|
181
|
+
*
|
|
182
|
+
* The marker is `dist/src/cli/main.js` — the exact file `cli.js` loads, so the
|
|
183
|
+
* thing being timestamped is the thing that will actually run. It is compared
|
|
184
|
+
* against the newest mtime under `src/` and of `tsconfig.json` (a compiler
|
|
185
|
+
* option change invalidates a build as surely as an edit does).
|
|
186
|
+
*
|
|
187
|
+
* Three shapes are distinguished because their repairs differ:
|
|
188
|
+
*
|
|
189
|
+
* - `cli.js` present, `dist/` absent — the placeholder-binary shape from the
|
|
190
|
+
* ceremony. The loader exists, so the checkout *looks* installed; nothing
|
|
191
|
+
* behind it does.
|
|
192
|
+
* - marker older than sources — the stale-checkout shape. Verbs that exist in
|
|
193
|
+
* `src/` are missing from the binary.
|
|
194
|
+
* - no `src/` at all — a published install, where freshness is not a question
|
|
195
|
+
* that can be asked. `skip`, not a silent pass.
|
|
196
|
+
*
|
|
197
|
+
* Note the self-reference: doctor itself runs *from* `dist`, so a completely
|
|
198
|
+
* absent `dist` means `cli.js` already refused and this code never ran. The
|
|
199
|
+
* check is still implemented for that shape because `--root` can point it at
|
|
200
|
+
* another tree, and because "the binary you ran is not the tree you edited" is
|
|
201
|
+
* exactly the confusion it exists to name.
|
|
202
|
+
*/
|
|
203
|
+
export function checkBuildFreshness(root) {
|
|
204
|
+
const { loader, marker, sources } = installationPaths(root);
|
|
205
|
+
const { loaderMtime, markerMtime, sourceMtime, newestSource } = dateInstallation(root);
|
|
206
|
+
if (markerMtime === null) {
|
|
207
|
+
return {
|
|
208
|
+
check: "build-freshness",
|
|
209
|
+
status: "fail",
|
|
210
|
+
detail: loaderMtime === null
|
|
211
|
+
? `neither ${loader} nor ${marker} exists — ${root} is not an approval.md installation`
|
|
212
|
+
: `${loader} exists but ${marker} does not: this is an unbuilt checkout, a bin loader with no build behind it`,
|
|
213
|
+
fix: 'npm run build — in this checkout; if you are not sure this is the checkout you meant, `node -p "process.argv[1]"` names the one you just ran',
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
if (loaderMtime === null) {
|
|
217
|
+
return {
|
|
218
|
+
check: "build-freshness",
|
|
219
|
+
status: "fail",
|
|
220
|
+
detail: `${marker} exists but the bin loader ${loader} does not: \`approval\` on PATH cannot reach this build`,
|
|
221
|
+
fix: "node dist/src/cli/main.js — invoke the build directly, or reinstall the package so `approval` on PATH reaches it",
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
if (sourceMtime === null || newestSource === null) {
|
|
225
|
+
return {
|
|
226
|
+
check: "build-freshness",
|
|
227
|
+
status: "skip",
|
|
228
|
+
detail: `${sources} is absent (a published install carries no sources), so the build cannot be dated against them; ${marker} is present`,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
if (newestSource > markerMtime) {
|
|
232
|
+
return {
|
|
233
|
+
check: "build-freshness",
|
|
234
|
+
status: "fail",
|
|
235
|
+
detail: `${marker} is older than the source tree (build ${new Date(markerMtime).toISOString()}, newest source ${new Date(newestSource).toISOString()}): you are running a STALE BUILD, and verbs added since it was compiled are simply absent`,
|
|
236
|
+
fix: "npm run build",
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
check: "build-freshness",
|
|
241
|
+
status: "pass",
|
|
242
|
+
detail: `${marker} built ${new Date(markerMtime).toISOString()}, not older than the source tree`,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Is `root`'s build older than `root`'s sources?
|
|
247
|
+
*
|
|
248
|
+
* `null` when the question does not apply — a published install with no `src/`,
|
|
249
|
+
* or a tree that is not an installation at all. A caller that wants the full
|
|
250
|
+
* three-way answer asks {@link checkBuildFreshness}; this is the boolean the
|
|
251
|
+
* preflight acts on, and "cannot tell" must not read as "stale" or the preflight
|
|
252
|
+
* would rebuild a tree it has no business compiling.
|
|
253
|
+
*/
|
|
254
|
+
export function distStale(root) {
|
|
255
|
+
let dates;
|
|
256
|
+
try {
|
|
257
|
+
dates = dateInstallation(root);
|
|
258
|
+
}
|
|
259
|
+
catch (cause) {
|
|
260
|
+
if (cause instanceof ScanError)
|
|
261
|
+
return null;
|
|
262
|
+
throw cause;
|
|
263
|
+
}
|
|
264
|
+
const { loaderMtime, markerMtime, sourceMtime, newestSource } = dates;
|
|
265
|
+
// No sources to date the build against: a published install, or a tree that
|
|
266
|
+
// is not an installation at all. Either way "stale" is not a claim that can
|
|
267
|
+
// be made, and a preflight that read "cannot tell" as "stale" would compile a
|
|
268
|
+
// directory nobody asked it to compile.
|
|
269
|
+
if (sourceMtime === null || newestSource === null)
|
|
270
|
+
return null;
|
|
271
|
+
if (loaderMtime === null && markerMtime === null)
|
|
272
|
+
return null;
|
|
273
|
+
// A loader with no build behind it: the placeholder-binary shape. `npm run
|
|
274
|
+
// build` is exactly the repair.
|
|
275
|
+
if (markerMtime === null)
|
|
276
|
+
return true;
|
|
277
|
+
return newestSource > markerMtime;
|
|
278
|
+
}
|
|
279
|
+
/** The machine-readable refusal codes. Frozen public API, distinct by repair. */
|
|
280
|
+
export const PREFLIGHT_REFUSAL_CODES = [
|
|
281
|
+
"up-preflight-behind-ahead",
|
|
282
|
+
"up-preflight-log-diverged",
|
|
283
|
+
"up-preflight-dirty-protected",
|
|
284
|
+
"up-preflight-task-file-conflict",
|
|
285
|
+
];
|
|
286
|
+
const ZERO = {
|
|
287
|
+
behind_by: 0,
|
|
288
|
+
ahead_by: 0,
|
|
289
|
+
log_touched: false,
|
|
290
|
+
dist_stale: false,
|
|
291
|
+
reexec: false,
|
|
292
|
+
};
|
|
293
|
+
/** `git status --porcelain -uno` as a set of repo-relative paths. */
|
|
294
|
+
function dirtyPaths(root) {
|
|
295
|
+
const run = git(["status", "--porcelain", "-uno"], root);
|
|
296
|
+
const paths = new Set();
|
|
297
|
+
if (!run.ok)
|
|
298
|
+
return paths;
|
|
299
|
+
for (const line of run.stdout.split("\n")) {
|
|
300
|
+
if (line.length < 4)
|
|
301
|
+
continue;
|
|
302
|
+
// "XY <path>", and for a rename "XY <old> -> <new>". Both sides count: a
|
|
303
|
+
// rename in flight is a local modification of two paths.
|
|
304
|
+
const body = line.slice(3);
|
|
305
|
+
for (const part of body.split(" -> ")) {
|
|
306
|
+
const trimmed = part.trim().replace(/^"|"$/gu, "");
|
|
307
|
+
if (trimmed.length > 0)
|
|
308
|
+
paths.add(trimmed);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return paths;
|
|
312
|
+
}
|
|
313
|
+
/** The repo-relative paths the range `from..to` changes. */
|
|
314
|
+
function changedPaths(root, from, to) {
|
|
315
|
+
const run = git(["diff", "--name-only", `${from}..${to}`], root);
|
|
316
|
+
const paths = new Set();
|
|
317
|
+
if (!run.ok)
|
|
318
|
+
return paths;
|
|
319
|
+
for (const line of run.stdout.split("\n")) {
|
|
320
|
+
const trimmed = line.trim();
|
|
321
|
+
if (trimmed.length > 0)
|
|
322
|
+
paths.add(trimmed);
|
|
323
|
+
}
|
|
324
|
+
return paths;
|
|
325
|
+
}
|
|
326
|
+
/** `behind\tahead` from one `rev-list`, or `null` when git would not say. */
|
|
327
|
+
function counts(root, base) {
|
|
328
|
+
const run = git(["rev-list", "--left-right", "--count", `${base}...HEAD`], root);
|
|
329
|
+
if (!run.ok)
|
|
330
|
+
return null;
|
|
331
|
+
const parts = run.stdout.trim().split(/\s+/u);
|
|
332
|
+
const behind = Number(parts[0]);
|
|
333
|
+
const ahead = Number(parts[1]);
|
|
334
|
+
if (!Number.isInteger(behind) || !Number.isInteger(ahead))
|
|
335
|
+
return null;
|
|
336
|
+
return { behind, ahead };
|
|
337
|
+
}
|
|
338
|
+
function skipped(detail, root) {
|
|
339
|
+
return { ok: true, facts: { ...ZERO, action: "skipped" }, detail, warning: null, target: null, root };
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* The whole judgment, and not one byte of action.
|
|
343
|
+
*
|
|
344
|
+
* Every write the preflight is allowed to make lives in {@link runPreflight};
|
|
345
|
+
* this function reads. That split is what lets doctor report the same facts
|
|
346
|
+
* without any risk of doctor repairing something, which doctor has never done.
|
|
347
|
+
*/
|
|
348
|
+
export function inspectPreflight(input) {
|
|
349
|
+
const root = repoRoot(dirname(input.logPath));
|
|
350
|
+
if (root === null) {
|
|
351
|
+
return skipped(`${input.logPath} is not inside a git repository, so there is no origin to be behind`, null);
|
|
352
|
+
}
|
|
353
|
+
const remote = input.remote ?? "origin";
|
|
354
|
+
const branch = input.branch ?? currentBranch(root) ?? "main";
|
|
355
|
+
let base;
|
|
356
|
+
let warning = null;
|
|
357
|
+
if (input.fetch) {
|
|
358
|
+
// A repository with no remote configured is not an unreachable remote. It
|
|
359
|
+
// is a repository that has no origin to be behind — a local evidence repo,
|
|
360
|
+
// a fixture, a checkout somebody made with `git init` — and reporting
|
|
361
|
+
// "could not reach the remote" there would be a warning about a question
|
|
362
|
+
// nobody asked. Skipped, and therefore silent.
|
|
363
|
+
const configured = git(["remote", "get-url", remote], root);
|
|
364
|
+
if (!configured.ok) {
|
|
365
|
+
return skipped(`no ${remote} remote is configured in ${root}, so there is nothing to be behind`, root);
|
|
366
|
+
}
|
|
367
|
+
const fetched = fetchBase(root, remote, branch);
|
|
368
|
+
if (!fetched.ok) {
|
|
369
|
+
return {
|
|
370
|
+
ok: true,
|
|
371
|
+
facts: { ...ZERO, action: "fetch-failed" },
|
|
372
|
+
detail: `${remote}/${branch} could not be reached, so this checkout is running on the build it already has`,
|
|
373
|
+
warning: fetched.message,
|
|
374
|
+
target: null,
|
|
375
|
+
root,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
base = fetched.sha;
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
const ref = `refs/remotes/${remote}/${branch}`;
|
|
382
|
+
const resolved = git(["rev-parse", "--verify", "--quiet", `${ref}^{commit}`], root);
|
|
383
|
+
const sha = resolved.stdout.trim();
|
|
384
|
+
if (!resolved.ok || sha.length === 0) {
|
|
385
|
+
return skipped(`this checkout has no ${remote}/${branch} remote-tracking ref, so there is nothing to compare HEAD against (${failureText(resolved)})`, root);
|
|
386
|
+
}
|
|
387
|
+
base = sha;
|
|
388
|
+
warning = `judged against the last fetch of ${remote}/${branch}, not a fresh one`;
|
|
389
|
+
}
|
|
390
|
+
const counted = counts(root, base);
|
|
391
|
+
if (counted === null) {
|
|
392
|
+
return skipped(`git could not count the commits between HEAD and ${remote}/${branch}`, root);
|
|
393
|
+
}
|
|
394
|
+
const logRelative = repoPath(root, input.logPath);
|
|
395
|
+
const queueRelative = repoPath(root, input.queuePath);
|
|
396
|
+
const protectedPaths = new Set([logRelative, queueRelative]);
|
|
397
|
+
const upstream = counted.behind === 0 ? new Set() : changedPaths(root, "HEAD", base);
|
|
398
|
+
const logTouched = [...protectedPaths].some((path) => upstream.has(path));
|
|
399
|
+
const stale = distStale(input.root) ?? false;
|
|
400
|
+
const facts = (action) => ({
|
|
401
|
+
behind_by: counted.behind,
|
|
402
|
+
ahead_by: counted.ahead,
|
|
403
|
+
log_touched: logTouched,
|
|
404
|
+
dist_stale: stale,
|
|
405
|
+
action,
|
|
406
|
+
reexec: false,
|
|
407
|
+
});
|
|
408
|
+
// 1. Ahead. Nothing else is worth judging: whatever the upstream range holds,
|
|
409
|
+
// a fast-forward is not the operation for a checkout carrying commits the
|
|
410
|
+
// remote has never seen, and choosing a side is a decision.
|
|
411
|
+
if (counted.ahead > 0) {
|
|
412
|
+
return {
|
|
413
|
+
ok: false,
|
|
414
|
+
root,
|
|
415
|
+
facts: facts("refused"),
|
|
416
|
+
refusal: {
|
|
417
|
+
code: "up-preflight-behind-ahead",
|
|
418
|
+
headline: `this checkout has ${plural(counted.ahead, "commit")} ${remote}/${branch} does not`,
|
|
419
|
+
state: [
|
|
420
|
+
`on ${branch} in ${root}`,
|
|
421
|
+
`${plural(counted.ahead, "commit")} ahead, ${plural(counted.behind, "commit")} behind ${remote}/${branch}`,
|
|
422
|
+
"nothing was fetched into the working tree, and nothing was rebuilt",
|
|
423
|
+
],
|
|
424
|
+
steps: [
|
|
425
|
+
{
|
|
426
|
+
command: `git log --oneline ${remote}/${branch}..HEAD`,
|
|
427
|
+
note: "what this checkout is carrying",
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
command: `git push ${remote} HEAD:${branch}`,
|
|
431
|
+
note: "if those commits are meant to ship, this is the way out",
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
command: `git reset --keep ${remote}/${branch}`,
|
|
435
|
+
note: "ONLY once you have looked: this drops the local commits above",
|
|
436
|
+
},
|
|
437
|
+
],
|
|
438
|
+
footer: [
|
|
439
|
+
"--keep is the softest of the three: it refuses outright rather than overwriting an uncommitted change",
|
|
440
|
+
"why the runtime will not choose for you: docs/cli-reference.md#up",
|
|
441
|
+
],
|
|
442
|
+
next: `git log --oneline ${remote}/${branch}..HEAD`,
|
|
443
|
+
},
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
if (counted.behind === 0) {
|
|
447
|
+
return {
|
|
448
|
+
ok: true,
|
|
449
|
+
root,
|
|
450
|
+
target: base,
|
|
451
|
+
warning,
|
|
452
|
+
facts: facts(stale ? "rebuild" : "none"),
|
|
453
|
+
detail: stale
|
|
454
|
+
? `up to date with ${remote}/${branch}, and the build is older than the sources`
|
|
455
|
+
: `up to date with ${remote}/${branch}, on a build no older than the sources`,
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
const dirty = dirtyPaths(root);
|
|
459
|
+
// 2. The judgment the human could not make from `git status`: the upstream
|
|
460
|
+
// range rewrites the log or the queue, and the working copy has its own
|
|
461
|
+
// uncommitted version of one of them. `approval log sync` is the verb that
|
|
462
|
+
// does this — snapshot, baseline, fast-forward, reconcile, rebuild the
|
|
463
|
+
// projections — and this module deliberately does not reimplement it.
|
|
464
|
+
const collidingProtected = [...protectedPaths].filter((path) => upstream.has(path) && dirty.has(path));
|
|
465
|
+
if (collidingProtected.length > 0) {
|
|
466
|
+
return {
|
|
467
|
+
ok: false,
|
|
468
|
+
root,
|
|
469
|
+
facts: facts("refused"),
|
|
470
|
+
refusal: {
|
|
471
|
+
code: "up-preflight-log-diverged",
|
|
472
|
+
headline: `${remote}/${branch} changed ${collidingProtected.join(" and ")} and so did this working copy`,
|
|
473
|
+
state: [
|
|
474
|
+
`${plural(counted.behind, "commit")} behind ${remote}/${branch}`,
|
|
475
|
+
`changed on both sides: ${collidingProtected.join(", ")}`,
|
|
476
|
+
"the working log was not read, moved, or rewound",
|
|
477
|
+
],
|
|
478
|
+
steps: [
|
|
479
|
+
{
|
|
480
|
+
command: "approval log sync",
|
|
481
|
+
note: "snapshots the working log, fast-forwards, reconciles the chain",
|
|
482
|
+
},
|
|
483
|
+
{ command: "approval up", note: "again, once sync reports clean" },
|
|
484
|
+
],
|
|
485
|
+
footer: [
|
|
486
|
+
"a fast-forward over a log another process is appending to is how one chain becomes two",
|
|
487
|
+
"the ritual and what it refuses: docs/cli-reference.md#log-sync",
|
|
488
|
+
],
|
|
489
|
+
next: "approval log sync",
|
|
490
|
+
},
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
// 3. Any other local modification in the fast-forward's way. `--ff-only` would
|
|
494
|
+
// refuse rather than clobber it, so the refusal is reported here, where it
|
|
495
|
+
// can say which file and what the two ways out are.
|
|
496
|
+
const colliding = [...upstream].filter((path) => dirty.has(path)).sort();
|
|
497
|
+
if (colliding.length > 0) {
|
|
498
|
+
return {
|
|
499
|
+
ok: false,
|
|
500
|
+
root,
|
|
501
|
+
facts: facts("refused"),
|
|
502
|
+
refusal: {
|
|
503
|
+
code: "up-preflight-dirty-protected",
|
|
504
|
+
headline: `${plural(colliding.length, "file")} the fast-forward would overwrite ${colliding.length === 1 ? "is" : "are"} locally modified`,
|
|
505
|
+
state: [
|
|
506
|
+
`${plural(counted.behind, "commit")} behind ${remote}/${branch}`,
|
|
507
|
+
`modified here and upstream: ${colliding.slice(0, 5).join(", ")}${colliding.length > 5 ? ", …" : ""}`,
|
|
508
|
+
"nothing was merged and nothing was rebuilt",
|
|
509
|
+
],
|
|
510
|
+
steps: [
|
|
511
|
+
{ command: `git diff -- ${colliding[0] ?? ""}`, note: "what this checkout changed" },
|
|
512
|
+
{
|
|
513
|
+
command: "approval up --no-preflight",
|
|
514
|
+
note: "start on the current build and deal with the edit afterwards",
|
|
515
|
+
},
|
|
516
|
+
],
|
|
517
|
+
footer: [
|
|
518
|
+
"the preflight commits nothing and discards nothing: the edit is yours to land or drop",
|
|
519
|
+
"what the preflight will and will not do: docs/cli-reference.md#up",
|
|
520
|
+
],
|
|
521
|
+
next: `git diff -- ${colliding[0] ?? ""}`,
|
|
522
|
+
},
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
return {
|
|
526
|
+
ok: true,
|
|
527
|
+
root,
|
|
528
|
+
target: base,
|
|
529
|
+
warning,
|
|
530
|
+
facts: facts(stale ? "fast-forward+rebuild" : "fast-forward"),
|
|
531
|
+
detail: `${plural(counted.behind, "commit")} behind ${remote}/${branch}, and the upstream range is safe to fast-forward${stale ? "; the build is older than the sources" : ""}`,
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
function plural(count, noun) {
|
|
535
|
+
return `${String(count)} ${noun}${count === 1 ? "" : "s"}`;
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Inspect, then perform at most a fast-forward and a build.
|
|
539
|
+
*
|
|
540
|
+
* `npm run build` is spawned rather than shelled, with the checkout as its cwd,
|
|
541
|
+
* so nothing here depends on the operator's shell or on `npm` being on a
|
|
542
|
+
* particular PATH entry. Its output is discarded and its exit status is the
|
|
543
|
+
* whole answer: a build that fails is a failed preflight, and the runtime does
|
|
544
|
+
* not start on a half-compiled tree.
|
|
545
|
+
*/
|
|
546
|
+
export function runPreflight(input, spawnBuild = npmBuild) {
|
|
547
|
+
const report = inspectPreflight(input);
|
|
548
|
+
if (!report.ok)
|
|
549
|
+
return { ok: false, facts: report.facts, refusal: report.refusal };
|
|
550
|
+
const { facts } = report;
|
|
551
|
+
if (facts.action === "skipped" || facts.action === "fetch-failed") {
|
|
552
|
+
return { ok: true, facts, detail: report.detail, warning: report.warning };
|
|
553
|
+
}
|
|
554
|
+
// What the task-file reconciliation below cleared, or `null` when it had
|
|
555
|
+
// nothing to do. It rides out on the warning line so the aside directory is
|
|
556
|
+
// printed where the operator is already looking.
|
|
557
|
+
let cleared = null;
|
|
558
|
+
if (facts.behind_by > 0 && report.root !== null && report.target !== null) {
|
|
559
|
+
const root = report.root;
|
|
560
|
+
const target = report.target;
|
|
561
|
+
let merged = git(["merge", "--ff-only", target], root);
|
|
562
|
+
if (!merged.ok) {
|
|
563
|
+
const reconciled = reconcileUntrackedTaskFiles(root, target, failureText(merged));
|
|
564
|
+
if (reconciled.kind === "refused") {
|
|
565
|
+
return { ok: false, facts: { ...facts, action: "refused" }, refusal: reconciled.refusal };
|
|
566
|
+
}
|
|
567
|
+
if (reconciled.kind === "failed") {
|
|
568
|
+
return {
|
|
569
|
+
ok: false,
|
|
570
|
+
facts: { ...facts, action: "refused" },
|
|
571
|
+
failed: { step: reconciled.step, message: reconciled.message },
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
if (reconciled.kind === "cleared") {
|
|
575
|
+
cleared = reconciled.note;
|
|
576
|
+
// Once. A second failure is a different failure — the merge was asked
|
|
577
|
+
// again only because the thing that stopped it is provably gone.
|
|
578
|
+
merged = git(["merge", "--ff-only", target], root);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
if (!merged.ok) {
|
|
582
|
+
return {
|
|
583
|
+
ok: false,
|
|
584
|
+
facts: { ...facts, action: "refused" },
|
|
585
|
+
failed: { step: "git merge --ff-only", message: failureText(merged) },
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
// Staleness is asked AGAIN, after the merge, and the second answer is the one
|
|
590
|
+
// that counts. `inspectPreflight` dated `dist/` against the sources as they
|
|
591
|
+
// were BEFORE the fast-forward; a fast-forward that lands three days of `src/`
|
|
592
|
+
// is precisely what makes a build stale, and reporting the pre-merge answer
|
|
593
|
+
// would leave the operator running a binary older than the code just pulled.
|
|
594
|
+
// Doctor's row keeps the pre-merge answer because doctor merges nothing.
|
|
595
|
+
const stale = (report.root === null ? facts.dist_stale : distStale(input.root)) ?? facts.dist_stale;
|
|
596
|
+
const build = input.build ?? true;
|
|
597
|
+
const behind = facts.behind_by > 0;
|
|
598
|
+
const action = !stale
|
|
599
|
+
? behind
|
|
600
|
+
? "fast-forward"
|
|
601
|
+
: "none"
|
|
602
|
+
: build
|
|
603
|
+
? behind
|
|
604
|
+
? "fast-forward+rebuild"
|
|
605
|
+
: "rebuild"
|
|
606
|
+
: behind
|
|
607
|
+
? "fast-forward+build-skipped"
|
|
608
|
+
: "build-skipped";
|
|
609
|
+
const settled = { ...facts, dist_stale: stale, action };
|
|
610
|
+
// `--no-build` is the only path that starts the runtime on a build it has just
|
|
611
|
+
// dated as stale, so it is the only path that has to say so. The sentence goes
|
|
612
|
+
// on the warning channel rather than into `detail`, because it is not what the
|
|
613
|
+
// preflight found; it is what the operator asked it not to do about it.
|
|
614
|
+
const warning = stale && !build
|
|
615
|
+
? [
|
|
616
|
+
report.warning,
|
|
617
|
+
`${join(input.root, "dist")} is older than the sources and --no-build was given: starting on a STALE BUILD, so verbs added since it was compiled are absent`,
|
|
618
|
+
]
|
|
619
|
+
.filter((line) => line !== null)
|
|
620
|
+
.join("; ")
|
|
621
|
+
: report.warning;
|
|
622
|
+
// Built in the INSTALLATION root, which is the tree whose `dist/` was dated —
|
|
623
|
+
// not in the repository root, which is where the fast-forward happened. In the
|
|
624
|
+
// primary checkout they are the same directory; anywhere they are not, dating
|
|
625
|
+
// one tree and compiling another would be the preflight lying about its work.
|
|
626
|
+
if (stale && build) {
|
|
627
|
+
const built = spawnBuild(input.root);
|
|
628
|
+
if (!built.ok) {
|
|
629
|
+
return {
|
|
630
|
+
ok: false,
|
|
631
|
+
facts: { ...settled, action: "refused" },
|
|
632
|
+
failed: { step: "npm run build", message: built.message },
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
// A rebuild means the process that ran the preflight is no longer the build
|
|
637
|
+
// the preflight produced (APRV-215, coordinator's amendment). Node has already
|
|
638
|
+
// loaded this module tree, so continuing here would start the writer on the
|
|
639
|
+
// code the operator was trying to leave behind — which is the exact defect
|
|
640
|
+
// this task exists to remove. Hand the caller a plan to re-exec into the
|
|
641
|
+
// fresh build instead. See {@link reexecPlan} for why it is a plan rather
|
|
642
|
+
// than a spawn.
|
|
643
|
+
const plan = stale && build ? reexecPlan(input.root) : null;
|
|
644
|
+
return {
|
|
645
|
+
ok: true,
|
|
646
|
+
facts: { ...settled, reexec: plan !== null },
|
|
647
|
+
detail: report.detail,
|
|
648
|
+
warning: [warning, cleared].filter((part) => part !== null).join("; ") || null,
|
|
649
|
+
...(plan === null ? {} : { reexec: plan }),
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
// ---------------------------------------------------------------------------
|
|
653
|
+
// Untracked task files in the fast-forward's way (APRV-300)
|
|
654
|
+
// ---------------------------------------------------------------------------
|
|
655
|
+
/**
|
|
656
|
+
* The one directory this reconciliation is allowed to touch.
|
|
657
|
+
*
|
|
658
|
+
* Backlog.md task files are the only path in this repository where the same
|
|
659
|
+
* file is routinely authored in two checkouts at once: a lane files a task on
|
|
660
|
+
* its branch and the primary files it again with `backlog task create`, so the
|
|
661
|
+
* primary holds untracked bytes at a path the incoming commit carries. Nothing
|
|
662
|
+
* else has that shape, and widening the prefix would turn a narrow, provable
|
|
663
|
+
* case into a general licence to move an operator's files.
|
|
664
|
+
*/
|
|
665
|
+
const TASK_FILE_PREFIX = "backlog/tasks/";
|
|
666
|
+
/** Git's own sentence when a fast-forward would clobber an untracked file. */
|
|
667
|
+
const UNTRACKED_HEADLINE = "untracked working tree files would be overwritten";
|
|
668
|
+
/**
|
|
669
|
+
* The paths out of that message, or `null` when this was some other failure.
|
|
670
|
+
*
|
|
671
|
+
* {@link failureText} has already joined git's lines with ` | `, so the shape
|
|
672
|
+
* parsed here is `error: The following untracked working tree files would be
|
|
673
|
+
* overwritten by merge: | <path> | <path> | Please move or remove them…`. A
|
|
674
|
+
* path git chose to quote (`core.quotePath`, a name with a control byte or a
|
|
675
|
+
* non-ASCII byte) is answered as `null` rather than unquoted by hand: guessing
|
|
676
|
+
* the spelling of a file about to be moved is the one mistake this whole
|
|
677
|
+
* function exists to avoid, and the existing refusal is a fine answer.
|
|
678
|
+
*/
|
|
679
|
+
function untrackedCollisions(message) {
|
|
680
|
+
const parts = message.split(" | ").map((part) => part.trim());
|
|
681
|
+
const start = parts.findIndex((part) => part.includes(UNTRACKED_HEADLINE));
|
|
682
|
+
if (start < 0)
|
|
683
|
+
return null;
|
|
684
|
+
const paths = [];
|
|
685
|
+
for (const part of parts.slice(start + 1)) {
|
|
686
|
+
if (part.length === 0)
|
|
687
|
+
continue;
|
|
688
|
+
if (part.startsWith("Please ") || part === "Aborting")
|
|
689
|
+
break;
|
|
690
|
+
if (part.startsWith('"'))
|
|
691
|
+
return null;
|
|
692
|
+
paths.push(part);
|
|
693
|
+
}
|
|
694
|
+
return paths.length === 0 ? null : paths;
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Clear untracked task files out of a fast-forward's way, or refuse saying why.
|
|
698
|
+
*
|
|
699
|
+
* The incident (2026-09-07): a lane filed `backlog/tasks/aprv-299` on its
|
|
700
|
+
* branch and its pull request merged, while the primary checkout held the same
|
|
701
|
+
* path untracked from its own `backlog task create`. `git merge --ff-only`
|
|
702
|
+
* refuses to write over an untracked file, so `approval up` refused, and its
|
|
703
|
+
* next-steps text pointed at `git status`, which cannot say whether the local
|
|
704
|
+
* copy holds anything the incoming one does not. That is the question, and it
|
|
705
|
+
* is answerable, so it is answered here.
|
|
706
|
+
*
|
|
707
|
+
* Three verdicts, in this order, each with a different claim behind it:
|
|
708
|
+
*
|
|
709
|
+
* - **identical bytes** — the local file says nothing the incoming file does
|
|
710
|
+
* not say. Removing it loses nothing, so it is removed;
|
|
711
|
+
* - **every line also in the incoming copy** — the primary's copy is a subset
|
|
712
|
+
* of what main now carries (the ordinary shape: a stub filed by hand, then
|
|
713
|
+
* the lane's copy with a plan and criteria added). Nothing is lost by
|
|
714
|
+
* letting the incoming copy land, but "nothing is lost" is a judgment about
|
|
715
|
+
* an operator's file, so the bytes are moved aside rather than deleted and
|
|
716
|
+
* the destination is printed;
|
|
717
|
+
* - **anything else** — the local copy has lines main lacks. That is a
|
|
718
|
+
* question about which version is wanted, and no verb here will pick.
|
|
719
|
+
*
|
|
720
|
+
* Two passes, and the order is the safety property, exactly as APRV-225's
|
|
721
|
+
* payload reconciliation in `cli/log-sync.ts`: every file is judged before any
|
|
722
|
+
* file is touched, so a refusal over the last one cannot have already removed
|
|
723
|
+
* the first.
|
|
724
|
+
*/
|
|
725
|
+
function reconcileUntrackedTaskFiles(root, target, message) {
|
|
726
|
+
const collisions = untrackedCollisions(message);
|
|
727
|
+
if (collisions === null)
|
|
728
|
+
return { kind: "declined" };
|
|
729
|
+
// One path outside `backlog/tasks/` and the whole set is declined. A
|
|
730
|
+
// reconciliation that cleared what it understood and then refused anyway
|
|
731
|
+
// would have moved an operator's files for a merge that was never going to
|
|
732
|
+
// run (AC3).
|
|
733
|
+
if (!collisions.every((path) => path.startsWith(TASK_FILE_PREFIX)))
|
|
734
|
+
return { kind: "declined" };
|
|
735
|
+
const verdicts = [];
|
|
736
|
+
for (const relative of collisions) {
|
|
737
|
+
let local;
|
|
738
|
+
try {
|
|
739
|
+
local = readIfPresent(join(root, relative));
|
|
740
|
+
}
|
|
741
|
+
catch (cause) {
|
|
742
|
+
return {
|
|
743
|
+
kind: "failed",
|
|
744
|
+
step: "reading an untracked task file",
|
|
745
|
+
message: `${relative} stopped the fast-forward and could not be read to judge it: ${detailOf(cause)}. Nothing was moved.`,
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
// Gone between the merge's complaint and this read: nothing to clear, and
|
|
749
|
+
// nothing to weigh either. The retry will find out.
|
|
750
|
+
if (local === null)
|
|
751
|
+
continue;
|
|
752
|
+
const incoming = showBlob(root, target, relative);
|
|
753
|
+
if (incoming === null) {
|
|
754
|
+
return {
|
|
755
|
+
kind: "failed",
|
|
756
|
+
step: `git show ${target}:${relative}`,
|
|
757
|
+
message: `the fast-forward stopped on the untracked ${relative} and git could not read the incoming copy to compare it against. Nothing was moved.`,
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
if (incoming.equals(local)) {
|
|
761
|
+
verdicts.push({ kind: "remove", relative });
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
764
|
+
const only = linesOnlyIn(local, incoming);
|
|
765
|
+
verdicts.push(only === 0 ? { kind: "aside", relative } : { kind: "diverged", relative, only });
|
|
766
|
+
}
|
|
767
|
+
const diverged = verdicts.find((verdict) => verdict.kind === "diverged");
|
|
768
|
+
if (diverged !== undefined) {
|
|
769
|
+
const local = join(root, diverged.relative);
|
|
770
|
+
return {
|
|
771
|
+
kind: "refused",
|
|
772
|
+
refusal: {
|
|
773
|
+
code: "up-preflight-task-file-conflict",
|
|
774
|
+
headline: `the untracked ${diverged.relative} has ${plural(diverged.only, "line")} the incoming copy does not`,
|
|
775
|
+
state: [
|
|
776
|
+
`yours: ${local}`,
|
|
777
|
+
`incoming: ${target.slice(0, 12)}:${diverged.relative}`,
|
|
778
|
+
`${plural(diverged.only, "line")} only yours has`,
|
|
779
|
+
"nothing was merged, nothing was moved, and nothing was rebuilt",
|
|
780
|
+
],
|
|
781
|
+
steps: [
|
|
782
|
+
{
|
|
783
|
+
command: `git show ${target.slice(0, 12)}:${diverged.relative} | diff - ${JSON.stringify(local)}`,
|
|
784
|
+
note: "the two copies, side by side",
|
|
785
|
+
},
|
|
786
|
+
{
|
|
787
|
+
command: `mv ${JSON.stringify(local)} ${JSON.stringify(`${local}.mine`)}`,
|
|
788
|
+
note: "keep yours out of the way, then run approval up again",
|
|
789
|
+
},
|
|
790
|
+
],
|
|
791
|
+
footer: [
|
|
792
|
+
"an identical copy is removed and a copy main already contains is moved aside; this one is neither",
|
|
793
|
+
"what the preflight will and will not do: docs/cli-reference.md#up",
|
|
794
|
+
],
|
|
795
|
+
next: `git show ${target.slice(0, 12)}:${diverged.relative}`,
|
|
796
|
+
},
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
const removals = verdicts.filter((verdict) => verdict.kind === "remove");
|
|
800
|
+
const asides = verdicts.filter((verdict) => verdict.kind === "aside");
|
|
801
|
+
if (removals.length === 0 && asides.length === 0)
|
|
802
|
+
return { kind: "declined" };
|
|
803
|
+
// The moves go first and the removals second, so a failure part-way has
|
|
804
|
+
// preserved every byte it had a reason to preserve. Either way the message
|
|
805
|
+
// names what already moved: a half-finished clearing an operator cannot see
|
|
806
|
+
// is worse than the collision it was clearing.
|
|
807
|
+
const asideRoot = asidePath(root);
|
|
808
|
+
const moved = [];
|
|
809
|
+
const sofar = () => moved.length === 0 ? "" : ` Already moved aside: ${moved.join(", ")}.`;
|
|
810
|
+
for (const verdict of asides) {
|
|
811
|
+
const from = join(root, verdict.relative);
|
|
812
|
+
const to = join(asideRoot, verdict.relative);
|
|
813
|
+
try {
|
|
814
|
+
mkdirSync(dirname(to), { recursive: true });
|
|
815
|
+
moveFile(from, to);
|
|
816
|
+
}
|
|
817
|
+
catch (cause) {
|
|
818
|
+
return {
|
|
819
|
+
kind: "failed",
|
|
820
|
+
step: "moving an untracked task file aside",
|
|
821
|
+
message: `${verdict.relative} could not be moved to ${to}: ${detailOf(cause)}. Nothing was merged.${sofar()}`,
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
moved.push(to);
|
|
825
|
+
}
|
|
826
|
+
for (const verdict of removals) {
|
|
827
|
+
try {
|
|
828
|
+
rmSync(join(root, verdict.relative), { force: true });
|
|
829
|
+
}
|
|
830
|
+
catch (cause) {
|
|
831
|
+
return {
|
|
832
|
+
kind: "failed",
|
|
833
|
+
step: "removing an untracked task file",
|
|
834
|
+
message: `${verdict.relative} is byte-identical to the incoming copy and could not be removed: ${detailOf(cause)}. Nothing was merged.${sofar()}`,
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
const said = [];
|
|
839
|
+
if (removals.length > 0) {
|
|
840
|
+
said.push(`${plural(removals.length, "untracked task file")} byte-identical to the incoming copy ${removals.length === 1 ? "was" : "were"} removed`);
|
|
841
|
+
}
|
|
842
|
+
if (moved.length > 0) {
|
|
843
|
+
said.push(`${plural(moved.length, "untracked task file")} whose every line the incoming copy already carries ${moved.length === 1 ? "was" : "were"} moved to ${moved.join(", ")}`);
|
|
844
|
+
}
|
|
845
|
+
return { kind: "cleared", note: `${said.join("; ")}, and the fast-forward was retried` };
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* How many lines of `local` do not appear anywhere in `incoming`.
|
|
849
|
+
*
|
|
850
|
+
* A set of the incoming lines rather than a diff, and deliberately: the
|
|
851
|
+
* question is not whether the two files line up, it is whether the local copy
|
|
852
|
+
* holds any *content* main has not got. A task file reordered by the Backlog.md
|
|
853
|
+
* CLI, or one whose sections were rewritten in place, is the same information
|
|
854
|
+
* in a different arrangement, and zero here is exactly the claim that moving
|
|
855
|
+
* the local copy aside loses nothing.
|
|
856
|
+
*/
|
|
857
|
+
function linesOnlyIn(local, incoming) {
|
|
858
|
+
const carried = new Set(incoming.toString("utf8").split("\n"));
|
|
859
|
+
return local
|
|
860
|
+
.toString("utf8")
|
|
861
|
+
.split("\n")
|
|
862
|
+
.filter((line) => !carried.has(line)).length;
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* Where a moved-aside file goes: a sibling of the checkout, dated.
|
|
866
|
+
*
|
|
867
|
+
* Outside the repository on purpose. Inside it, the file would still be
|
|
868
|
+
* untracked, `git status` would still show it, and the next fast-forward could
|
|
869
|
+
* collide with it all over again — which is to say the move would have solved
|
|
870
|
+
* nothing. A sibling directory is somewhere `ls ..` finds, the date makes two
|
|
871
|
+
* runs on two days two directories, and nothing here ever removes one: it is
|
|
872
|
+
* the operator's copy, kept until they say otherwise.
|
|
873
|
+
*/
|
|
874
|
+
function asidePath(root) {
|
|
875
|
+
const day = new Date().toISOString().slice(0, 10);
|
|
876
|
+
return join(dirname(root), `${basename(root)}-preflight-aside-${day}`);
|
|
877
|
+
}
|
|
878
|
+
/** `rename`, falling back to copy-and-unlink when the two sit on two devices. */
|
|
879
|
+
function moveFile(from, to) {
|
|
880
|
+
try {
|
|
881
|
+
renameSync(from, to);
|
|
882
|
+
}
|
|
883
|
+
catch (cause) {
|
|
884
|
+
if (cause.code !== "EXDEV")
|
|
885
|
+
throw cause;
|
|
886
|
+
copyFileSync(from, to);
|
|
887
|
+
rmSync(from, { force: true });
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
/** The bytes at `path`, or `null` when there is no file there. */
|
|
891
|
+
function readIfPresent(path) {
|
|
892
|
+
try {
|
|
893
|
+
return readFileSync(path);
|
|
894
|
+
}
|
|
895
|
+
catch (cause) {
|
|
896
|
+
if (cause.code === "ENOENT")
|
|
897
|
+
return null;
|
|
898
|
+
throw new ScanError(`${path} could not be read: ${detailOf(cause)}`);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* The plan to re-exec, or `null` when there is nothing to re-exec into.
|
|
903
|
+
*
|
|
904
|
+
* The entry is resolved against the checkout root rather than against this
|
|
905
|
+
* module's own location, and that distinction is the whole point: this module
|
|
906
|
+
* is running FROM the stale build, so `import.meta.url` names exactly the tree
|
|
907
|
+
* being replaced. `<root>/dist/src/cli/main.js` is the file `cli.js` loads and
|
|
908
|
+
* the file `checkBuildFreshness` dates, so it is the one the build just wrote.
|
|
909
|
+
*
|
|
910
|
+
* `process.argv.slice(2)` rather than a reconstruction of the parsed flags: the
|
|
911
|
+
* child must run the command the operator actually typed, including the verb,
|
|
912
|
+
* and a rebuilt argv would be this module's opinion of what they meant.
|
|
913
|
+
* `--no-preflight` is appended so the child cannot preflight again, which is
|
|
914
|
+
* both the loop guard and the honest thing — the checkout is already current.
|
|
915
|
+
*/
|
|
916
|
+
function reexecPlan(root) {
|
|
917
|
+
const entry = join(root, "dist", "src", "cli", "main.js");
|
|
918
|
+
if (!existsSync(entry))
|
|
919
|
+
return null;
|
|
920
|
+
const argv = process.argv.slice(2);
|
|
921
|
+
if (argv.includes("--no-preflight"))
|
|
922
|
+
return null;
|
|
923
|
+
return { entry, argv: [...argv, "--no-preflight"] };
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* Run the fresh build in a child process and become its exit code.
|
|
927
|
+
*
|
|
928
|
+
* `stdio: "inherit"` so the child owns the terminal exactly as the parent would
|
|
929
|
+
* have: the daemon's lines, the token panel, a channel's prompts and an
|
|
930
|
+
* operator's ctrl-c all behave as though no re-exec happened. SIGINT and
|
|
931
|
+
* SIGTERM are forwarded rather than handled, because the child is the writer
|
|
932
|
+
* and the shutdown sequence that matters is its own.
|
|
933
|
+
*
|
|
934
|
+
* A child killed by the signal we forwarded exits 0: the operator asked the
|
|
935
|
+
* runtime to stop and it stopped, which is a clean stop under any spelling.
|
|
936
|
+
* Any other signal is reported as an I/O failure, because a writer that
|
|
937
|
+
* vanished is a fact about the machine rather than a decision of the runtime.
|
|
938
|
+
*/
|
|
939
|
+
export function reexecFreshBuild(plan, cwd) {
|
|
940
|
+
return new Promise((resolve) => {
|
|
941
|
+
const child = spawn(process.execPath, [plan.entry, ...plan.argv], {
|
|
942
|
+
cwd,
|
|
943
|
+
stdio: "inherit",
|
|
944
|
+
env: process.env,
|
|
945
|
+
});
|
|
946
|
+
const forward = (signal) => () => {
|
|
947
|
+
child.kill(signal);
|
|
948
|
+
};
|
|
949
|
+
const onInt = forward("SIGINT");
|
|
950
|
+
const onTerm = forward("SIGTERM");
|
|
951
|
+
process.on("SIGINT", onInt);
|
|
952
|
+
process.on("SIGTERM", onTerm);
|
|
953
|
+
const done = (code) => {
|
|
954
|
+
process.removeListener("SIGINT", onInt);
|
|
955
|
+
process.removeListener("SIGTERM", onTerm);
|
|
956
|
+
resolve(code);
|
|
957
|
+
};
|
|
958
|
+
child.on("error", () => {
|
|
959
|
+
done(EXIT_IO);
|
|
960
|
+
});
|
|
961
|
+
child.on("exit", (code, signal) => {
|
|
962
|
+
if (code !== null)
|
|
963
|
+
return done(code);
|
|
964
|
+
done(signal === "SIGINT" || signal === "SIGTERM" ? EXIT_OK : EXIT_IO);
|
|
965
|
+
});
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
/** One preflight line as a human sentence, and where it belongs. */
|
|
969
|
+
export function describePreflightEvent(event) {
|
|
970
|
+
if (event.event === "preflight_warning") {
|
|
971
|
+
return { text: `approval: preflight — ${event.message}`, stderr: true };
|
|
972
|
+
}
|
|
973
|
+
const commits = `${String(event.behind_by)} commit${event.behind_by === 1 ? "" : "s"}`;
|
|
974
|
+
const did = {
|
|
975
|
+
none: "already at the remote tip, on a build no older than the sources",
|
|
976
|
+
rebuild: "rebuilt a stale build",
|
|
977
|
+
"fast-forward": `fast-forwarded ${commits}`,
|
|
978
|
+
"fast-forward+rebuild": `fast-forwarded ${commits} and rebuilt`,
|
|
979
|
+
"build-skipped": "left a stale build alone (--no-build)",
|
|
980
|
+
"fast-forward+build-skipped": `fast-forwarded ${commits} and left a stale build alone (--no-build)`,
|
|
981
|
+
refused: "refused",
|
|
982
|
+
skipped: "skipped",
|
|
983
|
+
"fetch-failed": "could not reach the remote, so this is the build it already had",
|
|
984
|
+
};
|
|
985
|
+
const running = event.commit === null ? "" : `; now running ${event.commit.slice(0, 12)}`;
|
|
986
|
+
// The re-exec is stated, not implied. An operator watching a foreground
|
|
987
|
+
// process replace itself deserves to be told, and it is the sentence that
|
|
988
|
+
// makes "now running <sha>" a claim about THIS process rather than a wish.
|
|
989
|
+
const handover = event.reexec ? ", in a fresh process on the new build" : "";
|
|
990
|
+
return { text: `up: preflight — ${did[event.action]}${running}${handover}`, stderr: false };
|
|
991
|
+
}
|
|
992
|
+
/**
|
|
993
|
+
* Run the preflight, print what it did, and answer whether the caller may start.
|
|
994
|
+
*
|
|
995
|
+
* `ok: false` carries nothing but the fact: everything a human or a machine
|
|
996
|
+
* needs has already been written by `refuse`, and the caller's only remaining
|
|
997
|
+
* job is to return the exit code.
|
|
998
|
+
*/
|
|
999
|
+
export function startupPreflight(input) {
|
|
1000
|
+
const outcome = runPreflight({
|
|
1001
|
+
logPath: input.logPath,
|
|
1002
|
+
queuePath: input.queuePath,
|
|
1003
|
+
root: input.root ?? installationRoot(),
|
|
1004
|
+
fetch: true,
|
|
1005
|
+
build: input.build,
|
|
1006
|
+
...(input.remote === null ? {} : { remote: input.remote }),
|
|
1007
|
+
...(input.branch === null ? {} : { branch: input.branch }),
|
|
1008
|
+
});
|
|
1009
|
+
if (!outcome.ok) {
|
|
1010
|
+
input.refuse(renderPreflightRefusal(outcome, input.json));
|
|
1011
|
+
return { ok: false, reexec: null };
|
|
1012
|
+
}
|
|
1013
|
+
// A preflight with no repository to look at says nothing. The line reports
|
|
1014
|
+
// what the preflight DID, and "there is no origin here" is a property of the
|
|
1015
|
+
// deployment rather than an event in it: a log-only install outside git would
|
|
1016
|
+
// otherwise open every start with a line about a question it cannot ask.
|
|
1017
|
+
// Doctor's `main-behind-origin` row is where that state is visible.
|
|
1018
|
+
if (outcome.facts.action === "skipped")
|
|
1019
|
+
return { ok: true, reexec: null };
|
|
1020
|
+
if (outcome.warning !== null) {
|
|
1021
|
+
input.emit({ event: "preflight_warning", message: outcome.warning });
|
|
1022
|
+
}
|
|
1023
|
+
// Emitted BEFORE the re-exec, and by the parent, because this is the only
|
|
1024
|
+
// place the whole story is known: the child runs with `--no-preflight` and
|
|
1025
|
+
// has nothing to say about a fast-forward it did not perform. The commit is
|
|
1026
|
+
// read after the merge, so the line names the code the child is about to run.
|
|
1027
|
+
input.emit({
|
|
1028
|
+
event: "preflight",
|
|
1029
|
+
commit: headCommit(input.logPath),
|
|
1030
|
+
detail: outcome.detail,
|
|
1031
|
+
...outcome.facts,
|
|
1032
|
+
});
|
|
1033
|
+
return { ok: true, reexec: outcome.reexec ?? null };
|
|
1034
|
+
}
|
|
1035
|
+
/** The short sha this checkout is on, or `null` when git will not say. */
|
|
1036
|
+
function headCommit(logPath) {
|
|
1037
|
+
const root = repoRoot(dirname(logPath));
|
|
1038
|
+
if (root === null)
|
|
1039
|
+
return null;
|
|
1040
|
+
const run = git(["rev-parse", "HEAD"], root);
|
|
1041
|
+
const sha = run.stdout.trim();
|
|
1042
|
+
return run.ok && sha.length > 0 ? sha : null;
|
|
1043
|
+
}
|
|
1044
|
+
/**
|
|
1045
|
+
* A preflight refusal, on both surfaces (APRV-129).
|
|
1046
|
+
*
|
|
1047
|
+
* The human surface is the runbook: the code, the state, one runnable command
|
|
1048
|
+
* per numbered line, and the rationale compressed into the footer. The machine
|
|
1049
|
+
* surface is one object carrying the stable code and the same facts the success
|
|
1050
|
+
* path carries, so a supervisor can branch on `error.code` without reading a
|
|
1051
|
+
* word of it.
|
|
1052
|
+
*/
|
|
1053
|
+
export function renderPreflightRefusal(outcome, json) {
|
|
1054
|
+
if ("failed" in outcome) {
|
|
1055
|
+
const message = `\`${outcome.failed.step}\` failed: ${outcome.failed.message}`;
|
|
1056
|
+
if (json) {
|
|
1057
|
+
return `${JSON.stringify({
|
|
1058
|
+
error: { code: "up-preflight-failed", message },
|
|
1059
|
+
preflight: outcome.facts,
|
|
1060
|
+
})}\n`;
|
|
1061
|
+
}
|
|
1062
|
+
// A failed build is its own shape of this refusal: the fast-forward may
|
|
1063
|
+
// already have landed, so the checkout is current and the BUILD is not, and
|
|
1064
|
+
// the runtime refuses rather than starting the daemon and the hook on the
|
|
1065
|
+
// stale code the rebuild existed to replace. The compiler's own output has
|
|
1066
|
+
// already gone to this terminal (see `npmBuild`), so the runbook quotes the
|
|
1067
|
+
// exit code and points at the command that reproduces it.
|
|
1068
|
+
const build = outcome.failed.step === "npm run build";
|
|
1069
|
+
return `${runbook(style({ json }), "up-preflight-failed", message, {
|
|
1070
|
+
state: build
|
|
1071
|
+
? [
|
|
1072
|
+
"the build failed, so nothing was started: a daemon on a stale build is the defect this preflight exists to remove",
|
|
1073
|
+
"the fast-forward, if there was one, stands; nothing was reset and nothing was stashed",
|
|
1074
|
+
]
|
|
1075
|
+
: ["the preflight stopped part-way; nothing was reset and nothing was stashed"],
|
|
1076
|
+
steps: build
|
|
1077
|
+
? [
|
|
1078
|
+
{ command: "npm run build", note: "the same build, with the whole error" },
|
|
1079
|
+
{
|
|
1080
|
+
command: "approval up --no-build",
|
|
1081
|
+
note: "ONLY if you mean it: starts on the stale build",
|
|
1082
|
+
},
|
|
1083
|
+
]
|
|
1084
|
+
: [
|
|
1085
|
+
{ command: "git status --short", note: "what this checkout looks like now" },
|
|
1086
|
+
{ command: "approval up --no-preflight", note: "start on the current build" },
|
|
1087
|
+
],
|
|
1088
|
+
})}\n`;
|
|
1089
|
+
}
|
|
1090
|
+
const { refusal } = outcome;
|
|
1091
|
+
if (json) {
|
|
1092
|
+
return `${JSON.stringify({
|
|
1093
|
+
error: { code: refusal.code, message: refusal.headline, next: refusal.next },
|
|
1094
|
+
preflight: outcome.facts,
|
|
1095
|
+
})}\n`;
|
|
1096
|
+
}
|
|
1097
|
+
return `${runbook(style({ json }), refusal.code, refusal.headline, {
|
|
1098
|
+
state: refusal.state,
|
|
1099
|
+
steps: refusal.steps,
|
|
1100
|
+
footer: refusal.footer,
|
|
1101
|
+
})}\n`;
|
|
1102
|
+
}
|
|
1103
|
+
/**
|
|
1104
|
+
* `npm run build`, in the installation root, with its output on the terminal.
|
|
1105
|
+
*
|
|
1106
|
+
* The compile is watched rather than swallowed: a build the operator cannot see
|
|
1107
|
+
* is a pause of unknown length, and a build that FAILS is a compiler error they
|
|
1108
|
+
* need in front of them, not three trimmed lines quoted back inside a runbook.
|
|
1109
|
+
* So both of the child's streams go to this process's stderr — inherited, so
|
|
1110
|
+
* the child writes to the terminal directly and a `tsc` progress line is not
|
|
1111
|
+
* buffered until the end.
|
|
1112
|
+
*
|
|
1113
|
+
* Stdout is deliberately not inherited: under `--json` this process's stdout is
|
|
1114
|
+
* the event stream, and a compiler that printed one line into it would break
|
|
1115
|
+
* every consumer parsing it. Sending the build's stdout to fd 2 keeps the
|
|
1116
|
+
* machine surface exactly as it was while the human still sees the whole build.
|
|
1117
|
+
*
|
|
1118
|
+
* The exit code is the message. `npm run` exits with the script's own status,
|
|
1119
|
+
* so it is the number the operator would have seen running the build by hand,
|
|
1120
|
+
* and it is what the refusal quotes.
|
|
1121
|
+
*/
|
|
1122
|
+
function npmBuild(root) {
|
|
1123
|
+
const result = spawnSync("npm", ["run", "build"], { cwd: root, stdio: ["ignore", 2, 2] });
|
|
1124
|
+
if (result.error !== undefined || result.status === null) {
|
|
1125
|
+
return { ok: false, message: detailOf(result.error ?? "npm did not run") };
|
|
1126
|
+
}
|
|
1127
|
+
if (result.status === 0)
|
|
1128
|
+
return { ok: true, message: "" };
|
|
1129
|
+
return {
|
|
1130
|
+
ok: false,
|
|
1131
|
+
message: `npm run build exited ${String(result.status)} in ${root} (its output is above)`,
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
// ---------------------------------------------------------------------------
|
|
1135
|
+
// Doctor's row
|
|
1136
|
+
// ---------------------------------------------------------------------------
|
|
1137
|
+
/**
|
|
1138
|
+
* `main-behind-origin`, doctor's twenty-first row.
|
|
1139
|
+
*
|
|
1140
|
+
* It makes no network call (`fetch: false`), for the reason every doctor row
|
|
1141
|
+
* makes none it was not asked for: doctor is a report, and a report that reached
|
|
1142
|
+
* the network to be more accurate would be doing something on its own account.
|
|
1143
|
+
* So the answer is as fresh as the last fetch, and the detail says so.
|
|
1144
|
+
*
|
|
1145
|
+
* `fix` strings stay inside `FIX_COMMAND_PREFIXES`: they name `approval` verbs,
|
|
1146
|
+
* never `git`. That constraint predates this row and is the right one — a repair
|
|
1147
|
+
* line telling an operator to reset a branch would be doctor making a decision.
|
|
1148
|
+
*/
|
|
1149
|
+
export function checkMainBehindOrigin(logPath, queuePath, root) {
|
|
1150
|
+
const report = inspectPreflight({ logPath, queuePath, root, fetch: false });
|
|
1151
|
+
if (!report.ok) {
|
|
1152
|
+
return {
|
|
1153
|
+
check: "main-behind-origin",
|
|
1154
|
+
status: "fail",
|
|
1155
|
+
detail: `${report.refusal.code}: ${report.refusal.headline} (as of the last fetch)`,
|
|
1156
|
+
fix: report.refusal.code === "up-preflight-log-diverged"
|
|
1157
|
+
? "approval log sync — snapshot the working log, fast-forward, reconcile the chain"
|
|
1158
|
+
: `approval up — it refuses with the exact next command, which here begins \`${report.refusal.next}\``,
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
if (report.facts.action === "skipped") {
|
|
1162
|
+
return { check: "main-behind-origin", status: "skip", detail: report.detail };
|
|
1163
|
+
}
|
|
1164
|
+
const suffix = report.warning === null ? "" : ` (${report.warning})`;
|
|
1165
|
+
if (report.facts.behind_by === 0 && !report.facts.dist_stale) {
|
|
1166
|
+
return { check: "main-behind-origin", status: "pass", detail: `${report.detail}${suffix}` };
|
|
1167
|
+
}
|
|
1168
|
+
return {
|
|
1169
|
+
check: "main-behind-origin",
|
|
1170
|
+
status: "pass",
|
|
1171
|
+
detail: `${report.detail}; upstream ${report.facts.log_touched ? "DOES" : "does not"} touch the working log or queue${suffix}`,
|
|
1172
|
+
fix: "approval up — fast-forwards and rebuilds when it is safe, and refuses with the next command when it is not",
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
//# sourceMappingURL=preflight.js.map
|