looply-cli 0.0.0-stage → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +137 -3
- package/dist/.src-hash +1 -0
- package/dist/acceptance-observe.d.ts +108 -0
- package/dist/acceptance-observe.d.ts.map +1 -0
- package/dist/acceptance-observe.js +152 -0
- package/dist/acceptance-observe.js.map +1 -0
- package/dist/agent-cmd.d.ts +131 -0
- package/dist/agent-cmd.d.ts.map +1 -0
- package/dist/agent-cmd.js +154 -0
- package/dist/agent-cmd.js.map +1 -0
- package/dist/agent-contained.d.ts +114 -0
- package/dist/agent-contained.d.ts.map +1 -0
- package/dist/agent-contained.js +292 -0
- package/dist/agent-contained.js.map +1 -0
- package/dist/agent-driver.d.ts +130 -0
- package/dist/agent-driver.d.ts.map +1 -0
- package/dist/agent-driver.js +170 -0
- package/dist/agent-driver.js.map +1 -0
- package/dist/agent-envelope.d.ts +94 -0
- package/dist/agent-envelope.d.ts.map +1 -0
- package/dist/agent-envelope.js +214 -0
- package/dist/agent-envelope.js.map +1 -0
- package/dist/agent-exec.d.ts +182 -0
- package/dist/agent-exec.d.ts.map +1 -0
- package/dist/agent-exec.js +125 -0
- package/dist/agent-exec.js.map +1 -0
- package/dist/agent-runtime.d.ts +23 -0
- package/dist/agent-runtime.d.ts.map +1 -0
- package/dist/agent-runtime.js +143 -0
- package/dist/agent-runtime.js.map +1 -0
- package/dist/approval-gate-read.d.ts +66 -0
- package/dist/approval-gate-read.d.ts.map +1 -0
- package/dist/approval-gate-read.js +138 -0
- package/dist/approval-gate-read.js.map +1 -0
- package/dist/approval-resolution.d.ts +87 -0
- package/dist/approval-resolution.d.ts.map +1 -0
- package/dist/approval-resolution.js +100 -0
- package/dist/approval-resolution.js.map +1 -0
- package/dist/approval-resumes.d.ts +102 -0
- package/dist/approval-resumes.d.ts.map +1 -0
- package/dist/approval-resumes.js +167 -0
- package/dist/approval-resumes.js.map +1 -0
- package/dist/ask-brain.d.ts +167 -0
- package/dist/ask-brain.d.ts.map +1 -0
- package/dist/ask-brain.js +355 -0
- package/dist/ask-brain.js.map +1 -0
- package/dist/autonomy-authority.d.ts +80 -0
- package/dist/autonomy-authority.d.ts.map +1 -0
- package/dist/autonomy-authority.js +82 -0
- package/dist/autonomy-authority.js.map +1 -0
- package/dist/autonomy-controller.d.ts +321 -0
- package/dist/autonomy-controller.d.ts.map +1 -0
- package/dist/autonomy-controller.js +517 -0
- package/dist/autonomy-controller.js.map +1 -0
- package/dist/autonomy-store.d.ts +327 -0
- package/dist/autonomy-store.d.ts.map +1 -0
- package/dist/autonomy-store.js +581 -0
- package/dist/autonomy-store.js.map +1 -0
- package/dist/autonomy-tick.d.ts +257 -0
- package/dist/autonomy-tick.d.ts.map +1 -0
- package/dist/autonomy-tick.js +616 -0
- package/dist/autonomy-tick.js.map +1 -0
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +2727 -0
- package/dist/bin.js.map +1 -0
- package/dist/brain-advice.d.ts +21 -0
- package/dist/brain-advice.d.ts.map +1 -0
- package/dist/brain-advice.js +51 -0
- package/dist/brain-advice.js.map +1 -0
- package/dist/candidate-replay-runtime.d.ts +125 -0
- package/dist/candidate-replay-runtime.d.ts.map +1 -0
- package/dist/candidate-replay-runtime.js +188 -0
- package/dist/candidate-replay-runtime.js.map +1 -0
- package/dist/checkout-carry.d.ts +76 -0
- package/dist/checkout-carry.d.ts.map +1 -0
- package/dist/checkout-carry.js +125 -0
- package/dist/checkout-carry.js.map +1 -0
- package/dist/citation-readers.d.ts +26 -0
- package/dist/citation-readers.d.ts.map +1 -0
- package/dist/citation-readers.js +102 -0
- package/dist/citation-readers.js.map +1 -0
- package/dist/command-receipts.d.ts +79 -0
- package/dist/command-receipts.d.ts.map +1 -0
- package/dist/command-receipts.js +143 -0
- package/dist/command-receipts.js.map +1 -0
- package/dist/command-runner.d.ts +32 -0
- package/dist/command-runner.d.ts.map +1 -0
- package/dist/command-runner.js +108 -0
- package/dist/command-runner.js.map +1 -0
- package/dist/compatible-sets.d.ts +67 -0
- package/dist/compatible-sets.d.ts.map +1 -0
- package/dist/compatible-sets.js +81 -0
- package/dist/compatible-sets.js.map +1 -0
- package/dist/compile-check.d.ts +31 -0
- package/dist/compile-check.d.ts.map +1 -0
- package/dist/compile-check.js +34 -0
- package/dist/compile-check.js.map +1 -0
- package/dist/compile-store.d.ts +16 -0
- package/dist/compile-store.d.ts.map +1 -0
- package/dist/compile-store.js +21 -0
- package/dist/compile-store.js.map +1 -0
- package/dist/completion.d.ts +106 -0
- package/dist/completion.d.ts.map +1 -0
- package/dist/completion.js +131 -0
- package/dist/completion.js.map +1 -0
- package/dist/config.d.ts +549 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +763 -0
- package/dist/config.js.map +1 -0
- package/dist/control-file.d.ts +142 -0
- package/dist/control-file.d.ts.map +1 -0
- package/dist/control-file.js +144 -0
- package/dist/control-file.js.map +1 -0
- package/dist/coordinator-state.d.ts +131 -0
- package/dist/coordinator-state.d.ts.map +1 -0
- package/dist/coordinator-state.js +380 -0
- package/dist/coordinator-state.js.map +1 -0
- package/dist/coordinator.d.ts +95 -0
- package/dist/coordinator.d.ts.map +1 -0
- package/dist/coordinator.js +97 -0
- package/dist/coordinator.js.map +1 -0
- package/dist/cursor-store.d.ts +53 -0
- package/dist/cursor-store.d.ts.map +1 -0
- package/dist/cursor-store.js +98 -0
- package/dist/cursor-store.js.map +1 -0
- package/dist/daemon-cmd.d.ts +239 -0
- package/dist/daemon-cmd.d.ts.map +1 -0
- package/dist/daemon-cmd.js +1751 -0
- package/dist/daemon-cmd.js.map +1 -0
- package/dist/daemon-lock.d.ts +140 -0
- package/dist/daemon-lock.d.ts.map +1 -0
- package/dist/daemon-lock.js +352 -0
- package/dist/daemon-lock.js.map +1 -0
- package/dist/daemon-recovery.d.ts +78 -0
- package/dist/daemon-recovery.d.ts.map +1 -0
- package/dist/daemon-recovery.js +161 -0
- package/dist/daemon-recovery.js.map +1 -0
- package/dist/daemon.d.ts +368 -0
- package/dist/daemon.d.ts.map +1 -0
- package/dist/daemon.js +601 -0
- package/dist/daemon.js.map +1 -0
- package/dist/declared-response-shape.d.ts +93 -0
- package/dist/declared-response-shape.d.ts.map +1 -0
- package/dist/declared-response-shape.js +200 -0
- package/dist/declared-response-shape.js.map +1 -0
- package/dist/declared-surfaces.d.ts +16 -0
- package/dist/declared-surfaces.d.ts.map +1 -0
- package/dist/declared-surfaces.js +19 -0
- package/dist/declared-surfaces.js.map +1 -0
- package/dist/depends-review.d.ts +113 -0
- package/dist/depends-review.d.ts.map +1 -0
- package/dist/depends-review.js +110 -0
- package/dist/depends-review.js.map +1 -0
- package/dist/doctor.d.ts +91 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +71 -0
- package/dist/doctor.js.map +1 -0
- package/dist/edge-approval.d.ts +105 -0
- package/dist/edge-approval.d.ts.map +1 -0
- package/dist/edge-approval.js +100 -0
- package/dist/edge-approval.js.map +1 -0
- package/dist/evidence-cache.d.ts +45 -0
- package/dist/evidence-cache.d.ts.map +1 -0
- package/dist/evidence-cache.js +90 -0
- package/dist/evidence-cache.js.map +1 -0
- package/dist/evidence-cmd.d.ts +133 -0
- package/dist/evidence-cmd.d.ts.map +1 -0
- package/dist/evidence-cmd.js +347 -0
- package/dist/evidence-cmd.js.map +1 -0
- package/dist/evidence-facts.d.ts +40 -0
- package/dist/evidence-facts.d.ts.map +1 -0
- package/dist/evidence-facts.js +114 -0
- package/dist/evidence-facts.js.map +1 -0
- package/dist/exclusive-lock.d.ts +52 -0
- package/dist/exclusive-lock.d.ts.map +1 -0
- package/dist/exclusive-lock.js +150 -0
- package/dist/exclusive-lock.js.map +1 -0
- package/dist/exec-domains.d.ts +140 -0
- package/dist/exec-domains.d.ts.map +1 -0
- package/dist/exec-domains.js +287 -0
- package/dist/exec-domains.js.map +1 -0
- package/dist/exemption-store.d.ts +197 -0
- package/dist/exemption-store.d.ts.map +1 -0
- package/dist/exemption-store.js +266 -0
- package/dist/exemption-store.js.map +1 -0
- package/dist/explicit-start-gate.d.ts +96 -0
- package/dist/explicit-start-gate.d.ts.map +1 -0
- package/dist/explicit-start-gate.js +103 -0
- package/dist/explicit-start-gate.js.map +1 -0
- package/dist/fs-trouble.d.ts +70 -0
- package/dist/fs-trouble.d.ts.map +1 -0
- package/dist/fs-trouble.js +98 -0
- package/dist/fs-trouble.js.map +1 -0
- package/dist/gate.d.ts +278 -0
- package/dist/gate.d.ts.map +1 -0
- package/dist/gate.js +278 -0
- package/dist/gate.js.map +1 -0
- package/dist/git-driver.d.ts +519 -0
- package/dist/git-driver.d.ts.map +1 -0
- package/dist/git-driver.js +900 -0
- package/dist/git-driver.js.map +1 -0
- package/dist/git-push-executor.d.ts +41 -0
- package/dist/git-push-executor.d.ts.map +1 -0
- package/dist/git-push-executor.js +142 -0
- package/dist/git-push-executor.js.map +1 -0
- package/dist/glob.d.ts +26 -0
- package/dist/glob.d.ts.map +1 -0
- package/dist/glob.js +93 -0
- package/dist/glob.js.map +1 -0
- package/dist/goal-adaptive.d.ts +295 -0
- package/dist/goal-adaptive.d.ts.map +1 -0
- package/dist/goal-adaptive.js +393 -0
- package/dist/goal-adaptive.js.map +1 -0
- package/dist/goal-compile.d.ts +342 -0
- package/dist/goal-compile.d.ts.map +1 -0
- package/dist/goal-compile.js +444 -0
- package/dist/goal-compile.js.map +1 -0
- package/dist/goal-depends-executor.d.ts +55 -0
- package/dist/goal-depends-executor.d.ts.map +1 -0
- package/dist/goal-depends-executor.js +202 -0
- package/dist/goal-depends-executor.js.map +1 -0
- package/dist/goal-execution-evidence.d.ts +135 -0
- package/dist/goal-execution-evidence.d.ts.map +1 -0
- package/dist/goal-execution-evidence.js +159 -0
- package/dist/goal-execution-evidence.js.map +1 -0
- package/dist/goal-gap.d.ts +140 -0
- package/dist/goal-gap.d.ts.map +1 -0
- package/dist/goal-gap.js +197 -0
- package/dist/goal-gap.js.map +1 -0
- package/dist/goal-met-target.d.ts +98 -0
- package/dist/goal-met-target.d.ts.map +1 -0
- package/dist/goal-met-target.js +92 -0
- package/dist/goal-met-target.js.map +1 -0
- package/dist/goal-ownership.d.ts +416 -0
- package/dist/goal-ownership.d.ts.map +1 -0
- package/dist/goal-ownership.js +576 -0
- package/dist/goal-ownership.js.map +1 -0
- package/dist/goal-runs.d.ts +82 -0
- package/dist/goal-runs.d.ts.map +1 -0
- package/dist/goal-runs.js +120 -0
- package/dist/goal-runs.js.map +1 -0
- package/dist/goal-store.d.ts +146 -0
- package/dist/goal-store.d.ts.map +1 -0
- package/dist/goal-store.js +294 -0
- package/dist/goal-store.js.map +1 -0
- package/dist/goal-view.d.ts +156 -0
- package/dist/goal-view.d.ts.map +1 -0
- package/dist/goal-view.js +499 -0
- package/dist/goal-view.js.map +1 -0
- package/dist/governed-approvals.d.ts +152 -0
- package/dist/governed-approvals.d.ts.map +1 -0
- package/dist/governed-approvals.js +246 -0
- package/dist/governed-approvals.js.map +1 -0
- package/dist/governed-broker.d.ts +169 -0
- package/dist/governed-broker.d.ts.map +1 -0
- package/dist/governed-broker.js +196 -0
- package/dist/governed-broker.js.map +1 -0
- package/dist/governed-run.d.ts +220 -0
- package/dist/governed-run.d.ts.map +1 -0
- package/dist/governed-run.js +406 -0
- package/dist/governed-run.js.map +1 -0
- package/dist/grant-reconcile.d.ts +63 -0
- package/dist/grant-reconcile.d.ts.map +1 -0
- package/dist/grant-reconcile.js +121 -0
- package/dist/grant-reconcile.js.map +1 -0
- package/dist/index-for-head.d.ts +56 -0
- package/dist/index-for-head.d.ts.map +1 -0
- package/dist/index-for-head.js +103 -0
- package/dist/index-for-head.js.map +1 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +55 -0
- package/dist/index.js.map +1 -0
- package/dist/init-detect.d.ts +39 -0
- package/dist/init-detect.d.ts.map +1 -0
- package/dist/init-detect.js +188 -0
- package/dist/init-detect.js.map +1 -0
- package/dist/integration-deps.d.ts +62 -0
- package/dist/integration-deps.d.ts.map +1 -0
- package/dist/integration-deps.js +251 -0
- package/dist/integration-deps.js.map +1 -0
- package/dist/integration-runtime.d.ts +458 -0
- package/dist/integration-runtime.d.ts.map +1 -0
- package/dist/integration-runtime.js +681 -0
- package/dist/integration-runtime.js.map +1 -0
- package/dist/integration.d.ts +45 -0
- package/dist/integration.d.ts.map +1 -0
- package/dist/integration.js +87 -0
- package/dist/integration.js.map +1 -0
- package/dist/integrator.d.ts +84 -0
- package/dist/integrator.d.ts.map +1 -0
- package/dist/integrator.js +193 -0
- package/dist/integrator.js.map +1 -0
- package/dist/lane-claims.d.ts +174 -0
- package/dist/lane-claims.d.ts.map +1 -0
- package/dist/lane-claims.js +218 -0
- package/dist/lane-claims.js.map +1 -0
- package/dist/lane-coverage.d.ts +87 -0
- package/dist/lane-coverage.d.ts.map +1 -0
- package/dist/lane-coverage.js +129 -0
- package/dist/lane-coverage.js.map +1 -0
- package/dist/ledger-replay.d.ts +115 -0
- package/dist/ledger-replay.d.ts.map +1 -0
- package/dist/ledger-replay.js +155 -0
- package/dist/ledger-replay.js.map +1 -0
- package/dist/ledger-store.d.ts +68 -0
- package/dist/ledger-store.d.ts.map +1 -0
- package/dist/ledger-store.js +111 -0
- package/dist/ledger-store.js.map +1 -0
- package/dist/loop-safety-observer.d.ts +132 -0
- package/dist/loop-safety-observer.d.ts.map +1 -0
- package/dist/loop-safety-observer.js +76 -0
- package/dist/loop-safety-observer.js.map +1 -0
- package/dist/loop-safety-store.d.ts +339 -0
- package/dist/loop-safety-store.d.ts.map +1 -0
- package/dist/loop-safety-store.js +800 -0
- package/dist/loop-safety-store.js.map +1 -0
- package/dist/machine-store.d.ts +54 -0
- package/dist/machine-store.d.ts.map +1 -0
- package/dist/machine-store.js +149 -0
- package/dist/machine-store.js.map +1 -0
- package/dist/mcp-main.d.ts +2 -0
- package/dist/mcp-main.d.ts.map +1 -0
- package/dist/mcp-main.js +116 -0
- package/dist/mcp-main.js.map +1 -0
- package/dist/mcp-server.d.ts +72 -0
- package/dist/mcp-server.d.ts.map +1 -0
- package/dist/mcp-server.js +259 -0
- package/dist/mcp-server.js.map +1 -0
- package/dist/model-routing.d.ts +75 -0
- package/dist/model-routing.d.ts.map +1 -0
- package/dist/model-routing.js +108 -0
- package/dist/model-routing.js.map +1 -0
- package/dist/model-transport.d.ts +188 -0
- package/dist/model-transport.d.ts.map +1 -0
- package/dist/model-transport.js +625 -0
- package/dist/model-transport.js.map +1 -0
- package/dist/notify-cadence.d.ts +55 -0
- package/dist/notify-cadence.d.ts.map +1 -0
- package/dist/notify-cadence.js +68 -0
- package/dist/notify-cadence.js.map +1 -0
- package/dist/notify-channel.d.ts +69 -0
- package/dist/notify-channel.d.ts.map +1 -0
- package/dist/notify-channel.js +151 -0
- package/dist/notify-channel.js.map +1 -0
- package/dist/notify.d.ts +71 -0
- package/dist/notify.d.ts.map +1 -0
- package/dist/notify.js +125 -0
- package/dist/notify.js.map +1 -0
- package/dist/observation-inventory.d.ts +65 -0
- package/dist/observation-inventory.d.ts.map +1 -0
- package/dist/observation-inventory.js +139 -0
- package/dist/observation-inventory.js.map +1 -0
- package/dist/owned-children.d.ts +45 -0
- package/dist/owned-children.d.ts.map +1 -0
- package/dist/owned-children.js +64 -0
- package/dist/owned-children.js.map +1 -0
- package/dist/packet-for-goal.d.ts +19 -0
- package/dist/packet-for-goal.d.ts.map +1 -0
- package/dist/packet-for-goal.js +32 -0
- package/dist/packet-for-goal.js.map +1 -0
- package/dist/pair-cmd.d.ts +85 -0
- package/dist/pair-cmd.d.ts.map +1 -0
- package/dist/pair-cmd.js +205 -0
- package/dist/pair-cmd.js.map +1 -0
- package/dist/partial-asker.d.ts +96 -0
- package/dist/partial-asker.d.ts.map +1 -0
- package/dist/partial-asker.js +117 -0
- package/dist/partial-asker.js.map +1 -0
- package/dist/pdf-text-lines.d.ts +17 -0
- package/dist/pdf-text-lines.d.ts.map +1 -0
- package/dist/pdf-text-lines.js +70 -0
- package/dist/pdf-text-lines.js.map +1 -0
- package/dist/pdf-text-worker.d.ts +2 -0
- package/dist/pdf-text-worker.d.ts.map +1 -0
- package/dist/pdf-text-worker.js +81 -0
- package/dist/pdf-text-worker.js.map +1 -0
- package/dist/pdf-text.d.ts +56 -0
- package/dist/pdf-text.d.ts.map +1 -0
- package/dist/pdf-text.js +64 -0
- package/dist/pdf-text.js.map +1 -0
- package/dist/pending-action.d.ts +154 -0
- package/dist/pending-action.d.ts.map +1 -0
- package/dist/pending-action.js +148 -0
- package/dist/pending-action.js.map +1 -0
- package/dist/preemptor.d.ts +40 -0
- package/dist/preemptor.d.ts.map +1 -0
- package/dist/preemptor.js +49 -0
- package/dist/preemptor.js.map +1 -0
- package/dist/programme-mode.d.ts +29 -0
- package/dist/programme-mode.d.ts.map +1 -0
- package/dist/programme-mode.js +50 -0
- package/dist/programme-mode.js.map +1 -0
- package/dist/progress-view.d.ts +14 -0
- package/dist/progress-view.d.ts.map +1 -0
- package/dist/progress-view.js +114 -0
- package/dist/progress-view.js.map +1 -0
- package/dist/project-graph-builder.d.ts +81 -0
- package/dist/project-graph-builder.d.ts.map +1 -0
- package/dist/project-graph-builder.js +246 -0
- package/dist/project-graph-builder.js.map +1 -0
- package/dist/project-graph-runtime.d.ts +86 -0
- package/dist/project-graph-runtime.d.ts.map +1 -0
- package/dist/project-graph-runtime.js +112 -0
- package/dist/project-graph-runtime.js.map +1 -0
- package/dist/project-index-builder.d.ts +96 -0
- package/dist/project-index-builder.d.ts.map +1 -0
- package/dist/project-index-builder.js +298 -0
- package/dist/project-index-builder.js.map +1 -0
- package/dist/project-index-facts.d.ts +51 -0
- package/dist/project-index-facts.d.ts.map +1 -0
- package/dist/project-index-facts.js +117 -0
- package/dist/project-index-facts.js.map +1 -0
- package/dist/project-index-store.d.ts +97 -0
- package/dist/project-index-store.d.ts.map +1 -0
- package/dist/project-index-store.js +140 -0
- package/dist/project-index-store.js.map +1 -0
- package/dist/project-memory-store.d.ts +127 -0
- package/dist/project-memory-store.d.ts.map +1 -0
- package/dist/project-memory-store.js +302 -0
- package/dist/project-memory-store.js.map +1 -0
- package/dist/proposal-store.d.ts +14 -0
- package/dist/proposal-store.d.ts.map +1 -0
- package/dist/proposal-store.js +19 -0
- package/dist/proposal-store.js.map +1 -0
- package/dist/prover.d.ts +80 -0
- package/dist/prover.d.ts.map +1 -0
- package/dist/prover.js +137 -0
- package/dist/prover.js.map +1 -0
- package/dist/provider-identity-store.d.ts +34 -0
- package/dist/provider-identity-store.d.ts.map +1 -0
- package/dist/provider-identity-store.js +72 -0
- package/dist/provider-identity-store.js.map +1 -0
- package/dist/provider-identity.d.ts +104 -0
- package/dist/provider-identity.d.ts.map +1 -0
- package/dist/provider-identity.js +167 -0
- package/dist/provider-identity.js.map +1 -0
- package/dist/provider-limit.d.ts +79 -0
- package/dist/provider-limit.d.ts.map +1 -0
- package/dist/provider-limit.js +148 -0
- package/dist/provider-limit.js.map +1 -0
- package/dist/provider-preflight.d.ts +171 -0
- package/dist/provider-preflight.d.ts.map +1 -0
- package/dist/provider-preflight.js +395 -0
- package/dist/provider-preflight.js.map +1 -0
- package/dist/provider-state.d.ts +120 -0
- package/dist/provider-state.d.ts.map +1 -0
- package/dist/provider-state.js +182 -0
- package/dist/provider-state.js.map +1 -0
- package/dist/providers.d.ts +132 -0
- package/dist/providers.d.ts.map +1 -0
- package/dist/providers.js +161 -0
- package/dist/providers.js.map +1 -0
- package/dist/publication-recorder.d.ts +117 -0
- package/dist/publication-recorder.d.ts.map +1 -0
- package/dist/publication-recorder.js +124 -0
- package/dist/publication-recorder.js.map +1 -0
- package/dist/pursuit-delivery.d.ts +24 -0
- package/dist/pursuit-delivery.d.ts.map +1 -0
- package/dist/pursuit-delivery.js +38 -0
- package/dist/pursuit-delivery.js.map +1 -0
- package/dist/queue-store.d.ts +132 -0
- package/dist/queue-store.d.ts.map +1 -0
- package/dist/queue-store.js +185 -0
- package/dist/queue-store.js.map +1 -0
- package/dist/queue-summary.d.ts +14 -0
- package/dist/queue-summary.d.ts.map +1 -0
- package/dist/queue-summary.js +82 -0
- package/dist/queue-summary.js.map +1 -0
- package/dist/queue-view.d.ts +20 -0
- package/dist/queue-view.d.ts.map +1 -0
- package/dist/queue-view.js +163 -0
- package/dist/queue-view.js.map +1 -0
- package/dist/queue-write-lease.d.ts +106 -0
- package/dist/queue-write-lease.d.ts.map +1 -0
- package/dist/queue-write-lease.js +282 -0
- package/dist/queue-write-lease.js.map +1 -0
- package/dist/rails-home.d.ts +162 -0
- package/dist/rails-home.d.ts.map +1 -0
- package/dist/rails-home.js +452 -0
- package/dist/rails-home.js.map +1 -0
- package/dist/rails-migrations.d.ts +75 -0
- package/dist/rails-migrations.d.ts.map +1 -0
- package/dist/rails-migrations.js +199 -0
- package/dist/rails-migrations.js.map +1 -0
- package/dist/rails-stores.d.ts +49 -0
- package/dist/rails-stores.d.ts.map +1 -0
- package/dist/rails-stores.js +100 -0
- package/dist/rails-stores.js.map +1 -0
- package/dist/reaper.d.ts +69 -0
- package/dist/reaper.d.ts.map +1 -0
- package/dist/reaper.js +152 -0
- package/dist/reaper.js.map +1 -0
- package/dist/record-store.d.ts +36 -0
- package/dist/record-store.d.ts.map +1 -0
- package/dist/record-store.js +78 -0
- package/dist/record-store.js.map +1 -0
- package/dist/recovery-io.d.ts +50 -0
- package/dist/recovery-io.d.ts.map +1 -0
- package/dist/recovery-io.js +122 -0
- package/dist/recovery-io.js.map +1 -0
- package/dist/recovery.d.ts +146 -0
- package/dist/recovery.d.ts.map +1 -0
- package/dist/recovery.js +110 -0
- package/dist/recovery.js.map +1 -0
- package/dist/redraw.d.ts +34 -0
- package/dist/redraw.d.ts.map +1 -0
- package/dist/redraw.js +62 -0
- package/dist/redraw.js.map +1 -0
- package/dist/rejected-evidence.d.ts +255 -0
- package/dist/rejected-evidence.d.ts.map +1 -0
- package/dist/rejected-evidence.js +324 -0
- package/dist/rejected-evidence.js.map +1 -0
- package/dist/report-cmd.d.ts +32 -0
- package/dist/report-cmd.d.ts.map +1 -0
- package/dist/report-cmd.js +201 -0
- package/dist/report-cmd.js.map +1 -0
- package/dist/repos-cmd.d.ts +80 -0
- package/dist/repos-cmd.d.ts.map +1 -0
- package/dist/repos-cmd.js +240 -0
- package/dist/repos-cmd.js.map +1 -0
- package/dist/request-body-shape.d.ts +93 -0
- package/dist/request-body-shape.d.ts.map +1 -0
- package/dist/request-body-shape.js +407 -0
- package/dist/request-body-shape.js.map +1 -0
- package/dist/requirement-claims.d.ts +89 -0
- package/dist/requirement-claims.d.ts.map +1 -0
- package/dist/requirement-claims.js +92 -0
- package/dist/requirement-claims.js.map +1 -0
- package/dist/retention-apply-runtime.d.ts +105 -0
- package/dist/retention-apply-runtime.d.ts.map +1 -0
- package/dist/retention-apply-runtime.js +269 -0
- package/dist/retention-apply-runtime.js.map +1 -0
- package/dist/reverify.d.ts +41 -0
- package/dist/reverify.d.ts.map +1 -0
- package/dist/reverify.js +69 -0
- package/dist/reverify.js.map +1 -0
- package/dist/round-records.d.ts +65 -0
- package/dist/round-records.d.ts.map +1 -0
- package/dist/round-records.js +61 -0
- package/dist/round-records.js.map +1 -0
- package/dist/round-runner.d.ts +924 -0
- package/dist/round-runner.d.ts.map +1 -0
- package/dist/round-runner.js +1693 -0
- package/dist/round-runner.js.map +1 -0
- package/dist/run-approval.d.ts +180 -0
- package/dist/run-approval.d.ts.map +1 -0
- package/dist/run-approval.js +199 -0
- package/dist/run-approval.js.map +1 -0
- package/dist/run-cmd.d.ts +359 -0
- package/dist/run-cmd.d.ts.map +1 -0
- package/dist/run-cmd.js +4418 -0
- package/dist/run-cmd.js.map +1 -0
- package/dist/run-delivery.d.ts +71 -0
- package/dist/run-delivery.d.ts.map +1 -0
- package/dist/run-delivery.js +104 -0
- package/dist/run-delivery.js.map +1 -0
- package/dist/run-ended.d.ts +75 -0
- package/dist/run-ended.d.ts.map +1 -0
- package/dist/run-ended.js +27 -0
- package/dist/run-ended.js.map +1 -0
- package/dist/run-ending-locator.d.ts +125 -0
- package/dist/run-ending-locator.d.ts.map +1 -0
- package/dist/run-ending-locator.js +133 -0
- package/dist/run-ending-locator.js.map +1 -0
- package/dist/run-launcher.d.ts +459 -0
- package/dist/run-launcher.d.ts.map +1 -0
- package/dist/run-launcher.js +652 -0
- package/dist/run-launcher.js.map +1 -0
- package/dist/run-lease.d.ts +208 -0
- package/dist/run-lease.d.ts.map +1 -0
- package/dist/run-lease.js +301 -0
- package/dist/run-lease.js.map +1 -0
- package/dist/run-refusal-marks.d.ts +189 -0
- package/dist/run-refusal-marks.d.ts.map +1 -0
- package/dist/run-refusal-marks.js +249 -0
- package/dist/run-refusal-marks.js.map +1 -0
- package/dist/run-start-marks.d.ts +153 -0
- package/dist/run-start-marks.d.ts.map +1 -0
- package/dist/run-start-marks.js +269 -0
- package/dist/run-start-marks.js.map +1 -0
- package/dist/scan-cmd.d.ts +14 -0
- package/dist/scan-cmd.d.ts.map +1 -0
- package/dist/scan-cmd.js +270 -0
- package/dist/scan-cmd.js.map +1 -0
- package/dist/scan-events.d.ts +50 -0
- package/dist/scan-events.d.ts.map +1 -0
- package/dist/scan-events.js +91 -0
- package/dist/scan-events.js.map +1 -0
- package/dist/scan.d.ts +191 -0
- package/dist/scan.d.ts.map +1 -0
- package/dist/scan.js +417 -0
- package/dist/scan.js.map +1 -0
- package/dist/scope-rule.d.ts +148 -0
- package/dist/scope-rule.d.ts.map +1 -0
- package/dist/scope-rule.js +121 -0
- package/dist/scope-rule.js.map +1 -0
- package/dist/self-repo-id.d.ts +18 -0
- package/dist/self-repo-id.d.ts.map +1 -0
- package/dist/self-repo-id.js +70 -0
- package/dist/self-repo-id.js.map +1 -0
- package/dist/serve-cmd.d.ts +115 -0
- package/dist/serve-cmd.d.ts.map +1 -0
- package/dist/serve-cmd.js +505 -0
- package/dist/serve-cmd.js.map +1 -0
- package/dist/serve.d.ts +167 -0
- package/dist/serve.d.ts.map +1 -0
- package/dist/serve.js +336 -0
- package/dist/serve.js.map +1 -0
- package/dist/server-client.d.ts +766 -0
- package/dist/server-client.d.ts.map +1 -0
- package/dist/server-client.js +475 -0
- package/dist/server-client.js.map +1 -0
- package/dist/session-adapter-runtime.d.ts +195 -0
- package/dist/session-adapter-runtime.d.ts.map +1 -0
- package/dist/session-adapter-runtime.js +373 -0
- package/dist/session-adapter-runtime.js.map +1 -0
- package/dist/session-checkpoint-runtime.d.ts +62 -0
- package/dist/session-checkpoint-runtime.d.ts.map +1 -0
- package/dist/session-checkpoint-runtime.js +268 -0
- package/dist/session-checkpoint-runtime.js.map +1 -0
- package/dist/session-control-transport.d.ts +106 -0
- package/dist/session-control-transport.d.ts.map +1 -0
- package/dist/session-control-transport.js +232 -0
- package/dist/session-control-transport.js.map +1 -0
- package/dist/session-observation-store.d.ts +98 -0
- package/dist/session-observation-store.d.ts.map +1 -0
- package/dist/session-observation-store.js +155 -0
- package/dist/session-observation-store.js.map +1 -0
- package/dist/session-observe-runtime.d.ts +123 -0
- package/dist/session-observe-runtime.d.ts.map +1 -0
- package/dist/session-observe-runtime.js +317 -0
- package/dist/session-observe-runtime.js.map +1 -0
- package/dist/session-store.d.ts +98 -0
- package/dist/session-store.d.ts.map +1 -0
- package/dist/session-store.js +116 -0
- package/dist/session-store.js.map +1 -0
- package/dist/sessions-cmd.d.ts +202 -0
- package/dist/sessions-cmd.d.ts.map +1 -0
- package/dist/sessions-cmd.js +609 -0
- package/dist/sessions-cmd.js.map +1 -0
- package/dist/settings-overlay.d.ts +53 -0
- package/dist/settings-overlay.d.ts.map +1 -0
- package/dist/settings-overlay.js +175 -0
- package/dist/settings-overlay.js.map +1 -0
- package/dist/setup-prompt.d.ts +34 -0
- package/dist/setup-prompt.d.ts.map +1 -0
- package/dist/setup-prompt.js +92 -0
- package/dist/setup-prompt.js.map +1 -0
- package/dist/setup.d.ts +103 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +283 -0
- package/dist/setup.js.map +1 -0
- package/dist/shared-grants.d.ts +164 -0
- package/dist/shared-grants.d.ts.map +1 -0
- package/dist/shared-grants.js +242 -0
- package/dist/shared-grants.js.map +1 -0
- package/dist/signals.d.ts +55 -0
- package/dist/signals.d.ts.map +1 -0
- package/dist/signals.js +85 -0
- package/dist/signals.js.map +1 -0
- package/dist/source-cmd.d.ts +201 -0
- package/dist/source-cmd.d.ts.map +1 -0
- package/dist/source-cmd.js +684 -0
- package/dist/source-cmd.js.map +1 -0
- package/dist/source-verdict.d.ts +99 -0
- package/dist/source-verdict.d.ts.map +1 -0
- package/dist/source-verdict.js +182 -0
- package/dist/source-verdict.js.map +1 -0
- package/dist/spend-ledger.d.ts +350 -0
- package/dist/spend-ledger.d.ts.map +1 -0
- package/dist/spend-ledger.js +546 -0
- package/dist/spend-ledger.js.map +1 -0
- package/dist/stale-run-sweep.d.ts +79 -0
- package/dist/stale-run-sweep.d.ts.map +1 -0
- package/dist/stale-run-sweep.js +116 -0
- package/dist/stale-run-sweep.js.map +1 -0
- package/dist/start-attempts.d.ts +385 -0
- package/dist/start-attempts.d.ts.map +1 -0
- package/dist/start-attempts.js +613 -0
- package/dist/start-attempts.js.map +1 -0
- package/dist/status-cmd.d.ts +28 -0
- package/dist/status-cmd.d.ts.map +1 -0
- package/dist/status-cmd.js +116 -0
- package/dist/status-cmd.js.map +1 -0
- package/dist/stop-follow-up.d.ts +107 -0
- package/dist/stop-follow-up.d.ts.map +1 -0
- package/dist/stop-follow-up.js +172 -0
- package/dist/stop-follow-up.js.map +1 -0
- package/dist/surface-lease.d.ts +118 -0
- package/dist/surface-lease.d.ts.map +1 -0
- package/dist/surface-lease.js +485 -0
- package/dist/surface-lease.js.map +1 -0
- package/dist/sync.d.ts +54 -0
- package/dist/sync.d.ts.map +1 -0
- package/dist/sync.js +93 -0
- package/dist/sync.js.map +1 -0
- package/dist/syncing-ledger.d.ts +45 -0
- package/dist/syncing-ledger.d.ts.map +1 -0
- package/dist/syncing-ledger.js +53 -0
- package/dist/syncing-ledger.js.map +1 -0
- package/dist/temp-fixtures.d.ts +68 -0
- package/dist/temp-fixtures.d.ts.map +1 -0
- package/dist/temp-fixtures.js +38 -0
- package/dist/temp-fixtures.js.map +1 -0
- package/dist/token-window.d.ts +84 -0
- package/dist/token-window.d.ts.map +1 -0
- package/dist/token-window.js +143 -0
- package/dist/token-window.js.map +1 -0
- package/dist/tool-bridge.d.ts +177 -0
- package/dist/tool-bridge.d.ts.map +1 -0
- package/dist/tool-bridge.js +329 -0
- package/dist/tool-bridge.js.map +1 -0
- package/dist/tool-call-record.d.ts +197 -0
- package/dist/tool-call-record.d.ts.map +1 -0
- package/dist/tool-call-record.js +276 -0
- package/dist/tool-call-record.js.map +1 -0
- package/dist/tool-protocol.d.ts +135 -0
- package/dist/tool-protocol.d.ts.map +1 -0
- package/dist/tool-protocol.js +104 -0
- package/dist/tool-protocol.js.map +1 -0
- package/dist/tool-worker.d.ts +3 -0
- package/dist/tool-worker.d.ts.map +1 -0
- package/dist/tool-worker.js +478 -0
- package/dist/tool-worker.js.map +1 -0
- package/dist/toolchain-mirror.d.ts +86 -0
- package/dist/toolchain-mirror.d.ts.map +1 -0
- package/dist/toolchain-mirror.js +307 -0
- package/dist/toolchain-mirror.js.map +1 -0
- package/dist/trusted-git.d.ts +243 -0
- package/dist/trusted-git.d.ts.map +1 -0
- package/dist/trusted-git.js +920 -0
- package/dist/trusted-git.js.map +1 -0
- package/dist/ui.d.ts +76 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +118 -0
- package/dist/ui.js.map +1 -0
- package/dist/untrusted-exec.d.ts +275 -0
- package/dist/untrusted-exec.d.ts.map +1 -0
- package/dist/untrusted-exec.js +479 -0
- package/dist/untrusted-exec.js.map +1 -0
- package/dist/untrusted-gate.d.ts +73 -0
- package/dist/untrusted-gate.d.ts.map +1 -0
- package/dist/untrusted-gate.js +226 -0
- package/dist/untrusted-gate.js.map +1 -0
- package/dist/uploader.d.ts +153 -0
- package/dist/uploader.d.ts.map +1 -0
- package/dist/uploader.js +204 -0
- package/dist/uploader.js.map +1 -0
- package/dist/verifier.d.ts +66 -0
- package/dist/verifier.d.ts.map +1 -0
- package/dist/verifier.js +170 -0
- package/dist/verifier.js.map +1 -0
- package/dist/verify-apply.d.ts +26 -0
- package/dist/verify-apply.d.ts.map +1 -0
- package/dist/verify-apply.js +36 -0
- package/dist/verify-apply.js.map +1 -0
- package/dist/verify-cmd.d.ts +44 -0
- package/dist/verify-cmd.d.ts.map +1 -0
- package/dist/verify-cmd.js +125 -0
- package/dist/verify-cmd.js.map +1 -0
- package/dist/verify-pass.d.ts +73 -0
- package/dist/verify-pass.d.ts.map +1 -0
- package/dist/verify-pass.js +103 -0
- package/dist/verify-pass.js.map +1 -0
- package/dist/welcome.d.ts +43 -0
- package/dist/welcome.d.ts.map +1 -0
- package/dist/welcome.js +116 -0
- package/dist/welcome.js.map +1 -0
- package/dist/workspace-provisioner.d.ts +65 -0
- package/dist/workspace-provisioner.d.ts.map +1 -0
- package/dist/workspace-provisioner.js +66 -0
- package/dist/workspace-provisioner.js.map +1 -0
- package/dist/workspace-quarantine.d.ts +64 -0
- package/dist/workspace-quarantine.d.ts.map +1 -0
- package/dist/workspace-quarantine.js +156 -0
- package/dist/workspace-quarantine.js.map +1 -0
- package/dist/workspace-residue-sweep.d.ts +113 -0
- package/dist/workspace-residue-sweep.d.ts.map +1 -0
- package/dist/workspace-residue-sweep.js +158 -0
- package/dist/workspace-residue-sweep.js.map +1 -0
- package/dist/workspace.d.ts +639 -0
- package/dist/workspace.d.ts.map +1 -0
- package/dist/workspace.js +2026 -0
- package/dist/workspace.js.map +1 -0
- package/package.json +52 -4
- package/win/CredProbe.cs +64 -0
- package/win/FakeClaude.cs +56 -0
- package/win/HandleProbe.cs +116 -0
- package/win/TokenProbe.cs +53 -0
- package/win/appcontainer.ps1 +500 -0
|
@@ -0,0 +1,1693 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RoundRunner = exports.DEFAULT_PAUSE_POLL_MS = exports.RunnerError = exports.PARTIAL_DISCARDED_CODE = exports.RUN_HALTED_CODE = exports.GATE_MACHINERY = exports.MAX_CONSECUTIVE_BLOCKED_ROUNDS = void 0;
|
|
4
|
+
exports.partialDiscardedText = partialDiscardedText;
|
|
5
|
+
const core_1 = require("@looply/core");
|
|
6
|
+
const gate_1 = require("./gate");
|
|
7
|
+
const glob_1 = require("./glob");
|
|
8
|
+
const scope_rule_1 = require("./scope-rule");
|
|
9
|
+
const rejected_evidence_1 = require("./rejected-evidence");
|
|
10
|
+
const verify_apply_1 = require("./verify-apply");
|
|
11
|
+
const reverify_1 = require("./reverify");
|
|
12
|
+
/**
|
|
13
|
+
* How many rounds may fail to run their gate before the run gives up.
|
|
14
|
+
*
|
|
15
|
+
* A blocked round costs the item nothing, which is the point — and which is
|
|
16
|
+
* also why the loop needs its own floor: without one, a machine with no
|
|
17
|
+
* containment would pick, implement, fail to gate and return the item, for ever
|
|
18
|
+
* and at full model cost.
|
|
19
|
+
*/
|
|
20
|
+
exports.MAX_CONSECUTIVE_BLOCKED_ROUNDS = 3;
|
|
21
|
+
/**
|
|
22
|
+
* WHAT THE GATE'S COMMANDS RESOLVE THROUGH, AND WHAT ITS STEPS FIND FOR THEMSELVES.
|
|
23
|
+
*
|
|
24
|
+
* `judgedBy` refuses to guess either, and this is where the guessing is done
|
|
25
|
+
* instead — in one named place that can be read and argued with, rather than
|
|
26
|
+
* inside a pure fold where it would look like a fact.
|
|
27
|
+
*
|
|
28
|
+
* `defining` is short on purpose. A config that MATTERS to a step is almost
|
|
29
|
+
* always named by that step's own command — `npx tsc --noEmit -p
|
|
30
|
+
* packages/cli/tsconfig.json` names its tsconfig, and `judgedBy` establishes
|
|
31
|
+
* that exactly, from two texts. What is left is the handful a command reaches
|
|
32
|
+
* through without ever writing down: `npm run x` and `npx x` both resolve
|
|
33
|
+
* through `package.json`, and a runner reads its own config by convention.
|
|
34
|
+
* Every basename added here widens a claim, so the bar is that the file
|
|
35
|
+
* decides what a gate step DOES.
|
|
36
|
+
*
|
|
37
|
+
* ⚠️ `discovered` IS THE WEAKER TIER AND `judgedBy` KEEPS IT APART FROM THE
|
|
38
|
+
* REST. `npx --no-install jest --ci` names no spec file; jest globs for them,
|
|
39
|
+
* and this repository declares the shape in `jest.config.js` rather than in
|
|
40
|
+
* any gate step. So a changed spec file is a claim from the file NAME, not
|
|
41
|
+
* from a step's declaration, and the sentence a person reads says so. That is
|
|
42
|
+
* "make the weaker claim and say so" — never "the suite is unchanged", which
|
|
43
|
+
* is the one thing no path pattern can establish.
|
|
44
|
+
*/
|
|
45
|
+
exports.GATE_MACHINERY = {
|
|
46
|
+
defining: ['package.json', 'jest.config.js', 'jest.config.ts', 'jest.config.cjs', 'jest.config.mjs'],
|
|
47
|
+
discovered: ['.spec.ts', '.spec.tsx', '.test.ts', '.test.tsx', '.spec.js', '.test.js'],
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* THE ALERT A RUN WRITES WHEN IT HALTS ITSELF (M6-12), immediately before
|
|
51
|
+
* `run.ended`. Every {@link RunHalt} carries a `because`, and until M6-12
|
|
52
|
+
* that sentence reached nothing: `run.ended` carries `stopReason` and totals,
|
|
53
|
+
* the crash path wrote `run-aborted`, the halt path wrote nothing, and the run
|
|
54
|
+
* child has no log. A person's Start placed under the umbrella in a repo
|
|
55
|
+
* without `looply/integration` ended `aborted` after one round and nobody
|
|
56
|
+
* could read why (found live in M6-11). The reason is one sentence, never
|
|
57
|
+
* parsed; `warn` when the halt is an abort, `info` for any other halt.
|
|
58
|
+
*/
|
|
59
|
+
exports.RUN_HALTED_CODE = 'run-halted';
|
|
60
|
+
/**
|
|
61
|
+
* THE ALERT A RUN WRITES WHEN A CHECKER WAS NOT SURE AND NOBODY COULD BE ASKED (M6-20).
|
|
62
|
+
*
|
|
63
|
+
* `proof.on_partial` defaults to `ask`, and nothing in this build can ask: no
|
|
64
|
+
* CLI code produces a `question.asked`, so `looply run`'s asker answers
|
|
65
|
+
* `discard` and says so to a terminal nobody is watching (the audit's F-03).
|
|
66
|
+
* The work a checker was unsure about was therefore thrown away in silence —
|
|
67
|
+
* the item's failure line said "proof partial: …", which reads as the checker
|
|
68
|
+
* having refused, and nothing anywhere said that the configured answer was to
|
|
69
|
+
* ASK and that asking was impossible.
|
|
70
|
+
*
|
|
71
|
+
* ⚠️ ONLY WHEN THE POLICY IS `ask`. A project that configured `discard` chose
|
|
72
|
+
* this; saying it back to them on every partial verdict would be noise. And the
|
|
73
|
+
* run still discards — making it ask is a change to the round protocol and the
|
|
74
|
+
* owner's decision, not this alert's business.
|
|
75
|
+
*/
|
|
76
|
+
exports.PARTIAL_DISCARDED_CODE = 'proof-partial-discarded';
|
|
77
|
+
/** What a partial proof discarded for want of anyone to ask says, in one sentence. */
|
|
78
|
+
function partialDiscardedText(itemTitle, reason) {
|
|
79
|
+
return (`the checker was not sure about "${itemTitle}" — ${reason} — and nothing on this machine can be asked, ` +
|
|
80
|
+
'so the work was discarded (proof.on_partial: ask)');
|
|
81
|
+
}
|
|
82
|
+
class RunnerError extends Error {
|
|
83
|
+
}
|
|
84
|
+
exports.RunnerError = RunnerError;
|
|
85
|
+
/** How often a paused run re-reads control.json. Overridable via `pausePollMs`. */
|
|
86
|
+
exports.DEFAULT_PAUSE_POLL_MS = 5_000;
|
|
87
|
+
class RoundRunner {
|
|
88
|
+
cfg;
|
|
89
|
+
deps;
|
|
90
|
+
id;
|
|
91
|
+
/**
|
|
92
|
+
* `proverRejected: 0` is set HERE rather than left absent, and the difference
|
|
93
|
+
* matters: this runner always counts, so a run in which the prover rejected
|
|
94
|
+
* nothing reports a PROVEN zero. Absent stays reserved for a producer that
|
|
95
|
+
* did not count at all — see {@link RunTotals.proverRejected}.
|
|
96
|
+
*/
|
|
97
|
+
totals = { rounds: 0, committed: 0, discarded: 0, abandoned: 0, proverRejected: 0 };
|
|
98
|
+
/** The most recent deterministic failure; see {@link RunSummary.lastFailure}. */
|
|
99
|
+
lastFailure;
|
|
100
|
+
/**
|
|
101
|
+
* The last rejection of each item IN THIS RUN, kept for the repair attempt.
|
|
102
|
+
* In memory on purpose: the durable copy is the evidence directory, and a
|
|
103
|
+
* later run that picks the item up again reads `lastFailure` off the item —
|
|
104
|
+
* it does not restore a candidate from a run it did not see rejected.
|
|
105
|
+
*/
|
|
106
|
+
rejections = new Map();
|
|
107
|
+
/**
|
|
108
|
+
* Set only on the path that ends the loop through {@link RoundRunnerDeps.haltRequested},
|
|
109
|
+
* and read only when writing `run.ended`. Held rather than threaded because
|
|
110
|
+
* `loop()` returns a stop reason and nothing else — widening that return to
|
|
111
|
+
* carry a payload only one of its eight answers can have would put the
|
|
112
|
+
* exception in the type every other caller reads.
|
|
113
|
+
*/
|
|
114
|
+
halt = null;
|
|
115
|
+
/** F29: the commit this run was cut from, set once, before the first round. */
|
|
116
|
+
cutBase = '';
|
|
117
|
+
/**
|
|
118
|
+
* Consecutive rounds whose gate could not run. Reset by any gate that reached
|
|
119
|
+
* a verdict, so it counts a machine that is broken now rather than one that
|
|
120
|
+
* was ever briefly unhealthy.
|
|
121
|
+
*/
|
|
122
|
+
consecutiveBlocked = 0;
|
|
123
|
+
/**
|
|
124
|
+
* Whether the round now running ended blocked.
|
|
125
|
+
*
|
|
126
|
+
* ⚠️ THE RESET LIVES IN THE LOOP, NOT IN THE ROUND, and that is the whole
|
|
127
|
+
* point of this second field. `runRound` has six exits before the gate — an
|
|
128
|
+
* implementer throw, `abandoned`, a railed path, a no-op, an external halt —
|
|
129
|
+
* and a reset placed anywhere inside it is a reset those exits skip. The
|
|
130
|
+
* counter would then read three blocked gates with ordinary rounds between
|
|
131
|
+
* them as "three in a row", and the halt sentence would be a false statement
|
|
132
|
+
* about the machine.
|
|
133
|
+
*/
|
|
134
|
+
blockedThisRound = false;
|
|
135
|
+
/** See {@link RunSummary.metUnavailableEnvironment}. */
|
|
136
|
+
metUnavailableEnvironment = false;
|
|
137
|
+
constructor(cfg, deps, id) {
|
|
138
|
+
this.cfg = cfg;
|
|
139
|
+
this.deps = deps;
|
|
140
|
+
this.id = id;
|
|
141
|
+
// Wiring mismatches surface here, not at hour three of an overnight run:
|
|
142
|
+
// proof enabled with no prover would discard (or worse, commit) every round,
|
|
143
|
+
// and `on_partial: ask` with nobody to ask has no honest fallback.
|
|
144
|
+
if (cfg.proof.enabled && !deps.prove) {
|
|
145
|
+
throw new RunnerError('proof.enabled is true but no prover is wired');
|
|
146
|
+
}
|
|
147
|
+
if (cfg.proof.enabled && cfg.proof.onPartial === 'ask' && !deps.resolvePartial) {
|
|
148
|
+
throw new RunnerError("proof.on_partial is 'ask' but nothing is wired to ask");
|
|
149
|
+
}
|
|
150
|
+
if (deps.exemptionsFor && !deps.spendExemption) {
|
|
151
|
+
throw new RunnerError('exemptionsFor is wired but no spendExemption sink — grants would never be spent');
|
|
152
|
+
}
|
|
153
|
+
if (deps.reverify && !deps.evidence) {
|
|
154
|
+
// §12 case 19's second trigger depends on the discard invalidating the
|
|
155
|
+
// cache. Without it a discarded round leaves a `hit` behind, and the next
|
|
156
|
+
// pick would reuse the exact evidence that just led into a wall.
|
|
157
|
+
throw new RunnerError('reverify is wired but no evidence.invalidate — a discard would leave stale evidence hittable');
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
async run() {
|
|
161
|
+
// Set here rather than as a field initialiser: `deps` is a constructor
|
|
162
|
+
// parameter property and is not initialised when field initialisers run.
|
|
163
|
+
if (this.deps.perRoundSafety === true)
|
|
164
|
+
this.roundFacts = [];
|
|
165
|
+
const dirty = await this.deps.git.changedPaths();
|
|
166
|
+
if (dirty.length > 0) {
|
|
167
|
+
// §12 case 1: a dirty tree at start would fold uncommitted human work into
|
|
168
|
+
// round 1's diff — and the first discard would destroy it.
|
|
169
|
+
throw new RunnerError(`working tree has ${dirty.length} changed path(s) — refusing to start (commit or stash first)`);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* WHERE THIS RUN STARTS, WHEN SOMETHING HAS AN OPINION ABOUT IT.
|
|
173
|
+
*
|
|
174
|
+
* ⚠️ THE RUNNER STILL KNOWS NOTHING ABOUT INTEGRATION. It asks the
|
|
175
|
+
* composition root for a start point and is handed one or not — the same
|
|
176
|
+
* arrangement as every other seam here. Absent means "wherever HEAD is",
|
|
177
|
+
* which is what every run has always done.
|
|
178
|
+
*/
|
|
179
|
+
const cutFromRef = (await this.deps.integrationBase?.()) ?? undefined;
|
|
180
|
+
await this.deps.git.createBranch(this.id.branch, cutFromRef);
|
|
181
|
+
const baseSha = await this.deps.git.headSha();
|
|
182
|
+
this.cutBase = baseSha;
|
|
183
|
+
if (cutFromRef !== undefined) {
|
|
184
|
+
// Both halves recorded on the ledger the run already owns: what was
|
|
185
|
+
// chosen, and what the run actually got.
|
|
186
|
+
await this.emit('alert', {
|
|
187
|
+
code: 'run_cut_from',
|
|
188
|
+
severity: 'info',
|
|
189
|
+
text: `cut from ${cutFromRef} at ${baseSha}`,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
const startedAt = this.deps.now();
|
|
193
|
+
await this.emit('run.started', {
|
|
194
|
+
// Spread FIRST, on purpose. Whatever the composition root supplied is
|
|
195
|
+
// overwritten by the six authoritative values below, so context can add to
|
|
196
|
+
// the header but can never lie about the run's own identity.
|
|
197
|
+
...this.deps.runContext,
|
|
198
|
+
runId: this.id.runId,
|
|
199
|
+
branch: this.id.branch,
|
|
200
|
+
baseSha,
|
|
201
|
+
minHours: this.cfg.run.minHours,
|
|
202
|
+
maxRounds: this.cfg.run.maxRounds,
|
|
203
|
+
lane: this.id.lane,
|
|
204
|
+
});
|
|
205
|
+
await this.deps.notifier?.runStarted({ minHours: this.cfg.run.minHours, baseSha });
|
|
206
|
+
// A control request belongs to the run it was aimed at. Clearing it here
|
|
207
|
+
// means last night's pause cannot wedge tonight's run before its first
|
|
208
|
+
// round — a hold nobody remembers setting is indistinguishable from a hang.
|
|
209
|
+
//
|
|
210
|
+
// ONLY WHEN BEGINNING THIS RUN WAS ITSELF A PERSON SAYING GO. That was the
|
|
211
|
+
// stated justification and it was applied unconditionally, which was
|
|
212
|
+
// correct while every run began with a tap. A run this machine decided to
|
|
213
|
+
// begin has no such authority: erasing a pause a person set would be the
|
|
214
|
+
// machine overruling them with an act they never performed.
|
|
215
|
+
if (this.deps.consumeControlOnStart !== false)
|
|
216
|
+
await this.deps.control?.consume();
|
|
217
|
+
let stopReason;
|
|
218
|
+
try {
|
|
219
|
+
stopReason = await this.loop(startedAt);
|
|
220
|
+
}
|
|
221
|
+
catch (e) {
|
|
222
|
+
// The loop threw where nothing caught it — a git failure, a ledger write
|
|
223
|
+
// that could not land, a queue write that lost its retry. Before this the
|
|
224
|
+
// run simply stopped: no `run.ended`, so `status.json` said `live` for
|
|
225
|
+
// ever, the phone showed a run that never finished, and the server's Run
|
|
226
|
+
// row never closed.
|
|
227
|
+
//
|
|
228
|
+
// `aborted` rather than any ordinary reason, because this run did not end
|
|
229
|
+
// its own loop. The totals are still exact — this process counted them —
|
|
230
|
+
// which is what separates it from the `crashed` a later process writes on
|
|
231
|
+
// a dead run's behalf.
|
|
232
|
+
//
|
|
233
|
+
// Emitting is best-effort and deliberately swallowed: if the ledger is
|
|
234
|
+
// what threw, trying to write the ending would replace the real error
|
|
235
|
+
// with a confusing one.
|
|
236
|
+
const elapsedMs = this.deps.now() - startedAt;
|
|
237
|
+
try {
|
|
238
|
+
await this.emit('alert', {
|
|
239
|
+
code: 'run-aborted',
|
|
240
|
+
severity: 'critical',
|
|
241
|
+
text: `the run stopped on an error: ${e.message}`,
|
|
242
|
+
});
|
|
243
|
+
await this.emit('run.ended', {
|
|
244
|
+
runId: this.id.runId,
|
|
245
|
+
elapsedMs,
|
|
246
|
+
stopReason: 'aborted',
|
|
247
|
+
totals: this.totals,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
/* the ledger is the likeliest thing to have thrown; keep the original */
|
|
252
|
+
}
|
|
253
|
+
throw e;
|
|
254
|
+
}
|
|
255
|
+
const elapsedMs = this.deps.now() - startedAt;
|
|
256
|
+
/**
|
|
257
|
+
* A RUN THAT HALTS ITSELF SAYS WHY (M6-12) — one `alert` immediately
|
|
258
|
+
* before `run.ended`, carrying the halt's own sentence, exactly as the
|
|
259
|
+
* crash path above writes `run-aborted`. Only a halt: an ordinary ending
|
|
260
|
+
* (`command`, `max-rounds`, `queue-empty`, …) has nothing to explain.
|
|
261
|
+
* Best-effort like the crash path's: a ledger that refuses the alert must
|
|
262
|
+
* not stop the ending being written — and if the ledger is dead, the
|
|
263
|
+
* `run.ended` below says so by throwing where it always has.
|
|
264
|
+
*/
|
|
265
|
+
if (this.halt !== null && this.halt.because.length > 0) {
|
|
266
|
+
try {
|
|
267
|
+
await this.emit('alert', {
|
|
268
|
+
code: exports.RUN_HALTED_CODE,
|
|
269
|
+
severity: this.halt.stopReason === 'aborted' ? 'warn' : 'info',
|
|
270
|
+
text: this.halt.because,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
/* the ending below is the record; the alert was the courtesy */
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
await this.emit('run.ended', {
|
|
278
|
+
// Spread FIRST, for the reason `run.started` spreads `runContext` first:
|
|
279
|
+
// the caller may add attribution to the ending and can never restate the
|
|
280
|
+
// run's own facts, because the four below are written over whatever it
|
|
281
|
+
// sent.
|
|
282
|
+
...this.halt?.endedContext,
|
|
283
|
+
runId: this.id.runId,
|
|
284
|
+
elapsedMs,
|
|
285
|
+
stopReason,
|
|
286
|
+
totals: this.totals,
|
|
287
|
+
});
|
|
288
|
+
await this.deps.notifier?.runEnded({ totals: this.totals, elapsedMs, branch: this.id.branch, stopReason });
|
|
289
|
+
const summary = { stopReason, totals: this.totals, elapsedMs };
|
|
290
|
+
if (this.roundFacts !== null)
|
|
291
|
+
summary.rounds = this.roundFacts;
|
|
292
|
+
if (this.refusedBeforeFirstRound !== null)
|
|
293
|
+
summary.refusedBeforeFirstRound = this.refusedBeforeFirstRound;
|
|
294
|
+
if (this.surfaceViolation !== null)
|
|
295
|
+
summary.surfaceViolation = this.surfaceViolation;
|
|
296
|
+
if (this.metUnavailableEnvironment)
|
|
297
|
+
summary.metUnavailableEnvironment = true;
|
|
298
|
+
// Only when there was one: an absent field says "no deterministic failure",
|
|
299
|
+
// which is different from "a failure nobody could identify".
|
|
300
|
+
if (this.lastFailure !== undefined)
|
|
301
|
+
summary.lastFailure = this.lastFailure;
|
|
302
|
+
if (this.failedAtThisRound !== null)
|
|
303
|
+
summary.lastFailedAt = this.failedAtThisRound;
|
|
304
|
+
return summary;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* One record per round, or null when nothing asked for them.
|
|
308
|
+
*
|
|
309
|
+
* ⚠️ NULL RATHER THAN EMPTY, so "nobody asked" and "no rounds happened" stay
|
|
310
|
+
* different answers. A caller handed `[]` cannot tell which it got.
|
|
311
|
+
*/
|
|
312
|
+
roundFacts = null;
|
|
313
|
+
/** Why `beforeFirstRound` said no, so `run()` can put it on the summary. */
|
|
314
|
+
refusedBeforeFirstRound = null;
|
|
315
|
+
/** Set when a round wrote outside what this run declared. */
|
|
316
|
+
surfaceViolation = null;
|
|
317
|
+
/**
|
|
318
|
+
* Where THIS round failed. Reset at the top of every round, like
|
|
319
|
+
* `blockedThisRound` — a latched value would sign round N+1 with round N's
|
|
320
|
+
* failure, which is the shape that makes a detector fire on one real failure
|
|
321
|
+
* repeated by the recorder.
|
|
322
|
+
*/
|
|
323
|
+
failedAtThisRound = null;
|
|
324
|
+
/** Gate steps that passed in THIS round. Reset with everything else per-round. */
|
|
325
|
+
passingGatesThisRound = [];
|
|
326
|
+
/**
|
|
327
|
+
* The implementer's sentence about the round in flight, on its way to the
|
|
328
|
+
* ledger (DESIGN §9, `round.ended.note`).
|
|
329
|
+
*
|
|
330
|
+
* HELD ON THE RUNNER RATHER THAN THREADED THROUGH, because every one of the
|
|
331
|
+
* seven ways a round can end goes through `endRound` and the note belongs on
|
|
332
|
+
* all of them — most of all on the ones that threw the work away, where it is
|
|
333
|
+
* the only record of what was attempted. A parameter would have to be added to
|
|
334
|
+
* each exit and the next exit somebody writes would silently omit it.
|
|
335
|
+
*
|
|
336
|
+
* ⚠️ CLEARED AT THE TOP OF EVERY ROUND. A stale note is worse than none: it
|
|
337
|
+
* would attribute one round's account of itself to the next round's outcome.
|
|
338
|
+
*/
|
|
339
|
+
roundNote;
|
|
340
|
+
/** What this round’s repair was, for `round.ended`. Reset with {@link roundNote}. */
|
|
341
|
+
roundRepair;
|
|
342
|
+
/**
|
|
343
|
+
* WHAT THIS ROUND DID, FROM THE DIFFERENCE THE ROUND MADE.
|
|
344
|
+
*
|
|
345
|
+
* 🔴 THREE FIELDS ARE RUN-LEVEL ROLLUPS AND MUST NOT BE READ DIRECTLY.
|
|
346
|
+
*
|
|
347
|
+
* `this.lastFailure` is assigned when a failure happens and NEVER cleared, so
|
|
348
|
+
* reading it per round signs round N+1's record with round N's failure — the
|
|
349
|
+
* exact shape that would make `repeated_failure` fire on ONE real failure
|
|
350
|
+
* repeated by the recorder rather than by the machine. Same for
|
|
351
|
+
* `metUnavailableEnvironment`, which latches true for the rest of the run,
|
|
352
|
+
* and `totals.committed`, which only grows. The environment flag is read
|
|
353
|
+
* from `blockedThisRound`, which IS per-round, rather than compared.
|
|
354
|
+
*
|
|
355
|
+
* So each is compared against its value BEFORE the round: what changed during
|
|
356
|
+
* this round is this round's, and what did not is not.
|
|
357
|
+
*/
|
|
358
|
+
async recordRoundFact(round, committedBefore, failureBefore) {
|
|
359
|
+
if (this.roundFacts === null)
|
|
360
|
+
return;
|
|
361
|
+
const worked = this.totals.committed > committedBefore;
|
|
362
|
+
/**
|
|
363
|
+
* ⚠️ AND THE TREE IS ASKED FOR ONCE PER ROUND, NOT DERIVED. `oscillation`
|
|
364
|
+
* compares four consecutive fingerprints and returns null if any is null,
|
|
365
|
+
* so a fingerprint this cannot obtain switches the detector off rather than
|
|
366
|
+
* weakening it — which is why a failure here is recorded as null and the
|
|
367
|
+
* caller is left to see a run whose rounds carry no tree.
|
|
368
|
+
*/
|
|
369
|
+
const treeFingerprint = this.deps.git.headTreeSha === undefined ? null : await this.deps.git.headTreeSha().catch(() => null);
|
|
370
|
+
this.roundFacts.push({
|
|
371
|
+
round,
|
|
372
|
+
worked,
|
|
373
|
+
failedAt: worked ? null : this.failedAtThisRound,
|
|
374
|
+
passingGates: this.passingGatesThisRound,
|
|
375
|
+
treeFingerprint,
|
|
376
|
+
failure: this.lastFailure !== failureBefore ? (this.lastFailure ?? null) : null,
|
|
377
|
+
/**
|
|
378
|
+
* 🔴 `blockedThisRound`, NOT THE LATCHED RUN-LEVEL FLAG.
|
|
379
|
+
*
|
|
380
|
+
* This was `this.metUnavailableEnvironment && !metBefore`, which is true
|
|
381
|
+
* for the FIRST blocked round and false for every one after it, because
|
|
382
|
+
* the run-level flag latches and never clears. MEASURED: with containment
|
|
383
|
+
* broken from round 2 onward, rounds 3 and 4 were recorded as ordinary
|
|
384
|
+
* `gate_failed` attempts — two observations per run asserting the MODEL
|
|
385
|
+
* achieved nothing, for a fault in the MACHINE. That is precisely the
|
|
386
|
+
* poisoning the `blocked` verdict exists to prevent.
|
|
387
|
+
*
|
|
388
|
+
* `blockedThisRound` is reset at the top of every round, so it answers
|
|
389
|
+
* about this round and nothing else.
|
|
390
|
+
*/
|
|
391
|
+
metUnavailableEnvironment: this.blockedThisRound,
|
|
392
|
+
work: this.deps.queue.list(),
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
/** The round loop itself, so `run()` can put an honest ending on a throw. */
|
|
396
|
+
async loop(startedAt) {
|
|
397
|
+
for (let round = 1;; round++) {
|
|
398
|
+
// A signal is checked first: when a human killed the run, `run.ended`
|
|
399
|
+
// must say `signal`, not claim a budget limit happened to expire at the
|
|
400
|
+
// same moment (§12 case 11). Like the budget checks below it sits between
|
|
401
|
+
// rounds, so the in-flight round finishes before the run winds down.
|
|
402
|
+
if (this.deps.stopRequested?.())
|
|
403
|
+
return 'signal';
|
|
404
|
+
// AND BEFORE THE PICK, not only after the model turn. `pickNext` can
|
|
405
|
+
// route an item back through verification, which is a second full model
|
|
406
|
+
// invocation — so a halt armed by anything other than this loop's own
|
|
407
|
+
// last round would otherwise pay for one more before noticing. Nothing
|
|
408
|
+
// arms it that way today; the fence is here because the day something
|
|
409
|
+
// does, this is where the cost would land.
|
|
410
|
+
const armed = this.deps.haltRequested?.() ?? null;
|
|
411
|
+
if (armed !== null) {
|
|
412
|
+
this.halt = armed;
|
|
413
|
+
return armed.stopReason;
|
|
414
|
+
}
|
|
415
|
+
// The phone's pause/stop, read from control.json. It sits next to the
|
|
416
|
+
// signal check and for the same reason: both are a human's intent, and
|
|
417
|
+
// `run.ended` must say `command` rather than name whichever budget limit
|
|
418
|
+
// happened to expire at the same moment. Being here also means the
|
|
419
|
+
// in-flight round finishes first, exactly like a budget limit.
|
|
420
|
+
const control = await this.awaitControl(startedAt);
|
|
421
|
+
if (control !== 'continue')
|
|
422
|
+
return control;
|
|
423
|
+
// Budget checks sit between rounds, never inside one: a limit reached
|
|
424
|
+
// mid-round finishes that round first (§12 case 10).
|
|
425
|
+
if (this.totals.rounds >= this.cfg.run.maxRounds)
|
|
426
|
+
return 'max-rounds';
|
|
427
|
+
if (this.deps.now() - startedAt >= this.cfg.run.minHours * 3_600_000)
|
|
428
|
+
return 'min-hours';
|
|
429
|
+
const item = await this.pickNext();
|
|
430
|
+
if (item === undefined)
|
|
431
|
+
return 'queue-empty';
|
|
432
|
+
/**
|
|
433
|
+
* ⚠️ AFTER `pickNext`, NOT BEFORE. An empty work set ends the run at zero
|
|
434
|
+
* rounds, and a caller that had already spent something by then spent it
|
|
435
|
+
* on nothing. This is the first line at which a round is certain.
|
|
436
|
+
*/
|
|
437
|
+
if (round === 1 && this.deps.beforeFirstRound !== undefined) {
|
|
438
|
+
const may = await this.deps.beforeFirstRound();
|
|
439
|
+
if (!may.ok) {
|
|
440
|
+
this.refusedBeforeFirstRound = may.because ?? 'the first round was refused';
|
|
441
|
+
// A reasoned halt like any other: `run()` says why before it ends.
|
|
442
|
+
this.halt = { stopReason: 'aborted', because: this.refusedBeforeFirstRound };
|
|
443
|
+
return 'aborted';
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
this.blockedThisRound = false;
|
|
447
|
+
this.failedAtThisRound = null;
|
|
448
|
+
this.passingGatesThisRound = [];
|
|
449
|
+
const committedBefore = this.totals.committed;
|
|
450
|
+
const failureBefore = this.lastFailure;
|
|
451
|
+
const ended = await this.runRound(round, item);
|
|
452
|
+
// ⚠️ BEFORE `consecutiveBlocked` IS RESET, because `blockedThisRound` is
|
|
453
|
+
// what the record reads and the line below does not touch it.
|
|
454
|
+
await this.recordRoundFact(round, committedBefore, failureBefore);
|
|
455
|
+
/**
|
|
456
|
+
* A round that wrote outside its lease ends the RUN, not just the round.
|
|
457
|
+
* The round itself was already discarded where the trespass was seen —
|
|
458
|
+
* before the gate, before any commit. See `runRound`.
|
|
459
|
+
*/
|
|
460
|
+
if (this.surfaceViolation !== null) {
|
|
461
|
+
// The same facts the item's own failure line carries, so the ending
|
|
462
|
+
// and the item say one thing; `run()` writes it before `run.ended`.
|
|
463
|
+
const { paths, declared } = this.surfaceViolation;
|
|
464
|
+
this.halt = {
|
|
465
|
+
stopReason: 'aborted',
|
|
466
|
+
because: `a round wrote outside the surfaces this run declared (${declared.join(', ')}): ` +
|
|
467
|
+
`${paths.slice(0, 3).join(', ')}${paths.length > 3 ? ', …' : ''} — the run ends here`,
|
|
468
|
+
};
|
|
469
|
+
return 'aborted';
|
|
470
|
+
}
|
|
471
|
+
if (!this.blockedThisRound)
|
|
472
|
+
this.consecutiveBlocked = 0;
|
|
473
|
+
this.totals.rounds += 1;
|
|
474
|
+
const soFar = this.deps.now() - startedAt;
|
|
475
|
+
await this.deps.notifier?.roundFinished({
|
|
476
|
+
outcome: ended.outcome,
|
|
477
|
+
itemTitle: ended.title,
|
|
478
|
+
elapsedMs: soFar,
|
|
479
|
+
remainingMs: Math.max(0, this.cfg.run.minHours * 3_600_000 - soFar),
|
|
480
|
+
});
|
|
481
|
+
// AFTER the round is counted and reported, and BEFORE the cooldown. The
|
|
482
|
+
// round genuinely happened and belongs in the totals; the pause exists to
|
|
483
|
+
// space out the next one, and there is no next one.
|
|
484
|
+
if (ended.halt) {
|
|
485
|
+
this.halt = ended.halt;
|
|
486
|
+
return ended.halt.stopReason;
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* ⚠️ AFTER THE ROUND IS COUNTED AND REPORTED. The round genuinely
|
|
490
|
+
* happened and belongs in the totals; what has changed is that there
|
|
491
|
+
* should not be another one.
|
|
492
|
+
*/
|
|
493
|
+
if (this.deps.baseStale !== undefined) {
|
|
494
|
+
const because = await this.deps.baseStale(this.cutBase);
|
|
495
|
+
if (because !== null) {
|
|
496
|
+
this.halt = { stopReason: 'aborted', because };
|
|
497
|
+
return 'aborted';
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
if (this.cfg.run.cooldownS > 0 && this.deps.sleep) {
|
|
501
|
+
await this.deps.sleep(this.cfg.run.cooldownS * 1000);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Consult the control file, holding here for as long as it says `pause`.
|
|
507
|
+
*
|
|
508
|
+
* Returns `continue` to start the next round, or the reason the run should
|
|
509
|
+
* wind down. `stop` ends the run; `pause` keeps it alive on its branch with
|
|
510
|
+
* its queue intact, so resuming continues the same run instead of paying for
|
|
511
|
+
* a fresh one — that is the whole difference between the two.
|
|
512
|
+
*/
|
|
513
|
+
async awaitControl(startedAt) {
|
|
514
|
+
const control = this.deps.control;
|
|
515
|
+
if (!control)
|
|
516
|
+
return 'continue';
|
|
517
|
+
let announced = false;
|
|
518
|
+
for (;;) {
|
|
519
|
+
const req = await control.read();
|
|
520
|
+
if (req === null || req.kind === 'resume') {
|
|
521
|
+
// Only when this run actually held. `resume` is also the cleared state
|
|
522
|
+
// of the control file, so it is the ordinary reading between rounds —
|
|
523
|
+
// announcing it unconditionally would put a line in the ledger for
|
|
524
|
+
// every round of every run, into an alerts array nothing trims.
|
|
525
|
+
if (announced) {
|
|
526
|
+
await this.emit('alert', {
|
|
527
|
+
code: 'run-resumed',
|
|
528
|
+
severity: 'info',
|
|
529
|
+
text: `resumed${req?.actor ? ` by ${req.actor}` : ''} — picking up the next round`,
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
return 'continue';
|
|
533
|
+
}
|
|
534
|
+
if (req.kind === 'stop') {
|
|
535
|
+
/**
|
|
536
|
+
* NOT CONSUMED — BY ANY RUN (M6-14). A person's own run used to clear
|
|
537
|
+
* the file the instant it acted on it, and the reason given was that a
|
|
538
|
+
* stop left on disk would end the person's next `looply run` before
|
|
539
|
+
* its first round — which stopped being true the day a person's run
|
|
540
|
+
* began consuming the file on START (`consumeControlOnStart`, above):
|
|
541
|
+
* their next Start clears it before its first poll. What the clear
|
|
542
|
+
* still did was speak for them to the autonomy tick. Found live
|
|
543
|
+
* (M6-13): the stand-in's stop ended the placed run, the run wrote
|
|
544
|
+
* `resume` over it, and the next tick found no hold and reserved a
|
|
545
|
+
* Start for the pursuit it was still permitted; only the cancel's timing
|
|
546
|
+
* prevented a machine run.
|
|
547
|
+
*
|
|
548
|
+
* An autonomous run never consumed it (nobody is standing there, and
|
|
549
|
+
* wiping the instruction is how a tap meant to stop the night came to
|
|
550
|
+
* stop one run out of eight). Now no run does. The run still stops;
|
|
551
|
+
* the instruction stays where the person left it, for the tick to
|
|
552
|
+
* hold on, until a person's own Start or resume says otherwise.
|
|
553
|
+
*/
|
|
554
|
+
return 'command';
|
|
555
|
+
}
|
|
556
|
+
// A hold that cannot sleep is a spin, so a runner with no sleep wired
|
|
557
|
+
// treats pause as the end of the run rather than burning a core.
|
|
558
|
+
if (!this.deps.sleep)
|
|
559
|
+
return 'command';
|
|
560
|
+
if (!announced) {
|
|
561
|
+
announced = true;
|
|
562
|
+
// There is no pause event in the schema, and a run that simply stops
|
|
563
|
+
// emitting looks identical to a dead one from the phone. This is the
|
|
564
|
+
// difference between "paused" and "hung".
|
|
565
|
+
await this.emit('alert', {
|
|
566
|
+
code: 'run-paused',
|
|
567
|
+
severity: 'info',
|
|
568
|
+
text: `paused${req.actor ? ` by ${req.actor}` : ''} — resume from the app to continue this run`,
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
// A paused run still answers Ctrl-C and still ends at its budget: a hold
|
|
572
|
+
// only the phone can lift is a wedged process the moment the phone is flat.
|
|
573
|
+
if (this.deps.stopRequested?.())
|
|
574
|
+
return 'signal';
|
|
575
|
+
if (this.deps.now() - startedAt >= this.cfg.run.minHours * 3_600_000)
|
|
576
|
+
return 'min-hours';
|
|
577
|
+
await this.deps.sleep(this.deps.pausePollMs ?? exports.DEFAULT_PAUSE_POLL_MS);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
/** Topmost pickable item. Order IS priority (core `Item`), so the sort is the pick. */
|
|
581
|
+
async topPickable() {
|
|
582
|
+
/**
|
|
583
|
+
* `pickableItems` replaces the bare `isPickable` filter and does the same job
|
|
584
|
+
* plus one more: an item whose prerequisites are not satisfied never reaches
|
|
585
|
+
* the sort. That ordering is the enforcement — a priority cannot promote an
|
|
586
|
+
* item past an unmet prerequisite, because such an item is not in the list.
|
|
587
|
+
*
|
|
588
|
+
* ⚠️ THE SORT IS UNCHANGED. An item that names no prerequisites, which is
|
|
589
|
+
* every item written before `after` existed, is picked exactly as before.
|
|
590
|
+
*
|
|
591
|
+
* The same sentence now covers an item's cross-repo needs: one that declares
|
|
592
|
+
* none is picked exactly as before, and one that declares an unmet need is
|
|
593
|
+
* not in the list. A blocked sibling therefore delays nothing but the work
|
|
594
|
+
* that actually depends on it.
|
|
595
|
+
*/
|
|
596
|
+
return (0, core_1.pickableItems)(this.deps.queue.list(), await this.deps.crossRepo?.()).sort((a, b) => a.orderKey < b.orderKey ? -1 : a.orderKey > b.orderKey ? 1 : 0)[0];
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* The pick, routed through the re-verification triggers (§12 case 19): the
|
|
600
|
+
* topmost item is handed to a round only when `decideReverify` judges its
|
|
601
|
+
* cached evidence fresh. A `reverify` decision — HEAD moved past the threshold,
|
|
602
|
+
* or no cache entry at all, which is the post-discard and never-verified state
|
|
603
|
+
* — sends the item back through verification instead, and the queue is re-read
|
|
604
|
+
* because the verdict may have routed the item out of `ready`, promoting the
|
|
605
|
+
* next one.
|
|
606
|
+
*/
|
|
607
|
+
async pickNext() {
|
|
608
|
+
const verified = new Set();
|
|
609
|
+
for (;;) {
|
|
610
|
+
const top = await this.topPickable();
|
|
611
|
+
if (top === undefined || this.deps.reverify === undefined)
|
|
612
|
+
return top;
|
|
613
|
+
// Already verified within this pick: the premise is current even when the
|
|
614
|
+
// verdict carried nothing to cache (an injected verifier is not forced
|
|
615
|
+
// through the parser's evidence check) — asking again would loop forever
|
|
616
|
+
// on the same answer.
|
|
617
|
+
if (verified.has(top.id))
|
|
618
|
+
return top;
|
|
619
|
+
const head = await this.deps.git.headSha();
|
|
620
|
+
const decision = await (0, reverify_1.decideReverify)(this.deps.reverify.cache.lookup(top.id, head), head, this.deps.reverify.countCommitsTouching);
|
|
621
|
+
if (decision.kind === 'reuse')
|
|
622
|
+
return top;
|
|
623
|
+
verified.add(top.id);
|
|
624
|
+
await this.reverifyItem(top, head);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Send an item back through verification and apply the verdict to the queue
|
|
629
|
+
* (§10, §12 cases 17–19). Confirmed/amended refills the evidence cache at the
|
|
630
|
+
* HEAD the verifier judged against, so the next lookup is a hit; every other
|
|
631
|
+
* verdict routes the item out of `ready` and the pick moves on.
|
|
632
|
+
*/
|
|
633
|
+
async reverifyItem(picked, head) {
|
|
634
|
+
const routing = this.deps.reverify;
|
|
635
|
+
if (routing === undefined)
|
|
636
|
+
throw new RunnerError('reverifyItem called without reverify wired');
|
|
637
|
+
const item = await this.patchItem(picked.id, { state: 'verifying' });
|
|
638
|
+
const outcome = await routing.verify(item);
|
|
639
|
+
// Shared with the verification pass over a scan's drafts (verify-apply.ts),
|
|
640
|
+
// so an item becoming pickable never depends on which route reached it.
|
|
641
|
+
const decided = (0, verify_apply_1.applyVerifyVerdict)(item, outcome, (id) => this.deps.queue.get(id));
|
|
642
|
+
await this.emit('item.verified', { itemId: item.id, ...decided.outcome });
|
|
643
|
+
await this.patchItem(item.id, decided.patch);
|
|
644
|
+
if (decided.cacheEvidence) {
|
|
645
|
+
await routing.cache.put(item.id, head, decided.cacheEvidence);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Returns how the round ended plus the item's title, for the notifier — and
|
|
650
|
+
* the halt, when this round is also the last one.
|
|
651
|
+
*/
|
|
652
|
+
async runRound(round, picked) {
|
|
653
|
+
const attempt = picked.attempts + 1;
|
|
654
|
+
let state = 'picked';
|
|
655
|
+
// Before anything can end this round. See {@link roundNote}.
|
|
656
|
+
this.roundNote = undefined;
|
|
657
|
+
this.roundRepair = undefined;
|
|
658
|
+
await this.emit('round.picked', { round, itemId: picked.id, attempt, title: picked.title });
|
|
659
|
+
const item = await this.patchItem(picked.id, { state: 'in_progress' });
|
|
660
|
+
state = (0, core_1.transition)(state, 'implementing');
|
|
661
|
+
await this.emit('round.phase', { round, phase: 'implementing' });
|
|
662
|
+
/**
|
|
663
|
+
* BEFORE THE PAID TURN, AND AFTER THE TRANSITION TO `implementing`.
|
|
664
|
+
*
|
|
665
|
+
* Both halves of that position are load-bearing. Before, because the whole
|
|
666
|
+
* point is not to buy a model turn a broken machine cannot use. After,
|
|
667
|
+
* because `TRANSITIONS` allows `implementing → discarded` and does NOT
|
|
668
|
+
* allow `picked → discarded`; moving this one line up throws
|
|
669
|
+
* `RoundTransitionError` and takes the run down.
|
|
670
|
+
*
|
|
671
|
+
* The blocked machinery is reused verbatim rather than re-decided: no
|
|
672
|
+
* attempt spent, item back to `ready` rather than `closed`, no failure
|
|
673
|
+
* signature, no `gate.result` and so no false `gate_failed`, the same bound
|
|
674
|
+
* of three, and `metUnavailableEnvironment` reaching the unchanged exclusion in
|
|
675
|
+
* `run-cmd.ts`.
|
|
676
|
+
*/
|
|
677
|
+
/**
|
|
678
|
+
* WRAPPED, because this was the ONE un-try'd await in `runRound` — sitting
|
|
679
|
+
* directly above the `implement` call that IS wrapped, for the reason the
|
|
680
|
+
* comment there gives. The preflight closure can throw: `containedRoot()`
|
|
681
|
+
* does an unguarded `mkdirSync`, and the backend's own `mkdtempSync` sits
|
|
682
|
+
* outside its `try`. A full or locked `%TEMP%` would therefore take the run
|
|
683
|
+
* down with no `run.ended`, strand the item `in_progress`, and let recovery
|
|
684
|
+
* spend an attempt — the exact inversion of what this check is for.
|
|
685
|
+
*/
|
|
686
|
+
let ready;
|
|
687
|
+
try {
|
|
688
|
+
ready = await this.deps.environmentReady?.();
|
|
689
|
+
}
|
|
690
|
+
catch (err) {
|
|
691
|
+
ready = {
|
|
692
|
+
ok: false,
|
|
693
|
+
unavailable: true,
|
|
694
|
+
timedOut: false,
|
|
695
|
+
ms: 0,
|
|
696
|
+
output: `the environment check itself failed: ${err.message}`,
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
if (ready?.unavailable === true) {
|
|
700
|
+
const why = 'round blocked: a container could not be made — the environment is unavailable, ' +
|
|
701
|
+
'not the change. No attempt was spent';
|
|
702
|
+
this.blockedThisRound = true;
|
|
703
|
+
this.consecutiveBlocked += 1;
|
|
704
|
+
/**
|
|
705
|
+
* ⚠️ ON THE FIRST BLOCKED ROUND, NOT AT THE BOUND. Setting this only when
|
|
706
|
+
* the run aborts left three other endings reachable first — `max-rounds`
|
|
707
|
+
* (which config allows as low as 1), `queue-empty`, `min-hours` — and each
|
|
708
|
+
* of those is then recorded as `gate_failed` with an unchanged tree
|
|
709
|
+
* fingerprint, which is what feeds `noProgress`. The bound governs the
|
|
710
|
+
* ABORT; the exclusion governs any run that met an unavailable
|
|
711
|
+
* environment, whenever it happens to stop.
|
|
712
|
+
*/
|
|
713
|
+
this.metUnavailableEnvironment = true;
|
|
714
|
+
state = (0, core_1.transition)(state, 'discarded');
|
|
715
|
+
const outcome = await this.discardAndReturn(round, state, item.id, why, false);
|
|
716
|
+
if (this.consecutiveBlocked >= exports.MAX_CONSECUTIVE_BLOCKED_ROUNDS) {
|
|
717
|
+
return {
|
|
718
|
+
outcome,
|
|
719
|
+
title: item.title,
|
|
720
|
+
halt: {
|
|
721
|
+
stopReason: 'aborted',
|
|
722
|
+
endedContext: { blockedReason: why },
|
|
723
|
+
because: `${exports.MAX_CONSECUTIVE_BLOCKED_ROUNDS} rounds in a row could not make a container — ` +
|
|
724
|
+
'nothing was charged to the queue; fix the machine and start again',
|
|
725
|
+
},
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
return { outcome, title: item.title };
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* A repair attempt starts from what the last one left, explained. Built
|
|
732
|
+
* BEFORE the model is invoked and after the environment check, so a
|
|
733
|
+
* restored candidate is never put into a tree no container can gate.
|
|
734
|
+
*/
|
|
735
|
+
const repair = await this.prepareRepair(item, attempt);
|
|
736
|
+
let impl;
|
|
737
|
+
try {
|
|
738
|
+
impl = await this.deps.implement(item, attempt, repair);
|
|
739
|
+
}
|
|
740
|
+
catch (err) {
|
|
741
|
+
// The prover-error's sibling: `runImplementer` already returns `abandoned`
|
|
742
|
+
// for a timeout or an unparseable result, so a *throw* here is the process
|
|
743
|
+
// itself failing (spawn ENOENT, transport down) — and it used to escape
|
|
744
|
+
// runRound and kill the whole overnight run. The round ends `abandoned`
|
|
745
|
+
// with the tree discarded, but the item goes back to `ready`, not
|
|
746
|
+
// `closed`: the crash says nothing about the item, and closing it would
|
|
747
|
+
// cost the item as well as the round. Critical alert for the same reason
|
|
748
|
+
// as the prover's — a dead agent binary abandons every round until
|
|
749
|
+
// someone hears.
|
|
750
|
+
const why = err instanceof Error ? err.message : String(err);
|
|
751
|
+
await this.emit('alert', {
|
|
752
|
+
code: 'implementer-error',
|
|
753
|
+
severity: 'critical',
|
|
754
|
+
text: `implementer invocation failed: ${why}`,
|
|
755
|
+
});
|
|
756
|
+
state = (0, core_1.transition)(state, 'abandoned');
|
|
757
|
+
const outcome = await this.discardAndRequeue(round, state, item.id, attempt, `implementer error: ${why}`, undefined, { stage: 'implementer' });
|
|
758
|
+
return { outcome, title: item.title };
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* ASKED BEFORE THE RESULT IS READ, and that ordering is the invariant.
|
|
762
|
+
*
|
|
763
|
+
* The turn came back across a boundary the composition root recognises and
|
|
764
|
+
* this loop does not. Everything after this line — the gate, the commit
|
|
765
|
+
* permit, the prover, the commit itself — is work done on the far side of a
|
|
766
|
+
* point where stopping was the answer, so none of it happens.
|
|
767
|
+
*
|
|
768
|
+
* `discarded`, and it is a statement about the ROUND'S LOCAL WORK rather
|
|
769
|
+
* than about why the run stopped. Nothing here passed a gate and nothing was
|
|
770
|
+
* proven, and un-gated work has never been kept by any path in this file;
|
|
771
|
+
* calling it `abandoned` would additionally say the item taught us nothing
|
|
772
|
+
* and close it, which is false — the item is exactly as workable as it was,
|
|
773
|
+
* and the next attempt should have it back.
|
|
774
|
+
*/
|
|
775
|
+
const halt = this.deps.haltRequested?.() ?? null;
|
|
776
|
+
if (halt !== null) {
|
|
777
|
+
state = (0, core_1.transition)(state, 'discarded');
|
|
778
|
+
const outcome = await this.discardAndReturn(round, state, item.id, halt.because);
|
|
779
|
+
return { outcome, title: item.title, halt };
|
|
780
|
+
}
|
|
781
|
+
if (impl.kind === 'abandoned') {
|
|
782
|
+
// §12 case 4. The tree is cleaned, never consulted: §2.4 forbids deciding
|
|
783
|
+
// from it, but a dead agent's half-written edits must not leak into the
|
|
784
|
+
// next round's diff. `closed`, not `ready`: nothing was learned, and a
|
|
785
|
+
// requeue here is the "same item forever" loop.
|
|
786
|
+
await this.discardTree(item.id);
|
|
787
|
+
state = (0, core_1.transition)(state, 'abandoned');
|
|
788
|
+
const outcome = await this.endRound(round, state, item.id, { state: 'closed', lastFailure: impl.reason });
|
|
789
|
+
return { outcome, title: item.title };
|
|
790
|
+
}
|
|
791
|
+
const result = impl.result;
|
|
792
|
+
// From here every exit is an `endRound`, and every one of them carries this.
|
|
793
|
+
this.roundNote = (0, core_1.normalizeRoundNote)(result.note);
|
|
794
|
+
if (result.outcome === 'blocked') {
|
|
795
|
+
// §8: emit the exemption request and move to the next item. The runner
|
|
796
|
+
// never waits for the phone — the question outlives the round.
|
|
797
|
+
const blocked = result.blocked ?? { path: '(unknown)', reason: 'blocked with no details' };
|
|
798
|
+
// 🔴 `rails.exemptions: 'never'` MEANS IT DOES NOT ASK — and until now
|
|
799
|
+
// nothing branched on it. The setting was parsed, validated as one of two
|
|
800
|
+
// words, and offered on the phone, while an agent that reported a block
|
|
801
|
+
// produced this request under either value. Somebody who had said "never
|
|
802
|
+
// ask me" was asked anyway.
|
|
803
|
+
//
|
|
804
|
+
// ⚠️ IT STILL BLOCKS. `never` is about the QUESTION, not the outcome: the
|
|
805
|
+
// item is blocked either way, the tree is discarded either way, and the
|
|
806
|
+
// run picks the next item either way. Suppressing `item.blocked` too
|
|
807
|
+
// would turn a preference about interruptions into silently losing work.
|
|
808
|
+
if (this.cfg.rails.exemptions === 'ask') {
|
|
809
|
+
await this.emit('exemption.requested', {
|
|
810
|
+
itemId: item.id,
|
|
811
|
+
path: blocked.path,
|
|
812
|
+
reason: blocked.reason,
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
await this.emit('item.blocked', { itemId: item.id, reason: `${blocked.path}: ${blocked.reason}` });
|
|
816
|
+
await this.discardTree(item.id);
|
|
817
|
+
state = (0, core_1.transition)(state, 'abandoned');
|
|
818
|
+
const outcome = await this.endRound(round, state, item.id, { state: 'blocked', blockedReason: blocked.reason });
|
|
819
|
+
return { outcome, title: item.title };
|
|
820
|
+
}
|
|
821
|
+
if (result.outcome === 'no-op') {
|
|
822
|
+
// §2.4: "nothing to do" is a statement the agent makes; the tree is not
|
|
823
|
+
// asked for a second opinion. The discard clears any stray writes anyway.
|
|
824
|
+
await this.discardTree(item.id);
|
|
825
|
+
state = (0, core_1.transition)(state, 'abandoned');
|
|
826
|
+
const outcome = await this.endRound(round, state, item.id, { state: 'closed' });
|
|
827
|
+
return { outcome, title: item.title };
|
|
828
|
+
}
|
|
829
|
+
state = (0, core_1.transition)(state, 'gating');
|
|
830
|
+
await this.emit('round.phase', { round, phase: 'gating' });
|
|
831
|
+
const changed = await this.deps.git.changedPaths();
|
|
832
|
+
/**
|
|
833
|
+
* 🔴 A DECLARATION IS A HOPE; THIS LIST IS FACT — AND IT IS CHECKED HERE,
|
|
834
|
+
* BEFORE THE GATE AND BEFORE ANY COMMIT.
|
|
835
|
+
*
|
|
836
|
+
* The obvious place is after the round, and it is the wrong one twice over:
|
|
837
|
+
* a successful round has COMMITTED by then, so "discard the round" would
|
|
838
|
+
* mean rewriting a commit — the one thing this codebase never does — and
|
|
839
|
+
* `changedPaths` after a commit is empty, so the check would silently only
|
|
840
|
+
* ever see rounds that had already failed.
|
|
841
|
+
*
|
|
842
|
+
* Here the writes are still in the working tree, and the existing discard
|
|
843
|
+
* path applies unchanged.
|
|
844
|
+
*
|
|
845
|
+
* ⚠️ AND IT ENDS THE RUN RATHER THAN RETRYING THE ITEM. A run that wrote
|
|
846
|
+
* outside its lease has demonstrated its DECLARATION is wrong; a retry
|
|
847
|
+
* would run the same wrong declaration again.
|
|
848
|
+
*/
|
|
849
|
+
const declaredSurfaces = this.deps.declaredSurfaces ?? [];
|
|
850
|
+
if (declaredSurfaces.length > 0) {
|
|
851
|
+
const trespass = (0, core_1.surfaceViolations)(changed, declaredSurfaces);
|
|
852
|
+
if (trespass.length > 0) {
|
|
853
|
+
const paths = trespass.map((v) => v.path);
|
|
854
|
+
this.surfaceViolation = { paths, declared: declaredSurfaces };
|
|
855
|
+
const why = `it wrote outside the surfaces it declared (${declaredSurfaces.join(', ')}): ` +
|
|
856
|
+
`${paths.slice(0, 3).join(', ')}${paths.length > 3 ? ', …' : ''}`;
|
|
857
|
+
/**
|
|
858
|
+
* 🔴 THE TRANSITION, WHICH THIS SITE ALONE WAS MISSING — SO THE SAFETY
|
|
859
|
+
* CHECK'S OWN SUCCESS PATH THREW.
|
|
860
|
+
*
|
|
861
|
+
* `state` is `gating` here, `endRound` calls `outcomeOf(state)`, and a
|
|
862
|
+
* non-terminal state makes that null and throws `RunnerError: round
|
|
863
|
+
* ended in non-terminal state gating`. The throw happens BEFORE
|
|
864
|
+
* `patchItem`, so the item is never moved off `in_progress`: the item
|
|
865
|
+
* is stranded, the run dies with a stack trace instead of the sentence
|
|
866
|
+
* explaining the trespass, and the next run finds an item claimed by a
|
|
867
|
+
* process that is gone.
|
|
868
|
+
*
|
|
869
|
+
* MEASURED: a round writing one path outside its declared surface threw
|
|
870
|
+
* rather than discarding. Every other `discardAndReturn` caller — the
|
|
871
|
+
* environment block, the external halt, the blocked round — transitions
|
|
872
|
+
* first; this one was written last and did not.
|
|
873
|
+
*/
|
|
874
|
+
state = (0, core_1.transition)(state, 'discarded');
|
|
875
|
+
const outcome = await this.discardAndReturn(round, state, item.id, why, false);
|
|
876
|
+
return { outcome, title: item.title };
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
// The declaration is a hope; the diff is fact (E-04). What the agent said
|
|
880
|
+
// it touched against what `git status` shows — new, untracked files
|
|
881
|
+
// included — goes to the front of the round's note, counts first, where a
|
|
882
|
+
// person reads it. Recorded, not acted on: the gate decides this round
|
|
883
|
+
// exactly as before. A truthful account adds nothing.
|
|
884
|
+
const touched = (0, core_1.describeTouched)((0, core_1.touchedVsChanged)(result.filesTouched, changed));
|
|
885
|
+
/**
|
|
886
|
+
* AND WHAT THIS ROUND DID TO THE THINGS THAT JUDGED IT (the audit's G-03).
|
|
887
|
+
*
|
|
888
|
+
* A round is kept or discarded by a gate it is permitted to edit — its spec
|
|
889
|
+
* files sit inside the scope it is told it may change, and `package.json` is
|
|
890
|
+
* on no rail — and until now the result said nothing about whether it had
|
|
891
|
+
* edited them. Same seam and the same rule as E-04 above: RECORDED, NOT
|
|
892
|
+
* ACTED ON. The gate decides this round exactly as before, adding a test is
|
|
893
|
+
* the point rather than a suspicion, and a round that touched nothing that
|
|
894
|
+
* judges it adds no characters at all.
|
|
895
|
+
*
|
|
896
|
+
* ⚠️ `changedEntries` CARRIES THE STATUS AND `changedPaths` DOES NOT, which is
|
|
897
|
+
* the difference between a test that was deleted and a test that was added —
|
|
898
|
+
* the two ends of what this exists to tell apart. It is optional on
|
|
899
|
+
* `RunnerGit` for the reason stated there, so a driver that cannot answer
|
|
900
|
+
* yields paths with no status and the sentence says `touched` rather than
|
|
901
|
+
* inventing one.
|
|
902
|
+
*/
|
|
903
|
+
const statusesAreReal = this.deps.git.changedEntries !== undefined;
|
|
904
|
+
const entries = statusesAreReal
|
|
905
|
+
? await this.deps.git.changedEntries()
|
|
906
|
+
: changed.map((path) => ({ path, status: '' }));
|
|
907
|
+
const judged = (0, core_1.describeJudged)((0, core_1.judgedBy)(entries, { steps: this.cfg.gates, ...exports.GATE_MACHINERY, matches: glob_1.matchesGlob }));
|
|
908
|
+
/**
|
|
909
|
+
* E-04 first, then G-03, then the implementer's own sentence. Both
|
|
910
|
+
* machine-made facts keep their counts ahead of the prose, where a
|
|
911
|
+
* truncation cannot hide them, and neither displaces the other; with
|
|
912
|
+
* nothing to report from either, this composes the note it composed before.
|
|
913
|
+
*/
|
|
914
|
+
const facts = [touched, judged].filter((s) => s !== '').join(' — ');
|
|
915
|
+
if (facts !== '') {
|
|
916
|
+
this.roundNote = (0, core_1.normalizeRoundNote)(this.roundNote === undefined ? facts : `${facts} — ${this.roundNote}`);
|
|
917
|
+
}
|
|
918
|
+
// What each step printed, for the evidence of a rejection. Collected here
|
|
919
|
+
// and nowhere else: the result object stays digest-only by contract.
|
|
920
|
+
const diagnostics = [];
|
|
921
|
+
const gate = await (0, gate_1.runGate)({
|
|
922
|
+
steps: this.cfg.gates,
|
|
923
|
+
changedPaths: changed,
|
|
924
|
+
runner: this.deps.gateRunner,
|
|
925
|
+
diagnostics: (d) => diagnostics.push(d),
|
|
926
|
+
interlocks: {
|
|
927
|
+
changedPaths: changed,
|
|
928
|
+
rails: this.cfg.rails,
|
|
929
|
+
envValues: this.collectEnvValues(),
|
|
930
|
+
itemId: item.id,
|
|
931
|
+
now: this.deps.now(),
|
|
932
|
+
exemptions: this.deps.exemptionsFor?.(item.id) ?? [],
|
|
933
|
+
/**
|
|
934
|
+
* The scope interlock (ABORT_SCOPE, 93).
|
|
935
|
+
*
|
|
936
|
+
* 🔴 `changedEntries` IS PASSED ONLY WHEN THE STATUSES ARE REAL. A git
|
|
937
|
+
* driver that cannot report them yields `status: ''` for every path,
|
|
938
|
+
* and `''` is not a pure addition — so handing those over would abort
|
|
939
|
+
* every round under an additive grant, on the strength of a field the
|
|
940
|
+
* driver never filled in. An unmeasurable diff leaves the check unrun,
|
|
941
|
+
* which `checkScope` treats as absence of input rather than a finding.
|
|
942
|
+
*/
|
|
943
|
+
...(statusesAreReal ? { changedEntries: entries } : {}),
|
|
944
|
+
scope: this.cfg.scope,
|
|
945
|
+
},
|
|
946
|
+
});
|
|
947
|
+
/**
|
|
948
|
+
* A GATE THAT COULD NOT RUN IS NOT A GATE THAT WENT RED.
|
|
949
|
+
*
|
|
950
|
+
* `blocked` means the step never executed — no contained toolchain, or
|
|
951
|
+
* containment could not be made — so nothing about this item's code was
|
|
952
|
+
* measured. Charging it would spend an attempt and, three rounds later,
|
|
953
|
+
* close an item nothing ever examined.
|
|
954
|
+
*
|
|
955
|
+
* It also records no failure signature, which matters beyond this file: the
|
|
956
|
+
* signature is what a later reader uses to decide that the same failure
|
|
957
|
+
* keeps coming back, and a machine with no containment would otherwise
|
|
958
|
+
* produce a perfectly repeatable one on every item it touched.
|
|
959
|
+
*/
|
|
960
|
+
const blocked = gate.verdict === 'blocked';
|
|
961
|
+
const blockedStep = blocked ? gate.steps.find((s) => !s.skipped && !s.ok) : undefined;
|
|
962
|
+
const blockedWhy = blocked
|
|
963
|
+
? `gate blocked: step ${blockedStep?.step ?? '(unknown)'} could not run — ` +
|
|
964
|
+
`the environment is unavailable, not the change. No attempt was spent`
|
|
965
|
+
: undefined;
|
|
966
|
+
for (const s of gate.steps)
|
|
967
|
+
await this.emit('gate.step', { round, ...s });
|
|
968
|
+
const gateData = {
|
|
969
|
+
round,
|
|
970
|
+
// The wire never carries `blocked` (see `GateVerdict`): an older reader
|
|
971
|
+
// must see the red it would have seen, and `blockedReason` is the widened
|
|
972
|
+
// field that tells a newer one why nothing was charged.
|
|
973
|
+
verdict: blocked ? 'red' : gate.verdict,
|
|
974
|
+
steps: gate.steps,
|
|
975
|
+
};
|
|
976
|
+
if (gate.abort)
|
|
977
|
+
gateData.abortCode = gate.abort.code;
|
|
978
|
+
if (blockedWhy !== undefined)
|
|
979
|
+
gateData.blockedReason = blockedWhy;
|
|
980
|
+
await this.emit('gate.result', gateData);
|
|
981
|
+
if (blocked) {
|
|
982
|
+
this.blockedThisRound = true;
|
|
983
|
+
this.consecutiveBlocked += 1;
|
|
984
|
+
// Same rule as the preflight branch above: the exclusion is about having
|
|
985
|
+
// met an unavailable environment, not about how the run happened to end.
|
|
986
|
+
this.metUnavailableEnvironment = true;
|
|
987
|
+
state = (0, core_1.transition)(state, (0, core_1.afterGate)(gate.verdict));
|
|
988
|
+
// `discardAndReturn`, not `discardAndRequeue`: the tree is still cleaned,
|
|
989
|
+
// the reason still travels on the item, and the attempt is NOT spent —
|
|
990
|
+
// the same distinction an external halt already earns.
|
|
991
|
+
const outcome = await this.discardAndReturn(round, state, item.id, blockedWhy, false);
|
|
992
|
+
if (this.consecutiveBlocked >= exports.MAX_CONSECUTIVE_BLOCKED_ROUNDS) {
|
|
993
|
+
/**
|
|
994
|
+
* BOUNDED, because "spends nothing" and "retries for ever" together are
|
|
995
|
+
* an unbounded loop against a machine that is not going to get better on
|
|
996
|
+
* its own. `aborted` is the honest existing reason — the run was alive
|
|
997
|
+
* and met a boundary it could not get over — and it keeps `stopReason`
|
|
998
|
+
* off the wire-change list.
|
|
999
|
+
*/
|
|
1000
|
+
return {
|
|
1001
|
+
outcome,
|
|
1002
|
+
title: item.title,
|
|
1003
|
+
halt: {
|
|
1004
|
+
stopReason: 'aborted',
|
|
1005
|
+
/**
|
|
1006
|
+
* `endedContext` because `because` reaches nothing on this path:
|
|
1007
|
+
* it is read only where an EXTERNAL halt ends a round, and
|
|
1008
|
+
* `run.ended` spreads `endedContext` and nothing else. Without it
|
|
1009
|
+
* the operator sees three red gates and an unexplained abort.
|
|
1010
|
+
*/
|
|
1011
|
+
endedContext: { blockedReason: blockedWhy },
|
|
1012
|
+
because: `${exports.MAX_CONSECUTIVE_BLOCKED_ROUNDS} rounds in a row could not run their gate — ` +
|
|
1013
|
+
`${blockedStep?.step === undefined ? 'the environment is unavailable' : `step ${blockedStep.step} never executed`}. ` +
|
|
1014
|
+
`Nothing was charged to the queue; fix the machine and start again`,
|
|
1015
|
+
},
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
return { outcome, title: item.title };
|
|
1019
|
+
}
|
|
1020
|
+
/**
|
|
1021
|
+
* ⚠️ RECORDED WHATEVER THE VERDICT. A gate that went green on three of five
|
|
1022
|
+
* steps and red on the fourth still proved those three, and a later round
|
|
1023
|
+
* that proves a different three is progress the fingerprint must be able to
|
|
1024
|
+
* see.
|
|
1025
|
+
*/
|
|
1026
|
+
this.passingGatesThisRound = gate.steps.filter((s) => !s.skipped && s.ok).map((s) => s.step);
|
|
1027
|
+
if (gate.verdict !== 'green') {
|
|
1028
|
+
/**
|
|
1029
|
+
* ⚠️ RECORDED WHERE IT HAPPENED. `gate_failed` was the producer's default
|
|
1030
|
+
* for every non-committing round, which made it the answer for rounds
|
|
1031
|
+
* that never reached a gate at all. Setting it here means the word means
|
|
1032
|
+
* what the outcome list says it means: a deterministic gate refused it.
|
|
1033
|
+
*/
|
|
1034
|
+
this.failedAtThisRound = 'gate';
|
|
1035
|
+
if (gate.verdict === 'abort') {
|
|
1036
|
+
// §12 cases 8–9: an abort is an alert, not just a tallied red — the gate
|
|
1037
|
+
// itself may be broken, and someone should hear tonight, not at review.
|
|
1038
|
+
await this.emit('alert', {
|
|
1039
|
+
code: 'gate-abort',
|
|
1040
|
+
severity: 'critical',
|
|
1041
|
+
text: gate.abort?.reason ?? 'interlock tripped',
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
const failed = gate.steps.find((s) => !s.skipped && !s.ok);
|
|
1045
|
+
const failedStep = failed?.step;
|
|
1046
|
+
if (failed !== undefined) {
|
|
1047
|
+
/**
|
|
1048
|
+
* Held for the SUMMARY, not for this round's decision — the round is
|
|
1049
|
+
* already being discarded. Loop safety needs to know whether the same
|
|
1050
|
+
* failure keeps coming back, and only the runner sees the gate result.
|
|
1051
|
+
*
|
|
1052
|
+
* `exitCode` is null because the gate's step result does not carry one:
|
|
1053
|
+
* `CommandResult` reports `ok`, not a code. The field stays in
|
|
1054
|
+
* `FailureFacts` because it is part of what identifies a failure where a
|
|
1055
|
+
* caller does have it, and null is honest about not having it here.
|
|
1056
|
+
*/
|
|
1057
|
+
this.lastFailure = {
|
|
1058
|
+
step: failed.step,
|
|
1059
|
+
exitCode: null,
|
|
1060
|
+
outputDigest: failed.outputDigest ?? null,
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
const why = gate.verdict === 'abort'
|
|
1064
|
+
? `gate abort: ${gate.abort?.reason ?? 'interlock tripped'}`
|
|
1065
|
+
: `gate red: step ${failedStep ?? '(unknown)'} failed`;
|
|
1066
|
+
state = (0, core_1.transition)(state, (0, core_1.afterGate)(gate.verdict));
|
|
1067
|
+
const outcome = await this.discardAndRequeue(round, state, item.id, attempt, why, result.testsAdded, {
|
|
1068
|
+
stage: 'gate',
|
|
1069
|
+
gate,
|
|
1070
|
+
diagnostics,
|
|
1071
|
+
implementer: impl,
|
|
1072
|
+
});
|
|
1073
|
+
return { outcome, title: item.title };
|
|
1074
|
+
}
|
|
1075
|
+
// The gate-side half of the commit permit, checked before proof so a round
|
|
1076
|
+
// that can never commit does not pay for a prover call. `proofEnabled: false`
|
|
1077
|
+
// because the proof key is turned below via the state machine — a `partial`
|
|
1078
|
+
// answered "keep" is a human turning it, which a strict verdict check here
|
|
1079
|
+
// would wrongly veto. The load-bearing case: a *required* step skipped by its
|
|
1080
|
+
// `when:` filter leaves the gate green and must still block the commit —
|
|
1081
|
+
// tabsy stamped 166 trailers with a check that never once ran.
|
|
1082
|
+
const permit = (0, core_1.mayCommit)({
|
|
1083
|
+
steps: gate.steps,
|
|
1084
|
+
requiredSteps: (0, gate_1.requiredSteps)(this.cfg.gates),
|
|
1085
|
+
proofEnabled: false,
|
|
1086
|
+
});
|
|
1087
|
+
if (!permit.ok) {
|
|
1088
|
+
state = (0, core_1.transition)(state, 'discarded');
|
|
1089
|
+
const outcome = await this.discardAndRequeue(round, state, item.id, attempt, `commit refused: ${permit.reason ?? 'unknown'}`, result.testsAdded, { stage: 'commit', gate, diagnostics, implementer: impl });
|
|
1090
|
+
return { outcome, title: item.title };
|
|
1091
|
+
}
|
|
1092
|
+
state = (0, core_1.transition)(state, 'proving');
|
|
1093
|
+
await this.emit('round.phase', { round, phase: 'proving' });
|
|
1094
|
+
let proof;
|
|
1095
|
+
if (this.cfg.proof.enabled && this.deps.prove) {
|
|
1096
|
+
try {
|
|
1097
|
+
proof = await this.deps.prove({ item, changedPaths: changed });
|
|
1098
|
+
}
|
|
1099
|
+
catch (err) {
|
|
1100
|
+
// §12 case 15's harder sibling: an unparseable verdict is a rejection,
|
|
1101
|
+
// and a prover that *crashes* (spawn failure, transport down) must be
|
|
1102
|
+
// no kinder — this throw used to escape runRound and kill the whole
|
|
1103
|
+
// overnight run. Without the second key the round may never commit, so
|
|
1104
|
+
// the tree is discarded and the item requeued unproven: one lost
|
|
1105
|
+
// round, not the night. The alert is critical because a down prover
|
|
1106
|
+
// discards every round until someone hears.
|
|
1107
|
+
const why = err instanceof Error ? err.message : String(err);
|
|
1108
|
+
await this.emit('alert', {
|
|
1109
|
+
code: 'prover-error',
|
|
1110
|
+
severity: 'critical',
|
|
1111
|
+
text: `prover invocation failed: ${why}`,
|
|
1112
|
+
});
|
|
1113
|
+
/**
|
|
1114
|
+
* ⚠️ THE VERIFIER, NOT THE GATE. Recorded as its own kind because "a
|
|
1115
|
+
* deterministic gate refused it" is a different fact about a different
|
|
1116
|
+
* mechanism, and a later reader deciding what keeps going wrong was
|
|
1117
|
+
* being handed a category that had been flattened.
|
|
1118
|
+
*/
|
|
1119
|
+
this.failedAtThisRound = 'verifier';
|
|
1120
|
+
state = (0, core_1.transition)(state, 'discarded');
|
|
1121
|
+
const outcome = await this.discardAndRequeue(round, state, item.id, attempt, `prover error: ${why}`, result.testsAdded, { stage: 'proof', gate, diagnostics, implementer: impl });
|
|
1122
|
+
return { outcome, title: item.title };
|
|
1123
|
+
}
|
|
1124
|
+
await this.emit('proof.result', { round, verdict: proof.verdict, reason: proof.reason, confidence: proof.confidence }, 'prover');
|
|
1125
|
+
const resolution = (0, core_1.afterProof)(proof.verdict);
|
|
1126
|
+
let final;
|
|
1127
|
+
/** M6-20: set when the policy was `ask`, nothing could ask, and the answer was therefore `discard`. */
|
|
1128
|
+
let askedAndDiscarded = null;
|
|
1129
|
+
if (resolution.kind === 'decided') {
|
|
1130
|
+
final = resolution.state;
|
|
1131
|
+
}
|
|
1132
|
+
else {
|
|
1133
|
+
// `partial`: the round stays in `proving` until an answer exists.
|
|
1134
|
+
// on_partial keep/discard IS the answer; 'ask' delegates to whoever can
|
|
1135
|
+
// reach a human. The 'discard' fallback is unreachable (constructor
|
|
1136
|
+
// refuses ask-with-nothing-wired) but keeps the safe default explicit.
|
|
1137
|
+
const ask = this.deps.resolvePartial;
|
|
1138
|
+
const answer = this.cfg.proof.onPartial !== 'ask' ? this.cfg.proof.onPartial : ask ? await ask(item, proof) : 'discard';
|
|
1139
|
+
/**
|
|
1140
|
+
* M6-20: A DISCARD THE PROJECT DID NOT CHOOSE IS SAID OUT LOUD.
|
|
1141
|
+
*
|
|
1142
|
+
* Best-effort, like every other alert here: a ledger that refuses it
|
|
1143
|
+
* must not change what happens to the round. Written before the
|
|
1144
|
+
* outcome is applied so it sits with the round it belongs to.
|
|
1145
|
+
*/
|
|
1146
|
+
if (this.cfg.proof.onPartial === 'ask' && answer === 'discard') {
|
|
1147
|
+
askedAndDiscarded = partialDiscardedText(item.title, proof.reason);
|
|
1148
|
+
try {
|
|
1149
|
+
await this.emit('alert', {
|
|
1150
|
+
code: exports.PARTIAL_DISCARDED_CODE,
|
|
1151
|
+
severity: 'warn',
|
|
1152
|
+
text: askedAndDiscarded,
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
catch {
|
|
1156
|
+
/* the round's own record below is the one that must land */
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
final = (0, core_1.afterAnswer)(answer);
|
|
1160
|
+
}
|
|
1161
|
+
if (final !== 'committed') {
|
|
1162
|
+
/**
|
|
1163
|
+
* 🔴 THE VERIFIER REFUSED, AND THIS RECORDED IT AS A GATE FAILURE — the
|
|
1164
|
+
* exact case the producer's own docblock says it fixed.
|
|
1165
|
+
*
|
|
1166
|
+
* Only a prover CRASH ever set `'verifier'`. A prover that ran fine and
|
|
1167
|
+
* said `refuted` left `failedAtThisRound` at null, and null maps to
|
|
1168
|
+
* `gate_failed` — so the record asserted that a deterministic gate
|
|
1169
|
+
* refused the round, in a record that simultaneously said both gate
|
|
1170
|
+
* steps PASSED. Two contradictory statements in one row, and the
|
|
1171
|
+
* loop-safety detectors and the person both read the wrong one.
|
|
1172
|
+
*
|
|
1173
|
+
* A refusal is the prover doing its job, which is what makes recording
|
|
1174
|
+
* it as a gate failure so misleading: it is the single most important
|
|
1175
|
+
* signal that the model and the checker disagree, and it was filed
|
|
1176
|
+
* under the mechanism that had nothing to say.
|
|
1177
|
+
*/
|
|
1178
|
+
this.failedAtThisRound = 'verifier';
|
|
1179
|
+
state = (0, core_1.transition)(state, final);
|
|
1180
|
+
const outcome = await this.discardAndRequeue(round, state, item.id, attempt,
|
|
1181
|
+
// M6-20: the same sentence the alert carries, so the ITEM says why
|
|
1182
|
+
// too — "proof partial: …" alone reads as the checker refusing.
|
|
1183
|
+
askedAndDiscarded ?? `proof ${proof.verdict}: ${proof.reason}`, result.testsAdded, { stage: 'proof', gate, diagnostics, proof, implementer: impl });
|
|
1184
|
+
return { outcome, title: item.title };
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
/**
|
|
1188
|
+
* 🔴 THE LAST LOOK, BECAUSE EVERY CHECK ABOVE RAN ON A PRE-GATE SNAPSHOT
|
|
1189
|
+
* AND THE NEXT LINE IS `git add -A`.
|
|
1190
|
+
*
|
|
1191
|
+
* `changed` was read once, before the gate. The gate then runs the
|
|
1192
|
+
* PROJECT'S OWN COMMANDS — a build, a formatter, a codegen step, a test
|
|
1193
|
+
* suite that writes fixtures — and the prover runs after it. Anything any
|
|
1194
|
+
* of them wrote lands in the working tree, is staged by `stageAll`, and is
|
|
1195
|
+
* committed without ever having been shown to the rails interlock or the
|
|
1196
|
+
* surface check. Forbidden paths, production env files, CI workflow files:
|
|
1197
|
+
* all of them ride in through a window the checks were never told about.
|
|
1198
|
+
*
|
|
1199
|
+
* So the tree is read AGAIN here, and the two checks that guard a commit
|
|
1200
|
+
* are re-asked on what is actually about to be committed.
|
|
1201
|
+
*
|
|
1202
|
+
* ⚠️ AND THE FENCE IS ASKED HERE, WHICH IS THE ONLY PLACE IT MEANS
|
|
1203
|
+
* ANYTHING. `surface-lease.ts` says "EXPIRY EXISTS ONLY HERE, AND ONLY
|
|
1204
|
+
* BECAUSE OF THE FENCE" — and `checkFence` had NO production caller at all,
|
|
1205
|
+
* so the expiry shipped and the thing that made expiry safe did not. A
|
|
1206
|
+
* holder whose lease lapsed while a newer run took the surface could commit
|
|
1207
|
+
* with no comparison anywhere in the path. Two integers, at the moment of
|
|
1208
|
+
* the write, exactly as the design said.
|
|
1209
|
+
*/
|
|
1210
|
+
const atCommit = await this.deps.git.changedPaths();
|
|
1211
|
+
const lateTrespass = (0, core_1.surfaceViolations)(atCommit, this.deps.declaredSurfaces ?? []);
|
|
1212
|
+
if (lateTrespass.length > 0) {
|
|
1213
|
+
const paths = lateTrespass.map((v) => v.path);
|
|
1214
|
+
this.surfaceViolation = { paths, declared: this.deps.declaredSurfaces ?? [] };
|
|
1215
|
+
state = (0, core_1.transition)(state, 'discarded');
|
|
1216
|
+
const outcome = await this.discardAndReturn(round, state, item.id, `the gate or the prover wrote outside the surfaces this run declared ` +
|
|
1217
|
+
`(${(this.deps.declaredSurfaces ?? []).join(', ')}): ` +
|
|
1218
|
+
`${paths.slice(0, 3).join(', ')}${paths.length > 3 ? ', …' : ''}`, false);
|
|
1219
|
+
return { outcome, title: item.title };
|
|
1220
|
+
}
|
|
1221
|
+
const fenceTrouble = this.deps.checkFence?.();
|
|
1222
|
+
if (fenceTrouble !== undefined && !fenceTrouble.ok) {
|
|
1223
|
+
state = (0, core_1.transition)(state, 'discarded');
|
|
1224
|
+
const outcome = await this.discardAndReturn(round, state, item.id, fenceTrouble.because, false);
|
|
1225
|
+
return { outcome, title: item.title };
|
|
1226
|
+
}
|
|
1227
|
+
const lateRails = (0, gate_1.checkInterlocks)({
|
|
1228
|
+
changedPaths: atCommit,
|
|
1229
|
+
rails: this.cfg.rails,
|
|
1230
|
+
// The environment was judged before the gate and cannot have changed by
|
|
1231
|
+
// the gate writing files. Only the DIFF is re-asked here.
|
|
1232
|
+
envValues: [],
|
|
1233
|
+
itemId: item.id,
|
|
1234
|
+
now: this.deps.now(),
|
|
1235
|
+
exemptions: this.deps.exemptionsFor?.(item.id) ?? [],
|
|
1236
|
+
});
|
|
1237
|
+
if (lateRails !== null) {
|
|
1238
|
+
state = (0, core_1.transition)(state, 'discarded');
|
|
1239
|
+
const outcome = await this.discardAndReturn(round, state, item.id, `the gate or the prover wrote somewhere the rails forbid: ${lateRails.reason}` +
|
|
1240
|
+
(lateRails.paths?.length ? ` (${lateRails.paths.slice(0, 3).join(', ')})` : ''), false);
|
|
1241
|
+
return { outcome, title: item.title };
|
|
1242
|
+
}
|
|
1243
|
+
/**
|
|
1244
|
+
* 🔴 THE CROSS-REPO NEEDS ARE ASKED AGAIN HERE, AND THIS IS THE SECOND OF
|
|
1245
|
+
* THE TWO PLACES THE CHARTER NAMES.
|
|
1246
|
+
*
|
|
1247
|
+
* `OWNER_CHARTER.md` §6.D: *"Enforce at pick/admission where the decision is
|
|
1248
|
+
* used; check again at relevant acceptance boundaries if dependencies
|
|
1249
|
+
* changed."* The pick is the first; this is the second, and it exists
|
|
1250
|
+
* because a round is long. The item was pickable when it was picked; by the
|
|
1251
|
+
* time its work is about to be kept, the accepted set can have drifted, a
|
|
1252
|
+
* repository can have moved off the revision this item was written against,
|
|
1253
|
+
* or the set can have been replaced entirely. Keeping the work anyway would
|
|
1254
|
+
* commit code written against a state that no longer exists — which is the
|
|
1255
|
+
* same failure `checkFence` and the late rails guard, arriving from a
|
|
1256
|
+
* different direction.
|
|
1257
|
+
*
|
|
1258
|
+
* ⚠️ IT FAILS CLOSED. An item that declares needs and whose context cannot
|
|
1259
|
+
* be read now reaches `undeclared` and is discarded, exactly as it would at
|
|
1260
|
+
* the pick. "Could not establish" is not "still fine".
|
|
1261
|
+
*
|
|
1262
|
+
* ⚠️ AND THE EVIDENCE IS KEPT (`false`). Nothing about the item's own claim
|
|
1263
|
+
* was disproved — a sibling moved. Invalidating would route the item back
|
|
1264
|
+
* through the verifier for a reason that has nothing to do with it, which is
|
|
1265
|
+
* the mistake `discardAndReturn`'s own docblock records about environment
|
|
1266
|
+
* blocks.
|
|
1267
|
+
*/
|
|
1268
|
+
if ((item.needs?.length ?? 0) > 0) {
|
|
1269
|
+
const dependenciesNow = (await this.deps.crossRepo?.()) ?? { declared: [], set: null, current: new Map() };
|
|
1270
|
+
const moved = (0, core_1.itemCrossRepoBlockedBy)(item, dependenciesNow);
|
|
1271
|
+
if (moved.length > 0) {
|
|
1272
|
+
state = (0, core_1.transition)(state, 'discarded');
|
|
1273
|
+
const outcome = await this.discardAndReturn(round, state, item.id, `a repository this item depends on moved while the round was running: ${(0, core_1.describeItemCrossRepoBlockers)(moved)}`, false);
|
|
1274
|
+
return { outcome, title: item.title };
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
await this.deps.git.stageAll();
|
|
1278
|
+
const trailer = { steps: gate.steps };
|
|
1279
|
+
if (proof)
|
|
1280
|
+
trailer.proof = proof;
|
|
1281
|
+
if (gate.exemptionsUsed?.length) {
|
|
1282
|
+
// The grant appears on the commit itself, so a reader of the log can see
|
|
1283
|
+
// the door was opened — a silently-exempted rail defeats the audit (§8).
|
|
1284
|
+
trailer.exemptions = gate.exemptionsUsed.map((e) => ({ path: e.path, actor: e.actor }));
|
|
1285
|
+
}
|
|
1286
|
+
/**
|
|
1287
|
+
* 🔴 ALREADY DONE IS NOT AN ERROR, AND IT USED TO END THE RUN.
|
|
1288
|
+
*
|
|
1289
|
+
* An item whose behaviour a previous round already delivered leaves nothing
|
|
1290
|
+
* staged. `git commit` then exits 1, which becomes a `GitError`, which the
|
|
1291
|
+
* crash path turns into `run-aborted` — critical, run over, every remaining
|
|
1292
|
+
* item dropped. Measured live: gate green in 2,148 ms, prover `proven` at
|
|
1293
|
+
* 0.83, run killed.
|
|
1294
|
+
*
|
|
1295
|
+
* The honest reading is the one the evidence supports. The gate passed and
|
|
1296
|
+
* the prover affirmed this item's claim against the tree, so the behaviour
|
|
1297
|
+
* IS present — it simply arrived in an earlier commit. So the item is
|
|
1298
|
+
* marked `done` on that evidence, and the round ends without inventing a
|
|
1299
|
+
* commit that does not exist.
|
|
1300
|
+
*
|
|
1301
|
+
* ⚠️ `discarded`, NOT `committed`, because no new commit was made and a sha
|
|
1302
|
+
* would have to be borrowed from a round that already reported it. The item
|
|
1303
|
+
* is `done`, so `shouldRequeue` cannot spin it: a done item is not pickable.
|
|
1304
|
+
*/
|
|
1305
|
+
if (this.deps.git.hasStagedChanges !== undefined && !(await this.deps.git.hasStagedChanges())) {
|
|
1306
|
+
this.roundNote =
|
|
1307
|
+
`nothing left to commit — the gate and the prover both passed, so this item's work is ` +
|
|
1308
|
+
`already in the tree from an earlier round. Marked done rather than ending the run.`;
|
|
1309
|
+
state = (0, core_1.transition)(state, 'discarded');
|
|
1310
|
+
const outcome = await this.endRound(round, state, item.id, { state: 'done' }, { files: 0 });
|
|
1311
|
+
return { outcome, title: item.title };
|
|
1312
|
+
}
|
|
1313
|
+
const sha = await this.deps.git.commit(item.title, trailer);
|
|
1314
|
+
// Spend on commit, not on check: a discarded round left nothing behind, and
|
|
1315
|
+
// burning the grant on a failure would strand the retry outside its own
|
|
1316
|
+
// door. Spending happens after the commit exists, so the round the copy
|
|
1317
|
+
// names is a round that really landed; a spend-persist failure throws — a
|
|
1318
|
+
// grant we cannot record as spent would silently become a many-round door.
|
|
1319
|
+
for (const e of gate.exemptionsUsed ?? []) {
|
|
1320
|
+
await this.deps.spendExemption?.((0, core_1.markExemptionUsed)(e, round));
|
|
1321
|
+
}
|
|
1322
|
+
state = (0, core_1.transition)(state, 'committed');
|
|
1323
|
+
const outcome = await this.endRound(round, state, item.id, { state: 'done' }, { sha, files: changed.length, testsAdded: result.testsAdded });
|
|
1324
|
+
return { outcome, title: item.title };
|
|
1325
|
+
}
|
|
1326
|
+
/**
|
|
1327
|
+
* Discard the tree, and with it the item's cached evidence — §12 case 19's
|
|
1328
|
+
* "always after a discard". Unconditional across every discard path: a
|
|
1329
|
+
* requeued or later-unblocked item will be picked again, and its next attempt
|
|
1330
|
+
* must not start from the very file:line claims that just led into a wall.
|
|
1331
|
+
* (For an item closed by the same call the entry is merely dead weight.)
|
|
1332
|
+
*/
|
|
1333
|
+
async discardTree(itemId, invalidateEvidence = true) {
|
|
1334
|
+
await this.deps.git.discard();
|
|
1335
|
+
if (invalidateEvidence)
|
|
1336
|
+
await this.deps.evidence?.invalidate(itemId);
|
|
1337
|
+
}
|
|
1338
|
+
/**
|
|
1339
|
+
* Discard the tree and requeue: back to `ready` with attempt++ and the reason
|
|
1340
|
+
* carried, so the next attempt starts knowing why this one failed (§2.2).
|
|
1341
|
+
*/
|
|
1342
|
+
async discardAndRequeue(round, state, itemId, attempt, why, testsAdded, context = { stage: 'implementer' }) {
|
|
1343
|
+
/**
|
|
1344
|
+
* 🔴 RETAINED BEFORE THE RESET, NOT AFTER. `discardTree` is `reset --hard`
|
|
1345
|
+
* and the only copy of the candidate is the working tree. Every path that
|
|
1346
|
+
* spends an attempt comes through here, so every rejected candidate is
|
|
1347
|
+
* kept — or its absence is recorded on the round, by name.
|
|
1348
|
+
*/
|
|
1349
|
+
const kept = await this.retainRejected(round, itemId, attempt, why, context);
|
|
1350
|
+
await this.discardTree(itemId);
|
|
1351
|
+
return this.endRound(round, state, itemId,
|
|
1352
|
+
// Back to the queue, unless that was the last attempt this item gets.
|
|
1353
|
+
// `afterSpentAttempt` owns that decision so this and recovery — the only
|
|
1354
|
+
// two places an attempt is ever spent — cannot come to disagree about it.
|
|
1355
|
+
{ state: (0, core_1.afterSpentAttempt)(attempt), attempts: attempt, lastFailure: why }, { ...(testsAdded === undefined ? {} : { testsAdded }), ...kept });
|
|
1356
|
+
}
|
|
1357
|
+
/**
|
|
1358
|
+
* Write the candidate and its verdict where the reset cannot reach, and
|
|
1359
|
+
* answer the fields `round.ended` carries about it.
|
|
1360
|
+
*
|
|
1361
|
+
* NEVER THROWS. A failure to keep evidence is a fact about the round
|
|
1362
|
+
* (`evidenceIncomplete`), not a reason to leave the tree dirty — the discard
|
|
1363
|
+
* that follows this must run whatever happened here.
|
|
1364
|
+
*/
|
|
1365
|
+
async retainRejected(round, itemId, attempt, why, context) {
|
|
1366
|
+
const failedStep = context.gate?.steps.find((s) => !s.skipped && !s.ok)?.step;
|
|
1367
|
+
const named = failedStep === undefined ? {} : { failedStep };
|
|
1368
|
+
const sink = this.deps.rejectedEvidence;
|
|
1369
|
+
if (sink === undefined) {
|
|
1370
|
+
return { ...named, evidenceIncomplete: 'no evidence store is wired for this run' };
|
|
1371
|
+
}
|
|
1372
|
+
let input;
|
|
1373
|
+
try {
|
|
1374
|
+
input = await this.rejectionInput(round, itemId, attempt, why, context);
|
|
1375
|
+
}
|
|
1376
|
+
catch (e) {
|
|
1377
|
+
return { ...named, evidenceIncomplete: `evidence could not be gathered: ${e.message}` };
|
|
1378
|
+
}
|
|
1379
|
+
let outcome;
|
|
1380
|
+
try {
|
|
1381
|
+
outcome = await sink.retain(input);
|
|
1382
|
+
}
|
|
1383
|
+
catch (e) {
|
|
1384
|
+
outcome = { retained: false, because: `evidence store threw: ${e.message}` };
|
|
1385
|
+
}
|
|
1386
|
+
this.rejections.set(itemId, { input, outcome });
|
|
1387
|
+
return outcome.retained
|
|
1388
|
+
? { ...named, evidencePath: outcome.path }
|
|
1389
|
+
: { ...named, evidenceIncomplete: outcome.because };
|
|
1390
|
+
}
|
|
1391
|
+
async rejectionInput(round, itemId, attempt, why, context) {
|
|
1392
|
+
const git = this.deps.git;
|
|
1393
|
+
const item = this.deps.queue.get(itemId);
|
|
1394
|
+
const manifest = git.changedEntries !== undefined
|
|
1395
|
+
? await git.changedEntries()
|
|
1396
|
+
: (await git.changedPaths()).map((path) => ({ path, status: '??' }));
|
|
1397
|
+
/**
|
|
1398
|
+
* ⚠️ AN ABORTED CANDIDATE'S PATCH IS WITHHELD. The rails fired because the
|
|
1399
|
+
* change touched a path the project forbids; its content is precisely what
|
|
1400
|
+
* must not be copied into a record. The manifest still names the paths.
|
|
1401
|
+
*/
|
|
1402
|
+
const aborted = context.gate?.verdict === 'abort';
|
|
1403
|
+
let patch = null;
|
|
1404
|
+
let patchWithheld;
|
|
1405
|
+
if (aborted) {
|
|
1406
|
+
patchWithheld =
|
|
1407
|
+
'the rails aborted this candidate: it touched a forbidden path, and the content of ' +
|
|
1408
|
+
'such a change is not retained';
|
|
1409
|
+
}
|
|
1410
|
+
else if (git.patch === undefined) {
|
|
1411
|
+
patchWithheld = 'this git driver cannot produce a patch';
|
|
1412
|
+
}
|
|
1413
|
+
else {
|
|
1414
|
+
patch = await git.patch(rejected_evidence_1.REJECTED_PATCH_MAX_BYTES);
|
|
1415
|
+
}
|
|
1416
|
+
const impl = context.implementer;
|
|
1417
|
+
return {
|
|
1418
|
+
run: { runId: this.id.runId, projectId: this.id.projectId, lane: this.id.lane, branch: this.id.branch },
|
|
1419
|
+
round,
|
|
1420
|
+
attempt,
|
|
1421
|
+
itemId,
|
|
1422
|
+
itemTitle: item?.title ?? '',
|
|
1423
|
+
baseSha: await git.headSha(),
|
|
1424
|
+
why,
|
|
1425
|
+
stage: context.stage,
|
|
1426
|
+
...(context.gate === undefined
|
|
1427
|
+
? {}
|
|
1428
|
+
: {
|
|
1429
|
+
gate: {
|
|
1430
|
+
verdict: context.gate.verdict,
|
|
1431
|
+
steps: context.gate.steps,
|
|
1432
|
+
diagnostics: context.diagnostics ?? [],
|
|
1433
|
+
...(context.gate.abort === undefined ? {} : { abort: context.gate.abort }),
|
|
1434
|
+
},
|
|
1435
|
+
}),
|
|
1436
|
+
...(context.proof === undefined ? {} : { proof: context.proof }),
|
|
1437
|
+
...(impl === undefined
|
|
1438
|
+
? {}
|
|
1439
|
+
: {
|
|
1440
|
+
implementer: {
|
|
1441
|
+
result: impl.kind === 'result' ? impl.result : null,
|
|
1442
|
+
abandoned: impl.kind === 'abandoned' ? impl.reason : null,
|
|
1443
|
+
transcript: impl.transcript,
|
|
1444
|
+
},
|
|
1445
|
+
}),
|
|
1446
|
+
manifest,
|
|
1447
|
+
patch,
|
|
1448
|
+
...(patchWithheld === undefined ? {} : { patchWithheld }),
|
|
1449
|
+
};
|
|
1450
|
+
}
|
|
1451
|
+
/**
|
|
1452
|
+
* What this attempt is told about the last one, and whether that candidate
|
|
1453
|
+
* was put back first.
|
|
1454
|
+
*
|
|
1455
|
+
* RESTORED ONLY WHEN ALL OF THESE HOLD, and each refusal names itself:
|
|
1456
|
+
* - the driver can apply a patch and one was retained, whole
|
|
1457
|
+
* - HEAD is the base the candidate was written against
|
|
1458
|
+
* - every path it touched is inside the project's declared scope and
|
|
1459
|
+
* outside its rails — the candidate is re-gated afterwards regardless,
|
|
1460
|
+
* but a restore must not put a change somewhere the worker itself may
|
|
1461
|
+
* not write
|
|
1462
|
+
* A restore that fails leaves the tree as it was (`applyPatch` is
|
|
1463
|
+
* all-or-nothing, and the tree is discarded again to be sure) and the
|
|
1464
|
+
* worker is told it starts from the base, and why.
|
|
1465
|
+
*/
|
|
1466
|
+
async prepareRepair(item, attempt) {
|
|
1467
|
+
let last = this.rejections.get(item.id);
|
|
1468
|
+
let recoveredFrom;
|
|
1469
|
+
if (last === undefined) {
|
|
1470
|
+
// A restart between the rejection and this attempt: the memory is gone,
|
|
1471
|
+
// the retained evidence is not. See `recoverRejection`.
|
|
1472
|
+
const recovered = await this.recoverRejection(item);
|
|
1473
|
+
if (recovered === undefined)
|
|
1474
|
+
return undefined;
|
|
1475
|
+
last = recovered.last;
|
|
1476
|
+
recoveredFrom = recovered.from;
|
|
1477
|
+
}
|
|
1478
|
+
const { input, outcome } = last;
|
|
1479
|
+
const previous = {
|
|
1480
|
+
round: input.round,
|
|
1481
|
+
attempt: input.attempt,
|
|
1482
|
+
why: input.why,
|
|
1483
|
+
stage: input.stage,
|
|
1484
|
+
failedStep: input.gate?.steps.find((s) => !s.skipped && !s.ok)?.step ?? null,
|
|
1485
|
+
evidencePath: outcome.retained ? outcome.path : null,
|
|
1486
|
+
diagnostics: input.gate?.diagnostics ?? [],
|
|
1487
|
+
manifest: input.manifest,
|
|
1488
|
+
patch: input.patch === null ? null : { text: input.patch.text, truncated: input.patch.truncated },
|
|
1489
|
+
patchWithheld: input.patchWithheld ?? null,
|
|
1490
|
+
...(recoveredFrom === undefined ? {} : { recoveredFrom }),
|
|
1491
|
+
};
|
|
1492
|
+
const base = { attempt, maxAttempts: core_1.MAX_ITEM_EXECUTION_ATTEMPTS, previous };
|
|
1493
|
+
const refuse = (because) => this.noteRepair({ ...base, restored: { ok: false, because } });
|
|
1494
|
+
const git = this.deps.git;
|
|
1495
|
+
if (git.applyPatch === undefined)
|
|
1496
|
+
return refuse('this git driver cannot restore a patch');
|
|
1497
|
+
if (!outcome.retained || outcome.patchFile === null) {
|
|
1498
|
+
return refuse(input.patchWithheld ?? 'the previous candidate was not retained');
|
|
1499
|
+
}
|
|
1500
|
+
if (input.patch === null || input.patch.truncated) {
|
|
1501
|
+
return refuse('the retained patch is truncated and cannot be applied whole');
|
|
1502
|
+
}
|
|
1503
|
+
const head = await git.headSha();
|
|
1504
|
+
if (head !== input.baseSha) {
|
|
1505
|
+
return refuse(`the tree has moved on: the candidate was written against ${input.baseSha}, HEAD is ${head}`);
|
|
1506
|
+
}
|
|
1507
|
+
const paths = input.manifest.map((m) => m.path);
|
|
1508
|
+
/**
|
|
1509
|
+
* 🔴 THIS USED TO PERMIT `[...scope.free, ...scope.additive]`, WHICH IS
|
|
1510
|
+
* THE ONE THING `additive` PROMISES NOT TO MEAN. Restoring a candidate
|
|
1511
|
+
* writes its patch back into the tree, so a manifest that MODIFIED a file
|
|
1512
|
+
* under an additive-only grant was put back by the check that existed to
|
|
1513
|
+
* stop it — and this was the only place in the product where scope was
|
|
1514
|
+
* compared against anything at all.
|
|
1515
|
+
*
|
|
1516
|
+
* It now asks the same fold the gate interlock asks, so a restore cannot
|
|
1517
|
+
* be more permissive than the gate that will re-judge it.
|
|
1518
|
+
*/
|
|
1519
|
+
const restoreEntries = input.manifest.map((m) => ({ path: m.path, status: m.status }));
|
|
1520
|
+
const scopeFails = (0, scope_rule_1.scopeViolations)(restoreEntries, this.cfg.scope);
|
|
1521
|
+
if (scopeFails.length > 0) {
|
|
1522
|
+
const named = scopeFails.map((v) => `${v.path} (${v.because})`).join(', ');
|
|
1523
|
+
return refuse(`the candidate touched paths outside the declared scope: ${named}`);
|
|
1524
|
+
}
|
|
1525
|
+
const railed = (0, glob_1.filterMatching)(paths, this.cfg.rails.forbiddenPaths);
|
|
1526
|
+
if (railed.length > 0)
|
|
1527
|
+
return refuse(`the candidate touched railed paths: ${railed.join(', ')}`);
|
|
1528
|
+
const applied = await git.applyPatch(outcome.patchFile);
|
|
1529
|
+
if (!applied.ok) {
|
|
1530
|
+
await git.discard();
|
|
1531
|
+
return refuse(applied.because);
|
|
1532
|
+
}
|
|
1533
|
+
return this.noteRepair({ ...base, restored: { ok: true, paths } });
|
|
1534
|
+
}
|
|
1535
|
+
/** Records what this round’s repair was, for `round.ended`, and hands the context back unchanged. */
|
|
1536
|
+
noteRepair(ctx) {
|
|
1537
|
+
this.roundRepair = {
|
|
1538
|
+
previousAttempt: ctx.previous.attempt,
|
|
1539
|
+
restored: ctx.restored.ok,
|
|
1540
|
+
...(ctx.restored.ok ? { restoredPaths: [...ctx.restored.paths] } : { notRestored: ctx.restored.because }),
|
|
1541
|
+
...(ctx.previous.recoveredFrom === undefined ? {} : { recoveredFrom: ctx.previous.recoveredFrom }),
|
|
1542
|
+
};
|
|
1543
|
+
return ctx;
|
|
1544
|
+
}
|
|
1545
|
+
/**
|
|
1546
|
+
* The item’s last rejection from an EARLIER run, when the evidence store can
|
|
1547
|
+
* vouch for it — see `RejectedEvidenceSink.latestFor` for every check.
|
|
1548
|
+
*
|
|
1549
|
+
* ⚠️ A REFUSAL IS RECORDED, NOT SILENT. The attempt then goes ahead briefed
|
|
1550
|
+
* from `lastFailure` and starting from the base, exactly as before recovery
|
|
1551
|
+
* existed — but `round.ended` says a restart found evidence and why it was
|
|
1552
|
+
* not used, so "no repair context" can be told apart from "refused one".
|
|
1553
|
+
*/
|
|
1554
|
+
async recoverRejection(item) {
|
|
1555
|
+
const sink = this.deps.rejectedEvidence;
|
|
1556
|
+
if (sink?.latestFor === undefined)
|
|
1557
|
+
return undefined;
|
|
1558
|
+
const found = await sink.latestFor({
|
|
1559
|
+
itemId: item.id,
|
|
1560
|
+
projectId: this.id.projectId,
|
|
1561
|
+
lane: this.id.lane,
|
|
1562
|
+
runId: this.id.runId,
|
|
1563
|
+
attemptsSpent: item.attempts,
|
|
1564
|
+
lastFailure: item.lastFailure,
|
|
1565
|
+
});
|
|
1566
|
+
if (!found.found) {
|
|
1567
|
+
if (found.because !== null) {
|
|
1568
|
+
this.roundRepair = { previousAttempt: null, restored: false, recoveryRefused: found.because };
|
|
1569
|
+
}
|
|
1570
|
+
return undefined;
|
|
1571
|
+
}
|
|
1572
|
+
return {
|
|
1573
|
+
last: { input: found.input, outcome: found.outcome },
|
|
1574
|
+
from: { runId: found.input.run.runId, evidencePath: found.outcome.path },
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1577
|
+
/**
|
|
1578
|
+
* Discard the tree and put the item back exactly as it was found.
|
|
1579
|
+
*
|
|
1580
|
+
* THE ATTEMPT IS NOT SPENT, and that is the whole difference from
|
|
1581
|
+
* `discardAndRequeue`. An attempt is the budget for an item's implementation
|
|
1582
|
+
* FAILING, and nothing failed here — the loop was stopped from outside before
|
|
1583
|
+
* this round could reach a verdict. Spending one anyway would mean an item
|
|
1584
|
+
* that keeps meeting the same external boundary is `closed` by
|
|
1585
|
+
* `afterSpentAttempt` after three interruptions, without a single check ever
|
|
1586
|
+
* having gone red, and while whatever the run was waiting for may since have
|
|
1587
|
+
* been answered.
|
|
1588
|
+
*
|
|
1589
|
+
* The tree is still discarded, because every path in this file discards
|
|
1590
|
+
* un-gated work: `run()` refuses to start on a dirty tree, and leftovers
|
|
1591
|
+
* would fold into the next round's diff.
|
|
1592
|
+
*
|
|
1593
|
+
* The reason still travels on the item, which is what a person or a later
|
|
1594
|
+
* attempt reads to find out why this round produced nothing.
|
|
1595
|
+
*/
|
|
1596
|
+
async discardAndReturn(round, state, itemId, why,
|
|
1597
|
+
/**
|
|
1598
|
+
* Whether to drop the item's cached evidence along with the tree.
|
|
1599
|
+
*
|
|
1600
|
+
* ⚠️ FALSE FOR AN ENVIRONMENT BLOCK, and that is not an optimisation.
|
|
1601
|
+
* §12 case 19's rationale for invalidating is "its next attempt must not
|
|
1602
|
+
* start from the very file:line claims that just led into a wall" — and
|
|
1603
|
+
* nothing ran, so no wall was met. Invalidating anyway makes the item a
|
|
1604
|
+
* guaranteed cache miss, and `pickNext` then routes it through the VERIFIER
|
|
1605
|
+
* before the next round: a paid model call, on the machine the preflight
|
|
1606
|
+
* just declared unusable. Measured at one verifier turn per blocked round.
|
|
1607
|
+
*/
|
|
1608
|
+
invalidateEvidence = true) {
|
|
1609
|
+
await this.discardTree(itemId, invalidateEvidence);
|
|
1610
|
+
return this.endRound(round, state, itemId, { state: 'ready', lastFailure: why });
|
|
1611
|
+
}
|
|
1612
|
+
async endRound(round, state, itemId, patch, extra = {}) {
|
|
1613
|
+
const outcome = (0, core_1.outcomeOf)(state);
|
|
1614
|
+
if (outcome === null)
|
|
1615
|
+
throw new RunnerError(`round ended in non-terminal state ${state}`);
|
|
1616
|
+
await this.patchItem(itemId, patch);
|
|
1617
|
+
// The note goes on before `extra`, so a caller that has something better to
|
|
1618
|
+
// say about this particular ending can still say it.
|
|
1619
|
+
await this.emit('round.ended', {
|
|
1620
|
+
round,
|
|
1621
|
+
outcome,
|
|
1622
|
+
itemId,
|
|
1623
|
+
...(this.roundNote === undefined ? {} : { note: this.roundNote }),
|
|
1624
|
+
...(this.roundRepair === undefined ? {} : { repair: this.roundRepair }),
|
|
1625
|
+
...extra,
|
|
1626
|
+
});
|
|
1627
|
+
this.totals[outcome] += 1;
|
|
1628
|
+
/**
|
|
1629
|
+
* WHERE THIS RED CAME FROM, counted — see {@link RunTotals.proverRejected}.
|
|
1630
|
+
*
|
|
1631
|
+
* `failedAtThisRound` is already maintained on every round regardless of
|
|
1632
|
+
* `run.per_round_safety`, so this costs nothing and needs no flag. It is
|
|
1633
|
+
* set to `'verifier'` exactly where the prover refutes or returns an
|
|
1634
|
+
* unusable verdict, and to `'gate'` where a check went red — which is the
|
|
1635
|
+
* distinction the summary was inventing.
|
|
1636
|
+
*/
|
|
1637
|
+
if (outcome === 'discarded' && this.failedAtThisRound === 'verifier') {
|
|
1638
|
+
this.totals.proverRejected = (this.totals.proverRejected ?? 0) + 1;
|
|
1639
|
+
}
|
|
1640
|
+
return outcome;
|
|
1641
|
+
}
|
|
1642
|
+
/**
|
|
1643
|
+
* Queue writes ride the store's optimistic concurrency. When the phone edited
|
|
1644
|
+
* the item mid-round the runner loses the version race — its write records
|
|
1645
|
+
* what physically happened to the round, so it refetches and lands on top of
|
|
1646
|
+
* the curation edit rather than crashing an overnight run over the race.
|
|
1647
|
+
*/
|
|
1648
|
+
async patchItem(id, patch) {
|
|
1649
|
+
const current = this.deps.queue.get(id);
|
|
1650
|
+
if (current === undefined)
|
|
1651
|
+
throw new RunnerError(`item ${id} vanished from the queue mid-round`);
|
|
1652
|
+
const first = await this.deps.queue.update(id, current.version, patch);
|
|
1653
|
+
if (first.ok)
|
|
1654
|
+
return first.item;
|
|
1655
|
+
if (first.reason === 'stale-version') {
|
|
1656
|
+
const again = this.deps.queue.get(id);
|
|
1657
|
+
if (again !== undefined) {
|
|
1658
|
+
const second = await this.deps.queue.update(id, again.version, patch);
|
|
1659
|
+
if (second.ok)
|
|
1660
|
+
return second.item;
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
throw new RunnerError(`queue write failed for ${id}: ${first.reason}`);
|
|
1664
|
+
}
|
|
1665
|
+
/** Same inputs `doctor` probes: prod env files that exist, plus the live env. */
|
|
1666
|
+
collectEnvValues() {
|
|
1667
|
+
const values = [];
|
|
1668
|
+
for (const file of this.cfg.rails.prodEnvFiles) {
|
|
1669
|
+
const text = this.deps.readEnvFile(file);
|
|
1670
|
+
if (text !== null)
|
|
1671
|
+
values.push(text);
|
|
1672
|
+
}
|
|
1673
|
+
for (const v of Object.values(this.deps.env)) {
|
|
1674
|
+
if (v)
|
|
1675
|
+
values.push(v);
|
|
1676
|
+
}
|
|
1677
|
+
return values;
|
|
1678
|
+
}
|
|
1679
|
+
async emit(kind, data, actor = 'runner') {
|
|
1680
|
+
const input = {
|
|
1681
|
+
ts: new Date(this.deps.now()).toISOString(),
|
|
1682
|
+
runId: this.id.runId,
|
|
1683
|
+
projectId: this.id.projectId,
|
|
1684
|
+
lane: this.id.lane,
|
|
1685
|
+
kind,
|
|
1686
|
+
actor,
|
|
1687
|
+
data,
|
|
1688
|
+
};
|
|
1689
|
+
await this.deps.ledger.append(input);
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
exports.RoundRunner = RoundRunner;
|
|
1693
|
+
//# sourceMappingURL=round-runner.js.map
|