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,1751 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SKIP_REFUSED_REASON = exports.PLACEMENT_WAIT_MS = exports.CLI_VERSION = void 0;
|
|
4
|
+
exports.pairCommand = pairCommand;
|
|
5
|
+
exports.resolveProjectId = resolveProjectId;
|
|
6
|
+
exports.resolveProjectIdWithConsent = resolveProjectIdWithConsent;
|
|
7
|
+
exports.isYes = isYes;
|
|
8
|
+
exports.renderRegisterAsk = renderRegisterAsk;
|
|
9
|
+
exports.nodeRegisterAsk = nodeRegisterAsk;
|
|
10
|
+
exports.reviewRefusedReason = reviewRefusedReason;
|
|
11
|
+
exports.makeCommandHandler = makeCommandHandler;
|
|
12
|
+
exports.daemonCommand = daemonCommand;
|
|
13
|
+
exports.grantOf = grantOf;
|
|
14
|
+
const pursuit_delivery_1 = require("./pursuit-delivery");
|
|
15
|
+
const node_crypto_1 = require("node:crypto");
|
|
16
|
+
const node_path_1 = require("node:path");
|
|
17
|
+
const machine_store_1 = require("./machine-store");
|
|
18
|
+
const server_client_1 = require("./server-client");
|
|
19
|
+
const daemon_1 = require("./daemon");
|
|
20
|
+
const signals_1 = require("./signals");
|
|
21
|
+
const pair_cmd_1 = require("./pair-cmd");
|
|
22
|
+
const redraw_1 = require("./redraw");
|
|
23
|
+
const goal_ownership_1 = require("./goal-ownership");
|
|
24
|
+
const run_launcher_1 = require("./run-launcher");
|
|
25
|
+
const control_file_1 = require("./control-file");
|
|
26
|
+
const command_receipts_1 = require("./command-receipts");
|
|
27
|
+
const ledger_store_1 = require("./ledger-store");
|
|
28
|
+
const stale_run_sweep_1 = require("./stale-run-sweep");
|
|
29
|
+
const run_ending_locator_1 = require("./run-ending-locator");
|
|
30
|
+
const run_lease_1 = require("./run-lease");
|
|
31
|
+
const workspace_1 = require("./workspace");
|
|
32
|
+
const cursor_store_1 = require("./cursor-store");
|
|
33
|
+
const ledger_replay_1 = require("./ledger-replay");
|
|
34
|
+
const goal_runs_1 = require("./goal-runs");
|
|
35
|
+
const goal_store_1 = require("./goal-store");
|
|
36
|
+
const core_1 = require("@looply/core");
|
|
37
|
+
const autonomy_store_1 = require("./autonomy-store");
|
|
38
|
+
const start_attempts_1 = require("./start-attempts");
|
|
39
|
+
const autonomy_tick_1 = require("./autonomy-tick");
|
|
40
|
+
const grant_reconcile_1 = require("./grant-reconcile");
|
|
41
|
+
const run_start_marks_1 = require("./run-start-marks");
|
|
42
|
+
const run_refusal_marks_1 = require("./run-refusal-marks");
|
|
43
|
+
const approval_gate_read_1 = require("./approval-gate-read");
|
|
44
|
+
const explicit_start_gate_1 = require("./explicit-start-gate");
|
|
45
|
+
const approval_resumes_1 = require("./approval-resumes");
|
|
46
|
+
const governed_approvals_1 = require("./governed-approvals");
|
|
47
|
+
const core_2 = require("@looply/core");
|
|
48
|
+
const git_driver_1 = require("./git-driver");
|
|
49
|
+
const setup_prompt_1 = require("./setup-prompt");
|
|
50
|
+
const ui_1 = require("./ui");
|
|
51
|
+
const rails_stores_1 = require("./rails-stores");
|
|
52
|
+
const daemon_lock_1 = require("./daemon-lock");
|
|
53
|
+
const coordinator_1 = require("./coordinator");
|
|
54
|
+
const coordinator_state_1 = require("./coordinator-state");
|
|
55
|
+
const ask_brain_1 = require("./ask-brain");
|
|
56
|
+
const depends_review_1 = require("./depends-review");
|
|
57
|
+
const brain_advice_1 = require("./brain-advice");
|
|
58
|
+
const index_for_head_1 = require("./index-for-head");
|
|
59
|
+
const packet_for_goal_1 = require("./packet-for-goal");
|
|
60
|
+
const project_index_facts_1 = require("./project-index-facts");
|
|
61
|
+
const project_memory_store_1 = require("./project-memory-store");
|
|
62
|
+
const workspace_provisioner_1 = require("./workspace-provisioner");
|
|
63
|
+
const preemptor_1 = require("./preemptor");
|
|
64
|
+
const stop_follow_up_1 = require("./stop-follow-up");
|
|
65
|
+
const programme_mode_1 = require("./programme-mode");
|
|
66
|
+
const reaper_1 = require("./reaper");
|
|
67
|
+
const integrator_1 = require("./integrator");
|
|
68
|
+
const integration_deps_1 = require("./integration-deps");
|
|
69
|
+
const completion_1 = require("./completion");
|
|
70
|
+
const lane_coverage_1 = require("./lane-coverage");
|
|
71
|
+
/**
|
|
72
|
+
* Composition roots for `looply pair` and `looply daemon`.
|
|
73
|
+
*
|
|
74
|
+
* Both need the same three things — a machine identity on disk, a server client
|
|
75
|
+
* carrying its token, and a project id for this repo — so they are assembled
|
|
76
|
+
* here rather than twice.
|
|
77
|
+
*/
|
|
78
|
+
exports.CLI_VERSION = '0.1.0';
|
|
79
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
80
|
+
const sha256 = (s) => (0, node_crypto_1.createHash)('sha256').update(s, 'utf8').digest('hex');
|
|
81
|
+
async function pairCommand(opts) {
|
|
82
|
+
const store = new machine_store_1.MachineStore((0, machine_store_1.machineFilePath)());
|
|
83
|
+
const client = new server_client_1.ServerClient({ baseUrl: opts.serverUrl });
|
|
84
|
+
// A repo checked out at a filesystem root has an empty basename, which would
|
|
85
|
+
// send a blank machine name to the server and show as a blank row in the app.
|
|
86
|
+
const machineName = opts.machineName ?? ((0, node_path_1.basename)(opts.cwd) || 'this machine');
|
|
87
|
+
opts.log(`server : ${opts.serverUrl}`);
|
|
88
|
+
opts.log(`machine : ${machineName}`);
|
|
89
|
+
const redrawer = new redraw_1.Redrawer((0, redraw_1.nodeRedrawSink)());
|
|
90
|
+
const result = await (0, pair_cmd_1.runPair)({
|
|
91
|
+
serverUrl: opts.serverUrl,
|
|
92
|
+
machineName,
|
|
93
|
+
store,
|
|
94
|
+
client,
|
|
95
|
+
log: opts.log,
|
|
96
|
+
// Drawn in place. A reissue replaces the box instead of printing a second
|
|
97
|
+
// one below it, so the only code on screen is the one that still works.
|
|
98
|
+
render: (info, secondsLeft) => redrawer.draw((0, pair_cmd_1.renderPairCode)(info, secondsLeft)),
|
|
99
|
+
sleep,
|
|
100
|
+
now: () => Date.now(),
|
|
101
|
+
});
|
|
102
|
+
if (!result.paired) {
|
|
103
|
+
opts.log(`pair : gave up — ${result.reason ?? 'unknown'}`);
|
|
104
|
+
return 1;
|
|
105
|
+
}
|
|
106
|
+
opts.log(' next: `looply daemon` to keep this machine connected.');
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Resolve this repo to a server-side project, registering it the first time.
|
|
111
|
+
*
|
|
112
|
+
* The mapping is cached on the machine so a second run does not create a second
|
|
113
|
+
* project row for the same directory, and the server keys on
|
|
114
|
+
* `(machineId, repoPathHash)` so even a lost cache converges rather than
|
|
115
|
+
* duplicating.
|
|
116
|
+
*/
|
|
117
|
+
async function resolveProjectId(store, client, machineId, cwd, name) {
|
|
118
|
+
const hash = (0, machine_store_1.repoPathHash)(cwd, sha256);
|
|
119
|
+
const cached = store.projectFor(hash);
|
|
120
|
+
if (cached)
|
|
121
|
+
return cached;
|
|
122
|
+
const r = await client.registerProject({ machineId, name, repoPathHash: hash });
|
|
123
|
+
store.rememberProject(hash, r.project.id);
|
|
124
|
+
return r.project.id;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Resolve this repo to a project, asking before creating one that does not exist.
|
|
128
|
+
*
|
|
129
|
+
* Registration is the one irreversible thing the daemon does on startup, and it
|
|
130
|
+
* used to happen silently: `looply daemon` in the wrong folder — a home
|
|
131
|
+
* directory, a scratch clone, a tarball someone unpacked — put a permanent row
|
|
132
|
+
* on the server that nobody asked for and, until recently, nothing could remove.
|
|
133
|
+
* A directory this machine already knows is never asked about, so the ordinary
|
|
134
|
+
* case is unchanged and the question only appears when something new is about to
|
|
135
|
+
* be created.
|
|
136
|
+
*/
|
|
137
|
+
async function resolveProjectIdWithConsent(store, client, machineId, cwd, name, gate) {
|
|
138
|
+
const cached = store.projectFor((0, machine_store_1.repoPathHash)(cwd, sha256));
|
|
139
|
+
if (cached)
|
|
140
|
+
return { kind: 'known', projectId: cached };
|
|
141
|
+
if (!gate.assumeYes) {
|
|
142
|
+
if (!gate.ask)
|
|
143
|
+
return { kind: 'refused', reason: 'no-terminal' };
|
|
144
|
+
if (!(await gate.ask({ cwd, name })))
|
|
145
|
+
return { kind: 'refused', reason: 'declined' };
|
|
146
|
+
}
|
|
147
|
+
return { kind: 'registered', projectId: await resolveProjectId(store, client, machineId, cwd, name) };
|
|
148
|
+
}
|
|
149
|
+
/** Blank means no. The safe answer to "shall I create something permanent" is the default one. */
|
|
150
|
+
function isYes(raw) {
|
|
151
|
+
return /^(y|yes)$/i.test(raw.trim());
|
|
152
|
+
}
|
|
153
|
+
function renderRegisterAsk(info) {
|
|
154
|
+
return `\n${(0, ui_1.box)([
|
|
155
|
+
'No looply project is registered for this directory:',
|
|
156
|
+
'',
|
|
157
|
+
` ${(0, ui_1.bold)(info.cwd)}`,
|
|
158
|
+
'',
|
|
159
|
+
(0, ui_1.grey)('Saying yes creates one on the server, called'),
|
|
160
|
+
` ${(0, ui_1.bold)(info.name)}`,
|
|
161
|
+
(0, ui_1.grey)('and it stays there until somebody deletes it.'),
|
|
162
|
+
], { title: 'register this directory?', tone: ui_1.volt })}\n`;
|
|
163
|
+
}
|
|
164
|
+
/** The terminal half: print the box, read one answer, put the prompt away. */
|
|
165
|
+
function nodeRegisterAsk(write) {
|
|
166
|
+
return async (info) => {
|
|
167
|
+
write(renderRegisterAsk(info));
|
|
168
|
+
const asker = (0, setup_prompt_1.nodeAsker)();
|
|
169
|
+
try {
|
|
170
|
+
return isYes(await asker.question(` ${(0, ui_1.volt)('›')} ${(0, ui_1.dim)('[y/N]')} `));
|
|
171
|
+
}
|
|
172
|
+
finally {
|
|
173
|
+
asker.close();
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
/** How long a person's Start waits for the coordinator to place its goal. */
|
|
178
|
+
exports.PLACEMENT_WAIT_MS = 60_000;
|
|
179
|
+
/**
|
|
180
|
+
* M6-17: WHAT THE DAEMON SAYS WHEN THE PHONE ASKS FOR A VERB THIS BUILD DOES
|
|
181
|
+
* NOT CARRY OUT. The refusal was a bare `refused` — the phone saw the word and
|
|
182
|
+
* nothing else (the audit's I-06), while the review it was tapping on writes
|
|
183
|
+
* one row on the server and reaches no machine (J-03). The kit's plan says the
|
|
184
|
+
* wiring is a later, governed cycle and that the honest first step is the
|
|
185
|
+
* truth on both surfaces: the reason travels on the wire the way M6-10's
|
|
186
|
+
* ceiling refusal does, stored with the receipt, replayed on a redelivery.
|
|
187
|
+
*/
|
|
188
|
+
exports.SKIP_REFUSED_REASON = 'skip is not carried out by this build — a run picks its own next item; stop it if the current one should not go on';
|
|
189
|
+
function reviewRefusedReason(kind) {
|
|
190
|
+
return (`${kind} is not carried out by this machine — a review is recorded on the server and reaches no machine yet; ` +
|
|
191
|
+
'nothing has been pushed or reverted');
|
|
192
|
+
}
|
|
193
|
+
function makeCommandHandler(log, starter, control,
|
|
194
|
+
/**
|
|
195
|
+
* What this machine has already finished with.
|
|
196
|
+
*
|
|
197
|
+
* Optional so every existing caller and test keeps working without one; when
|
|
198
|
+
* present it is what stops a redelivered command being carried out twice.
|
|
199
|
+
*/
|
|
200
|
+
receipts,
|
|
201
|
+
/** The project this daemon is for, when it knows. Commands for others are not ours. */
|
|
202
|
+
projectId,
|
|
203
|
+
/**
|
|
204
|
+
* How this machine lets go of a goal's work.
|
|
205
|
+
*
|
|
206
|
+
* Optional like `starter`, and for the same reason: a caller that cannot
|
|
207
|
+
* carry a release out must leave it queued rather than ack something it
|
|
208
|
+
* never did.
|
|
209
|
+
*/
|
|
210
|
+
releaser,
|
|
211
|
+
/**
|
|
212
|
+
* Where a grant of autonomy is written down.
|
|
213
|
+
*
|
|
214
|
+
* Optional like the rest, and for the same reason: a daemon with no store to
|
|
215
|
+
* put it in must leave the command queued rather than ack a permission it
|
|
216
|
+
* did not record. A grant that was acked and not stored is the worst of both
|
|
217
|
+
* -- the phone shows it was received, and nothing on this machine knows.
|
|
218
|
+
*/
|
|
219
|
+
autonomy,
|
|
220
|
+
/**
|
|
221
|
+
* THE LAST AUTHORITY BOUNDARY BEFORE A RUN EXISTS.
|
|
222
|
+
*
|
|
223
|
+
* Asked immediately before the Start seam, and not a moment earlier, because
|
|
224
|
+
* everything between the question and the spawn is a window in which a person
|
|
225
|
+
* can be asked something. A gate read at the top of `dispatch` would answer
|
|
226
|
+
* about a world that had already moved.
|
|
227
|
+
*
|
|
228
|
+
* Optional like everything beside it: a daemon that cannot ask has no
|
|
229
|
+
* approvals to be held by, and every existing caller and test keeps working
|
|
230
|
+
* without one.
|
|
231
|
+
*/
|
|
232
|
+
approvals,
|
|
233
|
+
/**
|
|
234
|
+
* M6-9: WHERE THE COORDINATOR PLACED A GOAL THIS CADENCE, when it has.
|
|
235
|
+
*
|
|
236
|
+
* A person's Start on a placed goal goes to that checkout — the same answer
|
|
237
|
+
* the autonomy tick hands the launcher — and that checkout's lease is the
|
|
238
|
+
* explicit-start carrier. Found live (M6-8): without this the person's run
|
|
239
|
+
* landed in the project checkout, found an empty queue, compiled through the
|
|
240
|
+
* agent and ended `aborted`, while the work it was resumed for sat in the
|
|
241
|
+
* checkout the machine had made for it. Optional so every existing caller is
|
|
242
|
+
* unchanged; absent, or a goal placed nowhere, is today's path.
|
|
243
|
+
*/
|
|
244
|
+
workspaceFor,
|
|
245
|
+
/**
|
|
246
|
+
* M6-10: the owner's Start ceiling per grant (`machine.programme.limits.starts_per_grant`).
|
|
247
|
+
* The daemon's own ceiling was core's MAX; a declared one lower than that is
|
|
248
|
+
* honoured here — a grant asking for more is refused, naming the limit,
|
|
249
|
+
* never clamped. Absent, core's MAX as before.
|
|
250
|
+
*/
|
|
251
|
+
startCeiling,
|
|
252
|
+
/**
|
|
253
|
+
* M6-14: whether the coordinator is still placing a goal (see
|
|
254
|
+
* {@link PlacementInProgress}). Optional so every existing caller is
|
|
255
|
+
* unchanged; absent, a goal placed nowhere goes to the project checkout as
|
|
256
|
+
* before.
|
|
257
|
+
*/
|
|
258
|
+
placing) {
|
|
259
|
+
// When each waiting Start was first seen, by command id — so the bound is
|
|
260
|
+
// measured from the first poll that found the goal being placed.
|
|
261
|
+
const waitingSince = new Map();
|
|
262
|
+
return async (command) => {
|
|
263
|
+
// A machine can host several projects, and `pop` scopes only to the
|
|
264
|
+
// machine. Without this a daemon running in project A would faithfully
|
|
265
|
+
// carry out a start-run meant for project B — spawning a run in the wrong
|
|
266
|
+
// repository. Left for the daemon that owns it rather than refused.
|
|
267
|
+
if (projectId !== undefined && command.projectId !== projectId) {
|
|
268
|
+
log(`daemon : ${command.kind} is for another project here — leaving it queued.`);
|
|
269
|
+
return 'retry';
|
|
270
|
+
}
|
|
271
|
+
// Already finished with. The lease cannot answer this: it stops existing
|
|
272
|
+
// when the run ends, so a start-run redelivered after its run completed
|
|
273
|
+
// would find a free tree and start a second one.
|
|
274
|
+
const priorReceipt = receipts?.seen(command.id);
|
|
275
|
+
if (priorReceipt) {
|
|
276
|
+
log(`daemon : ${command.kind} was already carried out here (${priorReceipt.outcome}).`);
|
|
277
|
+
// The SAME answer as the first time, reason included. A redelivery is a
|
|
278
|
+
// repeated question, not a new one, and answering it with a barer version
|
|
279
|
+
// of the original would make a lost ack quietly cost the explanation.
|
|
280
|
+
return priorReceipt.outcome === 'refused' && priorReceipt.reason !== undefined
|
|
281
|
+
? { disposition: 'refused', reason: priorReceipt.reason }
|
|
282
|
+
: priorReceipt.outcome;
|
|
283
|
+
}
|
|
284
|
+
// An unreadable receipts file means we cannot prove a consequential command
|
|
285
|
+
// has not already run. Refused rather than retried — retrying will not make
|
|
286
|
+
// the file readable — and terminal, so it does not repeat every three
|
|
287
|
+
// seconds for the redelivery window. Deleting the file clears it.
|
|
288
|
+
if (receipts?.status() === 'unreadable' && !(0, command_receipts_1.isRepeatable)(command.kind)) {
|
|
289
|
+
log(`daemon : cannot tell whether ${command.kind} already ran here — refusing it.`);
|
|
290
|
+
log(' .looply/state/commands.json is unreadable; delete it to reset.');
|
|
291
|
+
return 'refused';
|
|
292
|
+
}
|
|
293
|
+
const decision = await dispatch(command);
|
|
294
|
+
const disposition = typeof decision === 'string' ? decision : decision.disposition;
|
|
295
|
+
const reason = typeof decision === 'string' ? undefined : decision.reason;
|
|
296
|
+
// Recorded before the ack, so a crash between the two leaves the receipt
|
|
297
|
+
// rather than losing it: the redelivery that follows is then recognised
|
|
298
|
+
// instead of re-executed. The reason goes down with it for the same
|
|
299
|
+
// reason — a receipt that outlives the ack has to be able to answer as
|
|
300
|
+
// fully as the ack would have.
|
|
301
|
+
if (receipts && disposition !== 'retry') {
|
|
302
|
+
await receipts.record(command.id, command.kind, disposition, reason);
|
|
303
|
+
}
|
|
304
|
+
return decision;
|
|
305
|
+
};
|
|
306
|
+
async function dispatch(command) {
|
|
307
|
+
switch (command.kind) {
|
|
308
|
+
case 'start-run': {
|
|
309
|
+
if (!starter) {
|
|
310
|
+
log('daemon : start-run is not carried out here — leaving it queued.');
|
|
311
|
+
return 'retry';
|
|
312
|
+
}
|
|
313
|
+
// Read defensively: the payload is `unknown` all the way from the
|
|
314
|
+
// phone, stored verbatim as Json, handed back through Redis, and this
|
|
315
|
+
// build may be older or newer than the one that wrote the row.
|
|
316
|
+
const read = (0, run_launcher_1.readStartPayload)(command.payload);
|
|
317
|
+
if (!read.ok) {
|
|
318
|
+
// Refused rather than run, because the other option is starting an
|
|
319
|
+
// UNBOUND run over the whole queue while the phone waits for a goal
|
|
320
|
+
// that will never bind — the failure this reader exists to prevent.
|
|
321
|
+
// Terminal: a durable row will not read any better in three seconds.
|
|
322
|
+
log(`daemon : start-run ${read.problem} — refusing it.`);
|
|
323
|
+
return 'refused';
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* ASKED HERE, WITH NOTHING BETWEEN THIS AND THE SPAWN.
|
|
327
|
+
*
|
|
328
|
+
* A person tapping Start while a question of theirs is still open would
|
|
329
|
+
* buy a run that meets the same boundary and files nothing new — the
|
|
330
|
+
* request key is derived from the run — so it would spend a Start to
|
|
331
|
+
* arrive exactly where it began. And a person tapping Start after an
|
|
332
|
+
* answer is the ONE thing that resumes a pursuit, so this is also where
|
|
333
|
+
* that answer is picked up.
|
|
334
|
+
*
|
|
335
|
+
* `undefined` when this daemon cannot ask; the run itself still refuses
|
|
336
|
+
* to file anything it cannot get an approval for, so the boundary is
|
|
337
|
+
* never crossed silently.
|
|
338
|
+
*/
|
|
339
|
+
// M6-9: a goal the coordinator has placed is started where it was placed.
|
|
340
|
+
const placed = read.start.goalId === undefined ? undefined : workspaceFor?.(read.start.goalId);
|
|
341
|
+
// M6-14: a goal the coordinator is still placing — its checkout asked
|
|
342
|
+
// for or made, the placement not yet recorded — is not a goal placed
|
|
343
|
+
// nowhere. The Start waits for the placement rather than running in
|
|
344
|
+
// the project checkout; past the bound it is refused, with the reason.
|
|
345
|
+
if (placed === undefined && read.start.goalId !== undefined && placing?.placing(read.start.goalId) === true) {
|
|
346
|
+
const now = placing.now?.() ?? Date.now();
|
|
347
|
+
const since = waitingSince.get(command.id) ?? now;
|
|
348
|
+
waitingSince.set(command.id, since);
|
|
349
|
+
const waitMs = placing.waitMs ?? exports.PLACEMENT_WAIT_MS;
|
|
350
|
+
if (now - since < waitMs) {
|
|
351
|
+
log(`daemon : start-run for ${read.start.goalId} waits for the coordinator to place it`);
|
|
352
|
+
return 'retry';
|
|
353
|
+
}
|
|
354
|
+
waitingSince.delete(command.id);
|
|
355
|
+
const reason = `the coordinator did not place ${read.start.goalId} within ${Math.round(waitMs / 1000)} s — ` +
|
|
356
|
+
'try again, or turn the coordinator off to start it here';
|
|
357
|
+
log(`daemon : start-run refused — ${reason}`);
|
|
358
|
+
return { disposition: 'refused', reason };
|
|
359
|
+
}
|
|
360
|
+
waitingSince.delete(command.id);
|
|
361
|
+
if (placed !== undefined) {
|
|
362
|
+
log(`daemon : start-run for ${read.start.goalId} goes to the checkout the coordinator placed it in — ${placed}`);
|
|
363
|
+
}
|
|
364
|
+
const decision = approvals === undefined ? {} : await approvals(read.start, command.id, placed);
|
|
365
|
+
if (decision.refuse) {
|
|
366
|
+
log(`daemon : start-run refused — ${decision.refuse.reason}`);
|
|
367
|
+
// Terminal for a definite answer, queued for one nobody obtained. A
|
|
368
|
+
// person whose approval is still open taps Start again when they have
|
|
369
|
+
// decided; a machine that could not reach the server may reach it on
|
|
370
|
+
// the next poll.
|
|
371
|
+
return decision.refuse.terminal
|
|
372
|
+
? { disposition: 'refused', reason: decision.refuse.reason }
|
|
373
|
+
: 'retry';
|
|
374
|
+
}
|
|
375
|
+
// The command IS the request here, and says so. `requestId` is what
|
|
376
|
+
// every durable record is keyed by; `origin` is what the machine
|
|
377
|
+
// consults when it needs to know whether a person is actually behind
|
|
378
|
+
// this Start. For a human command the two carry the same string, which
|
|
379
|
+
// is exactly what keeps every existing lease, mark and receipt readable.
|
|
380
|
+
const started = await starter.start({
|
|
381
|
+
...read.start,
|
|
382
|
+
requestId: command.id,
|
|
383
|
+
origin: { kind: 'human-command', commandId: command.id },
|
|
384
|
+
...(placed === undefined ? {} : { workspace: placed }),
|
|
385
|
+
...(decision.governedApprovalId === undefined
|
|
386
|
+
? {}
|
|
387
|
+
: { governedApprovalId: decision.governedApprovalId }),
|
|
388
|
+
...(decision.resumedFromApprovalId === undefined
|
|
389
|
+
? {}
|
|
390
|
+
: { resumedFromApprovalId: decision.resumedFromApprovalId }),
|
|
391
|
+
});
|
|
392
|
+
if (!started.started) {
|
|
393
|
+
// A refusal that retrying cannot fix — something is already working
|
|
394
|
+
// here, or we cannot prove otherwise. Left queued it would be offered
|
|
395
|
+
// again every three seconds for the whole redelivery window, which is
|
|
396
|
+
// noise rather than resilience.
|
|
397
|
+
if ('terminal' in started) {
|
|
398
|
+
log(`daemon : start-run refused — ${started.reason}`);
|
|
399
|
+
// The run's own words, carried through. This is the line the phase
|
|
400
|
+
// exists for: without the reason, "this goal's work is done" and
|
|
401
|
+
// "nothing here can move without you" both reached the phone as a
|
|
402
|
+
// bare `refused`, which is the same as saying nothing.
|
|
403
|
+
return { disposition: 'refused', reason: started.reason };
|
|
404
|
+
}
|
|
405
|
+
if ('waiting' in started) {
|
|
406
|
+
// A child is going and has not proven a Run yet. Left queued on
|
|
407
|
+
// purpose: acking here is precisely the lie this phase removed, and
|
|
408
|
+
// the launcher will not spawn a second child for the same request.
|
|
409
|
+
log(`daemon : ${started.reason} — waiting for it to prove it before acking.`);
|
|
410
|
+
return 'retry';
|
|
411
|
+
}
|
|
412
|
+
log(`daemon : start-run failed — ${started.reason}. Leaving it queued.`);
|
|
413
|
+
return 'retry';
|
|
414
|
+
}
|
|
415
|
+
// Reached only with durable proof that this command's run crossed the
|
|
416
|
+
// start boundary. The run id comes from that proof, so it names the run
|
|
417
|
+
// the phone's tap actually produced rather than one we assume.
|
|
418
|
+
log(`daemon : run confirmed started${started.runId === undefined ? '' : ` (${started.runId})`}${started.pid === undefined ? '' : ` (pid ${started.pid})`}${read.start.goalId === undefined ? '' : ` for goal ${read.start.goalId}`}${read.start.hours === undefined ? '' : `, ${read.start.hours}h budget`}${started.already ? ' — already in progress here' : ''}.`);
|
|
419
|
+
return 'done';
|
|
420
|
+
}
|
|
421
|
+
case 'release-goal-items': {
|
|
422
|
+
if (!releaser) {
|
|
423
|
+
log('daemon : release-goal-items is not carried out here — leaving it queued.');
|
|
424
|
+
return 'retry';
|
|
425
|
+
}
|
|
426
|
+
const named = releaseOf(command.payload);
|
|
427
|
+
if (named === null) {
|
|
428
|
+
// The server builds this payload itself and puts exactly one field in
|
|
429
|
+
// it, so an unreadable one is a build that disagrees with the server
|
|
430
|
+
// rather than a transient fault. Terminal.
|
|
431
|
+
log('daemon : release-goal-items names a goal this build cannot read — refusing it.');
|
|
432
|
+
return 'refused';
|
|
433
|
+
}
|
|
434
|
+
const out = await releaser.release(named.goalId, named.goalVersion);
|
|
435
|
+
if ('busy' in out) {
|
|
436
|
+
// Left queued on purpose: the queue file is shared with a live run,
|
|
437
|
+
// and this is the one thing that must not race it.
|
|
438
|
+
log(`daemon : ${out.busy}.`);
|
|
439
|
+
return 'retry';
|
|
440
|
+
}
|
|
441
|
+
log(`daemon : goal ${named.goalId} let go of ${out.released} item(s)` +
|
|
442
|
+
(out.heldByARun ? `, ${out.heldByARun} still being worked on` : '') +
|
|
443
|
+
(out.keptAsRecord ? `, ${out.keptAsRecord} finished and kept` : '') +
|
|
444
|
+
'.');
|
|
445
|
+
return 'done';
|
|
446
|
+
}
|
|
447
|
+
case 'authorize-goal-autonomy': {
|
|
448
|
+
// WRITTEN DOWN, AND NOTHING ELSE.
|
|
449
|
+
//
|
|
450
|
+
// Not a Start. `start-run` authorizes one run and this authorizes
|
|
451
|
+
// several, but the acts are separate on purpose: turning a grant into
|
|
452
|
+
// a Start here would make the grant indistinguishable from the tap
|
|
453
|
+
// that begins work, and the whole point of a bounded permission is
|
|
454
|
+
// that the machine decides WHEN to spend it, later, under its own
|
|
455
|
+
// budget. Nothing in 6D1R spends one; there is no controller yet.
|
|
456
|
+
if (!autonomy) {
|
|
457
|
+
log('daemon : authorize-goal-autonomy is not carried out here — leaving it queued.');
|
|
458
|
+
return 'retry';
|
|
459
|
+
}
|
|
460
|
+
const ceiling = startCeiling ?? core_1.MAX_AUTONOMOUS_STARTS;
|
|
461
|
+
const asked = askedStarts(command.payload);
|
|
462
|
+
if (asked !== null && asked > ceiling) {
|
|
463
|
+
log(`daemon : the grant asks for ${asked} Start(s) and this machine's programme limit is ${ceiling} — refusing it.`);
|
|
464
|
+
return {
|
|
465
|
+
disposition: 'refused',
|
|
466
|
+
reason: `this machine's programme limit is ${ceiling} Start(s) per grant; the grant asked for ${asked}`,
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
const grant = grantOf(command.payload, ceiling);
|
|
470
|
+
if (grant === null) {
|
|
471
|
+
// The server builds this payload itself, so an unreadable one is a
|
|
472
|
+
// build that disagrees with the server rather than a transient
|
|
473
|
+
// fault. Terminal, with a reason the phone can show.
|
|
474
|
+
log('daemon : the grant is in a shape this build cannot read — refusing it.');
|
|
475
|
+
return {
|
|
476
|
+
disposition: 'refused',
|
|
477
|
+
reason: 'this machine could not read the terms of that permission',
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
try {
|
|
481
|
+
// Idempotent by controllerId. A redelivery of the same grant writes
|
|
482
|
+
// nothing at all; a controllerId already held whose TERMS have moved
|
|
483
|
+
// is refused rather than adopted, because from 6D1R-B one
|
|
484
|
+
// controllerId is one act of permission forever and a second
|
|
485
|
+
// authorization arrives as an id this machine has never seen.
|
|
486
|
+
await autonomy.adopt(grant);
|
|
487
|
+
}
|
|
488
|
+
catch (e) {
|
|
489
|
+
// NOT EVERY FAILURE IS AN ANSWER. A refusal is durable and the person
|
|
490
|
+
// has to notice it and authorize again; a locked file is neither
|
|
491
|
+
// their fault nor a lasting fact. Adopt is idempotent by
|
|
492
|
+
// controllerId, so leaving it queued costs nothing and the redelivery
|
|
493
|
+
// writes nothing if the first one did land.
|
|
494
|
+
if (e.transient === true) {
|
|
495
|
+
log('daemon : the grant could not be recorded just now — leaving it queued.');
|
|
496
|
+
return 'retry';
|
|
497
|
+
}
|
|
498
|
+
log(`daemon : the grant could not be recorded — refusing it.`);
|
|
499
|
+
return { disposition: 'refused', reason: e.message };
|
|
500
|
+
}
|
|
501
|
+
log(`daemon : autonomy recorded for goal ${grant.goalId} at version ` +
|
|
502
|
+
`${grant.goalVersion} — up to ${grant.maxStarts} Start(s), until ${grant.expiresAt}. ` +
|
|
503
|
+
'Nothing is started by this.');
|
|
504
|
+
return 'done';
|
|
505
|
+
}
|
|
506
|
+
case 'pause':
|
|
507
|
+
case 'resume':
|
|
508
|
+
case 'stop': {
|
|
509
|
+
if (!control) {
|
|
510
|
+
log(`daemon : ${command.kind} is not carried out here — leaving it queued.`);
|
|
511
|
+
return 'retry';
|
|
512
|
+
}
|
|
513
|
+
// The run is a separate, detached process (see run-launcher.ts), so the
|
|
514
|
+
// request goes on disk and the runner picks it up between rounds. It is
|
|
515
|
+
// written even when no run is in progress: the phone's intent outlives
|
|
516
|
+
// the moment, and a pause that only lands if a round happens to be
|
|
517
|
+
// running is a pause you cannot trust.
|
|
518
|
+
await control.request(command.kind, command.actor);
|
|
519
|
+
log(command.kind === 'pause'
|
|
520
|
+
? 'daemon : pause written — the current round finishes, then the run holds.'
|
|
521
|
+
: command.kind === 'resume'
|
|
522
|
+
? 'daemon : resume written — a holding run picks up its next round.'
|
|
523
|
+
: 'daemon : stop written — the run ends after the current round.');
|
|
524
|
+
return 'done';
|
|
525
|
+
}
|
|
526
|
+
case 'scan': {
|
|
527
|
+
if (!starter?.scan) {
|
|
528
|
+
log('daemon : scan is not carried out here — leaving it queued.');
|
|
529
|
+
return 'retry';
|
|
530
|
+
}
|
|
531
|
+
const scan = await starter.scan();
|
|
532
|
+
if (!scan.started) {
|
|
533
|
+
log(`daemon : scan failed — ${scan.reason}. Leaving it queued.`);
|
|
534
|
+
return 'retry';
|
|
535
|
+
}
|
|
536
|
+
log(scan.already
|
|
537
|
+
? 'daemon : scan received, but a scan is already running here.'
|
|
538
|
+
: `daemon : scan started${scan.pid === undefined ? '' : ` (pid ${scan.pid})`} — its passes stream to your phone as they finish.`);
|
|
539
|
+
return 'done';
|
|
540
|
+
}
|
|
541
|
+
// The three verbs this build does not implement. `refused` rather than
|
|
542
|
+
// `retry`, and the distinction is now load-bearing: commands are really
|
|
543
|
+
// redelivered until they reach a terminal state, so leaving these unacked
|
|
544
|
+
// would have the server offer them again every three seconds for the life
|
|
545
|
+
// of the machine. Refusing ends them, and says why.
|
|
546
|
+
case 'skip':
|
|
547
|
+
log(`daemon : skip is not carried out by this version — refusing it: ${exports.SKIP_REFUSED_REASON}.`);
|
|
548
|
+
return { disposition: 'refused', reason: exports.SKIP_REFUSED_REASON };
|
|
549
|
+
case 'approve':
|
|
550
|
+
case 'undo': {
|
|
551
|
+
// M6-17: the reason on the wire, not only in this log.
|
|
552
|
+
const reason = reviewRefusedReason(command.kind);
|
|
553
|
+
log(`daemon : ${command.kind} is not carried out by this version — refusing it: ${reason}.`);
|
|
554
|
+
return { disposition: 'refused', reason };
|
|
555
|
+
}
|
|
556
|
+
default:
|
|
557
|
+
// A newer phone talking to an older daemon. Refused rather than left
|
|
558
|
+
// queued: the tap will be stale long before anyone upgrades the CLI,
|
|
559
|
+
// and an instruction nothing here can dispatch is better ended with a
|
|
560
|
+
// reason than offered forever.
|
|
561
|
+
log(`daemon : unrecognised command ${String(command.kind)} — refusing it.`);
|
|
562
|
+
return 'refused';
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
async function daemonCommand(opts) {
|
|
567
|
+
const store = new machine_store_1.MachineStore((0, machine_store_1.machineFilePath)());
|
|
568
|
+
const identity = store.load();
|
|
569
|
+
if (!identity) {
|
|
570
|
+
opts.log('daemon : this machine has not paired. Run `looply pair` first.');
|
|
571
|
+
return 1;
|
|
572
|
+
}
|
|
573
|
+
const client = new server_client_1.ServerClient({ baseUrl: identity.serverUrl }).withToken(identity.token);
|
|
574
|
+
let projectId;
|
|
575
|
+
try {
|
|
576
|
+
const resolved = await resolveProjectIdWithConsent(store, client, identity.machineId, opts.cwd, opts.config.project.name, opts.gate ?? {});
|
|
577
|
+
if (resolved.kind === 'refused') {
|
|
578
|
+
opts.log(resolved.reason === 'declined'
|
|
579
|
+
? 'daemon : nothing registered, nothing started. Run it inside the project you meant.'
|
|
580
|
+
: 'daemon : this directory is not a registered project and there is no terminal to ask on. `looply daemon --yes` registers it.');
|
|
581
|
+
return 1;
|
|
582
|
+
}
|
|
583
|
+
projectId = resolved.projectId;
|
|
584
|
+
}
|
|
585
|
+
catch (e) {
|
|
586
|
+
if (e instanceof server_client_1.ServerError && e.isAuth) {
|
|
587
|
+
opts.log('daemon : this machine\'s access was revoked. Run `looply pair` again.');
|
|
588
|
+
return 1;
|
|
589
|
+
}
|
|
590
|
+
if (e instanceof server_client_1.ServerError && e.isOffline) {
|
|
591
|
+
// Registration is the one thing that genuinely needs the server, so this
|
|
592
|
+
// is a refusal rather than a degraded start — a daemon with no project id
|
|
593
|
+
// has nothing to attach its events to.
|
|
594
|
+
opts.log(`daemon : cannot reach ${identity.serverUrl} to register this project.`);
|
|
595
|
+
return 1;
|
|
596
|
+
}
|
|
597
|
+
throw e;
|
|
598
|
+
}
|
|
599
|
+
opts.log(`machine : ${identity.machineId}`);
|
|
600
|
+
opts.log(`project : ${projectId}`);
|
|
601
|
+
opts.log(`server : ${identity.serverUrl}`);
|
|
602
|
+
opts.log(`mode : ${(0, programme_mode_1.describeProgramme)(opts.config)}`);
|
|
603
|
+
/**
|
|
604
|
+
* 🔴 BOTH CALLBACKS, BECAUSE NEITHER WAS EVER WIRED HERE.
|
|
605
|
+
*
|
|
606
|
+
* The RUN wires both correctly and says what it is doing. The daemon
|
|
607
|
+
* constructed its watcher with neither, so a first Ctrl-C printed NOTHING —
|
|
608
|
+
* a person saw a daemon that appeared to ignore them, pressed again, saw
|
|
609
|
+
* nothing again, and needed a third press with no explanation for any of it.
|
|
610
|
+
*
|
|
611
|
+
* ⚠️ AND THE FIRST PRESS IS STILL GRACEFUL. The daemon holds delivery state
|
|
612
|
+
* — cursors, receipts, the outbound buffer — and killing it mid-flush loses
|
|
613
|
+
* the tail of a night's ledger. The distinction between the two presses is
|
|
614
|
+
* the whole reason to say anything at all.
|
|
615
|
+
*/
|
|
616
|
+
const signals = new signals_1.SignalWatcher({
|
|
617
|
+
proc: (0, signals_1.nodeSignalSource)(),
|
|
618
|
+
onGraceful: () => {
|
|
619
|
+
opts.log('');
|
|
620
|
+
opts.log('daemon : stopping after this tick. Press Ctrl-C again to stop now.');
|
|
621
|
+
},
|
|
622
|
+
onForce: () => {
|
|
623
|
+
opts.log('daemon : stopping now — anything not yet shipped stays on disk for the next start.');
|
|
624
|
+
},
|
|
625
|
+
});
|
|
626
|
+
signals.install();
|
|
627
|
+
const receipts = new command_receipts_1.CommandReceipts((0, node_path_1.join)(opts.cwd, git_driver_1.STATE_DIR, 'state'), ledger_store_1.nodeLedgerFs);
|
|
628
|
+
if ((await receipts.open()) === 'unreadable') {
|
|
629
|
+
opts.log('daemon : .looply/state/commands.json is unreadable — commands that');
|
|
630
|
+
opts.log(' spawn work will be refused until it is deleted.');
|
|
631
|
+
}
|
|
632
|
+
// The standing daemon is the one process that owns delivery state. Its
|
|
633
|
+
// usage line has always said it ships the ledger and until now nothing fed
|
|
634
|
+
// it: no producer was wired, so it flushed an empty buffer forever while a
|
|
635
|
+
// crashed run's tail sat on disk with nothing that would ever look at it.
|
|
636
|
+
const cursors = new cursor_store_1.CursorStore((0, node_path_1.join)(opts.cwd, git_driver_1.STATE_DIR, 'state'), ledger_store_1.nodeLedgerFs);
|
|
637
|
+
if ((await cursors.open()) === 'unreadable') {
|
|
638
|
+
opts.log('daemon : .looply/state/cursor.json is unreadable — replaying from the start.');
|
|
639
|
+
opts.log(' Duplicates are discarded by the server, so this is safe, only slower.');
|
|
640
|
+
}
|
|
641
|
+
// What a previous run left behind, settled before the loop starts.
|
|
642
|
+
//
|
|
643
|
+
// After `projectId` is resolved and before the daemon runs, and both halves
|
|
644
|
+
// of that are load-bearing rather than tidy. After, because a recovery event
|
|
645
|
+
// stamped with anything but the id the server registered is dropped by ingest
|
|
646
|
+
// on arrival — the exact defect `run-identity.spec.ts` guards. Before, so the
|
|
647
|
+
// first replay pass ships what this appends, rather than it waiting a tick.
|
|
648
|
+
//
|
|
649
|
+
// The append goes through the ledger rather than straight into the daemon's
|
|
650
|
+
// buffer on purpose: it is what rewrites `status.json`, so `looply status`
|
|
651
|
+
// locally stops reading `live`, and it survives a daemon that dies before it
|
|
652
|
+
// ever flushes. An `enqueue` would do neither.
|
|
653
|
+
/**
|
|
654
|
+
* Built once and called BOTH at startup and on the loop's sweep.
|
|
655
|
+
*
|
|
656
|
+
* Startup alone was enough while nothing waited on a run: if the last run had
|
|
657
|
+
* been killed, the next daemon start tidied up. It stops being enough the
|
|
658
|
+
* moment something waits for `run.ended` — a SIGKILLed run never writes one,
|
|
659
|
+
* so on a daemon that stays up for a week that run stays un-ended for a week,
|
|
660
|
+
* and whatever is waiting waits for ever.
|
|
661
|
+
*
|
|
662
|
+
* Idempotent by contract: it answers `idle` when there is no lease, when the
|
|
663
|
+
* run is live, or when the ending is already written.
|
|
664
|
+
*/
|
|
665
|
+
/**
|
|
666
|
+
* 🔴 EVERY CHECKOUT THIS PROJECT EXECUTES IN, NOT ONLY THIS ONE. The sweep
|
|
667
|
+
* was built over `join(opts.cwd, STATE_DIR, 'state')` alone, so a run the
|
|
668
|
+
* coordinator placed in a linked worktree was never given its ending when it
|
|
669
|
+
* died — see `stale-run-sweep.ts`. The same factory drives the end-to-end
|
|
670
|
+
* proof, so what is proven is what runs here.
|
|
671
|
+
*/
|
|
672
|
+
const sweep = (0, stale_run_sweep_1.makeStaleRunSweep)({ cwd: opts.cwd, projectId, log: opts.log });
|
|
673
|
+
/**
|
|
674
|
+
* M6-5 — THE FOLLOW-UP ON A RUN THAT IGNORES ITS STOP (STATE §5 item 16).
|
|
675
|
+
*
|
|
676
|
+
* Same cadence as the sweep, after it: the sweep closes runs that died, this
|
|
677
|
+
* names runs that were asked to stop and began a round anyway — once, in the
|
|
678
|
+
* log and as ONE `alert` on the run's own ledger, which ships to the phone
|
|
679
|
+
* on the next flush. It never ends a run; that stays a person's act.
|
|
680
|
+
*/
|
|
681
|
+
const followUpOnStops = (0, stop_follow_up_1.nodeStopFollowUp)({ projectState: (0, git_driver_1.projectStateDir)(opts.cwd), projectId, log: opts.log });
|
|
682
|
+
const recoverStaleRun = async () => {
|
|
683
|
+
await sweep();
|
|
684
|
+
await followUpOnStops();
|
|
685
|
+
};
|
|
686
|
+
await recoverStaleRun();
|
|
687
|
+
// The server's goals, mirrored locally. Loaded before the loop so a daemon
|
|
688
|
+
// that starts offline still has whatever the last one saw, rather than an
|
|
689
|
+
// empty mirror it cannot distinguish from "this project has no goals".
|
|
690
|
+
//
|
|
691
|
+
// Read-only, and read by nothing yet: this phase writes the file and stops
|
|
692
|
+
// there. A goal is not consulted to decide what work to do until a later one.
|
|
693
|
+
const goals = new goal_store_1.GoalStore((0, git_driver_1.projectStateDir)(opts.cwd), ledger_store_1.nodeLedgerFs, opts.log);
|
|
694
|
+
await goals.load();
|
|
695
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
696
|
+
// THE AUTONOMY GRAPH, BUILT ONCE.
|
|
697
|
+
//
|
|
698
|
+
// Every authority the controller consults is the real one: the grant store,
|
|
699
|
+
// the attempt claims, the durable start and refusal marks, the ledger's
|
|
700
|
+
// run-ended reader, and the machine-authenticated server. Nothing here is a
|
|
701
|
+
// stub and nothing defaults to "authorized" — a controller that could not
|
|
702
|
+
// reach the server must not be able to spend a person's Start on the strength
|
|
703
|
+
// of a fallback.
|
|
704
|
+
/**
|
|
705
|
+
* 🔴 THE AUTHORITY'S WRITER AND ITS READERS DISAGREED ABOUT WHERE IT IS.
|
|
706
|
+
*
|
|
707
|
+
* This was `join(opts.cwd, STATE_DIR, 'state')` — a path built from the
|
|
708
|
+
* literal working directory — while `bin.ts`, the run and the control store
|
|
709
|
+
* all use `projectStateDir`, which resolves the REPOSITORY. The two agree
|
|
710
|
+
* only when the daemon is started from the repository root of a main
|
|
711
|
+
* worktree, which is why every measurement of it agreed.
|
|
712
|
+
*
|
|
713
|
+
* Start the daemon from `packages/cli`, or from a linked worktree, and the
|
|
714
|
+
* grants, the attempt claims and the start marks are written somewhere no
|
|
715
|
+
* reader looks: a controller re-spends a Start it already spent, and the run
|
|
716
|
+
* that did happen is invisible to the daemon that launched it.
|
|
717
|
+
*/
|
|
718
|
+
const stateDir = (0, git_driver_1.projectStateDir)(opts.cwd);
|
|
719
|
+
const readRunEnded_ = (0, run_ending_locator_1.makeRunEndedReader)({ cwd: opts.cwd });
|
|
720
|
+
// ONE control file, read by the autonomy tick and written by the command
|
|
721
|
+
// handler. The same path `looply run` reads: the daemon, the run and the
|
|
722
|
+
// controller are three processes over one repo, and this file is all they
|
|
723
|
+
// share about whether a person has asked the machine to hold.
|
|
724
|
+
// PROJECT-SCOPED: see `projectStateDir`. The run reads the same path.
|
|
725
|
+
// PROJECT-SCOPED: see `projectStateDir`. The run reads the same path.
|
|
726
|
+
const projectState = (0, git_driver_1.projectStateDir)(opts.cwd);
|
|
727
|
+
const controlFile = new control_file_1.ControlStore(projectState, ledger_store_1.nodeLedgerFs);
|
|
728
|
+
/**
|
|
729
|
+
* 🔴 THE PHONE'S BRAKE REACHED ONE RUN IN N, and this is the path that matters
|
|
730
|
+
* most — nobody is standing at the terminal to notice that two of three runs
|
|
731
|
+
* kept going.
|
|
732
|
+
*
|
|
733
|
+
* Writing one shared `control.json` is not enough once there are several
|
|
734
|
+
* executions: the FIRST to reach a round boundary consumes it by writing
|
|
735
|
+
* `resume` over it, and every other one then reads `resume` and carries on.
|
|
736
|
+
* `looply stop` had already been taught to write an addressed request per live
|
|
737
|
+
* execution; this had not, and had a different code path besides.
|
|
738
|
+
*
|
|
739
|
+
* ⚠️ WRITING ONLY. Reading stays on `controlFile` above, because the autonomy
|
|
740
|
+
* tick asks a different question — "has a person asked this machine to hold" —
|
|
741
|
+
* of the one file that answers for the project.
|
|
742
|
+
*/
|
|
743
|
+
const controlWriter = {
|
|
744
|
+
request: async (kind, actor) => {
|
|
745
|
+
const live = new workspace_1.RunRegistry(projectState).active();
|
|
746
|
+
const out = await (0, workspace_1.requestControlEverywhere)(projectState, live, kind, actor ?? 'phone', ledger_store_1.nodeLedgerFs, opts.cwd);
|
|
747
|
+
/**
|
|
748
|
+
* ⚠️ AND SAY WHICH RUNS IT MISSED. The reach report was discarded, so a
|
|
749
|
+
* `stop` that could not be written into one execution's state directory
|
|
750
|
+
* looked exactly like one that reached them all — on the path where
|
|
751
|
+
* nobody is at a terminal to notice.
|
|
752
|
+
*/
|
|
753
|
+
if (out.unreachable.length > 0) {
|
|
754
|
+
opts.log(`daemon : could not reach ${out.unreachable.join(', ')} — their state could not be written.`);
|
|
755
|
+
}
|
|
756
|
+
return { kind, requestedAt: new Date().toISOString(), ...(actor === undefined ? {} : { actor }) };
|
|
757
|
+
},
|
|
758
|
+
};
|
|
759
|
+
// Advances once per autonomy cadence so the scan does not always begin at
|
|
760
|
+
// the same grant. Fairness only: it decides who is ASKED first, never what
|
|
761
|
+
// the answer is.
|
|
762
|
+
//
|
|
763
|
+
// SEEDED, not zero. The scan looks at a bounded window, and a window that
|
|
764
|
+
// always opened at the same place would leave a machine holding more grants
|
|
765
|
+
// than the window is wide with a far controller it never reached — one
|
|
766
|
+
// starvation traded for another. Advancing solves that for a daemon that
|
|
767
|
+
// stays up; seeding solves it for one that does not, because two runs of a
|
|
768
|
+
// crash-looping daemon then open on different grants instead of the same one
|
|
769
|
+
// for ever.
|
|
770
|
+
//
|
|
771
|
+
// The clock is a fine source here precisely because this decides nothing:
|
|
772
|
+
// correctness does not depend on the order, only on every grant eventually
|
|
773
|
+
// getting a turn.
|
|
774
|
+
let autonomyTurn = Date.now();
|
|
775
|
+
// The same, for which accepted dependency edge a cadence raises first (`fileReviewed`).
|
|
776
|
+
let edgeTurn = Date.now();
|
|
777
|
+
const autonomyStore = new autonomy_store_1.AutonomyStore(stateDir, autonomy_store_1.nodeAutonomyFs);
|
|
778
|
+
const attempts = new start_attempts_1.StartAttempts(stateDir, start_attempts_1.nodeAttemptFs);
|
|
779
|
+
/**
|
|
780
|
+
* 🔴 PROJECT-SCOPED, BECAUSE THE RUN THAT WRITES THEM MAY BE IN ANOTHER
|
|
781
|
+
* CHECKOUT — AND THE CONTROLLER READS THESE, not the launcher's copy.
|
|
782
|
+
*
|
|
783
|
+
* `startMarkOf` is what `autonomy-controller` uses to decide whether a Start
|
|
784
|
+
* BEGAN. Reading it from the daemon's own execution state meant a
|
|
785
|
+
* coordinator-placed run's mark was never found, so three cadences later
|
|
786
|
+
* every placed goal was escalated with
|
|
787
|
+
*
|
|
788
|
+
* Start 1 was attempted 3 times and never began
|
|
789
|
+
*
|
|
790
|
+
* while its runs had started, worked and finished perfectly. The launcher's
|
|
791
|
+
* `provenStart` had the identical bug and was fixed with it; both readers
|
|
792
|
+
* have to agree with the writer, and the writer is a run that may be
|
|
793
|
+
* anywhere in this project.
|
|
794
|
+
*/
|
|
795
|
+
const marksDir = (0, git_driver_1.projectStateDir)(opts.cwd);
|
|
796
|
+
const startMarks = new run_start_marks_1.RunStartMarks(marksDir, ledger_store_1.nodeLedgerFs);
|
|
797
|
+
const refusalMarks = new run_refusal_marks_1.RunRefusalMarks(marksDir, ledger_store_1.nodeLedgerFs);
|
|
798
|
+
// Which of a person's answers have already been handed to a run. Read by the
|
|
799
|
+
// approval gate below and written at the Start boundary, by exclusive create,
|
|
800
|
+
// so two daemons over one project cannot both give out the same answer.
|
|
801
|
+
const resumes = new approval_resumes_1.ApprovalResumes(stateDir, start_attempts_1.nodeAttemptFs);
|
|
802
|
+
// Which approved action each Start took responsibility for. Written here, at
|
|
803
|
+
// the Start boundary, and read by the run to derive the claim key it must
|
|
804
|
+
// present — including after a crash, which is why it is durable at all.
|
|
805
|
+
const governed = new governed_approvals_1.GovernedApprovals(stateDir, start_attempts_1.nodeAttemptFs);
|
|
806
|
+
/**
|
|
807
|
+
* The safety history. Constructed once; RE-OPENED on every consultation.
|
|
808
|
+
*
|
|
809
|
+
* The observations are written by the RUN process, so a snapshot taken at
|
|
810
|
+
* startup is a fact about a moment that has passed — the same reason the
|
|
811
|
+
* approval gate is read from the server every tick rather than from anything
|
|
812
|
+
* this machine wrote down.
|
|
813
|
+
*/
|
|
814
|
+
// MACHINE-SCOPED, and it must be the SAME directory the run raises into,
|
|
815
|
+
// or every hold this gate reads is one no run ever wrote. Both go through
|
|
816
|
+
// `railsSafetyStore`, which is what keeps that true by construction.
|
|
817
|
+
/**
|
|
818
|
+
* THE COORDINATOR LOCK, ONCE IT HAS BEEN TAKEN.
|
|
819
|
+
*
|
|
820
|
+
* 🔴 IT WAS TAKEN ONCE AND NEVER LOOKED AT AGAIN. The daemon claimed it
|
|
821
|
+
* before `run()` and nothing consulted it afterwards, so if the file was
|
|
822
|
+
* removed — a person tidying `.looply`, a disk that filled and was cleared,
|
|
823
|
+
* a second daemon sweeping what it judged litter — this daemon carried on
|
|
824
|
+
* scheduling with no lock at all, for the rest of its life, silently. The
|
|
825
|
+
* whole point of the lock is that exactly one coordinator schedules.
|
|
826
|
+
*
|
|
827
|
+
* Null until it is held, so a tick before the claim schedules nothing.
|
|
828
|
+
*/
|
|
829
|
+
let coordinatorLock = null;
|
|
830
|
+
/**
|
|
831
|
+
* WHERE THE COORDINATOR PLACED EACH GOAL THIS CADENCE, and how many starts it
|
|
832
|
+
* decided were free. Both are filled by `prefer` and read by the two deps
|
|
833
|
+
* below it, within one tick.
|
|
834
|
+
*
|
|
835
|
+
* ⚠️ PROCESS-LOCAL AND DELIBERATELY NOT DURABLE. They describe ONE cadence's
|
|
836
|
+
* decision; a restarted daemon recomputes from the registry and the leases,
|
|
837
|
+
* which are the durable facts. Anything that outlived the cadence would be a
|
|
838
|
+
* second opinion about capacity, and the registry is the first.
|
|
839
|
+
*/
|
|
840
|
+
const placement = new Map();
|
|
841
|
+
/** F28 (corr-3): each goal's controller's last decision, for the next cadence's scheduler. */
|
|
842
|
+
const lastDecision = new Map();
|
|
843
|
+
let freeCapacity = 1;
|
|
844
|
+
/**
|
|
845
|
+
* M4-1 — THE MACHINE MAKES ITS OWN CHECKOUT, when the project asked.
|
|
846
|
+
*
|
|
847
|
+
* `machine.provision_workspaces` is off by default; then `provisioner` is
|
|
848
|
+
* null and the gatherer tells the scheduler so, and a `provision` action
|
|
849
|
+
* cannot even be emitted — every wait is the one it always was. On, the tick
|
|
850
|
+
* may ask for ONE checkout per cadence; it is made after the tick returns
|
|
851
|
+
* (the tick is synchronous by contract), with the same code a person uses,
|
|
852
|
+
* and the next cadence finds it idle and places the goal.
|
|
853
|
+
*/
|
|
854
|
+
const provisioner = opts.config.machine?.provisionWorkspaces === true ? (0, workspace_provisioner_1.nodeWorkspaceProvisioner)(opts.cwd) : null;
|
|
855
|
+
let toProvision = null;
|
|
856
|
+
/**
|
|
857
|
+
* M6-14: THE GOAL A CHECKOUT IS BEING MADE FOR, and the checkouts made but
|
|
858
|
+
* not yet placed. `provisioningFor` is set beside `toProvision` for the
|
|
859
|
+
* cadence the tick asks; `provisionedFor` holds a goal from the moment its
|
|
860
|
+
* checkout exists until the tick places it (or a minute passes). Between the
|
|
861
|
+
* two the placement map is empty for that goal, and that is the window a
|
|
862
|
+
* person's Start fell into (M6-13).
|
|
863
|
+
*/
|
|
864
|
+
let provisioningFor = null;
|
|
865
|
+
const provisionedFor = new Map();
|
|
866
|
+
/**
|
|
867
|
+
* M6-22: IS THIS MACHINE PURSUING THIS GOAL?
|
|
868
|
+
*
|
|
869
|
+
* Asked of the live grants — the same records the coordinator's own gatherer
|
|
870
|
+
* reads — and only while the coordinator is on, because with it off nothing
|
|
871
|
+
* places anything and a person's Start belongs in the project checkout, as it
|
|
872
|
+
* always did. `authorized` is the one state that means "the machine may spend
|
|
873
|
+
* Starts against this", and an expired grant is not live however it is
|
|
874
|
+
* labelled.
|
|
875
|
+
*
|
|
876
|
+
* ⚠️ IT ANSWERS "IS IT BEING PLACED", NOT "WHERE". The placement itself is
|
|
877
|
+
* still `workspaceFor`'s to answer, freshly, every poll; this only decides
|
|
878
|
+
* whether a Start waits for that answer or goes without one.
|
|
879
|
+
*/
|
|
880
|
+
const pursuing = (goalId) => {
|
|
881
|
+
if (opts.config.machine?.coordinator !== true)
|
|
882
|
+
return false;
|
|
883
|
+
const now = Date.now();
|
|
884
|
+
return autonomyStore
|
|
885
|
+
.all()
|
|
886
|
+
.some((g) => g.goalId === goalId && g.state === 'authorized' && Date.parse(g.expiresAt) > now);
|
|
887
|
+
};
|
|
888
|
+
/**
|
|
889
|
+
* M4-2 — THE MACHINE ASKS ONE OF ITS OWN RUNS TO STOP, when the project asked.
|
|
890
|
+
*
|
|
891
|
+
* `machine.preemption` is off by default; then `preemptor` is null and a
|
|
892
|
+
* `preempt` decision stays a line in `schedule.jsonl` that nothing acts on,
|
|
893
|
+
* as it always was. On, the first preempt of a tick is answered after the
|
|
894
|
+
* tick with the run's own graceful `stop` request — the one `looply stop`
|
|
895
|
+
* writes — never a kill; only a run the machine started can be named,
|
|
896
|
+
* because the gatherer reports a person's as not preemptible.
|
|
897
|
+
*/
|
|
898
|
+
const preemptor = opts.config.machine?.preemption === true ? (0, preemptor_1.nodePreemptor)(projectState, ledger_store_1.nodeLedgerFs) : null;
|
|
899
|
+
let toPreempt = null;
|
|
900
|
+
/**
|
|
901
|
+
* M4-4 — THE MACHINE RETIRES WHAT IT MADE, when the project asked.
|
|
902
|
+
*
|
|
903
|
+
* `machine.reap_provisioned` is off by default; then `reaper` is null and
|
|
904
|
+
* nothing is inspected or removed. On, after the tick and after any stop
|
|
905
|
+
* request, at most ONE checkout per cadence that the machine itself made,
|
|
906
|
+
* idle beyond `idle_checkout_minutes`, referenced by no run and holding no
|
|
907
|
+
* residue, is retired through the manual command's own reaping. A checkout
|
|
908
|
+
* a person cut is never touched.
|
|
909
|
+
*/
|
|
910
|
+
const reaper = opts.config.machine?.reapProvisioned === true
|
|
911
|
+
? (0, reaper_1.nodeReaper)(opts.cwd, projectState, { idleMinutes: opts.config.machine?.idleCheckoutMinutes ?? 60, log: opts.log })
|
|
912
|
+
: null;
|
|
913
|
+
/** M4-4: what the tick placed last cadence — a launch is not an arrival, and the reaper must not race it. */
|
|
914
|
+
let placedBefore = [];
|
|
915
|
+
/**
|
|
916
|
+
* M5-1 — THE MACHINE OFFERS A FINISHED RUN'S BRANCH TO THE LANE, when the project asked.
|
|
917
|
+
*
|
|
918
|
+
* `machine.integrate` is off by default; then `integrator` is null and
|
|
919
|
+
* nothing is listed or offered. On — and only with the lane's own gate,
|
|
920
|
+
* `run.integration`, on — one branch per cadence, after the reaper, goes
|
|
921
|
+
* through the very `integrationTick` the `looply integrate` command calls,
|
|
922
|
+
* with the command's own deps. A fast-forward or a refusal is the lane's
|
|
923
|
+
* decision, on record either way.
|
|
924
|
+
*/
|
|
925
|
+
/**
|
|
926
|
+
* RULE #2's READER FOR THE LANE (F18): the goal as the server holds it at the
|
|
927
|
+
* moment a candidate is judged, so the lane can regenerate the integrated
|
|
928
|
+
* receipt (`laneGoalReader`).
|
|
929
|
+
*
|
|
930
|
+
* ⚠️ PROGRAMME MODE ONLY. Absent `currency` is the lane's whole simple-mode
|
|
931
|
+
* guarantee — no receipt computed, no attempt field written — and a simple
|
|
932
|
+
* project keeps it.
|
|
933
|
+
*/
|
|
934
|
+
const goalNow =
|
|
935
|
+
// F31 (soun-1/corr-2): the SAME predicate the worker defers its coverage on — slice 1
|
|
936
|
+
// (`machine.programme: true`) included — so no goal closes with coverage judged by nobody.
|
|
937
|
+
!(0, programme_mode_1.programmeIsOn)(opts.config) ? undefined : (0, integration_deps_1.laneGoalReader)(() => client.projectGoals(projectId));
|
|
938
|
+
const integrator = opts.config.machine?.integrate === true ? (0, integrator_1.nodeIntegrator)(opts.cwd, projectState, opts.config, opts.log, goalNow) : null;
|
|
939
|
+
/**
|
|
940
|
+
* M5-2: publication as the THIRD KEY. Asked only when this daemon integrates
|
|
941
|
+
* for itself AND the lane is on — a hold on a lane nobody runs would never
|
|
942
|
+
* lift. Otherwise the tick is handed nothing, and a met goal closes its grant
|
|
943
|
+
* exactly as it did before this existed.
|
|
944
|
+
*/
|
|
945
|
+
/**
|
|
946
|
+
* ⚠️ IN PROGRAMME MODE THE THIRD KEY ALSO CARRIES STRICT COVERAGE, judged on the
|
|
947
|
+
* integration tip (F25, `withLaneCoverage`). The worker defers its fourth key to it.
|
|
948
|
+
*/
|
|
949
|
+
const thirdKey = integrator !== null && opts.config.run.integration === true
|
|
950
|
+
? goalNow === undefined
|
|
951
|
+
? (0, completion_1.nodePublication)(opts.cwd)
|
|
952
|
+
: (0, lane_coverage_1.withLaneCoverage)(
|
|
953
|
+
// F27: a programme's Starts judge the tree the lane holds, so a hold a Start can move buys one.
|
|
954
|
+
(0, completion_1.nodePublication)(opts.cwd, { rejudge: true }), (0, lane_coverage_1.nodeLaneCoverageDeps)(opts.cwd, new git_driver_1.GitDriver((0, git_driver_1.nodeGitExec)(opts.cwd)), client, projectId, goalNow))
|
|
955
|
+
: null;
|
|
956
|
+
const safety = (0, rails_stores_1.railsSafetyStore)(opts.cwd);
|
|
957
|
+
if ((await safety.open()) === 'unreadable') {
|
|
958
|
+
opts.log('daemon : the loop-safety history could not be read — autonomy will hold');
|
|
959
|
+
}
|
|
960
|
+
/**
|
|
961
|
+
* WHETHER A PERSON'S START MAY PROCEED, ASKED OF THE SERVER.
|
|
962
|
+
*
|
|
963
|
+
* The same derivation the controller holds on, at the other boundary. One
|
|
964
|
+
* reading answers both halves: whether to turn this Start away, and which
|
|
965
|
+
* answer — if any — it has just been given the right to carry.
|
|
966
|
+
*
|
|
967
|
+
* A START WITH NO PURSUIT IS NOT GATED. An approval belongs to a goal at a
|
|
968
|
+
* version, so a run that names none can have nothing outstanding; asking
|
|
969
|
+
* would be a request that could only ever come back empty.
|
|
970
|
+
*
|
|
971
|
+
* THE CLAIM IS TAKEN HERE, before the run exists, because that is where two
|
|
972
|
+
* Starts race. Losing it is not a refusal: the other Start has the answer and
|
|
973
|
+
* is about to hold the working tree, so this one carries on as an ordinary
|
|
974
|
+
* Start and meets the lease like any other.
|
|
975
|
+
*/
|
|
976
|
+
/**
|
|
977
|
+
* THE DECISION LIVES IN `explicit-start-gate.ts`, and this is the wiring.
|
|
978
|
+
*
|
|
979
|
+
* It was a closure here, over these same five values, which meant it had no
|
|
980
|
+
* export and no test seam — and that is a large part of why it could hand an
|
|
981
|
+
* approval id to `approved` only, while `refusesExplicitStart` admitted three
|
|
982
|
+
* more kinds so that something could go and look. Nothing looked, and the
|
|
983
|
+
* pursuit wedged. Injecting the collaborators costs nothing and makes the
|
|
984
|
+
* wiring answerable to a spec.
|
|
985
|
+
*/
|
|
986
|
+
const explicitStartGate = async (start, requestId, workspace) => {
|
|
987
|
+
// The pursuit is resolved HERE and nowhere further in. `decideExplicitStart`
|
|
988
|
+
// takes a gate rather than a way to read one, because the decision does not
|
|
989
|
+
// need to know what a goal is — and `goal-wiring.spec.ts` enforces exactly
|
|
990
|
+
// that confinement across the whole tree.
|
|
991
|
+
if (start.goalId === undefined || start.goalVersion === undefined)
|
|
992
|
+
return {};
|
|
993
|
+
/**
|
|
994
|
+
* A PERSON'S EXPLICIT START IS THE ESCAPE HATCH FROM A SAFETY HOLD, and it
|
|
995
|
+
* is spent HERE — before a run exists, in the same place the governed
|
|
996
|
+
* reservation is taken, because this is where two Starts race.
|
|
997
|
+
*
|
|
998
|
+
* ⚠️ IT IS ONE RETRY, NOT "SAFETY OFF". The incident is marked spent and the
|
|
999
|
+
* observation history is left exactly where it is, so a Run that also
|
|
1000
|
+
* achieves nothing raises a NEW incident. That is the difference between a
|
|
1001
|
+
* person saying "try once more" and a person disabling a brake, and there
|
|
1002
|
+
* is deliberately no flag anywhere that does the second.
|
|
1003
|
+
*
|
|
1004
|
+
* The refusal is RETRYABLE rather than terminal: losing this race means
|
|
1005
|
+
* another Start took the retry, and that Start may yet fail to produce a
|
|
1006
|
+
* run.
|
|
1007
|
+
*/
|
|
1008
|
+
if ((await safety.open()) === 'ok') {
|
|
1009
|
+
const incident = safety.activeIncident(start.goalId, start.goalVersion);
|
|
1010
|
+
if (incident !== null) {
|
|
1011
|
+
const spent = await safety.override(incident.id, requestId);
|
|
1012
|
+
if (!spent.ok)
|
|
1013
|
+
return { refuse: { reason: spent.because, terminal: false } };
|
|
1014
|
+
opts.log(`daemon : a person is trying again after ${incident.kind} (${incident.id})`);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
const gate = await (0, approval_gate_read_1.readApprovalGate)(client, {
|
|
1018
|
+
projectId,
|
|
1019
|
+
goalId: start.goalId,
|
|
1020
|
+
goalVersion: start.goalVersion,
|
|
1021
|
+
now: Date.now(),
|
|
1022
|
+
consumed: (ids) => resumes.consumed(ids),
|
|
1023
|
+
});
|
|
1024
|
+
return await (0, explicit_start_gate_1.decideExplicitStart)({
|
|
1025
|
+
governed,
|
|
1026
|
+
resumes,
|
|
1027
|
+
// The SAME durable lease the startup recovery gates on, not a second
|
|
1028
|
+
// notion of liveness. `RunLauncher.isRunning()` would not do: it knows
|
|
1029
|
+
// only about children THIS daemon spawned, so a run left by a previous
|
|
1030
|
+
// one would read as nobody.
|
|
1031
|
+
// M6-9: the placed checkout's lease when the goal was placed, the project's otherwise.
|
|
1032
|
+
carrier: () => new run_lease_1.RunLeaseStore((0, node_path_1.join)(workspace ?? opts.cwd, git_driver_1.STATE_DIR, 'state'), run_lease_1.nodeLeaseFs).inspect(),
|
|
1033
|
+
log: opts.log,
|
|
1034
|
+
}, gate, requestId);
|
|
1035
|
+
};
|
|
1036
|
+
const runStarter = (0, run_launcher_1.nodeRunLauncher)(opts.cwd, stateDir);
|
|
1037
|
+
/**
|
|
1038
|
+
* The brain's tools, built only when the project switched it on (M3-1).
|
|
1039
|
+
* Off — the default — costs nothing: no store opened, no transport built.
|
|
1040
|
+
*/
|
|
1041
|
+
const brain = opts.config.run.brain === true ? await (0, brain_advice_1.brainDepsFor)(opts.cwd, opts.config, opts.log) : null;
|
|
1042
|
+
const daemon = new daemon_1.Daemon({
|
|
1043
|
+
client,
|
|
1044
|
+
machineId: identity.machineId,
|
|
1045
|
+
projectId,
|
|
1046
|
+
cliVersion: exports.CLI_VERSION,
|
|
1047
|
+
cursors,
|
|
1048
|
+
goals,
|
|
1049
|
+
recoverStaleRun,
|
|
1050
|
+
/**
|
|
1051
|
+
* One controller transition per cadence.
|
|
1052
|
+
*
|
|
1053
|
+
* The tick itself is bounded: it reads durable facts, asks the server once,
|
|
1054
|
+
* and at most launches ONE detached child before returning. It does not
|
|
1055
|
+
* wait for a run — that would hold the heartbeat for hours.
|
|
1056
|
+
*/
|
|
1057
|
+
autonomyTick: async () => {
|
|
1058
|
+
/**
|
|
1059
|
+
* 🔴 THE HEARTBEAT BELONGS TO THE CADENCE, NOT TO THE SCHEDULER —
|
|
1060
|
+
* AND IT WAS REACHABLE ONLY THROUGH A BRANCH THAT IS OFF BY DEFAULT.
|
|
1061
|
+
*
|
|
1062
|
+
* `refresh()` had exactly one call site in the tree: inside `prefer()`,
|
|
1063
|
+
* AFTER `if (coordinator !== true) return []`. Every daemon takes the
|
|
1064
|
+
* lock; only a coordinator daemon ever said it was still there. MEASURED:
|
|
1065
|
+
* a default daemon holds its claim, never refreshes it, and ten minutes
|
|
1066
|
+
* later `heartbeatStale` judges it dead — so a second `looply daemon`
|
|
1067
|
+
* over the same repository takes the lock and both run, which is the one
|
|
1068
|
+
* outcome this lock exists to prevent.
|
|
1069
|
+
*
|
|
1070
|
+
* ⚠️ AND LOSING IT STOPS THE TICK, NOT ONLY THE ORDERING. The hazard is
|
|
1071
|
+
* two processes spending the same Start; refusing to schedule while
|
|
1072
|
+
* continuing to launch would not address it. A daemon that no longer
|
|
1073
|
+
* holds its claim does nothing autonomous until it is restarted.
|
|
1074
|
+
*/
|
|
1075
|
+
if (coordinatorLock !== null && !coordinatorLock.refresh()) {
|
|
1076
|
+
opts.log('daemon : this daemon no longer holds this repository’s lock — not acting autonomously.');
|
|
1077
|
+
placement.clear();
|
|
1078
|
+
freeCapacity = 1;
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
const gatherState = () => (0, coordinator_state_1.gatherScheduleState)(opts.cwd, {
|
|
1082
|
+
goals,
|
|
1083
|
+
autonomy: autonomyStore,
|
|
1084
|
+
registry: new workspace_1.RunRegistry(projectState),
|
|
1085
|
+
safety,
|
|
1086
|
+
/**
|
|
1087
|
+
* 🔴 THE MINIMUM OF EVERY CEILING, NOT THE COORDINATOR'S OWN.
|
|
1088
|
+
*
|
|
1089
|
+
* MEASURED by the end-to-end proof: with `max_workspaces: 2` the
|
|
1090
|
+
* scheduler correctly placed two goals in two checkouts and
|
|
1091
|
+
* exactly ONE run ever registered, because
|
|
1092
|
+
* `run.max_concurrent_runs` defaults to 1 and the second was
|
|
1093
|
+
* refused by the slot bound AFTER a workspace was cut, a goal
|
|
1094
|
+
* placed and a process spawned. Planning past a ceiling somebody
|
|
1095
|
+
* else enforces wastes exactly that much work every cadence.
|
|
1096
|
+
*/
|
|
1097
|
+
capacity: {
|
|
1098
|
+
maxWorkspaces: (0, core_2.effectiveConcurrency)({
|
|
1099
|
+
workspaces: opts.config.machine?.maxWorkspaces,
|
|
1100
|
+
runs: opts.config.run.maxConcurrentRuns,
|
|
1101
|
+
}),
|
|
1102
|
+
maxAgentsInFlight: (0, core_2.effectiveConcurrency)({
|
|
1103
|
+
agents: opts.config.machine?.maxAgentsInFlight,
|
|
1104
|
+
runs: opts.config.run.maxConcurrentRuns,
|
|
1105
|
+
}),
|
|
1106
|
+
provision: provisioner !== null,
|
|
1107
|
+
},
|
|
1108
|
+
/**
|
|
1109
|
+
* 🔴 WHETHER THIS PROGRAMME INTEGRATES, AND THEREFORE WHETHER
|
|
1110
|
+
* FINISHING LOCALLY IS FINISHING.
|
|
1111
|
+
*
|
|
1112
|
+
* Both halves, the same pair `describeProgramme` reports as off:
|
|
1113
|
+
* the machine must run the integrator AND the run must be allowed to
|
|
1114
|
+
* integrate. With either off there is no lane, nothing is ever
|
|
1115
|
+
* published, and reading "not integrated" as "not satisfied" would
|
|
1116
|
+
* stop every dependent for ever.
|
|
1117
|
+
*/
|
|
1118
|
+
integrates: opts.config.machine?.integrate === true && opts.config.run.integration === true,
|
|
1119
|
+
// F28 (corr-3): a grant whose controller is only waiting or holding takes no slot.
|
|
1120
|
+
held: new Set([...lastDecision].filter(([, kind]) => kind === 'wait' || kind === 'hold').map(([goalId]) => goalId)),
|
|
1121
|
+
// F34: in a programme whose brain proposes dependencies, a goal waits for its answer —
|
|
1122
|
+
// F36: an answer that STANDS (accepted); a refused one filed no edge, so it is not advice —
|
|
1123
|
+
// F36c: until the version has had its bounded number of answers (`advisedVersions`).
|
|
1124
|
+
...(brain !== null && (0, programme_mode_1.programmeIsOn)(opts.config) && opts.config.machine?.coordinator === true
|
|
1125
|
+
? { advised: (0, ask_brain_1.advisedVersions)(brain.store.all()) }
|
|
1126
|
+
: {}),
|
|
1127
|
+
now: () => Date.now(),
|
|
1128
|
+
});
|
|
1129
|
+
/**
|
|
1130
|
+
* M3-1 — THE BRAIN'S ADVICE FOR THIS CADENCE, ASKED BEFORE THE TICK.
|
|
1131
|
+
*
|
|
1132
|
+
* `run.brain` is off by default, and then `brain` is null and nothing
|
|
1133
|
+
* here runs: the coordinator receives no proposals, exactly as before.
|
|
1134
|
+
* On, every live authorised goal the mirror holds is asked about at most
|
|
1135
|
+
* once per (goal, version, tree, prompt digest) — the record on disk
|
|
1136
|
+
* answers a repeat — and at most ONE new question is put per cadence, so
|
|
1137
|
+
* the heartbeat waits for one model call, never for a fleet of them.
|
|
1138
|
+
* What comes back is advice: `decideSchedule` writes it into a start's
|
|
1139
|
+
* `because` and starts nothing it would not have started without it.
|
|
1140
|
+
* Asked BEFORE the tick because `prefer` is synchronous by contract and
|
|
1141
|
+
* a model is not.
|
|
1142
|
+
*/
|
|
1143
|
+
let advice;
|
|
1144
|
+
/**
|
|
1145
|
+
* 🔴 PROPOSED EDGES ARE CARRIED, NOT APPLIED. `dependsVerdict` is the
|
|
1146
|
+
* fence and it needs facts this cadence does not assemble — goal
|
|
1147
|
+
* versions, source versions, the graph. Holding them here rather than
|
|
1148
|
+
* dropping them is the difference between a suggestion that goes nowhere
|
|
1149
|
+
* and one that has somewhere to go next; applying one from a daemon tick
|
|
1150
|
+
* would be a provider's suggestion reaching the graph without passing a
|
|
1151
|
+
* person, which is what the fence and the approval path exist to prevent.
|
|
1152
|
+
*/
|
|
1153
|
+
let proposedEdges = [];
|
|
1154
|
+
if (brain !== null && opts.config.machine?.coordinator === true && coordinatorLock !== null) {
|
|
1155
|
+
try {
|
|
1156
|
+
const head = await (0, index_for_head_1.indexForHead)(opts.cwd);
|
|
1157
|
+
if (!head.ok) {
|
|
1158
|
+
opts.log(`brain : not asked this cadence — ${head.because}`);
|
|
1159
|
+
}
|
|
1160
|
+
else {
|
|
1161
|
+
const tick = await (0, ask_brain_1.proposalsForTick)(gatherState().goals, {
|
|
1162
|
+
projectId,
|
|
1163
|
+
treeSha: head.treeSha,
|
|
1164
|
+
facts: (0, project_index_facts_1.factsFromIndex)(head.snapshot, (0, project_memory_store_1.projectKeyFor)(opts.cwd, sha256)).map((d) => `${d.subject}: ${d.content}`),
|
|
1165
|
+
goalFor: (id) => goals.get(id),
|
|
1166
|
+
packetFor: async (goal) => (await (0, packet_for_goal_1.packetForGoal)(goal, (id) => client.sourceDetail(projectId, id),
|
|
1167
|
+
// F19: what the programme's other goals own, so a proposal is made knowing it.
|
|
1168
|
+
(0, programme_mode_1.programmeIsOn)(opts.config) ? goals.all() : [])).packet,
|
|
1169
|
+
}, brain, opts.log, undefined,
|
|
1170
|
+
// F34: in a programme each goal version is asked once.
|
|
1171
|
+
(0, programme_mode_1.programmeIsOn)(opts.config));
|
|
1172
|
+
advice = tick.advice;
|
|
1173
|
+
proposedEdges = tick.edges;
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
catch (e) {
|
|
1177
|
+
// The brain is optional; a failure to ask must not take the cadence with it.
|
|
1178
|
+
opts.log(`brain : not asked this cadence — ${e.message}`);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
/**
|
|
1182
|
+
* 🔴 EVERY PROPOSED EDGE IS PUT TO THE FENCE, AND NOTHING IS APPLIED.
|
|
1183
|
+
* `reviewEdges` answers `accepted` when every check a machine can make
|
|
1184
|
+
* passes; the edge is still not in the graph and will not be until
|
|
1185
|
+
* somebody puts it there. Writing one from a cadence would be a
|
|
1186
|
+
* provider's suggestion reaching the graph without passing a person.
|
|
1187
|
+
*
|
|
1188
|
+
* ⚠️ THE READS ARE PAID FOR ONLY WHEN THERE IS SOMETHING TO JUDGE.
|
|
1189
|
+
* `reviewEdges` short-circuits on an empty list before it touches either
|
|
1190
|
+
* reader, so a cadence that produced no edges makes no extra server call
|
|
1191
|
+
* — which is why it is safe to hang a network read off a heartbeat.
|
|
1192
|
+
*/
|
|
1193
|
+
if (proposedEdges.length > 0) {
|
|
1194
|
+
try {
|
|
1195
|
+
const review = await (0, depends_review_1.reviewEdges)(proposedEdges, {
|
|
1196
|
+
projectId,
|
|
1197
|
+
goals: async () => goals.all().map((g) => ({ id: g.id, version: g.version, dependsOn: g.dependsOn ?? [], projectId: g.projectId })),
|
|
1198
|
+
sourceShas: async () => new Map((await client.sources(projectId)).sources.map((s) => [s.id, s.sha256])),
|
|
1199
|
+
});
|
|
1200
|
+
(0, depends_review_1.reportReview)(review, opts.log);
|
|
1201
|
+
/**
|
|
1202
|
+
* 🔴 AND THE ACCEPTED ONES ARE PUT TO A PERSON. This is the two-key
|
|
1203
|
+
* rule's first key and nothing more: a machine raises the request,
|
|
1204
|
+
* and a person turns the second. `fileReviewed` files only an
|
|
1205
|
+
* `accepted` verdict — a stale or refused edge must never reach
|
|
1206
|
+
* somebody's phone, because asking about those spends the one thing
|
|
1207
|
+
* this system cannot replace, which is somebody's willingness to read
|
|
1208
|
+
* the NEXT question carefully.
|
|
1209
|
+
*
|
|
1210
|
+
* ⚠️ ONE PER HEARTBEAT, and the rest said out loud. See
|
|
1211
|
+
* `MAX_EDGE_FILINGS_PER_TICK` for why that bound is not hypothetical.
|
|
1212
|
+
*/
|
|
1213
|
+
const filing = await (0, depends_review_1.fileReviewed)(review,
|
|
1214
|
+
/**
|
|
1215
|
+
* 🔴 `'system'`, NOT A PERSON AND NOT A RUN ROLE. `Actor` offers
|
|
1216
|
+
* `runner`, `prover`, `keeper`, `system` and `human:<id>`; a cadence
|
|
1217
|
+
* is none of the three run roles, and writing a `human:` here would
|
|
1218
|
+
* be a machine signing somebody's name on the thing they are about
|
|
1219
|
+
* to be asked to approve.
|
|
1220
|
+
*/
|
|
1221
|
+
{ projectId, requestedBy: 'system' }, { createApproval: (p, input) => client.createApproval(p, input) },
|
|
1222
|
+
// F34: in a programme every accepted edge is filed the cadence it is proposed — a goal must not
|
|
1223
|
+
// start before the edge that holds it exists, and asking once per version keeps it to one per goal.
|
|
1224
|
+
(0, programme_mode_1.programmeIsOn)(opts.config) ? Number.MAX_SAFE_INTEGER : depends_review_1.MAX_EDGE_FILINGS_PER_TICK,
|
|
1225
|
+
// WHOSE TURN IT IS, so an edge already waiting for a decision
|
|
1226
|
+
// cannot hold the one filing a cadence allows for ever. Seeded
|
|
1227
|
+
// and process-local for the reasons `autonomyTurn` is.
|
|
1228
|
+
(edgeTurn += 1));
|
|
1229
|
+
(0, depends_review_1.reportFiling)(filing, opts.log);
|
|
1230
|
+
}
|
|
1231
|
+
catch (e) {
|
|
1232
|
+
/**
|
|
1233
|
+
* ⚠️ A FAILURE HERE IS NOT A FAILURE OF THE CADENCE. The edges stay
|
|
1234
|
+
* on record and the next cadence judges them against a newer world,
|
|
1235
|
+
* which is the right answer for a proposal whose whole verdict
|
|
1236
|
+
* depends on when it is asked.
|
|
1237
|
+
*/
|
|
1238
|
+
opts.log(`depends : not judged this cadence — ${e.message}`);
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
await (0, autonomy_tick_1.autonomyTick)({
|
|
1242
|
+
// WHOSE TURN IT IS FIRST. Process-local and deliberately not
|
|
1243
|
+
// durable: the ORDER controllers are served in is not a fact about
|
|
1244
|
+
// any of them, so a restarted daemon beginning again costs nothing.
|
|
1245
|
+
// It was never wired at all, which left the scan starting from the
|
|
1246
|
+
// same grant every cadence for the life of the process.
|
|
1247
|
+
rotation: () => (autonomyTurn += 1),
|
|
1248
|
+
/**
|
|
1249
|
+
* 🔴 AND THIS IS WHERE THE DOLLAR CEILING BECAME REAL. `withinEnvelope`
|
|
1250
|
+
* shipped fully specced with no production caller, no config key that
|
|
1251
|
+
* could construct an `Envelope`, and a spend ledger nothing ever read.
|
|
1252
|
+
* `machine.envelope` produces one; the tick enforces it at the moment
|
|
1253
|
+
* the machine is about to spend without asking.
|
|
1254
|
+
*
|
|
1255
|
+
* ⚠️ ABSENT MEANS NO CEILING, which is what every existing project has.
|
|
1256
|
+
*/
|
|
1257
|
+
...(opts.config.machine?.envelope === undefined
|
|
1258
|
+
? {}
|
|
1259
|
+
: {
|
|
1260
|
+
envelope: () => opts.config.machine?.envelope,
|
|
1261
|
+
estimateUsd: () => opts.config.machine?.envelope?.estimateUsdPerStart,
|
|
1262
|
+
runsInFlight: () => new workspace_1.RunRegistry(projectState).active().length,
|
|
1263
|
+
}),
|
|
1264
|
+
/**
|
|
1265
|
+
* 🔴 AND THIS IS WHERE THE SCHEDULER BECAME REACHABLE.
|
|
1266
|
+
*
|
|
1267
|
+
* `decideSchedule` and `coordinatorTick` were written, specced, and
|
|
1268
|
+
* imported by nothing that ships. Cycles, holds, dependency order and
|
|
1269
|
+
* the anti-starvation age boost all existed and could not affect one
|
|
1270
|
+
* overnight decision, while `coordinator.spec.ts` opened by describing
|
|
1271
|
+
* "the thing wired into the daemon".
|
|
1272
|
+
*
|
|
1273
|
+
* ⚠️ OFF UNLESS THE PROJECT ASKS — see `machine.coordinator`. With it
|
|
1274
|
+
* off this returns nothing and the tick rotates exactly as before.
|
|
1275
|
+
*
|
|
1276
|
+
* ⚠️ AND A TICK THAT REFUSES ORDERS NOTHING. `coordinatorTick` refuses
|
|
1277
|
+
* without the coordinator lock, and a refusal must not be read as "no
|
|
1278
|
+
* goal is worth doing": returning `[]` falls back to the old order,
|
|
1279
|
+
* which is a worse order rather than a wrong one.
|
|
1280
|
+
*/
|
|
1281
|
+
prefer: () => {
|
|
1282
|
+
/**
|
|
1283
|
+
* ⚠️ THE PLACEMENT IS CLEARED WHENEVER THIS RETURNS NOTHING, and a
|
|
1284
|
+
* refusal that left it standing was a defect of its own: `prefer`
|
|
1285
|
+
* returned `[]` while `workspaceFor` went on answering last
|
|
1286
|
+
* cadence's checkout and `launchesPerTick` last cadence's number, so
|
|
1287
|
+
* a daemon that had stopped deciding kept acting on a decision it no
|
|
1288
|
+
* longer stood behind.
|
|
1289
|
+
*/
|
|
1290
|
+
if (opts.config.machine?.coordinator !== true) {
|
|
1291
|
+
placement.clear();
|
|
1292
|
+
provisionedFor.clear();
|
|
1293
|
+
provisioningFor = null;
|
|
1294
|
+
freeCapacity = 1;
|
|
1295
|
+
return [];
|
|
1296
|
+
}
|
|
1297
|
+
// The lock is refreshed once per cadence in `autonomyTick` above,
|
|
1298
|
+
// which reaches a daemon in every mode; by here it is known held.
|
|
1299
|
+
if (coordinatorLock === null) {
|
|
1300
|
+
placement.clear();
|
|
1301
|
+
provisionedFor.clear();
|
|
1302
|
+
provisioningFor = null;
|
|
1303
|
+
freeCapacity = 1;
|
|
1304
|
+
return [];
|
|
1305
|
+
}
|
|
1306
|
+
try {
|
|
1307
|
+
const gathered = gatherState();
|
|
1308
|
+
// The advice rides on the same input the gatherer built; with the brain off it is the same object.
|
|
1309
|
+
const input = advice === undefined ? gathered : { ...gathered, proposals: advice };
|
|
1310
|
+
const tick = (0, coordinator_1.coordinatorTick)(opts.cwd, input);
|
|
1311
|
+
if (!tick.ok) {
|
|
1312
|
+
opts.log(`daemon : the coordinator did not decide — ${tick.because}.`);
|
|
1313
|
+
placement.clear();
|
|
1314
|
+
provisionedFor.clear();
|
|
1315
|
+
provisioningFor = null;
|
|
1316
|
+
freeCapacity = 1;
|
|
1317
|
+
return [];
|
|
1318
|
+
}
|
|
1319
|
+
/**
|
|
1320
|
+
* 🔴 ONE DECISION, THREE ANSWERS. The tick needs to know WHO to ask
|
|
1321
|
+
* first, WHERE each one runs, and HOW MANY it may launch — and all
|
|
1322
|
+
* three come from the same `decideSchedule` call. Computing them
|
|
1323
|
+
* separately would let the three disagree within one cadence, which
|
|
1324
|
+
* is how a goal gets placed in a workspace another goal was just
|
|
1325
|
+
* given.
|
|
1326
|
+
*/
|
|
1327
|
+
// M4-1: the checkout this tick asked for, if any — made after the tick. The
|
|
1328
|
+
// provisioner knows nothing about goals; whom it is for is said here.
|
|
1329
|
+
const askedFor = tick.actions.find((a) => a.kind === 'provision');
|
|
1330
|
+
toProvision = askedFor === undefined ? null : { because: `for ${askedFor.goalId} (${askedFor.because})` };
|
|
1331
|
+
provisioningFor = askedFor === undefined ? null : askedFor.goalId;
|
|
1332
|
+
// M4-2: the run this tick would stop, if any — asked after the tick. The
|
|
1333
|
+
// preemptor knows nothing about goals; whose run it is is said here.
|
|
1334
|
+
const victim = tick.actions.find((a) => a.kind === 'preempt');
|
|
1335
|
+
toPreempt =
|
|
1336
|
+
victim === undefined
|
|
1337
|
+
? null
|
|
1338
|
+
: { runId: victim.runId, who: `${victim.runId} (${victim.goalId}, started by the machine)`, because: victim.because };
|
|
1339
|
+
const starts = tick.actions.filter((a) => a.kind === 'start');
|
|
1340
|
+
placement.clear();
|
|
1341
|
+
for (const a of starts) {
|
|
1342
|
+
placement.set(a.goalId, a.observed.workspace);
|
|
1343
|
+
provisionedFor.delete(a.goalId);
|
|
1344
|
+
}
|
|
1345
|
+
// A checkout made a minute ago for a goal the tick has still not
|
|
1346
|
+
// placed is not a placement in progress any more (the grant may be
|
|
1347
|
+
// gone); a Start on that goal is then a goal placed nowhere.
|
|
1348
|
+
for (const [g, made] of [...provisionedFor]) {
|
|
1349
|
+
if (Date.now() - made.at > exports.PLACEMENT_WAIT_MS)
|
|
1350
|
+
provisionedFor.delete(g);
|
|
1351
|
+
}
|
|
1352
|
+
freeCapacity = starts.length;
|
|
1353
|
+
return starts.map((a) => a.goalId);
|
|
1354
|
+
}
|
|
1355
|
+
catch (e) {
|
|
1356
|
+
// A gatherer that throws must not take the night's daemon with it.
|
|
1357
|
+
opts.log(`daemon : the coordinator could not read this project — ${e.message}.`);
|
|
1358
|
+
placement.clear();
|
|
1359
|
+
provisionedFor.clear();
|
|
1360
|
+
provisioningFor = null;
|
|
1361
|
+
freeCapacity = 1;
|
|
1362
|
+
return [];
|
|
1363
|
+
}
|
|
1364
|
+
},
|
|
1365
|
+
/**
|
|
1366
|
+
* 🔴 THE SCHEDULER ALREADY CHOSE A CHECKOUT FOR EVERY START, and
|
|
1367
|
+
* nothing carried it. `ObservedPremise.workspace` has been on the start
|
|
1368
|
+
* action since Phase 3; the launcher keyed its live map on the literal
|
|
1369
|
+
* string `'run'` and peeked the project's own lease, so a second launch
|
|
1370
|
+
* was refused by the first however many workspaces were idle.
|
|
1371
|
+
*/
|
|
1372
|
+
workspaceFor: (goalId) => placement.get(goalId),
|
|
1373
|
+
onDecision: (goalId, kind) => {
|
|
1374
|
+
lastDecision.set(goalId, kind);
|
|
1375
|
+
},
|
|
1376
|
+
/**
|
|
1377
|
+
* 🔴 AND HOW MANY MAY GO AT ONCE. `decideSchedule` already bounds its
|
|
1378
|
+
* own answer by `maxWorkspaces` and `maxAgentsInFlight` minus what is
|
|
1379
|
+
* in flight, so the number of `start` actions IS the free capacity. The
|
|
1380
|
+
* tick used to launch one per cadence whatever it was told, which fills
|
|
1381
|
+
* N slots in N cadences and calls that a fleet.
|
|
1382
|
+
*/
|
|
1383
|
+
launchesPerTick: () => (opts.config.machine?.coordinator === true ? freeCapacity : 1),
|
|
1384
|
+
autonomy: autonomyStore,
|
|
1385
|
+
attempts,
|
|
1386
|
+
starter: runStarter,
|
|
1387
|
+
// REAL reconciliation. The server is the authority on whether a person
|
|
1388
|
+
// still permits this, and an unreachable server is `unreachable` — a
|
|
1389
|
+
// reason to wait — never `active` and never an ending.
|
|
1390
|
+
reconcile: async (grant) => {
|
|
1391
|
+
try {
|
|
1392
|
+
// ONE read, answering both halves: is this grant still authorized,
|
|
1393
|
+
// and is the wording it was authorized for still the wording.
|
|
1394
|
+
// Asking separately would act on two answers from two moments, with
|
|
1395
|
+
// an edit able to land in between.
|
|
1396
|
+
const { autonomy, goal } = await client.goalAutonomy(projectId, grant.goalId);
|
|
1397
|
+
const mine = autonomy.find((a) => a.controllerId === grant.controllerId) ?? null;
|
|
1398
|
+
return (0, grant_reconcile_1.reconcileGrant)(grant, mine, goal, Date.now());
|
|
1399
|
+
}
|
|
1400
|
+
catch (e) {
|
|
1401
|
+
return { kind: 'unreachable', because: e.message };
|
|
1402
|
+
}
|
|
1403
|
+
},
|
|
1404
|
+
projectTerminal: async (grant, state, because) => {
|
|
1405
|
+
const settlable = (0, grant_reconcile_1.settlableState)(state);
|
|
1406
|
+
// Nothing to report. `cancelled` is the case: a person's word, which
|
|
1407
|
+
// this machine may observe and stop on but may not write.
|
|
1408
|
+
if (settlable === null)
|
|
1409
|
+
return { ok: true };
|
|
1410
|
+
try {
|
|
1411
|
+
const { autonomy } = await client.goalAutonomy(projectId, grant.goalId);
|
|
1412
|
+
const mine = autonomy.find((a) => a.controllerId === grant.controllerId);
|
|
1413
|
+
if (!mine)
|
|
1414
|
+
return { ok: false, because: 'the server has no record of this grant' };
|
|
1415
|
+
// L4b: a met goal is settled WITH what key 2 confirmed, when the run wrote it.
|
|
1416
|
+
let delivery = null;
|
|
1417
|
+
if (settlable === 'satisfied') {
|
|
1418
|
+
await refusalMarks.load();
|
|
1419
|
+
delivery = (0, pursuit_delivery_1.deliveryForPursuit)((id) => refusalMarks.find(id), grant.controllerId);
|
|
1420
|
+
}
|
|
1421
|
+
await client.settleGoalAutonomy(projectId, grant.controllerId, {
|
|
1422
|
+
version: mine.version,
|
|
1423
|
+
state: settlable,
|
|
1424
|
+
reason: because,
|
|
1425
|
+
...(delivery === null ? {} : { delivery }),
|
|
1426
|
+
});
|
|
1427
|
+
return { ok: true };
|
|
1428
|
+
}
|
|
1429
|
+
catch (e) {
|
|
1430
|
+
return { ok: false, because: e.message };
|
|
1431
|
+
}
|
|
1432
|
+
},
|
|
1433
|
+
// A PERSON'S TAP, read straight off the control file the daemon
|
|
1434
|
+
// already writes when a pause/stop/resume command arrives.
|
|
1435
|
+
//
|
|
1436
|
+
// An unreadable control file reports a STOP rather than nothing. Not
|
|
1437
|
+
// being able to see whether somebody asked the machine to wait is not
|
|
1438
|
+
// the same as nobody having asked, and only one of those two mistakes
|
|
1439
|
+
// costs a person their evening.
|
|
1440
|
+
humanHold: async () => {
|
|
1441
|
+
// `readOrTrouble`, not `read`. This catch used to be unreachable:
|
|
1442
|
+
// ControlStore.read swallows the failure and answers null, so an
|
|
1443
|
+
// unreadable control file read as "nobody asked" and the machine
|
|
1444
|
+
// carried on — a fail-OPEN default under a comment claiming the
|
|
1445
|
+
// opposite.
|
|
1446
|
+
const out = await controlFile.readOrTrouble();
|
|
1447
|
+
if (!out.ok) {
|
|
1448
|
+
return { kind: 'stop', actor: 'this machine could not read the control file' };
|
|
1449
|
+
}
|
|
1450
|
+
const req = out.request;
|
|
1451
|
+
if (req === null || req.kind === 'resume')
|
|
1452
|
+
return null;
|
|
1453
|
+
return { kind: req.kind, ...(req.actor === undefined ? {} : { actor: req.actor }) };
|
|
1454
|
+
},
|
|
1455
|
+
/**
|
|
1456
|
+
* 🔴 READ FROM THE RUN'S OWN LEDGER, WHEREVER THE RUN IS. This was
|
|
1457
|
+
* `readRunEnded(stateDir, …)` — the PROJECT's ledger — and a run the
|
|
1458
|
+
* coordinator placed in another checkout writes its ending to that
|
|
1459
|
+
* checkout, so its ordinal never closed. The reader locates the run
|
|
1460
|
+
* through the registry record and the start mark and validates the
|
|
1461
|
+
* checkout before reading; an ordinary checkout takes the exact path
|
|
1462
|
+
* it always did. See `run-ending-locator.ts`.
|
|
1463
|
+
*/
|
|
1464
|
+
// PROGRAMME MODE, slice 1 (M6-7): off by default. On, the controller
|
|
1465
|
+
// continues from an `approved` decision and the Start carries it,
|
|
1466
|
+
// reserved once through the person's own explicit-start decision —
|
|
1467
|
+
// WITHOUT the person-only safety override that sits in front of it
|
|
1468
|
+
// for a tap (a loop-safety hold still holds in the controller).
|
|
1469
|
+
programme: opts.config.machine?.programme === true,
|
|
1470
|
+
continueFrom: (gate, requestId, workspace) => (0, explicit_start_gate_1.decideExplicitStart)({
|
|
1471
|
+
governed,
|
|
1472
|
+
resumes,
|
|
1473
|
+
carrier: () => new run_lease_1.RunLeaseStore((0, node_path_1.join)(workspace ?? opts.cwd, git_driver_1.STATE_DIR, 'state'), run_lease_1.nodeLeaseFs).inspect(),
|
|
1474
|
+
log: opts.log,
|
|
1475
|
+
}, gate, requestId),
|
|
1476
|
+
runEnded: async (runId) => readRunEnded_(runId),
|
|
1477
|
+
/**
|
|
1478
|
+
* WHAT THE SERVER SAYS THE PURSUIT IS WAITING FOR.
|
|
1479
|
+
*
|
|
1480
|
+
* Deliberately placed AFTER `runEnded`, and not because of ordering in
|
|
1481
|
+
* the object: `autonomy-wiring.spec.ts` reads the source text between
|
|
1482
|
+
* `reconcile:` and `runEnded:` to prove the reconciliation callback
|
|
1483
|
+
* never turns a caught error into an answer. A second callback with its
|
|
1484
|
+
* own catch inside that slice would fail a guard for a reason that has
|
|
1485
|
+
* nothing to do with it.
|
|
1486
|
+
*
|
|
1487
|
+
* FAILS CLOSED, in the two ways it can. Without a server there is
|
|
1488
|
+
* nobody to have asked, so a machine that cannot pair reports `unknown`
|
|
1489
|
+
* rather than `none` — the same choice `humanHold` above makes about an
|
|
1490
|
+
* unreadable control file, and for the same reason: not being able to
|
|
1491
|
+
* see whether somebody was asked is not the same as nobody having been.
|
|
1492
|
+
*
|
|
1493
|
+
* A CLAIM ALREADY GIVEN OUT IS NOT A BLOCKER, so the resumes are read
|
|
1494
|
+
* here and folded in. An answer is a message to one run, once; without
|
|
1495
|
+
* this the first refusal a goal ever received would hold it for the
|
|
1496
|
+
* rest of its life.
|
|
1497
|
+
*/
|
|
1498
|
+
approvalGate: (grant) => (0, approval_gate_read_1.readApprovalGate)(client, {
|
|
1499
|
+
projectId,
|
|
1500
|
+
goalId: grant.goalId,
|
|
1501
|
+
goalVersion: grant.goalVersion,
|
|
1502
|
+
now: Date.now(),
|
|
1503
|
+
// Asked with the ids that came back. `unknown` on trouble, never an
|
|
1504
|
+
// empty set: a claim that could not be read is not a claim that is
|
|
1505
|
+
// not there, and reading it as absent hands one answer to two runs.
|
|
1506
|
+
consumed: (ids) => resumes.consumed(ids),
|
|
1507
|
+
}),
|
|
1508
|
+
/**
|
|
1509
|
+
* Whether this machine has already decided that repeating is useless.
|
|
1510
|
+
*
|
|
1511
|
+
* RE-OPENED EVERY TICK, exactly like `startMarkOf` below and for the
|
|
1512
|
+
* identical reason: the observations are written by the RUN process, so
|
|
1513
|
+
* a snapshot taken when the daemon booted is of the moment before the
|
|
1514
|
+
* thing being looked for happened.
|
|
1515
|
+
*
|
|
1516
|
+
* FAILS CLOSED. A history that cannot be read HOLDS — a machine that
|
|
1517
|
+
* cannot see its own safety record must not spend a night on the
|
|
1518
|
+
* assumption that the record is empty, which is the same choice
|
|
1519
|
+
* `approvalGate` makes about an unreachable server.
|
|
1520
|
+
*/
|
|
1521
|
+
loopSafety: async (grant) => {
|
|
1522
|
+
if ((await safety.open()) !== 'ok') {
|
|
1523
|
+
return { held: true, because: 'the loop-safety history could not be read' };
|
|
1524
|
+
}
|
|
1525
|
+
const incident = safety.activeIncident(grant.goalId, grant.goalVersion);
|
|
1526
|
+
return incident === null ? { held: false } : { held: true, because: incident.because };
|
|
1527
|
+
},
|
|
1528
|
+
startMarkOf: async (requestId) => {
|
|
1529
|
+
// Re-read every time: the file is written by the RUN process, so a
|
|
1530
|
+
// snapshot taken here would be of the moment before the thing being
|
|
1531
|
+
// looked for happened.
|
|
1532
|
+
await startMarks.load();
|
|
1533
|
+
return startMarks.find(requestId);
|
|
1534
|
+
},
|
|
1535
|
+
refusalOf: async (requestId) => {
|
|
1536
|
+
await refusalMarks.load();
|
|
1537
|
+
const mark = refusalMarks.find(requestId);
|
|
1538
|
+
return mark === null
|
|
1539
|
+
? null
|
|
1540
|
+
: {
|
|
1541
|
+
reason: mark.reason,
|
|
1542
|
+
...(mark.code === undefined ? {} : { code: mark.code }),
|
|
1543
|
+
...(mark.branch === undefined ? {} : { branch: mark.branch }),
|
|
1544
|
+
};
|
|
1545
|
+
},
|
|
1546
|
+
...(thirdKey === null ? {} : { publicationOf: thirdKey }),
|
|
1547
|
+
now: () => Date.now(),
|
|
1548
|
+
log: opts.log,
|
|
1549
|
+
});
|
|
1550
|
+
// M4-1: one checkout per cadence, after the tick, only when it asked for one.
|
|
1551
|
+
const asked = toProvision;
|
|
1552
|
+
toProvision = null;
|
|
1553
|
+
const made = await (0, workspace_provisioner_1.provisionOnce)(asked, provisioner, opts.log);
|
|
1554
|
+
if (made.path !== null && provisioningFor !== null)
|
|
1555
|
+
provisionedFor.set(provisioningFor, { path: made.path, at: Date.now() });
|
|
1556
|
+
provisioningFor = null;
|
|
1557
|
+
// M4-2: one stop request per cadence, after the tick, only when it decided one.
|
|
1558
|
+
const stopAsk = toPreempt;
|
|
1559
|
+
toPreempt = null;
|
|
1560
|
+
await (0, preemptor_1.preemptOnce)(stopAsk, preemptor, opts.log);
|
|
1561
|
+
// M4-4: one checkout the machine made, retired per cadence, only with the lock held — never one
|
|
1562
|
+
// placed this cadence or last, never the one just provisioned.
|
|
1563
|
+
const placedNow = [...placement.values()];
|
|
1564
|
+
if (reaper !== null && coordinatorLock !== null)
|
|
1565
|
+
await reaper([...placedNow, ...placedBefore, ...(made.path === null ? [] : [made.path])]);
|
|
1566
|
+
placedBefore = placedNow;
|
|
1567
|
+
// M5-1: one branch offered to the integration lane per cadence, only with the lock held.
|
|
1568
|
+
if (integrator !== null && coordinatorLock !== null)
|
|
1569
|
+
await integrator();
|
|
1570
|
+
},
|
|
1571
|
+
// M5-6: every ledger this machine holds — the project's, the reaper's retired records, each registered
|
|
1572
|
+
// run's, each checkout's — the same list `looply goal` reads, re-listed every pass; one cursor per run as before.
|
|
1573
|
+
replay: (0, ledger_replay_1.makeLedgersReplay)(() => (0, goal_runs_1.ledgersOn)(stateDir, new workspace_1.RunRegistry(projectState), opts.cwd), ledger_store_1.nodeLedgerFs, { log: opts.log }),
|
|
1574
|
+
onCommand: makeCommandHandler(opts.log,
|
|
1575
|
+
// Given the state dir so it can read the run lease before spawning. The
|
|
1576
|
+
// daemon's own memory of what it started does not survive its restart;
|
|
1577
|
+
// the lease does, and that gap is the duplicate-runner window.
|
|
1578
|
+
// Given the state dir so it can read the run lease before spawning, AND
|
|
1579
|
+
// the durable start marks — the only thing that may report a run as
|
|
1580
|
+
// started, and therefore the only thing that may make a start-run
|
|
1581
|
+
// terminally done.
|
|
1582
|
+
(0, run_launcher_1.nodeRunLauncher)(opts.cwd, (0, node_path_1.join)(opts.cwd, git_driver_1.STATE_DIR, 'state')),
|
|
1583
|
+
// The same path `looply run` reads (see run-cmd.ts). The daemon and the
|
|
1584
|
+
// run are different processes over one repo, and this file is all they
|
|
1585
|
+
// share.
|
|
1586
|
+
// The FANNING writer, not the bare store: a phone Stop has to reach every
|
|
1587
|
+
// live execution, not just the first one to look. See `controlWriter`.
|
|
1588
|
+
controlWriter,
|
|
1589
|
+
// What this machine has already finished with. The lease covers "is a run
|
|
1590
|
+
// happening now"; this covers "did I already do this", which the lease
|
|
1591
|
+
// cannot answer because it stops existing when the run ends.
|
|
1592
|
+
receipts,
|
|
1593
|
+
// This daemon is for one project. `pop` scopes only to the machine, and a
|
|
1594
|
+
// machine can host several.
|
|
1595
|
+
projectId,
|
|
1596
|
+
// How this machine lets go of a goal's work. Given the lease store as
|
|
1597
|
+
// well as the queue, because it must not write `queue.json` while a run
|
|
1598
|
+
// is doing the same.
|
|
1599
|
+
(0, goal_ownership_1.nodeGoalReleaser)((0, node_path_1.join)(opts.cwd, git_driver_1.STATE_DIR, 'state'), ledger_store_1.nodeLedgerFs, new run_lease_1.RunLeaseStore((0, node_path_1.join)(opts.cwd, git_driver_1.STATE_DIR, 'state'), run_lease_1.nodeLeaseFs), exports.CLI_VERSION,
|
|
1600
|
+
// The same mirror the daemon pulls into, so carrying out a release also
|
|
1601
|
+
// records WHY, and a Start still in flight for the old version cannot
|
|
1602
|
+
// adopt work back into a goal that has been given up on.
|
|
1603
|
+
goals),
|
|
1604
|
+
// Where a grant of autonomy is written down. Its OWN directory of
|
|
1605
|
+
// receipts, not a field on any file two processes already share.
|
|
1606
|
+
// The same instance the controller reads. Two stores over one file would
|
|
1607
|
+
// be two caches of one fact.
|
|
1608
|
+
autonomyStore, explicitStartGate,
|
|
1609
|
+
// M6-9: the same placement the tick hands the launcher, for a person's Start too.
|
|
1610
|
+
(goalId) => placement.get(goalId),
|
|
1611
|
+
// M6-10: the owner's Start ceiling per grant, when declared.
|
|
1612
|
+
opts.config.machine?.programmeMode?.limits.startsPerGrant,
|
|
1613
|
+
/**
|
|
1614
|
+
* M6-14, widened in M6-22: while this machine is still deciding WHERE a
|
|
1615
|
+
* goal runs, a person's Start waits for it rather than falling to the
|
|
1616
|
+
* project checkout.
|
|
1617
|
+
*
|
|
1618
|
+
* Three ways to be in that state, and the third is the one M6-21 found:
|
|
1619
|
+
* a checkout is being made for it, one was made and the goal is not yet
|
|
1620
|
+
* placed in it, or this machine simply holds a live grant for the goal —
|
|
1621
|
+
* it is pursuing it, and the tick that says where has not run yet.
|
|
1622
|
+
*/
|
|
1623
|
+
{
|
|
1624
|
+
placing: (goalId) => provisioningFor === goalId || provisionedFor.has(goalId) || pursuing(goalId),
|
|
1625
|
+
}),
|
|
1626
|
+
log: opts.log,
|
|
1627
|
+
sleep,
|
|
1628
|
+
now: () => Date.now(),
|
|
1629
|
+
stopRequested: () => signals.requested(),
|
|
1630
|
+
});
|
|
1631
|
+
if (opts.once) {
|
|
1632
|
+
const beat = await daemon.beat();
|
|
1633
|
+
opts.log(`daemon : heartbeat ${beat ? 'ok' : 'failed'}`);
|
|
1634
|
+
signals.uninstall();
|
|
1635
|
+
return beat ? 0 : 1;
|
|
1636
|
+
}
|
|
1637
|
+
/**
|
|
1638
|
+
* 🔴 THE COORDINATOR LOCK, TAKEN AT LAST.
|
|
1639
|
+
*
|
|
1640
|
+
* `daemon-lock.ts` was written, specced, and never called by anything that
|
|
1641
|
+
* ships. So `coordinatorTick` — whose first act is to refuse without the lock
|
|
1642
|
+
* — could never return anything but a refusal, and, far more immediately, two
|
|
1643
|
+
* `looply daemon` processes over one repository both ran: two autonomy ticks
|
|
1644
|
+
* reading the same grants, each free to spend the same Start, with nothing in
|
|
1645
|
+
* either log mentioning the other. The file existed; the hazard it describes
|
|
1646
|
+
* was live.
|
|
1647
|
+
*
|
|
1648
|
+
* ⚠️ AFTER THE `--once` BRANCH, DELIBERATELY. A heartbeat check is not a
|
|
1649
|
+
* coordinator and must keep working beside a running daemon — refusing it
|
|
1650
|
+
* would break `looply daemon --once` on exactly the machines that are
|
|
1651
|
+
* working properly.
|
|
1652
|
+
*
|
|
1653
|
+
* ⚠️ AND IT IS A REFUSAL, NOT A WAIT. A second daemon that queued for the
|
|
1654
|
+
* lock would sit invisible until the first one stopped, which is the shape a
|
|
1655
|
+
* person reads as "it started fine".
|
|
1656
|
+
*/
|
|
1657
|
+
const taken = (0, daemon_lock_1.takeDaemonLock)(opts.cwd);
|
|
1658
|
+
if (!taken.ok) {
|
|
1659
|
+
opts.log(`daemon : ${taken.because}.`);
|
|
1660
|
+
signals.uninstall();
|
|
1661
|
+
return 1;
|
|
1662
|
+
}
|
|
1663
|
+
// Visible to the tick, which refreshes it and refuses to schedule without it.
|
|
1664
|
+
coordinatorLock = taken.lock;
|
|
1665
|
+
opts.log('daemon : connected. Ctrl-C to stop.');
|
|
1666
|
+
try {
|
|
1667
|
+
const final = await daemon.run();
|
|
1668
|
+
opts.log(`daemon : stopped — ${final.delivered} events delivered, ${final.buffered} still buffered`);
|
|
1669
|
+
return 0;
|
|
1670
|
+
}
|
|
1671
|
+
finally {
|
|
1672
|
+
coordinatorLock = null;
|
|
1673
|
+
taken.lock.release();
|
|
1674
|
+
signals.uninstall();
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
/**
|
|
1678
|
+
* The terms of a grant, off an `unknown` payload.
|
|
1679
|
+
*
|
|
1680
|
+
* Read defensively for the same reason `readStartPayload` is: the payload
|
|
1681
|
+
* travels from the server as Json and this build may be older or newer than
|
|
1682
|
+
* the one that wrote it. Every field is required -- there is no sensible
|
|
1683
|
+
* default for any of them, and a grant missing its ceiling or its expiry is
|
|
1684
|
+
* not a grant with a gap in it, it is not a grant.
|
|
1685
|
+
*/
|
|
1686
|
+
/** The Starts a grant payload asks for, when it says. Read before `grantOf` so a refusal can name the ceiling. */
|
|
1687
|
+
function askedStarts(payload) {
|
|
1688
|
+
if (typeof payload !== 'object' || payload === null)
|
|
1689
|
+
return null;
|
|
1690
|
+
const v = payload['maxStarts'];
|
|
1691
|
+
return typeof v === 'number' && Number.isInteger(v) ? v : null;
|
|
1692
|
+
}
|
|
1693
|
+
function grantOf(payload, ceiling = core_1.MAX_AUTONOMOUS_STARTS) {
|
|
1694
|
+
if (typeof payload !== 'object' || payload === null)
|
|
1695
|
+
return null;
|
|
1696
|
+
const p = payload;
|
|
1697
|
+
const { controllerId, goalId, goalVersion, maxStarts, expiresAt } = p;
|
|
1698
|
+
if (typeof controllerId !== 'string' || controllerId.length === 0)
|
|
1699
|
+
return null;
|
|
1700
|
+
if (typeof goalId !== 'string' || goalId.length === 0)
|
|
1701
|
+
return null;
|
|
1702
|
+
if (typeof goalVersion !== 'number' || !Number.isInteger(goalVersion) || goalVersion < 0) {
|
|
1703
|
+
return null;
|
|
1704
|
+
}
|
|
1705
|
+
// A ceiling of zero authorizes nothing and would be recorded as a live grant
|
|
1706
|
+
// that can never spend; a negative or fractional one is a build disagreeing
|
|
1707
|
+
// with the server. Both are refusals, not values to clamp.
|
|
1708
|
+
// THE MACHINE HOLDS ITS OWN CEILING.
|
|
1709
|
+
//
|
|
1710
|
+
// MAX_AUTONOMOUS_STARTS was enforced at exactly one site — the server,
|
|
1711
|
+
// where the row is minted — and the machine copied whatever arrived. So the
|
|
1712
|
+
// bound lived entirely on the far side of the wire: a bad migration, a
|
|
1713
|
+
// restored backup or a server bug carrying maxStarts: 100000 would have been
|
|
1714
|
+
// adopted verbatim, and reconciliation would have agreed with itself because
|
|
1715
|
+
// both numbers came off the same wrong row.
|
|
1716
|
+
//
|
|
1717
|
+
// A person's evening is spent HERE, so the bound that protects it belongs
|
|
1718
|
+
// here too. Refused rather than clamped: a grant whose terms this machine
|
|
1719
|
+
// does not recognise is not one to quietly reinterpret.
|
|
1720
|
+
if (typeof maxStarts !== 'number' || !Number.isInteger(maxStarts) || maxStarts < 1)
|
|
1721
|
+
return null;
|
|
1722
|
+
if (maxStarts > ceiling)
|
|
1723
|
+
return null;
|
|
1724
|
+
// PARSEABLE, not merely a string. An expiry that reads as NaN compares false
|
|
1725
|
+
// against every clock, so `Date.parse(expiresAt) <= now` would be false
|
|
1726
|
+
// forever and the grant would never run out of time.
|
|
1727
|
+
if (typeof expiresAt !== 'string' || Number.isNaN(Date.parse(expiresAt)))
|
|
1728
|
+
return null;
|
|
1729
|
+
return { controllerId, goalId, goalVersion, maxStarts, expiresAt };
|
|
1730
|
+
}
|
|
1731
|
+
/**
|
|
1732
|
+
* The goal and version a release names, off an `unknown` payload.
|
|
1733
|
+
*
|
|
1734
|
+
* `null` rather than `undefined` for an unreadable one: there is no "no goal"
|
|
1735
|
+
* case here — a release with no goal names nothing and can only be a mistake,
|
|
1736
|
+
* so both the missing and the malformed reading are the same refusal.
|
|
1737
|
+
*/
|
|
1738
|
+
function releaseOf(payload) {
|
|
1739
|
+
if (typeof payload !== 'object' || payload === null)
|
|
1740
|
+
return null;
|
|
1741
|
+
const p = payload;
|
|
1742
|
+
if (typeof p.goalId !== 'string' || p.goalId.length === 0)
|
|
1743
|
+
return null;
|
|
1744
|
+
// Required, not optional. The server stamps it on every release, and a
|
|
1745
|
+
// release without one could only record `abandoned` against whatever version
|
|
1746
|
+
// this machine happened to hold — a row no server ever wrote.
|
|
1747
|
+
if (typeof p.goalVersion !== 'number' || !Number.isInteger(p.goalVersion) || p.goalVersion < 0)
|
|
1748
|
+
return null;
|
|
1749
|
+
return { goalId: p.goalId, goalVersion: p.goalVersion };
|
|
1750
|
+
}
|
|
1751
|
+
//# sourceMappingURL=daemon-cmd.js.map
|