looply-cli 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +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 +2719 -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/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 +54 -6
- 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
package/dist/bin.js
ADDED
|
@@ -0,0 +1,2719 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.main = main;
|
|
5
|
+
const source_cmd_1 = require("./source-cmd");
|
|
6
|
+
const fs_1 = require("fs");
|
|
7
|
+
const tool_bridge_1 = require("./tool-bridge");
|
|
8
|
+
const node_crypto_1 = require("node:crypto");
|
|
9
|
+
const node_child_process_1 = require("node:child_process");
|
|
10
|
+
const path_1 = require("path");
|
|
11
|
+
const config_1 = require("./config");
|
|
12
|
+
const self_repo_id_1 = require("./self-repo-id");
|
|
13
|
+
const programme_mode_1 = require("./programme-mode");
|
|
14
|
+
const trusted_git_1 = require("./trusted-git");
|
|
15
|
+
const doctor_1 = require("./doctor");
|
|
16
|
+
const integration_runtime_1 = require("./integration-runtime");
|
|
17
|
+
const publication_recorder_1 = require("./publication-recorder");
|
|
18
|
+
const observation_inventory_1 = require("./observation-inventory");
|
|
19
|
+
const evidence_facts_1 = require("./evidence-facts");
|
|
20
|
+
const integration_deps_1 = require("./integration-deps");
|
|
21
|
+
const lane_claims_1 = require("./lane-claims");
|
|
22
|
+
const integration_runtime_2 = require("./integration-runtime");
|
|
23
|
+
const agent_contained_1 = require("./agent-contained");
|
|
24
|
+
const exec_domains_1 = require("./exec-domains");
|
|
25
|
+
const model_transport_1 = require("./model-transport");
|
|
26
|
+
const provider_state_1 = require("./provider-state");
|
|
27
|
+
const providers_1 = require("./providers");
|
|
28
|
+
const provider_preflight_1 = require("./provider-preflight");
|
|
29
|
+
const init_detect_1 = require("./init-detect");
|
|
30
|
+
const report_cmd_1 = require("./report-cmd");
|
|
31
|
+
const tool_call_record_1 = require("./tool-call-record");
|
|
32
|
+
const token_window_1 = require("./token-window");
|
|
33
|
+
const status_cmd_1 = require("./status-cmd");
|
|
34
|
+
const goal_view_1 = require("./goal-view");
|
|
35
|
+
const run_cmd_1 = require("./run-cmd");
|
|
36
|
+
const start_attempts_1 = require("./start-attempts");
|
|
37
|
+
const node_os_1 = require("node:os");
|
|
38
|
+
const autonomy_authority_1 = require("./autonomy-authority");
|
|
39
|
+
const scan_cmd_1 = require("./scan-cmd");
|
|
40
|
+
const verify_cmd_1 = require("./verify-cmd");
|
|
41
|
+
const queue_write_lease_1 = require("./queue-write-lease");
|
|
42
|
+
const workspace_quarantine_1 = require("./workspace-quarantine");
|
|
43
|
+
const queue_view_1 = require("./queue-view");
|
|
44
|
+
const queue_store_1 = require("./queue-store");
|
|
45
|
+
const ledger_store_1 = require("./ledger-store");
|
|
46
|
+
const git_driver_1 = require("./git-driver");
|
|
47
|
+
const repos_cmd_1 = require("./repos-cmd");
|
|
48
|
+
const core_1 = require("@looply/core");
|
|
49
|
+
const sessions_cmd_1 = require("./sessions-cmd");
|
|
50
|
+
const session_observe_runtime_1 = require("./session-observe-runtime");
|
|
51
|
+
const evidence_cmd_1 = require("./evidence-cmd");
|
|
52
|
+
const retention_apply_runtime_1 = require("./retention-apply-runtime");
|
|
53
|
+
const session_store_1 = require("./session-store");
|
|
54
|
+
const compatible_sets_1 = require("./compatible-sets");
|
|
55
|
+
const workspace_1 = require("./workspace");
|
|
56
|
+
const run_refusal_marks_1 = require("./run-refusal-marks");
|
|
57
|
+
const daemon_cmd_1 = require("./daemon-cmd");
|
|
58
|
+
const machine_store_1 = require("./machine-store");
|
|
59
|
+
const server_client_1 = require("./server-client");
|
|
60
|
+
const session_adapter_runtime_1 = require("./session-adapter-runtime");
|
|
61
|
+
const session_control_transport_1 = require("./session-control-transport");
|
|
62
|
+
const session_checkpoint_runtime_1 = require("./session-checkpoint-runtime");
|
|
63
|
+
const session_observation_store_1 = require("./session-observation-store");
|
|
64
|
+
const welcome_1 = require("./welcome");
|
|
65
|
+
const agent_cmd_1 = require("./agent-cmd");
|
|
66
|
+
const ui_1 = require("./ui");
|
|
67
|
+
const setup_1 = require("./setup");
|
|
68
|
+
const setup_prompt_1 = require("./setup-prompt");
|
|
69
|
+
const integration_1 = require("./integration");
|
|
70
|
+
/**
|
|
71
|
+
* `looply` entry point.
|
|
72
|
+
*
|
|
73
|
+
* Only the commands that are implemented are listed. A command that prints
|
|
74
|
+
* "not implemented yet" is worse than one that does not exist, because it looks
|
|
75
|
+
* like a broken feature rather than an unbuilt one.
|
|
76
|
+
*/
|
|
77
|
+
const USAGE = `looply — an autonomous build loop you drive from your phone
|
|
78
|
+
|
|
79
|
+
usage:
|
|
80
|
+
looply pair [--server <url>] pair this machine with your phone
|
|
81
|
+
looply daemon [--once] [--yes] stay connected: presence, and ship the ledger
|
|
82
|
+
looply init profile the repo and draft .looply/config.yaml
|
|
83
|
+
looply doctor [--config <path>] [--provider]
|
|
84
|
+
validate the config and run one full gate pass; --provider also asks the model to answer once
|
|
85
|
+
looply scan [--lens a,b] [--dry-run] read the code through several lenses and fill the queue
|
|
86
|
+
looply verify [--max N] check unverified items against the code, so a run can pick them
|
|
87
|
+
looply queue [--all] everything waiting, and why
|
|
88
|
+
looply integrate --init <ref> create looply/integration from a base you name
|
|
89
|
+
looply integrate --branch <name> [--goal <id>]
|
|
90
|
+
offer one finished branch to the integration queue:
|
|
91
|
+
fast-forward or nothing, checked at the commit that
|
|
92
|
+
would actually be published (needs run.integration)
|
|
93
|
+
looply runs [--reap] what this project is running; --reap retires proven-dead leftovers
|
|
94
|
+
looply runs --release <runId|slot-N|claim-lock>
|
|
95
|
+
clear a leftover a recycled pid makes look alive
|
|
96
|
+
looply source add <file> [--name <n>]
|
|
97
|
+
hand a Markdown (.md) or plain-text (.txt) document to
|
|
98
|
+
this project, verbatim, from the machine that holds it
|
|
99
|
+
looply source list what this project has been given
|
|
100
|
+
looply source show <id> one source: its statements, what each IS, where each came from,
|
|
101
|
+
and where each requirement stands against this repository
|
|
102
|
+
looply source reconcile <id> measure where each requirement stands against this repository
|
|
103
|
+
(HEAD's tree, by name — a place to look, never "implemented"), and record it
|
|
104
|
+
looply source coverage <id> which requirements no goal serves, which are served, and which a person
|
|
105
|
+
evidenced — a finished queue is not coverage
|
|
106
|
+
looply stop finish the round in flight, then stop
|
|
107
|
+
looply pause | looply resume hold between rounds, and let it go again
|
|
108
|
+
looply run [--hours N] [--dry-run] [--in <workspace> | --new-workspace]
|
|
109
|
+
start a run: pick, implement, gate, prove, keep or discard
|
|
110
|
+
--in runs it in another checkout of this repository, so two
|
|
111
|
+
runs can work at once without sharing a tree
|
|
112
|
+
--try-once-more spends a stopped goal's ONE retry (never
|
|
113
|
+
turns the brake off; the next failure stops it again)
|
|
114
|
+
looply status one-screen snapshot from .looply/state/status.json
|
|
115
|
+
looply report run report from the local ledger, alerts first
|
|
116
|
+
looply goal <id> one goal from this machine's records: the grant and the lane's answer,
|
|
117
|
+
compiles, proposals, the lane's attempts, each run's rounds — read-only
|
|
118
|
+
looply goals every mirrored goal, one line each, with its grant's standing
|
|
119
|
+
looply repos [--accept] [--published <repo>]
|
|
120
|
+
the other repositories this programme spans: where each one is,
|
|
121
|
+
and whether the revisions somebody accepted together still hold
|
|
122
|
+
looply evidence what this project has accumulated, and what the retention
|
|
123
|
+
policy would do with it. It deletes nothing.
|
|
124
|
+
looply sessions [--offer <file>] [--ack <id>]
|
|
125
|
+
sessions this machine did not start, asking to be managed;
|
|
126
|
+
observe-only — authorizing control is the server's to give
|
|
127
|
+
looply version
|
|
128
|
+
|
|
129
|
+
not built yet: item
|
|
130
|
+
`;
|
|
131
|
+
function loadConfig(path) {
|
|
132
|
+
const full = (0, path_1.resolve)(path);
|
|
133
|
+
let text;
|
|
134
|
+
try {
|
|
135
|
+
text = (0, fs_1.readFileSync)(full, 'utf8');
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
throw new config_1.ConfigError('<file>', `cannot read ${full}`);
|
|
139
|
+
}
|
|
140
|
+
return (0, config_1.parseConfig)(text);
|
|
141
|
+
}
|
|
142
|
+
/* istanbul ignore next — real fs and env; the logic lives in doctor.ts */
|
|
143
|
+
function nodeDoctorDeps() {
|
|
144
|
+
// Lifted to integration-deps.ts (M5-1) so doctor, the integrate command and the daemon ask the same questions.
|
|
145
|
+
return (0, integration_deps_1.nodeDoctorDepsFor)(process.cwd());
|
|
146
|
+
}
|
|
147
|
+
/* istanbul ignore next — real fs, tty and cwd; the logic lives in setup.ts */
|
|
148
|
+
async function init(argv) {
|
|
149
|
+
const target = (0, path_1.resolve)('.looply/config.yaml');
|
|
150
|
+
// Never overwrite: by the time init is re-run the file is the user's, full of
|
|
151
|
+
// hand-tuned rails and gates a fresh draft would silently throw away.
|
|
152
|
+
if ((0, fs_1.existsSync)(target)) {
|
|
153
|
+
process.stderr.write(`init: ${target} already exists — move it aside first for a fresh draft\n`);
|
|
154
|
+
return 1;
|
|
155
|
+
}
|
|
156
|
+
const profile = (0, init_detect_1.detectRepo)({
|
|
157
|
+
readFile: (p) => {
|
|
158
|
+
try {
|
|
159
|
+
return (0, fs_1.readFileSync)((0, path_1.resolve)(p), 'utf8');
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
}, (0, path_1.basename)(process.cwd()));
|
|
166
|
+
// What detection found, before anything is asked — so the questions arrive
|
|
167
|
+
// with context rather than out of nowhere.
|
|
168
|
+
process.stdout.write((0, ui_1.box)((0, ui_1.fields)([
|
|
169
|
+
['project', profile.name],
|
|
170
|
+
['manager', `${profile.packageManager} ${(0, ui_1.dim)(`(${profile.packageManagerEvidence})`)}`],
|
|
171
|
+
...(profile.workspaces.length
|
|
172
|
+
? [['spaces', profile.workspaces.join(', ')]]
|
|
173
|
+
: []),
|
|
174
|
+
[
|
|
175
|
+
'checks',
|
|
176
|
+
[profile.typecheck?.name, profile.testRunner?.name].filter(Boolean).join(', ') ||
|
|
177
|
+
(0, ui_1.dim)('none detected'),
|
|
178
|
+
],
|
|
179
|
+
]), { title: 'what this project looks like' }) + '\n');
|
|
180
|
+
// --yes for CI and for anyone who would rather edit the file than answer
|
|
181
|
+
// questions. Non-interactive stdin takes the same path: prompting a pipe
|
|
182
|
+
// hangs forever, which is the worst possible failure for a setup command.
|
|
183
|
+
const useDefaults = argv.includes('--yes') || argv.includes('-y') || !process.stdin.isTTY;
|
|
184
|
+
const asker = (0, setup_prompt_1.nodeAsker)();
|
|
185
|
+
let answers;
|
|
186
|
+
try {
|
|
187
|
+
answers = await (0, setup_prompt_1.runSetup)({
|
|
188
|
+
profile,
|
|
189
|
+
asker,
|
|
190
|
+
write: (s) => process.stdout.write(s),
|
|
191
|
+
useDefaults,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
finally {
|
|
195
|
+
asker.close();
|
|
196
|
+
}
|
|
197
|
+
(0, fs_1.mkdirSync)((0, path_1.resolve)('.looply'), { recursive: true });
|
|
198
|
+
(0, fs_1.writeFileSync)(target, (0, setup_1.renderConfig)(profile, answers));
|
|
199
|
+
const paired = (() => {
|
|
200
|
+
try {
|
|
201
|
+
return new machine_store_1.MachineStore((0, machine_store_1.machineFilePath)()).load() !== null;
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
})();
|
|
207
|
+
process.stdout.write((0, setup_prompt_1.renderSetupDone)((0, setup_1.summarise)(answers, profile), target, paired));
|
|
208
|
+
if (useDefaults && !answers.rails.length) {
|
|
209
|
+
process.stdout.write(` ${(0, ui_1.warn)('no rails were set — nothing in this repo is off limits. Edit forbidden_paths before a long run.')}\n\n`);
|
|
210
|
+
}
|
|
211
|
+
return 0;
|
|
212
|
+
}
|
|
213
|
+
async function doctor(argv) {
|
|
214
|
+
const at = argv.indexOf('--config');
|
|
215
|
+
const path = at >= 0 ? argv[at + 1] : '.looply/config.yaml';
|
|
216
|
+
if (!path) {
|
|
217
|
+
process.stderr.write('doctor: --config needs a path\n');
|
|
218
|
+
return 1;
|
|
219
|
+
}
|
|
220
|
+
const cfg = loadConfig(path);
|
|
221
|
+
process.stdout.write(`project : ${cfg.project.name}\n`);
|
|
222
|
+
process.stdout.write(`budget : ${cfg.run.minHours}h, up to ${cfg.run.maxRounds} rounds\n`);
|
|
223
|
+
process.stdout.write(`proof : ${cfg.proof.enabled ? 'on' : 'OFF'}\n`);
|
|
224
|
+
process.stdout.write(`notify : ${cfg.notify.channel} every ${cfg.notify.everyMin}min\n`);
|
|
225
|
+
process.stdout.write('gates :\n');
|
|
226
|
+
for (const g of cfg.gates) {
|
|
227
|
+
const bits = [g.required ? 'required' : null, g.when ? `when ${g.when}` : null].filter(Boolean);
|
|
228
|
+
process.stdout.write(` - ${g.name}${bits.length ? ` (${bits.join(', ')})` : ''}: ${g.run}\n`);
|
|
229
|
+
}
|
|
230
|
+
process.stdout.write(`rails : ${cfg.rails.forbiddenPaths.join(', ') || '(none)'}\n`);
|
|
231
|
+
process.stdout.write(`mode : ${(0, programme_mode_1.describeProgramme)(cfg)}\n`);
|
|
232
|
+
/**
|
|
233
|
+
* WHAT THE LAST WINDOW COST IN TOKENS, WHICH NOTHING HAS EVER SHOWN.
|
|
234
|
+
*
|
|
235
|
+
* These figures have been on every spend row since they were first parsed,
|
|
236
|
+
* and the only reader of that ledger sums dollars — the capability audit's
|
|
237
|
+
* E-05. This is the one command that already reports what this machine is and
|
|
238
|
+
* what it is about to do, so it is where they go. The filesystem read is here
|
|
239
|
+
* because the filesystem is; what may be said, and what may not, is decided
|
|
240
|
+
* in `token-window.ts`.
|
|
241
|
+
*
|
|
242
|
+
* The machine's own money window when it declares one, so the two figures a
|
|
243
|
+
* person compares are over the same hours; otherwise the stated default.
|
|
244
|
+
*/
|
|
245
|
+
const tokenWindowMs = cfg.machine?.envelope?.windowMs ?? token_window_1.DEFAULT_TOKEN_WINDOW_MS;
|
|
246
|
+
process.stdout.write((0, token_window_1.tokenLine)((0, token_window_1.tokensInWindow)(Date.now(), tokenWindowMs), tokenWindowMs) + String.fromCharCode(10));
|
|
247
|
+
const warnings = (0, config_1.configWarnings)(cfg);
|
|
248
|
+
for (const w of warnings)
|
|
249
|
+
process.stdout.write(`warning : ${w}\n`);
|
|
250
|
+
// The integration ref, asked here rather than at round 1 of a run (M6-12).
|
|
251
|
+
const refWarning = await (0, doctor_1.integrationRefWarning)(cfg, new git_driver_1.GitDriver((0, git_driver_1.nodeGitExec)(process.cwd())));
|
|
252
|
+
if (refWarning !== null)
|
|
253
|
+
process.stdout.write(`warning : ${refWarning}\n`);
|
|
254
|
+
process.stdout.write('doctor : running interlocks and one full gate pass (no agent call)\n');
|
|
255
|
+
const report = await (0, doctor_1.runDoctorGate)(cfg, nodeDoctorDeps());
|
|
256
|
+
/**
|
|
257
|
+
* 🔴 THIS SAID "UNTRUSTED EXECUTION IS CONTAINED" AND MEANT ONLY THE GATE.
|
|
258
|
+
*
|
|
259
|
+
* MEASURED on a machine printing exactly that line: a provider executable
|
|
260
|
+
* appended to and TRUNCATED the machine spend ledger, and wrote a path
|
|
261
|
+
* outside every declared `agent.runtime` grant. Both are true at once,
|
|
262
|
+
* because the AppContainer is applied to GATE STEPS — the project's own
|
|
263
|
+
* commands — and the contained-provider path was deliberately removed from
|
|
264
|
+
* the transport after two phases established the provider cannot run inside
|
|
265
|
+
* one.
|
|
266
|
+
*
|
|
267
|
+
* A single PASS line covering all subprocesses is therefore a claim nobody
|
|
268
|
+
* can honour. Four separate facts, four separate lines, and the one that is
|
|
269
|
+
* an ASSUMPTION says so in the word `assumed`.
|
|
270
|
+
*/
|
|
271
|
+
if (report.containment) {
|
|
272
|
+
const c = report.containment;
|
|
273
|
+
process.stdout.write(c.available
|
|
274
|
+
? `doctor : gate steps are contained (${c.backend})` + String.fromCharCode(10)
|
|
275
|
+
: `doctor : gate steps are NOT contained (${c.backend}) — ${c.because ?? 'unavailable'}` +
|
|
276
|
+
String.fromCharCode(10));
|
|
277
|
+
}
|
|
278
|
+
// The provider is not confined by anything, on any machine, in this build.
|
|
279
|
+
// Stated unconditionally because it does not vary: see `model-transport`.
|
|
280
|
+
process.stdout.write('doctor : the provider process is NOT confined — it runs as you, and is TRUSTED by' +
|
|
281
|
+
String.fromCharCode(10) +
|
|
282
|
+
' assumption. Run only provider binaries you trust with this machine.' +
|
|
283
|
+
String.fromCharCode(10));
|
|
284
|
+
process.stdout.write(`doctor : model tool access is mediated (${tool_bridge_1.TOOL_POLICIES.implementer.tools.join(', ')} for the` +
|
|
285
|
+
String.fromCharCode(10) +
|
|
286
|
+
` implementer; ${tool_bridge_1.TOOL_POLICIES.verifier.tools.join(', ')} for the verifier), and` +
|
|
287
|
+
String.fromCharCode(10) +
|
|
288
|
+
' off-project paths are refused at the bridge.' +
|
|
289
|
+
String.fromCharCode(10));
|
|
290
|
+
if (report.abort) {
|
|
291
|
+
process.stderr.write(`ABORT : ${report.abort.reason} (code ${report.abort.code})\n`);
|
|
292
|
+
return 1;
|
|
293
|
+
}
|
|
294
|
+
for (const s of report.steps) {
|
|
295
|
+
const label = s.ok ? 'ok ' : 'FAIL';
|
|
296
|
+
const why = s.timedOut ? ' — timed out' : '';
|
|
297
|
+
process.stdout.write(` ${label} ${s.step} (${s.ms}ms)${why}\n`);
|
|
298
|
+
if (!s.ok && s.output)
|
|
299
|
+
process.stdout.write(`${s.output.replace(/\s+$/, '')}\n`);
|
|
300
|
+
}
|
|
301
|
+
process.stdout.write(report.ok ? 'doctor : all gate steps pass\n' : 'doctor : gate steps failed\n');
|
|
302
|
+
/**
|
|
303
|
+
* 🔴 THE AGENT CHECK IS OPT-IN, AND ITS ABSENCE WAS A REAL HOLE.
|
|
304
|
+
*
|
|
305
|
+
* `doctor` deliberately makes NO agent call — it exists to find a wrong `tsc`
|
|
306
|
+
* in ninety seconds, and that was the right decision. But it left the other
|
|
307
|
+
* half unchecked: a wrong AGENT command was found at round 1 of an overnight
|
|
308
|
+
* run, by which point a person is asleep.
|
|
309
|
+
*
|
|
310
|
+
* ⚠️ OPT-IN BECAUSE IT SPENDS A MODEL CALL. Making it part of every `doctor`
|
|
311
|
+
* would put a cost on the check people run most often, and they would stop
|
|
312
|
+
* running it.
|
|
313
|
+
*/
|
|
314
|
+
if (argv.includes('--provider')) {
|
|
315
|
+
const resolved = (0, providers_1.resolveProvider)({
|
|
316
|
+
...(cfg.provider === undefined ? {} : { provider: cfg.provider }),
|
|
317
|
+
spec: cfg.agent,
|
|
318
|
+
});
|
|
319
|
+
if (!('spec' in resolved)) {
|
|
320
|
+
process.stderr.write(`provider: ${resolved.message}\n`);
|
|
321
|
+
return 1;
|
|
322
|
+
}
|
|
323
|
+
if (resolved.warning !== null)
|
|
324
|
+
process.stdout.write(`warning : ${resolved.warning}\n`);
|
|
325
|
+
/**
|
|
326
|
+
* 🔴 READINESS FIRST, AND IT COSTS NOTHING. `auth status` is local and prints
|
|
327
|
+
* the profile’s own state; asking the model whether it is logged in spends a
|
|
328
|
+
* turn to learn something a file already knows. More importantly it answers
|
|
329
|
+
* the RIGHT question: this is the profile and environment the run uses.
|
|
330
|
+
*
|
|
331
|
+
* ⚠️ AND IT REPORTS THE REASON IT WAS GIVEN, NOT A GUESS. Five states are kept
|
|
332
|
+
* apart in `provider-state.ts` precisely because only ONE of them means "log
|
|
333
|
+
* in"; flattening them sends somebody to re-authenticate an account that was
|
|
334
|
+
* never the trouble. Measured before this existed: an unauthenticated profile
|
|
335
|
+
* was reported as `refused`, advising a comparison of command-line flags.
|
|
336
|
+
*/
|
|
337
|
+
const profile = (0, provider_state_1.providerProfileDir)();
|
|
338
|
+
const exe = resolved.spec.executable ?? (0, provider_state_1.providerExecutableFor)((0, agent_contained_1.claudePackageDir)());
|
|
339
|
+
if (exe === null) {
|
|
340
|
+
process.stdout.write('provider: no provider runtime was found — Claude Code is not installed beside node,\n');
|
|
341
|
+
process.stdout.write(' and agent.executable names none.\n');
|
|
342
|
+
return 1;
|
|
343
|
+
}
|
|
344
|
+
const sessionDir = (0, model_transport_1.providerSessionDir)(process.cwd(), 'implementer');
|
|
345
|
+
const ready = (0, provider_state_1.checkProviderAuth)(exe, { ...(0, exec_domains_1.buildProviderEnv)(sessionDir), CLAUDE_CONFIG_DIR: profile });
|
|
346
|
+
if (!ready.ok) {
|
|
347
|
+
process.stdout.write(`provider: not ready — ${ready.reason}\n`);
|
|
348
|
+
process.stdout.write(` ${ready.because}\n`);
|
|
349
|
+
process.stdout.write(` profile: ${ready.profile}\n`);
|
|
350
|
+
process.stdout.write(' no model call was spent finding this out.\n');
|
|
351
|
+
return 1;
|
|
352
|
+
}
|
|
353
|
+
process.stdout.write(`provider: logged in (${ready.method}) for ${ready.profile}\n`);
|
|
354
|
+
process.stdout.write('provider: asking it to answer once (this spends a model call)\n');
|
|
355
|
+
// `exe` above is the path `checkProviderAuth` just used successfully, and
|
|
356
|
+
// the one the run would spawn. Handing it over keeps the check from
|
|
357
|
+
// refusing a provider the run starts perfectly well.
|
|
358
|
+
const pre = await (0, provider_preflight_1.preflightProvider)(resolved.spec, (0, provider_preflight_1.nodePreflightDeps)(resolved.spec.runtime ?? [], profile, exe));
|
|
359
|
+
process.stdout.write(`provider: ${pre.verdict} (${pre.ms}ms)\n`);
|
|
360
|
+
if (pre.detail)
|
|
361
|
+
process.stdout.write(` ${pre.detail}\n`);
|
|
362
|
+
if (pre.advice)
|
|
363
|
+
process.stdout.write(` ${pre.advice}\n`);
|
|
364
|
+
if (!(0, provider_preflight_1.preflightPasses)(pre))
|
|
365
|
+
return 1;
|
|
366
|
+
}
|
|
367
|
+
return report.ok ? 0 : 1;
|
|
368
|
+
}
|
|
369
|
+
/* istanbul ignore next — real fs and clock; the logic lives in status-cmd.ts */
|
|
370
|
+
function status() {
|
|
371
|
+
let text;
|
|
372
|
+
try {
|
|
373
|
+
text = (0, fs_1.readFileSync)((0, path_1.resolve)('.looply/state/status.json'), 'utf8');
|
|
374
|
+
}
|
|
375
|
+
catch (e) {
|
|
376
|
+
// Only "not there yet" maps to the friendly message; a permission error
|
|
377
|
+
// reported as "no run yet" would hide a real problem behind a calm screen.
|
|
378
|
+
if (e.code !== 'ENOENT')
|
|
379
|
+
throw e;
|
|
380
|
+
text = null;
|
|
381
|
+
}
|
|
382
|
+
const { out, code } = (0, status_cmd_1.renderStatus)(text, Date.now());
|
|
383
|
+
(code === 0 ? process.stdout : process.stderr).write(out);
|
|
384
|
+
return code;
|
|
385
|
+
}
|
|
386
|
+
/* istanbul ignore next — real fs; the logic lives in report-cmd.ts */
|
|
387
|
+
function report() {
|
|
388
|
+
let text;
|
|
389
|
+
try {
|
|
390
|
+
text = (0, fs_1.readFileSync)((0, path_1.resolve)('.looply/state/events.jsonl'), 'utf8');
|
|
391
|
+
}
|
|
392
|
+
catch (e) {
|
|
393
|
+
// Same rule as status: only "not there yet" is friendly; a permission
|
|
394
|
+
// error reported as "no run yet" would hide a real problem.
|
|
395
|
+
if (e.code !== 'ENOENT')
|
|
396
|
+
throw e;
|
|
397
|
+
text = null;
|
|
398
|
+
}
|
|
399
|
+
// The record of what the bridge served, when there is one. Read here for the
|
|
400
|
+
// same reason the ledger is: the filesystem stays out of report-cmd.ts. Its
|
|
401
|
+
// absence is the ordinary case — a scan, a verify, or a run before this
|
|
402
|
+
// existed — and renders nothing at all.
|
|
403
|
+
let toolText;
|
|
404
|
+
try {
|
|
405
|
+
toolText = (0, fs_1.readFileSync)((0, path_1.resolve)(`.looply/state/${tool_call_record_1.TOOL_CALL_FILE}`), 'utf8');
|
|
406
|
+
}
|
|
407
|
+
catch (e) {
|
|
408
|
+
if (e.code !== 'ENOENT')
|
|
409
|
+
throw e;
|
|
410
|
+
toolText = null;
|
|
411
|
+
}
|
|
412
|
+
const { out, code } = (0, report_cmd_1.renderReport)(text, toolText);
|
|
413
|
+
(code === 0 ? process.stdout : process.stderr).write(out);
|
|
414
|
+
return code;
|
|
415
|
+
}
|
|
416
|
+
/* istanbul ignore next — real fs; the logic lives in goal-view.ts */
|
|
417
|
+
async function goal(argv) {
|
|
418
|
+
const id = argv[0];
|
|
419
|
+
if (id === undefined || id.startsWith('--')) {
|
|
420
|
+
process.stderr.write('goal: which one? usage: looply goal <id>\n');
|
|
421
|
+
return 64;
|
|
422
|
+
}
|
|
423
|
+
const read = await (0, goal_view_1.readGoalFacts)(process.cwd(), id, { now: () => Date.now() });
|
|
424
|
+
if (!read.ok) {
|
|
425
|
+
process.stderr.write(`goal : ${read.because}\n`);
|
|
426
|
+
return read.code;
|
|
427
|
+
}
|
|
428
|
+
process.stdout.write((0, goal_view_1.renderGoalView)(read.facts, Date.now()));
|
|
429
|
+
return 0;
|
|
430
|
+
}
|
|
431
|
+
/* istanbul ignore next — real fs; the logic lives in goal-view.ts */
|
|
432
|
+
/**
|
|
433
|
+
* `looply repos` deps, against this checkout.
|
|
434
|
+
*
|
|
435
|
+
* 🔴 THE REVISION IS THE INTEGRATION REF WHERE THERE IS ONE, and HEAD otherwise
|
|
436
|
+
* — and it SAYS which. A compatible set is about what would be published, and
|
|
437
|
+
* for a project whose runs converge on `looply/integration` that is the
|
|
438
|
+
* integration ref, not whatever branch somebody left themselves on.
|
|
439
|
+
*
|
|
440
|
+
* ⚠️ NULL ON ANY FAILURE, never a guess. An unreadable repository must reach
|
|
441
|
+
* `combinedAcceptance` as absent so it answers `unmeasurable`; a fabricated sha
|
|
442
|
+
* would turn "I could not look" into "it has drifted".
|
|
443
|
+
*/
|
|
444
|
+
/**
|
|
445
|
+
* Adapters THIS BUILD speaks. A constant, not configuration: what a build can
|
|
446
|
+
* talk to is a property of the build, and letting a config name an adapter the
|
|
447
|
+
* code does not implement is how a session gets a label nothing backs.
|
|
448
|
+
*/
|
|
449
|
+
/** The same digest `daemon-cmd.ts` and `run-cmd.ts` key a project on. */
|
|
450
|
+
const sha256 = (text) => (0, node_crypto_1.createHash)('sha256').update(text, 'utf8').digest('hex');
|
|
451
|
+
/**
|
|
452
|
+
* ⚠️ HOW LONG ONE `--direct` WAITS, AND HOW OFTEN IT LOOKS.
|
|
453
|
+
*
|
|
454
|
+
* An unbounded wait on a directory a SESSION writes is this process hanging on
|
|
455
|
+
* the cooperation of the thing it is directing. Ten seconds is long enough for
|
|
456
|
+
* an adapter that is listening and short enough that one that is not does not
|
|
457
|
+
* hold a terminal.
|
|
458
|
+
*/
|
|
459
|
+
const DIRECT_WAIT_MS = 10_000;
|
|
460
|
+
const DIRECT_POLL_MS = 250;
|
|
461
|
+
const SUPPORTED_SESSION_ADAPTERS = ['looply-session-v1'];
|
|
462
|
+
/**
|
|
463
|
+
* `looply sessions` deps, against this checkout.
|
|
464
|
+
*
|
|
465
|
+
* 🔴 EVERY MEASUREMENT IS TAKEN HERE, AND NOTHING IS TAKEN FROM THE CLAIM. The
|
|
466
|
+
* claim supplies only the QUESTION — which workspace, which pid, which sha —
|
|
467
|
+
* and each answer comes from the machine. A field that could not be established
|
|
468
|
+
* is `null`, which `sessionAdoptionVerdict` turns into `unmeasurable`.
|
|
469
|
+
*/
|
|
470
|
+
/**
|
|
471
|
+
* `looply evidence` deps: the evidence stores of THIS project, on disk.
|
|
472
|
+
*
|
|
473
|
+
* 🔴 TWO STORES, NAMED, AND THE REST DELIBERATELY ABSENT. The policy's docblock
|
|
474
|
+
* names three places evidence accumulates. These are the two whose class is
|
|
475
|
+
* unambiguous from what they are:
|
|
476
|
+
*
|
|
477
|
+
* rejected/<runId>/ a candidate a round threw away — bounded raw diagnostics
|
|
478
|
+
* from a FAILED attempt, which is `failure-diagnostic`
|
|
479
|
+
* almost word for word.
|
|
480
|
+
* retired/<runId>/ the record a reaped run left behind. DURABLE: the reaper
|
|
481
|
+
* keeps these on purpose — they are "the evidence `--reap`
|
|
482
|
+
* needs" — so an age window must never touch them.
|
|
483
|
+
*
|
|
484
|
+
* ⚠️ The evidence CACHE and the compile transcript are NOT inventoried here.
|
|
485
|
+
* They are single files holding many entries, and the policy's unit is an
|
|
486
|
+
* artifact; splitting them needs a per-entry decision about what each one is,
|
|
487
|
+
* and guessing that in order to report a bigger number would be the opposite of
|
|
488
|
+
* the point. Recorded as unfinished rather than approximated.
|
|
489
|
+
*
|
|
490
|
+
* 🔴 `owned: true` IS ASSERTED ONLY FOR DIRECTORIES LOOPLY ITSELF WRITES.
|
|
491
|
+
* Both of these live under `.looply/state/` and are created by the round runner
|
|
492
|
+
* and the reaper. Nothing outside those paths is claimed, because the policy
|
|
493
|
+
* treats an unowned artifact as untouchable and the whole value of that rule is
|
|
494
|
+
* that it is not handed out loosely.
|
|
495
|
+
*/
|
|
496
|
+
function nodeEvidenceDeps() {
|
|
497
|
+
const stateDir = (0, git_driver_1.projectStateDir)(process.cwd());
|
|
498
|
+
return {
|
|
499
|
+
artifacts: async () => {
|
|
500
|
+
const out = [];
|
|
501
|
+
const walk = (store, cls) => {
|
|
502
|
+
const root = (0, path_1.join)(stateDir, store);
|
|
503
|
+
if (!(0, fs_1.existsSync)(root))
|
|
504
|
+
return;
|
|
505
|
+
for (const entry of (0, fs_1.readdirSync)(root, { withFileTypes: true })) {
|
|
506
|
+
const path = (0, path_1.join)(root, entry.name);
|
|
507
|
+
let bytes = 0;
|
|
508
|
+
let createdAtMs = 0;
|
|
509
|
+
try {
|
|
510
|
+
// A directory's size is the sum of what it holds; a stat on the
|
|
511
|
+
// directory itself reports the entry, not the contents.
|
|
512
|
+
const stack = [path];
|
|
513
|
+
while (stack.length > 0) {
|
|
514
|
+
const at = stack.pop();
|
|
515
|
+
const st = (0, fs_1.statSync)(at);
|
|
516
|
+
createdAtMs = createdAtMs === 0 ? st.mtimeMs : Math.min(createdAtMs, st.mtimeMs);
|
|
517
|
+
if (st.isDirectory()) {
|
|
518
|
+
for (const child of (0, fs_1.readdirSync)(at))
|
|
519
|
+
stack.push((0, path_1.join)(at, child));
|
|
520
|
+
}
|
|
521
|
+
else {
|
|
522
|
+
bytes += st.size;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
catch {
|
|
527
|
+
// Unreadable is not evidence about size. Left out rather than
|
|
528
|
+
// reported as zero, which would understate a store on purpose.
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
out.push({ id: entry.name, store, bytes, createdAtMs, class: cls, owned: true });
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
walk('rejected', 'failure-diagnostic');
|
|
535
|
+
walk('retired', 'durable');
|
|
536
|
+
/**
|
|
537
|
+
* 🔴 THE THIRD STORE, AND THE ONE THIS PRODUCT PROMISED ITSELF.
|
|
538
|
+
* `session-observation-store.ts` says observations get "a root a
|
|
539
|
+
* retention policy can be pointed at". Nothing pointed it until now, so
|
|
540
|
+
* every watch ever taken was still on disk and `looply evidence` could
|
|
541
|
+
* only list the directory under *uninventoried*. §5 D5: *"do not claim
|
|
542
|
+
* complete retention from an inventory of only two folders."*
|
|
543
|
+
*
|
|
544
|
+
* ⚠️ ITS CLASSIFICATION IS PER FILE AND COMES FROM THE RECORD, not from
|
|
545
|
+
* the directory it sits in — a session holds watches of different ages
|
|
546
|
+
* that ended in different ways. `inventoryObservations` reads each one;
|
|
547
|
+
* anything it cannot read is named under `uninventoried` instead of
|
|
548
|
+
* being planned over.
|
|
549
|
+
*/
|
|
550
|
+
out.push(...(0, observation_inventory_1.inventoryObservations)(stateDir).artifacts);
|
|
551
|
+
return out;
|
|
552
|
+
},
|
|
553
|
+
/**
|
|
554
|
+
* Everything under the state dir that the two classified stores do not
|
|
555
|
+
* cover. Reported, never planned over — see `reportUninventoried`.
|
|
556
|
+
*/
|
|
557
|
+
uninventoried: async () => {
|
|
558
|
+
const out = [];
|
|
559
|
+
if (!(0, fs_1.existsSync)(stateDir))
|
|
560
|
+
return out;
|
|
561
|
+
/** `null` when it could not be sized — never 0, which would be a claim. */
|
|
562
|
+
const sizeOf = (path) => {
|
|
563
|
+
let bytes = 0;
|
|
564
|
+
try {
|
|
565
|
+
const stack = [path];
|
|
566
|
+
while (stack.length > 0) {
|
|
567
|
+
const at = stack.pop();
|
|
568
|
+
const st = (0, fs_1.statSync)(at);
|
|
569
|
+
if (st.isDirectory())
|
|
570
|
+
for (const child of (0, fs_1.readdirSync)(at))
|
|
571
|
+
stack.push((0, path_1.join)(at, child));
|
|
572
|
+
else
|
|
573
|
+
bytes += st.size;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
catch {
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
579
|
+
return bytes;
|
|
580
|
+
};
|
|
581
|
+
for (const entry of (0, fs_1.readdirSync)(stateDir, { withFileTypes: true })) {
|
|
582
|
+
/**
|
|
583
|
+
* 🔴 THE CLASSIFIED STORES ARE SKIPPED HERE — `artifacts()` counts them
|
|
584
|
+
* — BUT ONLY FOR WHAT IT CAN READ. Its walk drops an entry it cannot
|
|
585
|
+
* `stat`, and this scan used to skip the whole directory, so such an
|
|
586
|
+
* entry was counted by NEITHER and named by neither. It is picked up
|
|
587
|
+
* here instead, under its store, so nothing falls between the two.
|
|
588
|
+
*
|
|
589
|
+
* ⚠️ Asked independently rather than by sharing state with
|
|
590
|
+
* `artifacts()`, which would make this answer depend on the order the
|
|
591
|
+
* two happen to be called in.
|
|
592
|
+
*/
|
|
593
|
+
/**
|
|
594
|
+
* 🔴 CLASSIFIED NOW, SO IT IS NOT COUNTED TWICE — but what could not
|
|
595
|
+
* be READ is still named, with the reason, because that is the whole
|
|
596
|
+
* job of this list. A record that does not parse is exactly the entry
|
|
597
|
+
* a person needs to see; it is the one the policy refused to touch.
|
|
598
|
+
*/
|
|
599
|
+
if (entry.name === observation_inventory_1.OBSERVATIONS_STORE) {
|
|
600
|
+
for (const why of (0, observation_inventory_1.inventoryObservations)(stateDir).unreadable) {
|
|
601
|
+
out.push({ name: `${observation_inventory_1.OBSERVATIONS_STORE}/${why}`, bytes: null });
|
|
602
|
+
}
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
if (entry.name === 'rejected' || entry.name === 'retired') {
|
|
606
|
+
const root = (0, path_1.join)(stateDir, entry.name);
|
|
607
|
+
let children;
|
|
608
|
+
try {
|
|
609
|
+
children = (0, fs_1.readdirSync)(root);
|
|
610
|
+
}
|
|
611
|
+
catch {
|
|
612
|
+
out.push({ name: entry.name, bytes: null });
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
for (const child of children) {
|
|
616
|
+
if (sizeOf((0, path_1.join)(root, child)) === null) {
|
|
617
|
+
out.push({ name: `${entry.name}/${child}`, bytes: null });
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
622
|
+
out.push({ name: entry.name, bytes: sizeOf((0, path_1.join)(stateDir, entry.name)) });
|
|
623
|
+
}
|
|
624
|
+
return out;
|
|
625
|
+
},
|
|
626
|
+
policy: evidence_cmd_1.evidencePolicy,
|
|
627
|
+
nowMs: () => Date.now(),
|
|
628
|
+
out: (l) => process.stdout.write(l + '\n'),
|
|
629
|
+
err: (l) => process.stderr.write(l + '\n'),
|
|
630
|
+
/* ── what `--apply` needs, and nothing else uses ────────────────────── */
|
|
631
|
+
pathOf: (a) => (0, path_1.join)(stateDir, a.store, a.id),
|
|
632
|
+
stateDir: () => stateDir,
|
|
633
|
+
apply: (intent) => (0, retention_apply_runtime_1.applyRetention)(intent, {
|
|
634
|
+
nowMs: () => Date.now(),
|
|
635
|
+
policy: (0, evidence_cmd_1.evidencePolicy)(),
|
|
636
|
+
facts: (id) => (0, evidence_facts_1.artifactFacts)(stateDir, id),
|
|
637
|
+
journalPath: (0, path_1.join)(stateDir, 'retention-apply.jsonl'),
|
|
638
|
+
lockPath: (0, path_1.join)(stateDir, 'retention-apply.lock'),
|
|
639
|
+
}),
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
function nodeSessionsDeps() {
|
|
643
|
+
const cwd = process.cwd();
|
|
644
|
+
const cfg = loadConfig((0, path_1.join)(cwd, git_driver_1.STATE_DIR, 'config.yaml'));
|
|
645
|
+
const stateDir = (0, git_driver_1.projectStateDir)(cwd);
|
|
646
|
+
const store = new session_store_1.SessionStore(stateDir, ledger_store_1.nodeLedgerFs);
|
|
647
|
+
let opened = false;
|
|
648
|
+
const open = async () => {
|
|
649
|
+
if (!opened) {
|
|
650
|
+
await store.open();
|
|
651
|
+
opened = true;
|
|
652
|
+
}
|
|
653
|
+
};
|
|
654
|
+
/**
|
|
655
|
+
* 🔴 ONE TRANSPORT, TWO READERS. `serverControl` and `directionContext` are
|
|
656
|
+
* the same read narrowed two ways, and building them from one object is what
|
|
657
|
+
* keeps that true: two separately-constructed transports would be two
|
|
658
|
+
* separate reads of the grant list, and the verb a caller checks could be
|
|
659
|
+
* checked against a scope the other read never saw.
|
|
660
|
+
*/
|
|
661
|
+
const transportDeps = {
|
|
662
|
+
machine: () => {
|
|
663
|
+
const identity = new machine_store_1.MachineStore((0, machine_store_1.machineFilePath)()).load();
|
|
664
|
+
if (identity === null)
|
|
665
|
+
return null;
|
|
666
|
+
const projectId = new machine_store_1.MachineStore((0, machine_store_1.machineFilePath)()).projectFor((0, machine_store_1.repoPathHash)(cwd, sha256));
|
|
667
|
+
if (projectId === null)
|
|
668
|
+
return null;
|
|
669
|
+
return { machineId: identity.machineId, projectId };
|
|
670
|
+
},
|
|
671
|
+
grants: async (projectId) => {
|
|
672
|
+
const identity = new machine_store_1.MachineStore((0, machine_store_1.machineFilePath)()).load();
|
|
673
|
+
if (identity === null)
|
|
674
|
+
throw new Error('this machine is not paired');
|
|
675
|
+
const client = new server_client_1.ServerClient({ baseUrl: identity.serverUrl, token: identity.token });
|
|
676
|
+
return (await client.sessionGrants(projectId)).grants;
|
|
677
|
+
},
|
|
678
|
+
recordAck: async (projectId, grantId, ack) => {
|
|
679
|
+
const identity = new machine_store_1.MachineStore((0, machine_store_1.machineFilePath)()).load();
|
|
680
|
+
if (identity === null)
|
|
681
|
+
throw new Error('this machine is not paired');
|
|
682
|
+
const client = new server_client_1.ServerClient({ baseUrl: identity.serverUrl, token: identity.token });
|
|
683
|
+
await client.acknowledgeSessionGrant(projectId, grantId, ack);
|
|
684
|
+
},
|
|
685
|
+
paths: (sessionId) => (0, session_adapter_runtime_1.adapterPaths)(stateDir, sessionId),
|
|
686
|
+
supportedAdapters: () => SUPPORTED_SESSION_ADAPTERS,
|
|
687
|
+
now: () => new Date().toISOString(),
|
|
688
|
+
note: (line) => process.stdout.write(` ${line}\n`),
|
|
689
|
+
};
|
|
690
|
+
/** What the last measurement left out, so the report can name it. */
|
|
691
|
+
let lastExcluded = [];
|
|
692
|
+
return {
|
|
693
|
+
readClaim: async (path) => {
|
|
694
|
+
try {
|
|
695
|
+
const parsed = JSON.parse((0, fs_1.readFileSync)(path, 'utf8'));
|
|
696
|
+
return isSessionClaimShape(parsed) ? parsed : null;
|
|
697
|
+
}
|
|
698
|
+
catch {
|
|
699
|
+
return null;
|
|
700
|
+
}
|
|
701
|
+
},
|
|
702
|
+
measure: async (claim, consent) => {
|
|
703
|
+
/**
|
|
704
|
+
* ⚠️ THE CHECKOUT IS READ, NOT TRUSTED. `null` on any failure, the same
|
|
705
|
+
* rule `nodeReposDeps` follows: an unreadable tree must reach the fold as
|
|
706
|
+
* absent so it answers `unmeasurable`.
|
|
707
|
+
*/
|
|
708
|
+
let workspaceAt = null;
|
|
709
|
+
try {
|
|
710
|
+
workspaceAt = await new git_driver_1.GitDriver((0, git_driver_1.nodeGitExec)(claim.workspace)).headSha();
|
|
711
|
+
}
|
|
712
|
+
catch {
|
|
713
|
+
workspaceAt = null;
|
|
714
|
+
}
|
|
715
|
+
/** A run this machine already has in that checkout. Exclusive control. */
|
|
716
|
+
let heldBy = null;
|
|
717
|
+
try {
|
|
718
|
+
const holder = new workspace_1.RunRegistry((0, workspace_1.workspacesRoot)(cwd), workspace_1.nodeProbe)
|
|
719
|
+
.active()
|
|
720
|
+
.find((r) => (0, path_1.resolve)(r.workspace) === (0, path_1.resolve)(claim.workspace));
|
|
721
|
+
heldBy = holder === undefined ? null : holder.runId;
|
|
722
|
+
}
|
|
723
|
+
catch {
|
|
724
|
+
heldBy = null;
|
|
725
|
+
}
|
|
726
|
+
/** The product's own liveness probe, with its third answer intact. */
|
|
727
|
+
let liveness;
|
|
728
|
+
try {
|
|
729
|
+
const probed = (0, workspace_1.nodeProbe)(claim.pid);
|
|
730
|
+
liveness = probed === 'alive' ? 'alive' : probed === 'dead' ? 'gone' : 'unknown';
|
|
731
|
+
}
|
|
732
|
+
catch {
|
|
733
|
+
liveness = 'unknown';
|
|
734
|
+
}
|
|
735
|
+
return {
|
|
736
|
+
supportedAdapters: SUPPORTED_SESSION_ADAPTERS,
|
|
737
|
+
repoDeclared: cfg.sessions?.repo !== undefined && cfg.sessions.repo === claim.repo,
|
|
738
|
+
hostIsThisMachine: (0, node_os_1.hostname)() === claim.host,
|
|
739
|
+
liveness,
|
|
740
|
+
workspaceAt,
|
|
741
|
+
workspaceHeldBy: heldBy,
|
|
742
|
+
// Absent config is `null`, NOT an empty allow-list: "nothing is
|
|
743
|
+
// permitted" and "nobody said" are different answers and only one of
|
|
744
|
+
// them is safe to act on.
|
|
745
|
+
scopeAllowed: cfg.sessions?.scope ?? null,
|
|
746
|
+
budgetOwnedHere: cfg.sessions?.budgetOwner === undefined ? null : cfg.sessions.budgetOwner === claim.budgetOwner,
|
|
747
|
+
controlAuthorized: consent.authorized,
|
|
748
|
+
sessionCooperating: consent.acked,
|
|
749
|
+
};
|
|
750
|
+
},
|
|
751
|
+
records: async () => {
|
|
752
|
+
await open();
|
|
753
|
+
return store.all();
|
|
754
|
+
},
|
|
755
|
+
/**
|
|
756
|
+
* 🔴 WHAT THE SERVER AND THE SESSION'S OWN CHANNEL, TOGETHER, SAY.
|
|
757
|
+
*
|
|
758
|
+
* Until this existed, `server.url` and `server.machine_token_file` were
|
|
759
|
+
* parsed by `config.ts` and read by NOBODY, and `sessions --offer` printed
|
|
760
|
+
* "this machine could not ask the server" because nothing in the CLI had
|
|
761
|
+
* ever asked it anything about a grant — a configuration block with no
|
|
762
|
+
* consumer, which is this codebase's most-named defect wearing different
|
|
763
|
+
* clothes.
|
|
764
|
+
*
|
|
765
|
+
* ⚠️ EVERY FAILURE HERE IS `null`, WHICH IS NOT A REFUSAL. Unpaired,
|
|
766
|
+
* unregistered, unreachable: none of them is a decision anybody made about
|
|
767
|
+
* this session, and the command prints them as "nothing was established"
|
|
768
|
+
* rather than as an answer.
|
|
769
|
+
*/
|
|
770
|
+
serverControl: (0, session_control_transport_1.serverControlFor)(transportDeps),
|
|
771
|
+
directionContext: (0, session_control_transport_1.directionContextFor)(transportDeps),
|
|
772
|
+
/**
|
|
773
|
+
* 🔴 WRITE THE INSTRUCTION, THEN WAIT — AND BOUND THE WAIT.
|
|
774
|
+
*
|
|
775
|
+
* An unbounded wait on a directory a SESSION writes is this process hanging
|
|
776
|
+
* on the cooperation of the thing it is directing. The bound is short, and
|
|
777
|
+
* running out of it is reported as "no answer yet" rather than as a failure:
|
|
778
|
+
* the instruction was delivered, and what happened next is unknown.
|
|
779
|
+
*
|
|
780
|
+
* ⚠️ THE RESULT FILE IS ARCHIVED, NOT DELETED. It is the only record that
|
|
781
|
+
* the session answered at all.
|
|
782
|
+
*/
|
|
783
|
+
direct: async (sessionId, instruction, waitMs) => {
|
|
784
|
+
const paths = (0, session_adapter_runtime_1.adapterPaths)(stateDir, sessionId);
|
|
785
|
+
(0, session_adapter_runtime_1.deliver)(paths, {
|
|
786
|
+
kind: 'instruction',
|
|
787
|
+
instructionId: instruction.id,
|
|
788
|
+
grantId: instruction.grantId,
|
|
789
|
+
verb: instruction.verb,
|
|
790
|
+
args: instruction.args,
|
|
791
|
+
at: instruction.at,
|
|
792
|
+
});
|
|
793
|
+
// F41: `--wait` asks for longer, bounded by `MAX_DIRECT_WAIT_S` where it is parsed.
|
|
794
|
+
const deadline = Date.now() + (waitMs ?? DIRECT_WAIT_MS);
|
|
795
|
+
for (;;) {
|
|
796
|
+
const found = (0, session_adapter_runtime_1.collectResults)(paths, [instruction], new Date().toISOString());
|
|
797
|
+
for (const r of found.results) {
|
|
798
|
+
if (r.ruling.kind === 'accept' && r.ruling.result.instructionId === instruction.id) {
|
|
799
|
+
(0, session_adapter_runtime_1.archiveResult)(paths, r.file);
|
|
800
|
+
return r.ruling.result;
|
|
801
|
+
}
|
|
802
|
+
if (r.ruling.kind === 'refused') {
|
|
803
|
+
process.stdout.write(` ${r.file}: not accepted — ${r.ruling.because}\n`);
|
|
804
|
+
(0, session_adapter_runtime_1.archiveResult)(paths, r.file);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
for (const blind of found.unreadable)
|
|
808
|
+
process.stdout.write(` blind: ${blind}\n`);
|
|
809
|
+
if (Date.now() >= deadline)
|
|
810
|
+
return null;
|
|
811
|
+
await new Promise((resolve) => setTimeout(resolve, DIRECT_POLL_MS));
|
|
812
|
+
}
|
|
813
|
+
},
|
|
814
|
+
append: async (record) => {
|
|
815
|
+
await open();
|
|
816
|
+
await store.append(record);
|
|
817
|
+
},
|
|
818
|
+
now: () => new Date().toISOString(),
|
|
819
|
+
/**
|
|
820
|
+
* The real watcher.
|
|
821
|
+
*
|
|
822
|
+
* ⚠️ `stillAuthorized` IS A CONSTANT FOR AN OBSERVE-ONLY WATCH, AND THE
|
|
823
|
+
* REASON IS WORTH READING RATHER THAN ASSUMING. An observe-only watch has
|
|
824
|
+
* no grant to withdraw — observation was never the half that needed
|
|
825
|
+
* permission — so there is no server answer to ask for and nothing that
|
|
826
|
+
* could come back false. Its bounds are the window's `until`, the session's
|
|
827
|
+
* liveness, the event cap and the stop flag, all of which are real and all
|
|
828
|
+
* of which this runtime honours.
|
|
829
|
+
*
|
|
830
|
+
* 🔴 A CONTROLLED WATCH ASKS FOR REAL (F42). A controlled session's watch
|
|
831
|
+
* outliving its grant is precisely the thing `stopFor` puts revocation
|
|
832
|
+
* first for, and LIVE-3 found this constant doing exactly that: the grant
|
|
833
|
+
* was revoked and the watch recorded on for its whole window. `sessions-cmd`
|
|
834
|
+
* now hands a watch attached under control the server's question as
|
|
835
|
+
* `subject.stillAuthorized`; the constant is only the observe-only default.
|
|
836
|
+
*/
|
|
837
|
+
watch: async (window, subject) => {
|
|
838
|
+
const { workspace, pid, stillAuthorized } = subject;
|
|
839
|
+
let stopped = false;
|
|
840
|
+
const onSignal = () => {
|
|
841
|
+
stopped = true;
|
|
842
|
+
};
|
|
843
|
+
process.once('SIGINT', onSignal);
|
|
844
|
+
process.once('SIGTERM', onSignal);
|
|
845
|
+
try {
|
|
846
|
+
return await (0, session_observe_runtime_1.watchSession)(window, { ...session_observe_runtime_1.DEFAULT_WATCH_OPTIONS, root: workspace }, {
|
|
847
|
+
now: () => new Date().toISOString(),
|
|
848
|
+
liveness: () => {
|
|
849
|
+
// The product's own probe, with its third answer intact: an
|
|
850
|
+
// unanswerable pid is `unknown`, and `stopFor` does not end a
|
|
851
|
+
// watch on it.
|
|
852
|
+
try {
|
|
853
|
+
const probed = (0, workspace_1.nodeProbe)(pid);
|
|
854
|
+
return probed === 'alive' ? 'alive' : probed === 'dead' ? 'gone' : 'unknown';
|
|
855
|
+
}
|
|
856
|
+
catch {
|
|
857
|
+
return 'unknown';
|
|
858
|
+
}
|
|
859
|
+
},
|
|
860
|
+
// F42: a controlled watch re-asks the server (`sessions-cmd` hands the
|
|
861
|
+
// question over); an observe-only one has no grant to withdraw.
|
|
862
|
+
stillAuthorized: stillAuthorized ?? (async () => true),
|
|
863
|
+
/**
|
|
864
|
+
* 🔴 AN ABSOLUTE PATH, NEVER THE BARE NAME, AND A GUARD IN THIS
|
|
865
|
+
* REPOSITORY CAUGHT THE FIRST DRAFT DOING IT WRONG. On Windows the
|
|
866
|
+
* cwd is searched before PATH, and the cwd HERE is the workspace of
|
|
867
|
+
* a session being watched — which is to say a directory whose
|
|
868
|
+
* contents are exactly what this code has no reason to trust. A
|
|
869
|
+
* bare `git` would have let the watched tree choose the program
|
|
870
|
+
* that reads it, in the one place where being watched is supposed
|
|
871
|
+
* to be safe.
|
|
872
|
+
*
|
|
873
|
+
* `trustedGitArgs` and `trustedGitEnv` come along for the same
|
|
874
|
+
* reason they do everywhere else: hooks off, signing off, and the
|
|
875
|
+
* whole `GIT_` namespace built rather than inherited.
|
|
876
|
+
*/
|
|
877
|
+
revision: () => {
|
|
878
|
+
const bin = (0, trusted_git_1.gitBinary)();
|
|
879
|
+
if (bin === null)
|
|
880
|
+
return null;
|
|
881
|
+
try {
|
|
882
|
+
return (0, node_child_process_1.execFileSync)(bin, [...(0, trusted_git_1.trustedGitArgs)(), 'rev-parse', 'HEAD'], {
|
|
883
|
+
cwd: workspace,
|
|
884
|
+
env: (0, trusted_git_1.trustedGitEnv)(process.env, workspace),
|
|
885
|
+
encoding: 'utf8',
|
|
886
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
887
|
+
}).trim();
|
|
888
|
+
}
|
|
889
|
+
catch {
|
|
890
|
+
return null;
|
|
891
|
+
}
|
|
892
|
+
},
|
|
893
|
+
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
894
|
+
askedToStop: () => stopped,
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
finally {
|
|
898
|
+
process.removeListener('SIGINT', onSignal);
|
|
899
|
+
process.removeListener('SIGTERM', onSignal);
|
|
900
|
+
}
|
|
901
|
+
},
|
|
902
|
+
/**
|
|
903
|
+
* 🔴 WHAT THIS MACHINE CAN ESTABLISH ABOUT A WORKSPACE IT DOES NOT CONTROL.
|
|
904
|
+
*
|
|
905
|
+
* ⚠️ THE LIVENESS PROBE KEEPS ITS THIRD ANSWER. `unknown` becomes a
|
|
906
|
+
* checkpoint labelled `unknown` rather than one labelled `still`, because a
|
|
907
|
+
* capture whose quality nobody established must not read as the reassuring
|
|
908
|
+
* one.
|
|
909
|
+
*/
|
|
910
|
+
recordObservation: async (record) => {
|
|
911
|
+
(0, session_observation_store_1.writeObservation)((0, session_observation_store_1.observationPaths)(stateDir, record.sessionId), record);
|
|
912
|
+
},
|
|
913
|
+
observations: async (sessionId) => {
|
|
914
|
+
const lines = [];
|
|
915
|
+
(0, session_observation_store_1.reportObservations)(sessionId, (0, session_observation_store_1.readObservations)((0, session_observation_store_1.observationPaths)(stateDir, sessionId)), (l) => lines.push(l));
|
|
916
|
+
return lines;
|
|
917
|
+
},
|
|
918
|
+
measureWorkspace: async (workspace, _sessionId, pid) => {
|
|
919
|
+
let liveness;
|
|
920
|
+
try {
|
|
921
|
+
const probed = (0, workspace_1.nodeProbe)(pid);
|
|
922
|
+
liveness = probed === 'alive' ? 'alive' : probed === 'dead' ? 'gone' : 'unknown';
|
|
923
|
+
}
|
|
924
|
+
catch {
|
|
925
|
+
liveness = 'unknown';
|
|
926
|
+
}
|
|
927
|
+
/** A managed run already writing there. A checkpoint of that is a capture of two authors. */
|
|
928
|
+
let heldBy = null;
|
|
929
|
+
try {
|
|
930
|
+
const holder = new workspace_1.RunRegistry((0, workspace_1.workspacesRoot)(cwd), workspace_1.nodeProbe)
|
|
931
|
+
.active()
|
|
932
|
+
.find((r) => (0, path_1.resolve)(r.workspace) === (0, path_1.resolve)(workspace));
|
|
933
|
+
heldBy = holder === undefined ? null : holder.runId;
|
|
934
|
+
}
|
|
935
|
+
catch {
|
|
936
|
+
heldBy = null;
|
|
937
|
+
}
|
|
938
|
+
const reading = (0, session_checkpoint_runtime_1.measureWorkspace)(workspace, liveness, heldBy);
|
|
939
|
+
// Carried to the report, which is the only place a person sees it.
|
|
940
|
+
lastExcluded = reading.excluded;
|
|
941
|
+
return reading;
|
|
942
|
+
},
|
|
943
|
+
writeCheckpoint: async (checkpoint) => {
|
|
944
|
+
const written = (0, session_checkpoint_runtime_1.writeCheckpoint)(stateDir, checkpoint);
|
|
945
|
+
const lines = [];
|
|
946
|
+
(0, session_checkpoint_runtime_1.reportCheckpoint)(written, checkpoint, (l) => lines.push(l), lastExcluded);
|
|
947
|
+
return lines;
|
|
948
|
+
},
|
|
949
|
+
newHandshakeId: () => `hs_${(0, node_crypto_1.randomUUID)().replace(/-/g, '').slice(0, 12)}`,
|
|
950
|
+
out: (l) => process.stdout.write(l + '\n'),
|
|
951
|
+
err: (l) => process.stderr.write(l + '\n'),
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
/** The adapter contract, checked before anything reads the claim as a claim. */
|
|
955
|
+
function isSessionClaimShape(v) {
|
|
956
|
+
if (typeof v !== 'object' || v === null)
|
|
957
|
+
return false;
|
|
958
|
+
const o = v;
|
|
959
|
+
const strings = ['sessionId', 'adapter', 'adapterVersion', 'repo', 'workspace', 'host', 'sourceSha', 'budgetOwner'];
|
|
960
|
+
if (!strings.every((k) => typeof o[k] === 'string'))
|
|
961
|
+
return false;
|
|
962
|
+
if (typeof o['pid'] !== 'number' || !Number.isInteger(o['pid']))
|
|
963
|
+
return false;
|
|
964
|
+
return Array.isArray(o['scope']) && o['scope'].every((x) => typeof x === 'string');
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* WHICH DECLARED REPOSITORY IS THIS ONE — by path, never by name.
|
|
968
|
+
*
|
|
969
|
+
* ⚠️ `path` is the only thing that says "this is me". The id is an identity a
|
|
970
|
+
* compatible set names and carries no location, and `name` in `project:` is a
|
|
971
|
+
* different namespace again — matching on either would make a project that
|
|
972
|
+
* happened to share a word with a sibling declare itself circular.
|
|
973
|
+
*
|
|
974
|
+
* Returns null when nothing declared resolves to here, which the fold reads as
|
|
975
|
+
* "not established" rather than "no need is circular".
|
|
976
|
+
*/
|
|
977
|
+
/**
|
|
978
|
+
* Why this run may not start yet, when this project declares cross-repo needs.
|
|
979
|
+
*
|
|
980
|
+
* 🔴 THE CHEAP HALF OF ENFORCEMENT, ASKED WHERE THE DIRTY-TREE CHECK IS ASKED.
|
|
981
|
+
* A need is about publication in ANOTHER repository, so answering it costs a
|
|
982
|
+
* `git rev-parse` per declared repo and a read of the local set records — no
|
|
983
|
+
* server, no model. Asking it here means a project whose dependency has not
|
|
984
|
+
* published does not spend a compile and a verify finding that out, which is
|
|
985
|
+
* exactly the waste `fe9f746` removed for a dirty tree.
|
|
986
|
+
*
|
|
987
|
+
* ⚠️ IT IS PROJECT-LEVEL, LIKE THE NEEDS THEMSELVES. It answers "may this
|
|
988
|
+
* project start work at all", never "may this item" — item-level needs are
|
|
989
|
+
* UNFINISHED and this does not pretend otherwise.
|
|
990
|
+
*
|
|
991
|
+
* ⚠️ AN UNREADABLE SIBLING BLOCKS, and that is the safe direction: the fold
|
|
992
|
+
* answers `unmeasured`, and starting dependent work on the strength of "I could
|
|
993
|
+
* not look" is the mistake every other fold in this package refuses.
|
|
994
|
+
*
|
|
995
|
+
* Returns null when the project declares no needs, or when every need is met.
|
|
996
|
+
*/
|
|
997
|
+
/**
|
|
998
|
+
* Where every declared repository stands, and what was accepted together.
|
|
999
|
+
*
|
|
1000
|
+
* 🔴 ONE BUILDER, TWO READERS. The project-level gate below asks it whether this
|
|
1001
|
+
* project may start at all; `pickableItems` asks it, through the run, whether
|
|
1002
|
+
* one ITEM's needs are met. Two copies of "what has published" would eventually
|
|
1003
|
+
* answer differently about the same moment, and the one that mattered would be
|
|
1004
|
+
* whichever the reader happened to call.
|
|
1005
|
+
*
|
|
1006
|
+
* ⚠️ IT DOES NOT SHORT-CIRCUIT ON "no project-level needs". That early return
|
|
1007
|
+
* belongs to the gate, not here: a project declaring none of its own can still
|
|
1008
|
+
* hold items that declare theirs, and a builder that gave up early would hand
|
|
1009
|
+
* the run an undefined context and block every one of them as `undeclared`.
|
|
1010
|
+
*/
|
|
1011
|
+
async function crossRepoContext(cwd, cfg) {
|
|
1012
|
+
const declared = cfg.repos ?? [];
|
|
1013
|
+
if (declared.length === 0)
|
|
1014
|
+
return undefined;
|
|
1015
|
+
const current = new Map();
|
|
1016
|
+
for (const repo of declared) {
|
|
1017
|
+
try {
|
|
1018
|
+
const git = new git_driver_1.GitDriver((0, git_driver_1.nodeGitExec)(repo.path));
|
|
1019
|
+
const branches = await git.listBranches(integration_1.INTEGRATION_BRANCH);
|
|
1020
|
+
const integration = branches.find((b) => b.name === integration_1.INTEGRATION_BRANCH);
|
|
1021
|
+
current.set(repo.id, integration === undefined ? await git.headSha() : integration.sha);
|
|
1022
|
+
}
|
|
1023
|
+
catch {
|
|
1024
|
+
// Left out of the map on purpose: absent is what the folds read as
|
|
1025
|
+
// "could not be read", and a fabricated sha would answer a different
|
|
1026
|
+
// question than the one asked.
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
const store = new compatible_sets_1.CompatibleSetStore((0, git_driver_1.projectStateDir)(cwd), ledger_store_1.nodeLedgerFs);
|
|
1030
|
+
await store.open();
|
|
1031
|
+
const records = store.all();
|
|
1032
|
+
const setId = (0, compatible_sets_1.newestSetId)(records);
|
|
1033
|
+
return {
|
|
1034
|
+
declared: declared.map((d) => d.id),
|
|
1035
|
+
self: (0, self_repo_id_1.selfRepoId)(declared, cwd),
|
|
1036
|
+
set: setId === null ? null : (0, compatible_sets_1.setFrom)(records, setId),
|
|
1037
|
+
current,
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
async function crossRepoNeedsTrouble(configPath) {
|
|
1041
|
+
let cfg;
|
|
1042
|
+
try {
|
|
1043
|
+
cfg = loadConfig(configPath);
|
|
1044
|
+
}
|
|
1045
|
+
catch {
|
|
1046
|
+
// Not this check's business. The run loads the same file a moment later and
|
|
1047
|
+
// reports a bad config far better than a guess from here could.
|
|
1048
|
+
return null;
|
|
1049
|
+
}
|
|
1050
|
+
const needs = cfg.crossRepoNeeds ?? [];
|
|
1051
|
+
if (needs.length === 0)
|
|
1052
|
+
return null;
|
|
1053
|
+
const cwd = process.cwd();
|
|
1054
|
+
const declared = cfg.repos ?? [];
|
|
1055
|
+
const current = new Map();
|
|
1056
|
+
for (const repo of declared) {
|
|
1057
|
+
try {
|
|
1058
|
+
const git = new git_driver_1.GitDriver((0, git_driver_1.nodeGitExec)(repo.path));
|
|
1059
|
+
const branches = await git.listBranches(integration_1.INTEGRATION_BRANCH);
|
|
1060
|
+
const integration = branches.find((b) => b.name === integration_1.INTEGRATION_BRANCH);
|
|
1061
|
+
current.set(repo.id, integration === undefined ? await git.headSha() : integration.sha);
|
|
1062
|
+
}
|
|
1063
|
+
catch {
|
|
1064
|
+
// Left out of the map on purpose: absent is what the fold reads as
|
|
1065
|
+
// "could not be read", and a fabricated sha would answer a different
|
|
1066
|
+
// question than the one asked.
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
const store = new compatible_sets_1.CompatibleSetStore((0, git_driver_1.projectStateDir)(cwd), ledger_store_1.nodeLedgerFs);
|
|
1070
|
+
await store.open();
|
|
1071
|
+
const records = store.all();
|
|
1072
|
+
const setId = (0, compatible_sets_1.newestSetId)(records);
|
|
1073
|
+
const blockers = (0, core_1.crossRepoBlockedBy)({
|
|
1074
|
+
needs,
|
|
1075
|
+
declared: declared.map((d) => d.id),
|
|
1076
|
+
self: (0, self_repo_id_1.selfRepoId)(declared, cwd),
|
|
1077
|
+
set: setId === null ? null : (0, compatible_sets_1.setFrom)(records, setId),
|
|
1078
|
+
current,
|
|
1079
|
+
});
|
|
1080
|
+
return (0, core_1.describeCrossRepoBlockers)(blockers);
|
|
1081
|
+
}
|
|
1082
|
+
function nodeReposDeps() {
|
|
1083
|
+
const cwd = process.cwd();
|
|
1084
|
+
const cfg = loadConfig((0, path_1.join)(cwd, git_driver_1.STATE_DIR, 'config.yaml'));
|
|
1085
|
+
const stateDir = (0, git_driver_1.projectStateDir)(cwd);
|
|
1086
|
+
const store = new compatible_sets_1.CompatibleSetStore(stateDir, ledger_store_1.nodeLedgerFs);
|
|
1087
|
+
let opened = false;
|
|
1088
|
+
const open = async () => {
|
|
1089
|
+
if (!opened) {
|
|
1090
|
+
await store.open();
|
|
1091
|
+
opened = true;
|
|
1092
|
+
}
|
|
1093
|
+
};
|
|
1094
|
+
return {
|
|
1095
|
+
declared: () => cfg.repos ?? [],
|
|
1096
|
+
self: () => (0, self_repo_id_1.selfRepoId)(cfg.repos ?? [], cwd),
|
|
1097
|
+
needs: () => cfg.crossRepoNeeds ?? [],
|
|
1098
|
+
// ⚠️ SHARED WITH THE DAEMON'S INTEGRATOR rather than written twice. Two
|
|
1099
|
+
// copies of "where is this repository" are two copies that can disagree,
|
|
1100
|
+
// and the one that disagrees is the one deciding whether a set drifted.
|
|
1101
|
+
revisionOf: publication_recorder_1.nodeRevisionOf,
|
|
1102
|
+
records: async () => {
|
|
1103
|
+
await open();
|
|
1104
|
+
return store.all();
|
|
1105
|
+
},
|
|
1106
|
+
append: async (record) => {
|
|
1107
|
+
await open();
|
|
1108
|
+
await store.append(record);
|
|
1109
|
+
},
|
|
1110
|
+
now: () => new Date().toISOString(),
|
|
1111
|
+
// `newId`'s prefixes are a closed vocabulary of DB entities; a compatible
|
|
1112
|
+
// set is not one of those, so it is generated here rather than widening it.
|
|
1113
|
+
newId: () => `set_${(0, node_crypto_1.randomUUID)().replace(/-/g, '').slice(0, 12)}`,
|
|
1114
|
+
out: (l) => process.stdout.write(l + '\n'),
|
|
1115
|
+
err: (l) => process.stderr.write(l + '\n'),
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
async function goals() {
|
|
1119
|
+
const read = await (0, goal_view_1.readGoalsList)(process.cwd(), { now: () => Date.now() });
|
|
1120
|
+
if (!read.ok) {
|
|
1121
|
+
process.stderr.write(`goals : ${read.because}\n`);
|
|
1122
|
+
return read.code;
|
|
1123
|
+
}
|
|
1124
|
+
process.stdout.write((0, goal_view_1.renderGoalsList)(read.entries));
|
|
1125
|
+
return 0;
|
|
1126
|
+
}
|
|
1127
|
+
const DEFAULT_SERVER = 'https://looply-api-607184ed6acc.herokuapp.com';
|
|
1128
|
+
/** Pair this machine with a phone, so runs can be watched and driven remotely. */
|
|
1129
|
+
async function pair(argv) {
|
|
1130
|
+
const at = argv.indexOf('--server');
|
|
1131
|
+
const serverUrl = at >= 0 && argv[at + 1] ? argv[at + 1] : DEFAULT_SERVER;
|
|
1132
|
+
const nameAt = argv.indexOf('--name');
|
|
1133
|
+
const machineName = nameAt >= 0 ? argv[nameAt + 1] : undefined;
|
|
1134
|
+
return (0, daemon_cmd_1.pairCommand)({
|
|
1135
|
+
cwd: process.cwd(),
|
|
1136
|
+
serverUrl,
|
|
1137
|
+
...(machineName ? { machineName } : {}),
|
|
1138
|
+
log: (line) => process.stdout.write(`${line}\n`),
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
/** Keep this machine connected: presence, and shipping the ledger. */
|
|
1142
|
+
async function daemon(argv) {
|
|
1143
|
+
const at = argv.indexOf('--config');
|
|
1144
|
+
const configPath = at >= 0 ? argv[at + 1] : '.looply/config.yaml';
|
|
1145
|
+
if (!configPath) {
|
|
1146
|
+
process.stderr.write('daemon: --config needs a path\n');
|
|
1147
|
+
return 1;
|
|
1148
|
+
}
|
|
1149
|
+
// Registering is irreversible, so an unknown directory is asked about. The
|
|
1150
|
+
// asker is only wired up when there is a terminal to answer on: with stdin
|
|
1151
|
+
// piped — a service unit, a CI job — a prompt is an indefinite hang, and the
|
|
1152
|
+
// gate refuses instead, telling you about --yes.
|
|
1153
|
+
const interactive = Boolean(process.stdin.isTTY);
|
|
1154
|
+
return (0, daemon_cmd_1.daemonCommand)({
|
|
1155
|
+
cwd: process.cwd(),
|
|
1156
|
+
config: loadConfig(configPath),
|
|
1157
|
+
once: argv.includes('--once'),
|
|
1158
|
+
log: (line) => process.stdout.write(`${line}\n`),
|
|
1159
|
+
gate: {
|
|
1160
|
+
assumeYes: argv.includes('--yes') || argv.includes('-y'),
|
|
1161
|
+
...(interactive ? { ask: (0, daemon_cmd_1.nodeRegisterAsk)((s) => process.stdout.write(s)) } : {}),
|
|
1162
|
+
},
|
|
1163
|
+
});
|
|
1164
|
+
}
|
|
1165
|
+
/** Seed or top up the queue by reading the code through several narrow lenses. */
|
|
1166
|
+
async function scan(argv) {
|
|
1167
|
+
const at = argv.indexOf('--config');
|
|
1168
|
+
const configPath = at >= 0 ? argv[at + 1] : '.looply/config.yaml';
|
|
1169
|
+
if (!configPath) {
|
|
1170
|
+
process.stderr.write('scan: --config needs a path\n');
|
|
1171
|
+
return 1;
|
|
1172
|
+
}
|
|
1173
|
+
const onlyAt = argv.indexOf('--lens');
|
|
1174
|
+
const only = onlyAt >= 0 && argv[onlyAt + 1] ? argv[onlyAt + 1].split(',') : undefined;
|
|
1175
|
+
const perAt = argv.indexOf('--per-lens');
|
|
1176
|
+
let perLens;
|
|
1177
|
+
if (perAt >= 0) {
|
|
1178
|
+
const n = Number(argv[perAt + 1]);
|
|
1179
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
1180
|
+
process.stderr.write('scan: --per-lens needs a positive whole number\n');
|
|
1181
|
+
return 1;
|
|
1182
|
+
}
|
|
1183
|
+
perLens = n;
|
|
1184
|
+
}
|
|
1185
|
+
const cfg = loadConfig(configPath);
|
|
1186
|
+
try {
|
|
1187
|
+
await (0, scan_cmd_1.scanProject)({
|
|
1188
|
+
cwd: process.cwd(),
|
|
1189
|
+
config: cfg,
|
|
1190
|
+
...(only ? { only } : {}),
|
|
1191
|
+
...(perLens !== undefined ? { perLens } : {}),
|
|
1192
|
+
dryRun: argv.includes('--dry-run'),
|
|
1193
|
+
log: (line) => process.stdout.write(`${line}\n`),
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
catch (e) {
|
|
1197
|
+
/**
|
|
1198
|
+
* ⚠️ A REFUSAL, NOT A CRASH. Writing the queue while a run holds it loses
|
|
1199
|
+
* the items silently; this says so and exits 2, the way every other
|
|
1200
|
+
* "somebody else is here" answer in this file does.
|
|
1201
|
+
*/
|
|
1202
|
+
if (e instanceof queue_write_lease_1.QueueBusy) {
|
|
1203
|
+
process.stderr.write(`\n scan: ${e.message}\n\n`);
|
|
1204
|
+
return 2;
|
|
1205
|
+
}
|
|
1206
|
+
throw e;
|
|
1207
|
+
}
|
|
1208
|
+
return 0;
|
|
1209
|
+
}
|
|
1210
|
+
/**
|
|
1211
|
+
* Check the drafts a scan produced.
|
|
1212
|
+
*
|
|
1213
|
+
* A run does this by itself when it has nothing pickable, so this command is
|
|
1214
|
+
* for doing it deliberately: it is the cheapest agent work looply does, and
|
|
1215
|
+
* seeing what survives contact with the code is worth knowing before spending
|
|
1216
|
+
* a round on anything.
|
|
1217
|
+
*/
|
|
1218
|
+
/**
|
|
1219
|
+
* THE INTEGRATION QUEUE'S PRODUCTION ENTRY POINT.
|
|
1220
|
+
*
|
|
1221
|
+
* ⚠️ NOT A SECOND SCHEDULER. It decides nothing about which goal runs; it
|
|
1222
|
+
* offers ONE finished branch, once, and answers whether it went in. The
|
|
1223
|
+
* coordinator still schedules, and this is the seam it — or a person — calls
|
|
1224
|
+
* when a run has finished and its work is ready to converge.
|
|
1225
|
+
*
|
|
1226
|
+
* ⚠️ GATED ON `run.integration`, like everything else in this phase. With the
|
|
1227
|
+
* flag off it says so and changes nothing.
|
|
1228
|
+
*/
|
|
1229
|
+
async function integrate(argv) {
|
|
1230
|
+
const cwd = process.cwd();
|
|
1231
|
+
const at = argv.indexOf('--config');
|
|
1232
|
+
const configPath = at >= 0 ? argv[at + 1] : '.looply/config.yaml';
|
|
1233
|
+
if (configPath === undefined) {
|
|
1234
|
+
process.stderr.write('integrate: --config needs a path\n');
|
|
1235
|
+
return 1;
|
|
1236
|
+
}
|
|
1237
|
+
const cfg = loadConfig(configPath);
|
|
1238
|
+
const enabled = cfg.run.integration === true;
|
|
1239
|
+
const git = new git_driver_1.GitDriver((0, git_driver_1.nodeGitExec)(cwd));
|
|
1240
|
+
const initAt = argv.indexOf('--init');
|
|
1241
|
+
if (initAt >= 0) {
|
|
1242
|
+
const from = argv[initAt + 1];
|
|
1243
|
+
if (from === undefined || from.startsWith('--')) {
|
|
1244
|
+
process.stderr.write('integrate: --init needs the ref to start integration from, and it is not guessed — ' +
|
|
1245
|
+
'that is where every future run will begin\n');
|
|
1246
|
+
return 1;
|
|
1247
|
+
}
|
|
1248
|
+
const out = await (0, integration_runtime_1.initialiseIntegration)(cwd, from, git);
|
|
1249
|
+
if (!out.ok) {
|
|
1250
|
+
process.stderr.write(`integrate: ${out.because}\n`);
|
|
1251
|
+
return 1;
|
|
1252
|
+
}
|
|
1253
|
+
process.stdout.write(out.already
|
|
1254
|
+
? `integrate: ${out.ref} already exists at ${out.sha.slice(0, 7)}\n`
|
|
1255
|
+
: `integrate: created ${out.ref} at ${out.sha.slice(0, 7)} (from ${from})\n`);
|
|
1256
|
+
return 0;
|
|
1257
|
+
}
|
|
1258
|
+
const branchAt = argv.indexOf('--branch');
|
|
1259
|
+
const branch = branchAt >= 0 ? argv[branchAt + 1] : undefined;
|
|
1260
|
+
if (branch === undefined || branch.startsWith('--')) {
|
|
1261
|
+
process.stderr.write('integrate: --branch needs the branch to offer\n');
|
|
1262
|
+
return 1;
|
|
1263
|
+
}
|
|
1264
|
+
const goalAt = argv.indexOf('--goal');
|
|
1265
|
+
const goalId = goalAt >= 0 ? argv[goalAt + 1] : undefined;
|
|
1266
|
+
/**
|
|
1267
|
+
* THE REQUIRED CHECKS, RUN AGAINST THE CANDIDATE IN ITS OWN CHECKOUT.
|
|
1268
|
+
*
|
|
1269
|
+
* 🔴 A CLEAN FAST-FORWARD IS NOT ACCEPTANCE. These are the project's own
|
|
1270
|
+
* gates — the same ones a round must pass — executed at the commit that would
|
|
1271
|
+
* actually be published, in a checkout cut for the purpose.
|
|
1272
|
+
*/
|
|
1273
|
+
// The lane's deps, built once for this command and the daemon (M5-1): the project's own gate in the candidate's checkout.
|
|
1274
|
+
/**
|
|
1275
|
+
* WHAT THIS BRANCH'S DELIVERY CLAIMED, AT ITS TIP — resolved exactly as the daemon's offer resolves
|
|
1276
|
+
* it (`integrateOnce`), so a person's `integrate` is held to the same proof: under programme
|
|
1277
|
+
* admission a candidate with no claim gets the strict lane, and one with a claim must pass what it
|
|
1278
|
+
* cites.
|
|
1279
|
+
*/
|
|
1280
|
+
const tip = (await git.refExists(branch)) ? await git.mergeBase(branch, branch) : null;
|
|
1281
|
+
// F33: the same containment the daemon's offer applies.
|
|
1282
|
+
const claim = tip === null
|
|
1283
|
+
? null
|
|
1284
|
+
: (0, lane_claims_1.deliveredClaimFor)(cwd, { branch, commitSha: tip }, await (0, lane_claims_1.deliveriesInHistory)(cwd, tip, (a, b) => git.isAncestor(a, b), (0, integration_runtime_2.readAttempts)(cwd)));
|
|
1285
|
+
const out = await (0, integration_runtime_1.integrationTick)(cwd, { branch, ...(goalId === undefined ? {} : { goalId }), ...(claim === null ? {} : { claim }) }, enabled, (0, integration_deps_1.nodeIntegrationDeps)(cwd, cfg, (line) => process.stdout.write(`${line}\n`), git));
|
|
1286
|
+
switch (out.kind) {
|
|
1287
|
+
case 'off':
|
|
1288
|
+
process.stdout.write('integrate: run.integration is off — nothing to do\n');
|
|
1289
|
+
return 0;
|
|
1290
|
+
case 'uninitialised':
|
|
1291
|
+
process.stderr.write(`integrate: ${out.because}\n`);
|
|
1292
|
+
return 2;
|
|
1293
|
+
case 'idle':
|
|
1294
|
+
process.stdout.write(`integrate: ${out.because}\n`);
|
|
1295
|
+
return 0;
|
|
1296
|
+
case 'published':
|
|
1297
|
+
process.stdout.write(`integrate: published ${branch} → ${out.attempt.published?.ref} ` +
|
|
1298
|
+
`(${String(out.attempt.published?.from).slice(0, 7)}..${out.attempt.published?.to.slice(0, 7)})\n`);
|
|
1299
|
+
return 0;
|
|
1300
|
+
case 'refused':
|
|
1301
|
+
process.stderr.write(`integrate: not published — ${out.attempt.because ?? 'no reason recorded'}\n`);
|
|
1302
|
+
return 2;
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
async function verify(argv) {
|
|
1306
|
+
const at = argv.indexOf('--config');
|
|
1307
|
+
const configPath = at >= 0 ? argv[at + 1] : '.looply/config.yaml';
|
|
1308
|
+
if (!configPath) {
|
|
1309
|
+
process.stderr.write('verify: --config needs a path\n');
|
|
1310
|
+
return 1;
|
|
1311
|
+
}
|
|
1312
|
+
const maxAt = argv.indexOf('--max');
|
|
1313
|
+
let max;
|
|
1314
|
+
if (maxAt >= 0) {
|
|
1315
|
+
const n = Number(argv[maxAt + 1]);
|
|
1316
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
1317
|
+
process.stderr.write('verify: --max needs a positive whole number\n');
|
|
1318
|
+
return 1;
|
|
1319
|
+
}
|
|
1320
|
+
max = n;
|
|
1321
|
+
}
|
|
1322
|
+
const log = (line) => { process.stdout.write(`${line}\n`); };
|
|
1323
|
+
let report;
|
|
1324
|
+
try {
|
|
1325
|
+
report = await (0, verify_cmd_1.verifyCommand)({
|
|
1326
|
+
cwd: process.cwd(),
|
|
1327
|
+
config: loadConfig(configPath),
|
|
1328
|
+
...(max === undefined ? {} : { max }),
|
|
1329
|
+
log,
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
catch (e) {
|
|
1333
|
+
// See the note in `scan`. A verify pass writes verdicts into queue.json,
|
|
1334
|
+
// and a live run would write its own snapshot back over every one of them.
|
|
1335
|
+
if (e instanceof queue_write_lease_1.QueueBusy) {
|
|
1336
|
+
process.stderr.write(`\n verify: ${e.message}\n\n`);
|
|
1337
|
+
return 2;
|
|
1338
|
+
}
|
|
1339
|
+
throw e;
|
|
1340
|
+
}
|
|
1341
|
+
if (report.checked === 0) {
|
|
1342
|
+
log('verify : nothing to check — no items are waiting to be confirmed.');
|
|
1343
|
+
return 0;
|
|
1344
|
+
}
|
|
1345
|
+
const verdicts = Object.entries(report.byVerdict)
|
|
1346
|
+
.map(([k, n]) => `${n} ${k}`)
|
|
1347
|
+
.join(' · ');
|
|
1348
|
+
log('');
|
|
1349
|
+
log(`verify : ${report.checked} checked — ${verdicts}`);
|
|
1350
|
+
log(`verify : ${report.ready} ready to work on`);
|
|
1351
|
+
if (report.failed)
|
|
1352
|
+
log(`verify : ${report.failed} could not be written back — run it again`);
|
|
1353
|
+
return 0;
|
|
1354
|
+
}
|
|
1355
|
+
/**
|
|
1356
|
+
* Is a run going right now?
|
|
1357
|
+
*
|
|
1358
|
+
* Read from the projection rather than from a process list: the run may be
|
|
1359
|
+
* on another terminal, and status.json is the thing both of them agree on.
|
|
1360
|
+
* A missing or unreadable file means no, which is the safe reading — the
|
|
1361
|
+
* request is written either way and the next run consumes it.
|
|
1362
|
+
*/
|
|
1363
|
+
function isRunLive() {
|
|
1364
|
+
try {
|
|
1365
|
+
const parsed = JSON.parse((0, fs_1.readFileSync)((0, path_1.resolve)('.looply/state/status.json'), 'utf8'));
|
|
1366
|
+
// `live` is top level. It was read as `parsed.run.live` — the shape
|
|
1367
|
+
// `looply agent --json` produces, not the shape LedgerStore writes — so
|
|
1368
|
+
// the answer was always undefined, `looply stop` always said no run was
|
|
1369
|
+
// going, and it never wrote the request. welcome() and agent() both read
|
|
1370
|
+
// the flat field correctly, which is what let the odd one out survive.
|
|
1371
|
+
return parsed?.live === true;
|
|
1372
|
+
}
|
|
1373
|
+
catch {
|
|
1374
|
+
return false;
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
/**
|
|
1378
|
+
* The queue, in the terminal.
|
|
1379
|
+
*
|
|
1380
|
+
* The screen exists for the *why*: a queue of fifteen where twelve are stale,
|
|
1381
|
+
* blocked or duplicates is a queue of three, and a count alone hides that.
|
|
1382
|
+
*/
|
|
1383
|
+
async function queue(argv) {
|
|
1384
|
+
const store = new queue_store_1.QueueStore((0, path_1.resolve)('.looply/state'), ledger_store_1.nodeLedgerFs);
|
|
1385
|
+
await store.open();
|
|
1386
|
+
for (const line of (0, queue_view_1.renderQueue)(store.list(), { all: argv.includes('--all') })) {
|
|
1387
|
+
process.stdout.write(line + '\n');
|
|
1388
|
+
}
|
|
1389
|
+
return 0;
|
|
1390
|
+
}
|
|
1391
|
+
/**
|
|
1392
|
+
* What this project is running, and what it left behind.
|
|
1393
|
+
*
|
|
1394
|
+
* 🔴 THE REAPER HAD NO WAY IN. Phase 1 shipped `createWorkspace`, `reapVerdict`
|
|
1395
|
+
* and `reapWorkspace` with NO production caller — so workspaces and registry
|
|
1396
|
+
* entries accumulated with nothing able to retire them, and the orphan lifecycle
|
|
1397
|
+
* the phase claimed to have was unreachable code. This is its home.
|
|
1398
|
+
*
|
|
1399
|
+
* Read-only without `--reap`, deliberately: a person should be able to see what
|
|
1400
|
+
* is going on without authorising a deletion in the same breath.
|
|
1401
|
+
*/
|
|
1402
|
+
async function runs(argv) {
|
|
1403
|
+
const projectState = (0, git_driver_1.projectStateDir)(process.cwd());
|
|
1404
|
+
const registry = new workspace_1.RunRegistry(projectState);
|
|
1405
|
+
const entries = registry.list();
|
|
1406
|
+
/**
|
|
1407
|
+
* 🔴 THE SLOTS ARE SHOWN, AND THEY USED NOT TO BE.
|
|
1408
|
+
*
|
|
1409
|
+
* The concurrency bound lives in `slot-<n>.json`, and `list()` filters those
|
|
1410
|
+
* out on purpose — counting them would report every run twice. But nothing
|
|
1411
|
+
* else showed them either, so a slot held by a record that had already gone
|
|
1412
|
+
* was invisible: `looply runs` printed "nothing running, and nothing left
|
|
1413
|
+
* behind" while every start refused with "this project already has 1
|
|
1414
|
+
* execution(s) holding a slot". Nothing in the CLI could see it, name it, or
|
|
1415
|
+
* clear it, and the project was wedged with no way out but deleting files by
|
|
1416
|
+
* hand.
|
|
1417
|
+
*/
|
|
1418
|
+
const slots = registry.slots();
|
|
1419
|
+
/**
|
|
1420
|
+
* 🔴 AND THE CHECKOUTS NOTHING ACCOUNTS FOR. `--new-workspace` cuts the
|
|
1421
|
+
* worktree BEFORE the run registers, so anything that ends the run in between
|
|
1422
|
+
* — a config error, a dirty tree, `--dry-run`, a crash — left a real checkout
|
|
1423
|
+
* this command could not see and `--reap` could not reach. One per failed
|
|
1424
|
+
* start, accumulating silently. Found by using the command.
|
|
1425
|
+
*/
|
|
1426
|
+
const { abandoned, busy } = (0, workspace_1.abandonedWorkspaces)(process.cwd(), registry);
|
|
1427
|
+
/**
|
|
1428
|
+
* 🔴 AND THE CLAIM LOCK, WHICH WAS INVISIBLE TO EVERY COMMAND. It is only
|
|
1429
|
+
* breakable when its owner is PROVEN dead, so one left by a process that is
|
|
1430
|
+
* alive but hung — or whose pid cannot be judged — denied every start of the
|
|
1431
|
+
* project with a message about "another start deciding" and nothing to look at.
|
|
1432
|
+
* That is the same wedge the slot files had before this listed them.
|
|
1433
|
+
*/
|
|
1434
|
+
const lock = registry.claimLock();
|
|
1435
|
+
/**
|
|
1436
|
+
* 🔴 A REAL REPOSITORY MAKES THESE ROWS UNREADABLE. Measured against a clone of
|
|
1437
|
+
* looply itself: a workspace path is
|
|
1438
|
+
* `C:\Users\…\.looply\workspaces\399072e3c9a57ef8\ws-20260904070437-dra9`,
|
|
1439
|
+
* and a box row carrying one came out at 170 characters — which wraps twice on
|
|
1440
|
+
* an 80-column terminal and turns a three-row table into nine lines of noise.
|
|
1441
|
+
* Every fixture used until now had a path short enough to hide it.
|
|
1442
|
+
*
|
|
1443
|
+
* The root is the same for every one of them, so it is said ONCE and the rows
|
|
1444
|
+
* carry the part that differs. A `--in` checkout is somewhere else entirely and
|
|
1445
|
+
* keeps its full path, because that is the only place it appears.
|
|
1446
|
+
*/
|
|
1447
|
+
const wsRootShown = (0, path_1.resolve)((0, workspace_1.workspacesRoot)(process.cwd()));
|
|
1448
|
+
const shortWs = (w) => {
|
|
1449
|
+
const r = (0, path_1.resolve)(w);
|
|
1450
|
+
return r.toLowerCase().startsWith(`${wsRootShown.toLowerCase()}${path_1.sep}`)
|
|
1451
|
+
? `…${path_1.sep}${r.slice(wsRootShown.length + 1)}`
|
|
1452
|
+
: r;
|
|
1453
|
+
};
|
|
1454
|
+
if (entries.length === 0 &&
|
|
1455
|
+
slots.length === 0 &&
|
|
1456
|
+
abandoned.length === 0 &&
|
|
1457
|
+
busy.length === 0 &&
|
|
1458
|
+
lock === null) {
|
|
1459
|
+
/**
|
|
1460
|
+
* ⚠️ AND `--release` STILL HAS TO ANSWER. This returned 0 with "nothing
|
|
1461
|
+
* running, and nothing left behind" BEFORE the flag was parsed, so
|
|
1462
|
+
* `looply runs --release anything-at-all` reported success on a clean
|
|
1463
|
+
* project — a command that did nothing, said nothing about it, and exited 0.
|
|
1464
|
+
*/
|
|
1465
|
+
if (argv.includes('--release')) {
|
|
1466
|
+
process.stderr.write(`\n nothing is registered for this project, so there is nothing to release\n\n`);
|
|
1467
|
+
return 2;
|
|
1468
|
+
}
|
|
1469
|
+
process.stdout.write(`\n ${(0, ui_1.dim)('nothing running, and nothing left behind')}\n\n`);
|
|
1470
|
+
return 0;
|
|
1471
|
+
}
|
|
1472
|
+
if (entries.length > 0) {
|
|
1473
|
+
const rows = entries.map((e) => e.kind === 'unreadable'
|
|
1474
|
+
? [
|
|
1475
|
+
'unreadable',
|
|
1476
|
+
// 🔴 AND HOW TO CLEAR IT. This printed the path and the reason and
|
|
1477
|
+
// stopped, and `--release` then refused the name it had just shown
|
|
1478
|
+
// — the one row a person most wants gone was the one nothing could
|
|
1479
|
+
// remove. It holds no slot, so this is clutter rather than a wedge,
|
|
1480
|
+
// but a listing that offers no way out stops being believed.
|
|
1481
|
+
`${e.path} — ${e.because}; \`looply runs --release ${(0, path_1.basename)(e.path).replace(/\.json$/, '')}\` clears it`,
|
|
1482
|
+
]
|
|
1483
|
+
: [
|
|
1484
|
+
e.kind === 'active' ? 'running' : e.kind === 'orphaned' ? 'orphaned' : 'unjudgeable',
|
|
1485
|
+
`${e.record.runId} ${e.record.branch} pid ${e.record.pid} ${shortWs(e.record.workspace)}`,
|
|
1486
|
+
]);
|
|
1487
|
+
process.stdout.write(`${(0, ui_1.box)((0, ui_1.fields)(rows), { title: 'executions' })}\n`);
|
|
1488
|
+
}
|
|
1489
|
+
if (lock !== null) {
|
|
1490
|
+
const who = lock.pid === null
|
|
1491
|
+
? 'nothing this can identify'
|
|
1492
|
+
: `pid ${lock.pid}, which is ${lock.liveness === 'alive' ? 'running' : lock.liveness === 'dead' ? 'gone' : 'not answering'}`;
|
|
1493
|
+
process.stdout.write(`${(0, ui_1.box)((0, ui_1.fields)([
|
|
1494
|
+
[
|
|
1495
|
+
'claim lock',
|
|
1496
|
+
`held by ${who} ${(0, ui_1.dim)(lock.liveness === 'dead' ? 'the next start will break it' : '`looply runs --release claim-lock` clears it')}`,
|
|
1497
|
+
],
|
|
1498
|
+
]), { title: 'starting' })}
|
|
1499
|
+
`);
|
|
1500
|
+
}
|
|
1501
|
+
if (slots.length > 0) {
|
|
1502
|
+
const rows = slots.map((s) => [
|
|
1503
|
+
`slot-${s.slot}`,
|
|
1504
|
+
s.record === null
|
|
1505
|
+
? `${(0, ui_1.warn)('unreadable')} — nothing can say whose it is; \`looply runs --release slot-${s.slot}\` clears it`
|
|
1506
|
+
: `${s.record.runId} pid ${s.record.pid}`,
|
|
1507
|
+
]);
|
|
1508
|
+
process.stdout.write(`${(0, ui_1.box)((0, ui_1.fields)(rows), { title: 'concurrency slots' })}\n`);
|
|
1509
|
+
}
|
|
1510
|
+
/**
|
|
1511
|
+
* 🔴 AND THE FILES THAT LOOK LIKE SLOTS AND ARE NOT.
|
|
1512
|
+
*
|
|
1513
|
+
* `slot-abc.json` and `slot-01.json` are deliberately not slots: neither
|
|
1514
|
+
* name round-trips, so `--release slot-<n>` could not clear them and a row
|
|
1515
|
+
* offering that would be a lie. They were then in no listing at all — a file
|
|
1516
|
+
* plainly on disk, mentioned nowhere, which is precisely the state this
|
|
1517
|
+
* command exists to make impossible. They are inert; they are also printed.
|
|
1518
|
+
*/
|
|
1519
|
+
const strays = registry.strayFiles();
|
|
1520
|
+
if (strays.length > 0) {
|
|
1521
|
+
process.stdout.write(` ${(0, ui_1.warn)('note')} — ${strays.length} file(s) in the slots directory nothing can name. ` +
|
|
1522
|
+
`Inert: nothing counts them and nothing reads them. Delete them if you like:\n`);
|
|
1523
|
+
for (const s of strays)
|
|
1524
|
+
process.stdout.write(` ${(0, ui_1.dim)(s)}\n`);
|
|
1525
|
+
process.stdout.write('\n');
|
|
1526
|
+
}
|
|
1527
|
+
if (abandoned.length > 0 || busy.length > 0) {
|
|
1528
|
+
/**
|
|
1529
|
+
* ⚠️ BUSY CHECKOUTS ARE SHOWN TOO. They used to be skipped in silence, so a
|
|
1530
|
+
* real directory on disk existed nowhere: no row, no count, no reason, and
|
|
1531
|
+
* this command printing "nothing running, and nothing left behind". They
|
|
1532
|
+
* are not offered for reaping — something is using them — but a person
|
|
1533
|
+
* looking at a directory that is plainly there deserves to see it.
|
|
1534
|
+
*/
|
|
1535
|
+
const rows = [
|
|
1536
|
+
...busy.map((w) => [
|
|
1537
|
+
'in use',
|
|
1538
|
+
`${shortWs(w)} ${(0, ui_1.dim)(`in use — \`looply runs --release ${(0, path_1.basename)(w)}\` clears its start marker`)}`,
|
|
1539
|
+
]),
|
|
1540
|
+
...abandoned.map((w) => {
|
|
1541
|
+
/**
|
|
1542
|
+
* 🔴 "ABANDONED" AND "KEPT ON PURPOSE" ARE DIFFERENT THINGS, and this
|
|
1543
|
+
* said the first about both. A checkout the reaper has already refused —
|
|
1544
|
+
* because it holds a person's uncommitted files, or because its git
|
|
1545
|
+
* config cannot be read — was listed as "no execution accounts for this
|
|
1546
|
+
* checkout", which is true and reads as "nothing is using this, remove
|
|
1547
|
+
* it". The quarantine record is what lets the row say which it is.
|
|
1548
|
+
*/
|
|
1549
|
+
const held = (0, workspace_quarantine_1.quarantineOf)(process.cwd(), w);
|
|
1550
|
+
return held === null
|
|
1551
|
+
? ['abandoned', `${shortWs(w)} ${(0, ui_1.dim)('no execution accounts for this checkout')}`]
|
|
1552
|
+
: [
|
|
1553
|
+
'quarantined',
|
|
1554
|
+
`${shortWs(w)} ${(0, ui_1.warn)(held.because)} ${(0, ui_1.dim)(`— \`looply runs --release ${(0, path_1.basename)(w)}\` when you have looked`)}`,
|
|
1555
|
+
];
|
|
1556
|
+
}),
|
|
1557
|
+
];
|
|
1558
|
+
process.stdout.write(`${(0, ui_1.box)((0, ui_1.fields)(rows), { title: 'workspaces' })}
|
|
1559
|
+
`);
|
|
1560
|
+
process.stdout.write(` ${(0, ui_1.dim)(`… = ${wsRootShown}`)}
|
|
1561
|
+
`);
|
|
1562
|
+
}
|
|
1563
|
+
/**
|
|
1564
|
+
* THE LAST RESORT, AND THE ONLY THING THAT CAN CLEAR A HOLD THAT LOOKS ALIVE.
|
|
1565
|
+
*
|
|
1566
|
+
* Liveness here is a pid, and pids are recycled. A run killed hard leaves both
|
|
1567
|
+
* its registry entry and its lease behind, so every automatic witness agrees
|
|
1568
|
+
* the run is going — and if a later process inherits that pid, they agree for
|
|
1569
|
+
* ever. The slot is never reclaimed and the project's concurrency is one
|
|
1570
|
+
* lower than it says, permanently, with nothing to point at.
|
|
1571
|
+
*
|
|
1572
|
+
* ⚠️ SO A PERSON SAYS SO, BY NAME. Not a flag on `--reap`, not a heuristic
|
|
1573
|
+
* about how old a record is, and nothing automatic: the name has to be typed.
|
|
1574
|
+
* It is the same shape as the one-time explicit release the owner accepted
|
|
1575
|
+
* for F7 — narrow, deliberate, and impossible to reach by accident.
|
|
1576
|
+
*
|
|
1577
|
+
* ⚠️ `slot-<n>` IS ACCEPTED AS WELL AS A runId, because the case this exists
|
|
1578
|
+
* for includes a slot file nothing can read — and a corrupt slot has no runId
|
|
1579
|
+
* to type.
|
|
1580
|
+
*
|
|
1581
|
+
* ⚠️ IT DOES NOT TOUCH THE WORKSPACE. Forgetting a hold is a statement about
|
|
1582
|
+
* bookkeeping; the checkout and everything in it stays exactly where it is,
|
|
1583
|
+
* and `--reap` — which proves death first — is still the only thing that
|
|
1584
|
+
* removes one.
|
|
1585
|
+
*/
|
|
1586
|
+
const releaseAt = argv.indexOf('--release');
|
|
1587
|
+
if (releaseAt !== -1) {
|
|
1588
|
+
const name = argv[releaseAt + 1];
|
|
1589
|
+
if (name === undefined || name.startsWith('--')) {
|
|
1590
|
+
process.stderr.write(`\n --release needs the runId or slot-<n> to clear, exactly as listed above\n\n`);
|
|
1591
|
+
return 2;
|
|
1592
|
+
}
|
|
1593
|
+
if (name === 'claim-lock') {
|
|
1594
|
+
/**
|
|
1595
|
+
* ⚠️ WARN WHEN ITS OWNER IS RUNNING. The box a few lines above may have
|
|
1596
|
+
* just said "held by pid N, which is running", and clearing it anyway lets
|
|
1597
|
+
* a second start into a decision another process is in the middle of.
|
|
1598
|
+
*/
|
|
1599
|
+
if (lock !== null && lock.liveness !== 'dead') {
|
|
1600
|
+
const how = lock.liveness === 'alive' ? 'answers' : 'cannot be judged';
|
|
1601
|
+
process.stdout.write(` ${(0, ui_1.warn)('note')} — pid ${lock.pid ?? '?'} ${how}. If that is a start still deciding, ` +
|
|
1602
|
+
`clearing this lets another one decide alongside it.
|
|
1603
|
+
`);
|
|
1604
|
+
}
|
|
1605
|
+
const cleared = registry.forgetClaimLock();
|
|
1606
|
+
process.stdout.write(cleared
|
|
1607
|
+
? ` claim-lock: cleared — the next start decides for itself
|
|
1608
|
+
|
|
1609
|
+
`
|
|
1610
|
+
: ` claim-lock: nothing to clear
|
|
1611
|
+
|
|
1612
|
+
`);
|
|
1613
|
+
return cleared ? 0 : 2;
|
|
1614
|
+
}
|
|
1615
|
+
const asSlot = /^slot-(\d+)$/.exec(name);
|
|
1616
|
+
if (asSlot !== null) {
|
|
1617
|
+
const n = Number.parseInt(asSlot[1] ?? '', 10);
|
|
1618
|
+
/**
|
|
1619
|
+
* 🔴 THE SAME WARNING ITS SIBLING GIVES. This branch cleared a slot with
|
|
1620
|
+
* no read and no probe, while the runId branch a few lines below probes
|
|
1621
|
+
* and says "pid N answers, so this may be a real run". The justification
|
|
1622
|
+
* for accepting `slot-<n>` at all is a slot nothing can read — and that
|
|
1623
|
+
* covers only the unreadable case, not one that reads perfectly and names
|
|
1624
|
+
* a process that is running right now.
|
|
1625
|
+
*/
|
|
1626
|
+
const holder = slots.find((sl) => sl.slot === n)?.record ?? null;
|
|
1627
|
+
if (holder !== null && (0, workspace_1.nodeProbe)(holder.pid) !== 'dead') {
|
|
1628
|
+
process.stdout.write(` ${(0, ui_1.warn)('note')} — slot-${n} names ${holder.runId}, pid ${holder.pid}, which answers. ` +
|
|
1629
|
+
`Clearing it lets another execution start alongside it.
|
|
1630
|
+
`);
|
|
1631
|
+
}
|
|
1632
|
+
const cleared = registry.forgetSlot(n);
|
|
1633
|
+
process.stdout.write(cleared
|
|
1634
|
+
? ` slot-${n}: cleared — the run's own record, if it has one, was left alone\n\n`
|
|
1635
|
+
: ` slot-${n}: nothing to clear\n\n`);
|
|
1636
|
+
return cleared ? 0 : 2;
|
|
1637
|
+
}
|
|
1638
|
+
/**
|
|
1639
|
+
* ⚠️ IT MUST BE ONE THIS PROJECT ACTUALLY LISTS. `forget` builds a path from
|
|
1640
|
+
* the name, so a typed `../../something` would otherwise reach outside the
|
|
1641
|
+
* registry. `RunRegistry` refuses that shape itself; this refuses it earlier,
|
|
1642
|
+
* with a sentence about the project rather than about path syntax.
|
|
1643
|
+
*/
|
|
1644
|
+
const known = entries.find((e) => e.kind !== 'unreadable' && e.record.runId === name);
|
|
1645
|
+
const holdsSlot = slots.some((s) => s.record?.runId === name);
|
|
1646
|
+
/**
|
|
1647
|
+
* 🔴 AND AN UNREADABLE RECORD IS CLEARABLE BY THE NAME IT IS PRINTED UNDER.
|
|
1648
|
+
*
|
|
1649
|
+
* `known` deliberately skips `unreadable` entries — there is no trustworthy
|
|
1650
|
+
* runId inside a file that will not parse, and `--release` must never act
|
|
1651
|
+
* on one. But the FILENAME is not inside the file: it is `<name>.json` in
|
|
1652
|
+
* a directory this owns, and it is what `looply runs` prints. So the one
|
|
1653
|
+
* entry a person is most likely to want gone was the one entry nothing
|
|
1654
|
+
* could remove: `--release` said "no execution called X is registered" and
|
|
1655
|
+
* `--reap` skipped it, for ever, with the path on screen the whole time.
|
|
1656
|
+
*
|
|
1657
|
+
* It holds no slot and blocks nothing, so this is clutter rather than a
|
|
1658
|
+
* wedge — but a listing that shows a thing and offers no way to clear it is
|
|
1659
|
+
* how a person learns to stop believing the listing.
|
|
1660
|
+
*/
|
|
1661
|
+
const unreadableHere = entries.find((e) => e.kind === 'unreadable' && (0, path_1.basename)(e.path) === `${name}.json`);
|
|
1662
|
+
/**
|
|
1663
|
+
* A workspace listed as `in use` whose start marker names a pid nothing can
|
|
1664
|
+
* distinguish from the run that wrote it.
|
|
1665
|
+
*
|
|
1666
|
+
* ⚠️ AFTER THE RECORD LOOKUP, DELIBERATELY. A name that is BOTH a known
|
|
1667
|
+
* runId and a workspace directory clears the RECORD — the more specific
|
|
1668
|
+
* thing, and what somebody reading the executions table means. This branch
|
|
1669
|
+
* used to come first and would silently have done the other one.
|
|
1670
|
+
*
|
|
1671
|
+
* 🔴 AND IT WARNS, BECAUSE THIS MARKER MAY BE THE CHECKOUT'S ONLY
|
|
1672
|
+
* PROTECTION. Between `worktree add` and the run taking its lease there is
|
|
1673
|
+
* no record and no lease — just this file. Clearing it with no probe and no
|
|
1674
|
+
* word let the very next `--reap` remove a live starting run's checkout.
|
|
1675
|
+
*/
|
|
1676
|
+
if (known === undefined && unreadableHere === undefined && !holdsSlot && busy.some((w) => (0, path_1.basename)(w) === name)) {
|
|
1677
|
+
const holder = (0, workspace_1.creationMarkerHolder)(process.cwd(), name);
|
|
1678
|
+
if (holder !== null && holder.liveness !== 'dead') {
|
|
1679
|
+
const how = holder.liveness === 'alive' ? 'answers' : 'cannot be judged';
|
|
1680
|
+
process.stdout.write(` ${(0, ui_1.warn)('note')} — pid ${holder.pid} ${how}. If that is a run still starting up, ` +
|
|
1681
|
+
`clearing this lets the next \`--reap\` remove its checkout.\n`);
|
|
1682
|
+
}
|
|
1683
|
+
const cleared = (0, workspace_1.forgetCreationMarker)(process.cwd(), name);
|
|
1684
|
+
process.stdout.write(cleared
|
|
1685
|
+
? ` ${name}: its start marker was cleared — the checkout was left alone\n\n`
|
|
1686
|
+
: ` ${name}: nothing to clear\n\n`);
|
|
1687
|
+
return cleared ? 0 : 2;
|
|
1688
|
+
}
|
|
1689
|
+
/**
|
|
1690
|
+
* 🔴 THE ONLY THING THAT CLEARS A QUARANTINE, and it must be a person.
|
|
1691
|
+
*
|
|
1692
|
+
* A quarantine never expires and nothing re-derives it away: not a clean
|
|
1693
|
+
* tree, not a later run, not a reap. This is the sentence in
|
|
1694
|
+
* `recovery.ts` — "looply cannot tell whose changes those are" — turned into
|
|
1695
|
+
* a command. It removes the RECORD and never the directory, so whatever
|
|
1696
|
+
* made looply refuse is still exactly where the person left it.
|
|
1697
|
+
*
|
|
1698
|
+
* ⚠️ BEFORE THE RECORD BRANCH, AND THAT ORDER IS THE FIX FOR A MEASURED
|
|
1699
|
+
* DATA-LOSS CHAIN. A workspace directory is NAMED for its runId, so
|
|
1700
|
+
* `--release <name>` matches BOTH this and the run-record branch. With the
|
|
1701
|
+
* record first, the person who read a quarantine refusal and typed the
|
|
1702
|
+
* command it printed cleared the RECORD instead — was told "cleared — the
|
|
1703
|
+
* workspace was left alone", which reads as confirmation — and thereby made
|
|
1704
|
+
* the checkout record-less, i.e. abandoned. The next `--reap` then removed
|
|
1705
|
+
* it. Looply's own advice was the shortest route to losing the checkout it
|
|
1706
|
+
* had just refused to touch.
|
|
1707
|
+
*
|
|
1708
|
+
* A person who has typed the name of a QUARANTINED workspace means the
|
|
1709
|
+
* quarantine. The record is left alone, and the message says so.
|
|
1710
|
+
*/
|
|
1711
|
+
const quarantinedAt = (0, path_1.join)((0, workspace_1.workspacesRoot)(process.cwd()), name);
|
|
1712
|
+
const held = (0, workspace_quarantine_1.quarantineOf)(process.cwd(), quarantinedAt);
|
|
1713
|
+
if (held !== null) {
|
|
1714
|
+
const gone = (0, workspace_quarantine_1.clearQuarantine)(process.cwd(), quarantinedAt);
|
|
1715
|
+
process.stdout.write(gone
|
|
1716
|
+
? ` ${name}: quarantine cleared — the checkout was left exactly as it is.\n` +
|
|
1717
|
+
` ${(0, ui_1.dim)(`it was held because: ${held.because}`)}\n` +
|
|
1718
|
+
(known === undefined
|
|
1719
|
+
? '\n'
|
|
1720
|
+
: ` ${(0, ui_1.dim)(`its execution record is untouched; \`looply runs --release ${name}\` again clears that`)}\n\n`)
|
|
1721
|
+
: ` ${name}: its quarantine record could not be removed\n\n`);
|
|
1722
|
+
return gone ? 0 : 2;
|
|
1723
|
+
}
|
|
1724
|
+
if (known === undefined && unreadableHere === undefined && !holdsSlot) {
|
|
1725
|
+
process.stderr.write(`\n no execution called ${name} is registered for this project\n\n`);
|
|
1726
|
+
return 2;
|
|
1727
|
+
}
|
|
1728
|
+
if (unreadableHere !== undefined && known === undefined) {
|
|
1729
|
+
process.stdout.write(` ${(0, ui_1.warn)('note')} — nothing can read this record, so nothing can say whose it was. ` +
|
|
1730
|
+
`It holds no slot.\n`);
|
|
1731
|
+
}
|
|
1732
|
+
if (known?.kind === 'active') {
|
|
1733
|
+
process.stdout.write(` ${(0, ui_1.warn)('note')} — pid ${known.record.pid} answers, so this may be a real run. ` +
|
|
1734
|
+
`Clearing it lets another execution start alongside it.\n`);
|
|
1735
|
+
}
|
|
1736
|
+
const out = registry.forget(name);
|
|
1737
|
+
process.stdout.write(out.forgot || out.slot !== null
|
|
1738
|
+
? ` ${name}: cleared${out.slot === null ? '' : ` (slot ${out.slot} freed)`} — the workspace was left alone\n\n`
|
|
1739
|
+
: ` ${name}: nothing to clear\n\n`);
|
|
1740
|
+
return 0;
|
|
1741
|
+
}
|
|
1742
|
+
if (!argv.includes('--reap')) {
|
|
1743
|
+
/**
|
|
1744
|
+
* 🔴 AND IT SAYS WHICH ONES `--reap` CAN ACTUALLY TAKE. This printed
|
|
1745
|
+
* "N orphaned — looply runs --reap retires the ones it can prove are dead"
|
|
1746
|
+
* for every orphan, including the ones it refuses permanently and
|
|
1747
|
+
* correctly: a crash in the main checkout, or in a `--in` checkout a
|
|
1748
|
+
* person chose. Following that advice does nothing, once a day, for ever.
|
|
1749
|
+
*/
|
|
1750
|
+
const orphans = entries.filter((e) => e.kind === 'orphaned');
|
|
1751
|
+
/**
|
|
1752
|
+
* 🔴 AND IT COUNTS THEM THE WAY THE REAPER REFUSES THEM.
|
|
1753
|
+
*
|
|
1754
|
+
* This asked `record.worktree`, which is true for ANY linked worktree —
|
|
1755
|
+
* including one a person made and ran `--in`. A crash there was announced
|
|
1756
|
+
* as "in a checkout looply made", and `--reap` then refused it, for ever.
|
|
1757
|
+
* `reapableCheckout` is the reaper's own permanent test, so the sentence
|
|
1758
|
+
* and the command behind it now agree by construction.
|
|
1759
|
+
*/
|
|
1760
|
+
const reapable = orphans.filter((e) => e.kind === 'orphaned' && (0, workspace_1.reapableCheckout)(e.record, process.cwd()).ok).length;
|
|
1761
|
+
if (orphans.length > 0) {
|
|
1762
|
+
process.stdout.write(reapable > 0
|
|
1763
|
+
? ` ${(0, ui_1.dim)(`${orphans.length} orphaned, ${reapable} in a checkout looply made — looply runs --reap`)}\n`
|
|
1764
|
+
: ` ${(0, ui_1.dim)(`${orphans.length} orphaned, none in a checkout looply made — looply runs --release <runId>`)}\n`);
|
|
1765
|
+
}
|
|
1766
|
+
if (abandoned.length > 0) {
|
|
1767
|
+
process.stdout.write(` ${(0, ui_1.dim)(`${abandoned.length} checkout(s) nothing accounts for — \`looply runs --reap\` retires those it can`)}
|
|
1768
|
+
`);
|
|
1769
|
+
}
|
|
1770
|
+
/**
|
|
1771
|
+
* ⚠️ PROVEN DEAD OR UNREADABLE — NOT MERELY "not active". `active()`
|
|
1772
|
+
* deliberately over-counts an unjudgeable pid as running, so counting
|
|
1773
|
+
* anything that is not `active` here made the same screen print
|
|
1774
|
+
* `unjudgeable · run_x pid 4` AND `1 slot(s) held by nothing that is
|
|
1775
|
+
* running`, and then recommend clearing it. A slot nobody can judge is
|
|
1776
|
+
* exactly the one a person should not be nudged into freeing.
|
|
1777
|
+
*/
|
|
1778
|
+
const stuck = slots.filter((s) => s.record === null ||
|
|
1779
|
+
entries.some((e) => e.kind === 'orphaned' && e.record.runId === s.record?.runId) ||
|
|
1780
|
+
!entries.some((e) => e.kind !== 'unreadable' && e.record.runId === s.record?.runId)).length;
|
|
1781
|
+
if (stuck > 0) {
|
|
1782
|
+
process.stdout.write(` ${(0, ui_1.dim)(`${stuck} slot(s) held by nothing that is running — \`looply runs --release <runId|slot-N>\` clears one`)}\n`);
|
|
1783
|
+
}
|
|
1784
|
+
process.stdout.write('\n');
|
|
1785
|
+
return 0;
|
|
1786
|
+
}
|
|
1787
|
+
const git = new git_driver_1.GitDriver((0, git_driver_1.nodeGitExec)(process.cwd()));
|
|
1788
|
+
/**
|
|
1789
|
+
* What looply itself put in those checkouts, so the reaper does not read its
|
|
1790
|
+
* own copy as the person's file and refuse for ever. See `workspaceResidue`.
|
|
1791
|
+
*
|
|
1792
|
+
* Best-effort: a project with no readable config is reaped exactly as before.
|
|
1793
|
+
*/
|
|
1794
|
+
let carried = [];
|
|
1795
|
+
try {
|
|
1796
|
+
carried = loadConfig((0, path_1.join)(process.cwd(), git_driver_1.STATE_DIR, 'config.yaml')).run.carryIntoCheckouts ?? [];
|
|
1797
|
+
}
|
|
1798
|
+
catch {
|
|
1799
|
+
/* no config, or an unreadable one: nothing is excused, which is the old behaviour */
|
|
1800
|
+
}
|
|
1801
|
+
for (const w of abandoned) {
|
|
1802
|
+
const out = await (0, workspace_1.reapAbandoned)(git, process.cwd(), w, undefined, undefined, undefined, carried);
|
|
1803
|
+
process.stdout.write(out.reaped
|
|
1804
|
+
? ` ${(0, path_1.basename)(w)}: retired — no execution accounted for it
|
|
1805
|
+
`
|
|
1806
|
+
: ` ${(0, path_1.basename)(w)}: ${(0, ui_1.warn)('kept')} — ${out.because ?? 'unknown'}
|
|
1807
|
+
`);
|
|
1808
|
+
}
|
|
1809
|
+
for (const e of entries) {
|
|
1810
|
+
if (e.kind !== 'orphaned')
|
|
1811
|
+
continue;
|
|
1812
|
+
const out = await (0, workspace_1.reapWorkspace)(git, registry, e.record, process.cwd(), undefined, undefined, undefined, carried);
|
|
1813
|
+
process.stdout.write(out.reaped
|
|
1814
|
+
? ` ${e.record.runId}: retired${out.evidenceAt === undefined ? '' : ` — its records kept at ${out.evidenceAt}`}\n`
|
|
1815
|
+
: ` ${e.record.runId}: ${(0, ui_1.warn)('kept')} — ${out.because ?? 'unknown'}` +
|
|
1816
|
+
/**
|
|
1817
|
+
* ⚠️ AND SAY WHAT TO DO ABOUT THE ONE KIND `--reap` CAN NEVER CLEAR.
|
|
1818
|
+
* A run that crashed in the MAIN checkout leaves a record whose
|
|
1819
|
+
* workspace no execution created, so this refuses it every time,
|
|
1820
|
+
* correctly and for ever. Without this a person is told 'kept' once a
|
|
1821
|
+
* day with no way forward.
|
|
1822
|
+
*/
|
|
1823
|
+
/**
|
|
1824
|
+
* ⚠️ EVERY REFUSAL `--reap` CANNOT RESOLVE BY RETRYING, not only the
|
|
1825
|
+
* main-checkout one. A `--in` checkout a person chose is refused for
|
|
1826
|
+
* exactly the same reason and just as permanently — looply did not
|
|
1827
|
+
* make it, so looply may not remove it — and without this the person
|
|
1828
|
+
* is nagged about it once a day with no way forward.
|
|
1829
|
+
*
|
|
1830
|
+
* A refusal about FILES is deliberately excluded: that one resolves
|
|
1831
|
+
* itself when the person moves or deletes them, and pointing them at
|
|
1832
|
+
* `--release` instead would be telling them to hide it.
|
|
1833
|
+
*/
|
|
1834
|
+
(out.because?.includes('workspaces root') === true ||
|
|
1835
|
+
out.because?.includes('main checkout') === true
|
|
1836
|
+
? ` — looply runs --release ${e.record.runId} clears the record`
|
|
1837
|
+
: '') +
|
|
1838
|
+
'\n');
|
|
1839
|
+
}
|
|
1840
|
+
process.stdout.write('\n');
|
|
1841
|
+
return 0;
|
|
1842
|
+
}
|
|
1843
|
+
/**
|
|
1844
|
+
* Ask a running loop to stop.
|
|
1845
|
+
*
|
|
1846
|
+
* The request goes to control.json and the run reads it between rounds, which
|
|
1847
|
+
* is also why --now kills nothing: a round interrupted halfway leaves a
|
|
1848
|
+
* half-written tree, and the next run refuses to start on one. The round in
|
|
1849
|
+
* flight always finishes.
|
|
1850
|
+
*/
|
|
1851
|
+
/**
|
|
1852
|
+
* 🔴 `pause` AND `resume`: TWO VERBS THE DAEMON HAS ALWAYS WRITTEN AND THE
|
|
1853
|
+
* TERMINAL COULD NEVER SAY.
|
|
1854
|
+
*
|
|
1855
|
+
* `ControlKind` has had all three since it was written, the run reads all
|
|
1856
|
+
* three, and the phone can send all three — but the CLI only ever wrote `stop`.
|
|
1857
|
+
* So a person at the machine had exactly one brake with one setting: end the
|
|
1858
|
+
* run. Wanting it to hold for ten minutes while they looked at something meant
|
|
1859
|
+
* stopping it and starting again, which spends a Start and loses the queue's
|
|
1860
|
+
* position.
|
|
1861
|
+
*
|
|
1862
|
+
* ⚠️ THE SAME FANNING WRITER AS `stop`, deliberately. A pause that reached the
|
|
1863
|
+
* first execution to look and no others would be exactly the defect Phase 1
|
|
1864
|
+
* measured and fixed for `stop` — and it would be silent, because a paused run
|
|
1865
|
+
* and a running one look identical from outside.
|
|
1866
|
+
*/
|
|
1867
|
+
/**
|
|
1868
|
+
* ⚠️ AND IT PASSES ARGV THROUGH. It used to hand `stop` an empty array, so
|
|
1869
|
+
* `looply pause --now` silently dropped the flag that `stop` exists to refuse
|
|
1870
|
+
* loudly — the one case where a person believes they did something drastic.
|
|
1871
|
+
*/
|
|
1872
|
+
async function pauseOrResume(kind, argv) {
|
|
1873
|
+
return stop(argv, kind);
|
|
1874
|
+
}
|
|
1875
|
+
async function stop(argv, kind = 'stop') {
|
|
1876
|
+
/**
|
|
1877
|
+
* 🔴 THE BRAKE HAS TO ADDRESS THE PROJECT, NOT A DIRECTORY.
|
|
1878
|
+
*
|
|
1879
|
+
* Phase 1 moved the run's `ControlStore` to the PROJECT state directory — so
|
|
1880
|
+
* that one stop would reach every workspace — and left this writer at the cwd.
|
|
1881
|
+
* The two then pointed at different files whenever a run was executing
|
|
1882
|
+
* anywhere but the main checkout: from the main checkout `isRunLive` found no
|
|
1883
|
+
* `status.json` and this wrote NOTHING while the run carried on, and from
|
|
1884
|
+
* inside a workspace it wrote a control file the run does not read. A human
|
|
1885
|
+
* brake that silently does nothing is worse than no brake at all.
|
|
1886
|
+
*
|
|
1887
|
+
* Liveness comes from the registry for the same reason. `status.json` is
|
|
1888
|
+
* execution-scoped by design, so asking one directory whether "a run" is going
|
|
1889
|
+
* cannot answer a question about N of them — and the registry probes the pid
|
|
1890
|
+
* rather than trusting a file.
|
|
1891
|
+
*
|
|
1892
|
+
* ⚠️ SIMPLE MODE IS UNCHANGED: in an ordinary checkout the project and
|
|
1893
|
+
* execution directories are the same path, and `isRunLive` still answers.
|
|
1894
|
+
*/
|
|
1895
|
+
const projectState = (0, git_driver_1.projectStateDir)(process.cwd());
|
|
1896
|
+
const running = new workspace_1.RunRegistry(projectState).active();
|
|
1897
|
+
const live = running.length > 0 || isRunLive();
|
|
1898
|
+
/**
|
|
1899
|
+
* ⚠️ ONE WRITER, SHARED WITH THE DAEMON. The shared `control.json` plus one
|
|
1900
|
+
* addressed request per live execution — see `requestControlEverywhere`. The
|
|
1901
|
+
* daemon had the same hole and a different code path; now it has neither.
|
|
1902
|
+
*/
|
|
1903
|
+
let addressed = [];
|
|
1904
|
+
let unreachable = [];
|
|
1905
|
+
/**
|
|
1906
|
+
* 🔴 A STANDING HOLD OUTLIVES THE RUN, SO `resume` MUST WRITE WITH NOTHING
|
|
1907
|
+
* RUNNING — AND IT DID NOT. THIS WAS A DEADLOCK WITH NO KEY.
|
|
1908
|
+
*
|
|
1909
|
+
* MEASURED: `looply pause` during a run, run ends, `control.json` still reads
|
|
1910
|
+
* `pause`. The daemon's autonomy tick reads that same project file to answer
|
|
1911
|
+
* "has a person asked this machine to hold" — its own docblock says so — so
|
|
1912
|
+
* the machine holds. `looply resume` then printed "no run is going — nothing
|
|
1913
|
+
* to resume", wrote NOTHING, exited 0, and left the hold exactly where it
|
|
1914
|
+
* was. The one documented way out of a hold could not clear one.
|
|
1915
|
+
*
|
|
1916
|
+
* ⚠️ `pause` IS THE SAME FACT FROM THE OTHER SIDE. Asking the machine to hold
|
|
1917
|
+
* BEFORE it starts anything is a coherent thing to want, and the daemon reads
|
|
1918
|
+
* the file that expresses it. Refusing to write it because nothing is running
|
|
1919
|
+
* yet is refusing the request at exactly the moment it is cheapest to honour.
|
|
1920
|
+
*
|
|
1921
|
+
* ⚠️ `stop` IS DIFFERENT AND STAYS DIFFERENT. It addresses a RUN. There is no
|
|
1922
|
+
* such thing as stopping a run that does not exist, and writing a standing
|
|
1923
|
+
* `stop` would leave an order that the next run consumes on its way past —
|
|
1924
|
+
* which is the "last night's stop wedges tonight's run" hazard the old
|
|
1925
|
+
* comment here was rightly worried about.
|
|
1926
|
+
*/
|
|
1927
|
+
const standing = kind !== 'stop';
|
|
1928
|
+
/** Whether a control file was actually written. Every message below reads this. */
|
|
1929
|
+
const wrote = live || standing;
|
|
1930
|
+
if (wrote) {
|
|
1931
|
+
const out = await (0, workspace_1.requestControlEverywhere)(projectState, running, kind, 'human:cli', ledger_store_1.nodeLedgerFs, process.cwd());
|
|
1932
|
+
addressed = out.reached;
|
|
1933
|
+
unreachable = out.unreachable;
|
|
1934
|
+
}
|
|
1935
|
+
if (running.length > 1) {
|
|
1936
|
+
/**
|
|
1937
|
+
* ⚠️ THE VERB IS THE ONE THE PERSON TYPED. This line was written for `stop`
|
|
1938
|
+
* and then reused by `pause` and `resume` without being read again, so
|
|
1939
|
+
* `looply resume` on three executions reported that three executions "were
|
|
1940
|
+
* told to stop" — the exact opposite of what had just been asked for, in
|
|
1941
|
+
* the one place a person looks to check the brake reached everything.
|
|
1942
|
+
*/
|
|
1943
|
+
const told = kind === 'resume' ? 'were told to carry on' : `were told to ${kind}`;
|
|
1944
|
+
process.stdout.write(`
|
|
1945
|
+
${(0, ui_1.dim)(`${addressed.length} of ${running.length} running executions ${told}.`)}
|
|
1946
|
+
`);
|
|
1947
|
+
}
|
|
1948
|
+
if (unreachable.length > 0) {
|
|
1949
|
+
process.stdout.write(` ${(0, ui_1.warn)('could not reach')} ${unreachable.join(', ')} — their state directories could not be written
|
|
1950
|
+
`);
|
|
1951
|
+
}
|
|
1952
|
+
process.stdout.write('\n ' + (0, queue_view_1.stopMessage)(live, kind) + '\n');
|
|
1953
|
+
/**
|
|
1954
|
+
* 🔴 `--now` IS REMOVED RATHER THAN IMPLEMENTED, AND THAT IS THE HONEST END
|
|
1955
|
+
* OF A FLAG THAT NEVER DID ANYTHING.
|
|
1956
|
+
*
|
|
1957
|
+
* It was parsed, it printed a sentence, and it changed nothing — while
|
|
1958
|
+
* `DESIGN.md` advertised "graceful … or immediate". A flag that is accepted
|
|
1959
|
+
* and ignored is worse than one that does not exist: a person who types it in
|
|
1960
|
+
* an emergency believes they have done something.
|
|
1961
|
+
*
|
|
1962
|
+
* And it cannot be implemented, deliberately. Phase 3 refused to add a hard
|
|
1963
|
+
* cancel because "a round in flight always finishes" is the invariant
|
|
1964
|
+
* RECOVERY rests on — cut one off and a dirty tree becomes looply's own
|
|
1965
|
+
* mid-round state, which the next run cannot tell from a person's edits. The
|
|
1966
|
+
* flag was advertising a capability the architecture is built to refuse.
|
|
1967
|
+
*
|
|
1968
|
+
* ⚠️ IT IS REFUSED LOUDLY, NOT IGNORED QUIETLY. Somebody's script may pass it.
|
|
1969
|
+
*/
|
|
1970
|
+
if (argv.includes('--now')) {
|
|
1971
|
+
process.stdout.write('\n ' +
|
|
1972
|
+
(0, ui_1.warn)('--now was removed') +
|
|
1973
|
+
' — it never did anything. A round in flight always finishes, because\n' +
|
|
1974
|
+
' cutting one off leaves a half-written tree the next run cannot tell from your edits.\n' +
|
|
1975
|
+
/**
|
|
1976
|
+
* 🔴 IT REPORTS WHAT WAS WRITTEN, FROM THE VARIABLE THAT DECIDED IT —
|
|
1977
|
+
* AND THE FIRST TWO VERSIONS OF THIS LINE BOTH LIED, IN OPPOSITE
|
|
1978
|
+
* DIRECTIONS.
|
|
1979
|
+
*
|
|
1980
|
+
* The original said "The stop above is in effect" unconditionally, so
|
|
1981
|
+
* `looply stop --now` with nothing running printed "no run is going"
|
|
1982
|
+
* and then, two lines later, the loudest possible reassurance
|
|
1983
|
+
* contradicting it. The repair replaced that with "nothing was
|
|
1984
|
+
* written" keyed on `live` — and by then `pause` and `resume` DID
|
|
1985
|
+
* write with nothing running, so `looply pause --now` announced that
|
|
1986
|
+
* nothing had been written over a control file it had just written.
|
|
1987
|
+
*
|
|
1988
|
+
* One false sentence about a write replaced by another is not a
|
|
1989
|
+
* repair; it is the same defect with a new value. So the sentence is
|
|
1990
|
+
* derived from `wrote`, which is the variable the write itself is
|
|
1991
|
+
* conditioned on, and the two cannot disagree without the compiler
|
|
1992
|
+
* noticing.
|
|
1993
|
+
*/
|
|
1994
|
+
(wrote
|
|
1995
|
+
? ` The ${kind} above is in effect.\n`
|
|
1996
|
+
: ' There was nothing running to stop, so nothing was written.\n'));
|
|
1997
|
+
}
|
|
1998
|
+
process.stdout.write('\n');
|
|
1999
|
+
return 0;
|
|
2000
|
+
}
|
|
2001
|
+
/**
|
|
2002
|
+
* Start a run.
|
|
2003
|
+
*
|
|
2004
|
+
* `--dry-run` prints the plan and calls no agent, which is how you find out the
|
|
2005
|
+
* queue is empty or proof is switched off in a second rather than at round one
|
|
2006
|
+
* of an overnight run.
|
|
2007
|
+
*/
|
|
2008
|
+
async function run(argv) {
|
|
2009
|
+
/**
|
|
2010
|
+
* WHERE THIS EXECUTION RUNS. Absent means here, which is Simple Mode.
|
|
2011
|
+
*
|
|
2012
|
+
* `--in` is the whole of Phase 1's parallel interface, and that is deliberate:
|
|
2013
|
+
* a second isolated execution is a second `looply run` in a second workspace,
|
|
2014
|
+
* and looply already knows how to be one run in one checkout. Nothing about
|
|
2015
|
+
* the no-flag path changes — same cwd, same state directory, same lease.
|
|
2016
|
+
*/
|
|
2017
|
+
const inAt = argv.indexOf('--in');
|
|
2018
|
+
let workspace;
|
|
2019
|
+
if (inAt >= 0) {
|
|
2020
|
+
const raw = argv[inAt + 1];
|
|
2021
|
+
if (raw === undefined || raw.startsWith('--')) {
|
|
2022
|
+
process.stderr.write('run: --in needs a path\n');
|
|
2023
|
+
return 1;
|
|
2024
|
+
}
|
|
2025
|
+
workspace = (0, path_1.resolve)(raw);
|
|
2026
|
+
if (!(0, fs_1.existsSync)(workspace)) {
|
|
2027
|
+
process.stderr.write(`run: no such directory: ${workspace}\n`);
|
|
2028
|
+
return 1;
|
|
2029
|
+
}
|
|
2030
|
+
/**
|
|
2031
|
+
* The same refusal every other verb gets, at the earliest point a person
|
|
2032
|
+
* can be told. A workspace that is not a repository root is one where
|
|
2033
|
+
* `reset --hard` would reach further than the person named.
|
|
2034
|
+
*/
|
|
2035
|
+
const misplaced = (0, trusted_git_1.repoRootTroubleAt)(workspace);
|
|
2036
|
+
if (misplaced !== null) {
|
|
2037
|
+
process.stderr.write(`run: ${misplaced}\n`);
|
|
2038
|
+
return 1;
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
/**
|
|
2042
|
+
* MAKE ONE, RATHER THAN BE HANDED ONE.
|
|
2043
|
+
*
|
|
2044
|
+
* 🔴 THE OTHER HALF OF THE LIFECYCLE WAS ALSO UNREACHABLE. `looply runs
|
|
2045
|
+
* --reap` only ever removes a checkout under this project's workspaces root —
|
|
2046
|
+
* that containment gate is what stops a reaper deleting a directory a person
|
|
2047
|
+
* made — and nothing in production ever put a checkout there.
|
|
2048
|
+
* `createWorkspace` had no caller, so `--reap` refused every orphan it was
|
|
2049
|
+
* ever shown, and the phase's own DEMO J passed by never reaching the code it
|
|
2050
|
+
* named.
|
|
2051
|
+
*
|
|
2052
|
+
* `--new-workspace` is the way in. Looply cuts the checkout, so looply may
|
|
2053
|
+
* later remove it; a `--in` path a person chose stays theirs, and `--reap`
|
|
2054
|
+
* still refuses it. That asymmetry is the point, not an oversight.
|
|
2055
|
+
*
|
|
2056
|
+
* ⚠️ MUTUALLY EXCLUSIVE WITH `--in`, because they answer the same question and
|
|
2057
|
+
* somebody who typed both meant one of them.
|
|
2058
|
+
*/
|
|
2059
|
+
if (argv.includes('--new-workspace')) {
|
|
2060
|
+
if (workspace !== undefined) {
|
|
2061
|
+
process.stderr.write('run: --in and --new-workspace both say where to run; pick one\n');
|
|
2062
|
+
return 1;
|
|
2063
|
+
}
|
|
2064
|
+
const here = process.cwd();
|
|
2065
|
+
const notARoot = (0, trusted_git_1.repoRootTroubleAt)(here);
|
|
2066
|
+
if (notARoot !== null) {
|
|
2067
|
+
process.stderr.write(`run: ${notARoot}\n`);
|
|
2068
|
+
return 1;
|
|
2069
|
+
}
|
|
2070
|
+
try {
|
|
2071
|
+
const git = new git_driver_1.GitDriver((0, git_driver_1.nodeGitExec)(here));
|
|
2072
|
+
const base = await git.headSha();
|
|
2073
|
+
const stamp = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 14);
|
|
2074
|
+
const tag = `ws-${stamp}-${Math.floor(Math.random() * 1e6).toString(36)}`;
|
|
2075
|
+
// Its own branch from the outset: a worktree cannot share a checked-out
|
|
2076
|
+
// branch with another, and the round runner cuts its run branch inside.
|
|
2077
|
+
//
|
|
2078
|
+
/**
|
|
2079
|
+
* 🔴 AND IT IS PROVISIONED BEFORE ANYTHING RUNS IN IT. A worktree holds
|
|
2080
|
+
* only what git tracks, the contained gate has no network by design, and
|
|
2081
|
+
* so a second worker's gate could not pass for any project with a
|
|
2082
|
+
* dependency — measured as four rounds red on the same missing toolchain.
|
|
2083
|
+
* `run.carry_into_checkouts` names what must travel; empty means nothing
|
|
2084
|
+
* is copied and this is byte-identical to what it was.
|
|
2085
|
+
*
|
|
2086
|
+
* Read from the config the run itself will read, so the two cannot
|
|
2087
|
+
* disagree about what this checkout needs.
|
|
2088
|
+
*/
|
|
2089
|
+
const carry = loadConfig((0, path_1.join)(here, git_driver_1.STATE_DIR, 'config.yaml')).run.carryIntoCheckouts ?? [];
|
|
2090
|
+
workspace = await (0, workspace_1.createWorkspace)(git, here, tag, `looply/${tag}`, base, (0, node_os_1.homedir)(), carry);
|
|
2091
|
+
process.stdout.write(` workspace: ${workspace}\n`);
|
|
2092
|
+
/**
|
|
2093
|
+
* ⚠️ AND IT SAYS WHAT IT CANNOT DO. The queue is EXECUTION-scoped — one
|
|
2094
|
+
* whole-file rewrite per checkout, which is what makes it safe without a
|
|
2095
|
+
* lock — so a freshly cut workspace starts with an empty one, and a run
|
|
2096
|
+
* there has nothing to pick.
|
|
2097
|
+
*
|
|
2098
|
+
* That is not an oversight: getting work INTO a second execution is a
|
|
2099
|
+
* coordinator's job, and Phase 1 is deliberately not one. What WOULD be an
|
|
2100
|
+
* oversight is a command that cuts a checkout, says nothing, and leaves a
|
|
2101
|
+
* person watching a run end with "nothing to do" and no idea why.
|
|
2102
|
+
*/
|
|
2103
|
+
process.stdout.write(` ${(0, ui_1.dim)('note: a new workspace starts with an empty queue — nothing assigns work to it yet.')}\n`);
|
|
2104
|
+
process.stdout.write(` ${(0, ui_1.dim)('`looply run --in <path>` runs in a checkout that already has one.')}\n`);
|
|
2105
|
+
}
|
|
2106
|
+
catch (e) {
|
|
2107
|
+
process.stderr.write(`run: could not make a workspace — ${e.message}\n`);
|
|
2108
|
+
return 1;
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
/**
|
|
2112
|
+
* 🔴 THE CHEAPEST REFUSAL, ASKED BEFORE THE TWO MOST EXPENSIVE STEPS.
|
|
2113
|
+
*
|
|
2114
|
+
* The runner refuses a dirty tree as `run()`'s first statement — §12 case 1,
|
|
2115
|
+
* because uncommitted work would be folded into round 1's diff and the first
|
|
2116
|
+
* discard would destroy it. That refusal is right and stays where it is.
|
|
2117
|
+
*
|
|
2118
|
+
* ⚠️ BUT `run()` IS CALLED AFTER COMPILE AND VERIFY, WHICH ARE MODEL CALLS.
|
|
2119
|
+
* Measured: a run against a tree with ONE changed path compiled the goal,
|
|
2120
|
+
* verified both items, and only then refused — **five provider calls across
|
|
2121
|
+
* two runs that bought nothing**. The question costs a `git status`.
|
|
2122
|
+
*
|
|
2123
|
+
* ⚠️ IT ASKS ABOUT THE TREE THE RUN WILL ACTUALLY USE, which is the property
|
|
2124
|
+
* that makes it safe: the runner asks the same question of the same checkout,
|
|
2125
|
+
* so this can only ever change WHEN the refusal happens, never WHETHER.
|
|
2126
|
+
*
|
|
2127
|
+
* 🔴 AND IT SKIPS `--dry-run`, WHICH NEVER REACHES THE RUNNER. A dry run
|
|
2128
|
+
* returns before any round begins and is precisely when somebody wants to ask
|
|
2129
|
+
* "what would happen" without committing their edits first; refusing it here
|
|
2130
|
+
* would be a new refusal, not an earlier one.
|
|
2131
|
+
*
|
|
2132
|
+
* ⚠️ An unreadable checkout is NOT refused here. That is the runner's own
|
|
2133
|
+
* question to answer with its own driver, and guessing at it from outside
|
|
2134
|
+
* would refuse runs this has no business judging.
|
|
2135
|
+
*/
|
|
2136
|
+
if (!argv.includes('--dry-run')) {
|
|
2137
|
+
const where = workspace ?? process.cwd();
|
|
2138
|
+
let dirty = null;
|
|
2139
|
+
try {
|
|
2140
|
+
dirty = await new git_driver_1.GitDriver((0, git_driver_1.nodeGitExec)(where)).changedPaths();
|
|
2141
|
+
}
|
|
2142
|
+
catch {
|
|
2143
|
+
dirty = null;
|
|
2144
|
+
}
|
|
2145
|
+
if (dirty !== null && dirty.length > 0) {
|
|
2146
|
+
process.stderr.write(`run: working tree has ${dirty.length} changed path(s) — refusing to start (commit or stash first)\n`);
|
|
2147
|
+
return 1;
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
const at = argv.indexOf('--config');
|
|
2151
|
+
const configPath = at >= 0 ? argv[at + 1] : '.looply/config.yaml';
|
|
2152
|
+
if (!configPath) {
|
|
2153
|
+
process.stderr.write('run: --config needs a path\n');
|
|
2154
|
+
return 1;
|
|
2155
|
+
}
|
|
2156
|
+
/**
|
|
2157
|
+
* 🔴 A DECLARED CROSS-REPO NEED GATES THE RUN, and it is asked before the
|
|
2158
|
+
* compile for the same reason the dirty tree is: the answer is local and free,
|
|
2159
|
+
* and finding it out after two model calls is money for nothing.
|
|
2160
|
+
*
|
|
2161
|
+
* ⚠️ A DRY RUN IS TOLD, NOT REFUSED. It exists to answer "what would happen",
|
|
2162
|
+
* and what would happen is precisely that this need blocks — so saying so is
|
|
2163
|
+
* the useful answer and refusing would withhold it.
|
|
2164
|
+
*/
|
|
2165
|
+
const needsTrouble = await crossRepoNeedsTrouble(configPath);
|
|
2166
|
+
if (needsTrouble !== null) {
|
|
2167
|
+
if (argv.includes('--dry-run')) {
|
|
2168
|
+
process.stdout.write(`dry run: this project would not start — ${needsTrouble}\n`);
|
|
2169
|
+
}
|
|
2170
|
+
else {
|
|
2171
|
+
process.stderr.write(`run: not starting — ${needsTrouble}\n`);
|
|
2172
|
+
process.stderr.write(' `looply repos` shows where each declared repository stands.\n');
|
|
2173
|
+
return 1;
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
const hoursAt = argv.indexOf('--hours');
|
|
2177
|
+
let hours;
|
|
2178
|
+
if (hoursAt >= 0) {
|
|
2179
|
+
const raw = Number(argv[hoursAt + 1]);
|
|
2180
|
+
if (!Number.isFinite(raw) || raw <= 0) {
|
|
2181
|
+
process.stderr.write('run: --hours needs a positive number\n');
|
|
2182
|
+
return 1;
|
|
2183
|
+
}
|
|
2184
|
+
hours = raw;
|
|
2185
|
+
}
|
|
2186
|
+
// Passed by whatever asked for this run, so the lease can record which
|
|
2187
|
+
// request it is carrying out. Absent for a hand-typed run, and REFUSED when
|
|
2188
|
+
// present but empty.
|
|
2189
|
+
//
|
|
2190
|
+
// The flag keeps the name it has always had while the value generalises: a
|
|
2191
|
+
// command id when a person tapped Start, `autonomyStartId(C, N)` when a
|
|
2192
|
+
// controller spent an ordinal. Nothing below this line parses it.
|
|
2193
|
+
//
|
|
2194
|
+
// Empty is refused rather than dropped because dropping it produces a run
|
|
2195
|
+
// that is attributed to NOTHING — no lease field, no start mark, no refusal
|
|
2196
|
+
// mark — which is invisible to whatever asked for it, for ever. That is a
|
|
2197
|
+
// worse outcome than not running, and it is a shape only a caller composing
|
|
2198
|
+
// its own identity can reach.
|
|
2199
|
+
const cmdAt = argv.indexOf('--command-id');
|
|
2200
|
+
const commandId = cmdAt >= 0 ? argv[cmdAt + 1] : undefined;
|
|
2201
|
+
if (cmdAt >= 0 && (commandId === undefined || commandId === '')) {
|
|
2202
|
+
process.stderr.write('run: --command-id needs a request identity\n');
|
|
2203
|
+
return 1;
|
|
2204
|
+
}
|
|
2205
|
+
// WHICH TRY THIS IS, when something is counting them.
|
|
2206
|
+
//
|
|
2207
|
+
// Present only for an autonomous Start. A human Start is bounded by the
|
|
2208
|
+
// server's redelivery window and needs no attempt of its own; giving it one
|
|
2209
|
+
// would put a second, smaller cap on a path that has worked for phases and
|
|
2210
|
+
// change behaviour nobody asked to change.
|
|
2211
|
+
const attemptAt = argv.indexOf('--request-attempt');
|
|
2212
|
+
let requestAttempt;
|
|
2213
|
+
if (attemptAt >= 0) {
|
|
2214
|
+
const raw = Number(argv[attemptAt + 1]);
|
|
2215
|
+
if (!Number.isInteger(raw) || raw < 1 || raw > start_attempts_1.MAX_START_REQUEST_ATTEMPTS) {
|
|
2216
|
+
process.stderr.write(`run: --request-attempt needs a whole number from 1 to ${start_attempts_1.MAX_START_REQUEST_ATTEMPTS}\n`);
|
|
2217
|
+
return 1;
|
|
2218
|
+
}
|
|
2219
|
+
if (commandId === undefined) {
|
|
2220
|
+
// An attempt number counts tries at a REQUEST. Without one there is
|
|
2221
|
+
// nothing to count, and nothing to file the claim under.
|
|
2222
|
+
process.stderr.write('run: --request-attempt needs a --command-id\n');
|
|
2223
|
+
return 1;
|
|
2224
|
+
}
|
|
2225
|
+
requestAttempt = raw;
|
|
2226
|
+
}
|
|
2227
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
2228
|
+
// `--autonomous` IS TRANSPORT, NOT PERMISSION.
|
|
2229
|
+
//
|
|
2230
|
+
// Its only effect is that beginning this run does not consume a person's
|
|
2231
|
+
// pending pause or stop. That is a real power — it lets a run proceed past an
|
|
2232
|
+
// instruction a person left — so a flag on a command line must not be enough
|
|
2233
|
+
// to claim it. Anyone who can type `looply run` could otherwise type
|
|
2234
|
+
// `--autonomous` and walk through somebody's Stop.
|
|
2235
|
+
//
|
|
2236
|
+
// So the flag only ANNOUNCES what the child must independently establish: an
|
|
2237
|
+
// attempt number in range, a request to file it under, and — below, at the
|
|
2238
|
+
// admission seam — an exclusive claim that actually succeeded. The claim is
|
|
2239
|
+
// the proof, because only the autonomy path produces one and the filesystem
|
|
2240
|
+
// decides who gets it.
|
|
2241
|
+
//
|
|
2242
|
+
// Announced without the rest, this is a misuse rather than a lesser request,
|
|
2243
|
+
// and it is refused outright instead of being quietly downgraded. Nothing
|
|
2244
|
+
// legitimate sends one without the other: the launcher emits both together.
|
|
2245
|
+
const announcedAutonomous = argv.includes('--autonomous');
|
|
2246
|
+
if (announcedAutonomous && requestAttempt === undefined) {
|
|
2247
|
+
process.stderr.write('run: --autonomous needs the --request-attempt it belongs to\n');
|
|
2248
|
+
return 1;
|
|
2249
|
+
}
|
|
2250
|
+
// The goal binding, carried across from the daemon. All three or none: a
|
|
2251
|
+
// goal with no version cannot be checked for freshness, and item ids with no
|
|
2252
|
+
// goal name work for nobody. Parsed strictly here rather than half-applied
|
|
2253
|
+
// later, because a run that starts UNBOUND when a goal was asked for works
|
|
2254
|
+
// the whole queue while the phone waits for a goal that never binds.
|
|
2255
|
+
const goalAt = argv.indexOf('--goal-id');
|
|
2256
|
+
const goalId = goalAt >= 0 ? argv[goalAt + 1] : undefined;
|
|
2257
|
+
const versionAt = argv.indexOf('--goal-version');
|
|
2258
|
+
const rawVersion = versionAt >= 0 ? argv[versionAt + 1] : undefined;
|
|
2259
|
+
const itemsAt = argv.indexOf('--item-ids');
|
|
2260
|
+
const rawItems = itemsAt >= 0 ? argv[itemsAt + 1] : undefined;
|
|
2261
|
+
let goalVersion;
|
|
2262
|
+
if (goalId !== undefined) {
|
|
2263
|
+
if (rawVersion === undefined) {
|
|
2264
|
+
process.stderr.write('run: --goal-id needs a --goal-version\n');
|
|
2265
|
+
return 1;
|
|
2266
|
+
}
|
|
2267
|
+
goalVersion = Number(rawVersion);
|
|
2268
|
+
if (!Number.isInteger(goalVersion) || goalVersion < 0) {
|
|
2269
|
+
process.stderr.write('run: --goal-version needs a whole number\n');
|
|
2270
|
+
return 1;
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
else if (rawVersion !== undefined || rawItems !== undefined) {
|
|
2274
|
+
process.stderr.write('run: --goal-version and --item-ids need a --goal-id\n');
|
|
2275
|
+
return 1;
|
|
2276
|
+
}
|
|
2277
|
+
// The answer this run is picking up, if a person started again after one.
|
|
2278
|
+
// Carried as an id, resolved against the server inside the run — the daemon
|
|
2279
|
+
// does not tell a run what a decision said, only which decision to go and
|
|
2280
|
+
// read. It needs a goal for the same reason the version does: the resolution
|
|
2281
|
+
// is checked against the pursuit this run is actually bound to.
|
|
2282
|
+
const resumeAt = argv.indexOf('--resumed-from-approval');
|
|
2283
|
+
const resumedFromApprovalId = resumeAt >= 0 ? argv[resumeAt + 1] : undefined;
|
|
2284
|
+
if (resumedFromApprovalId !== undefined) {
|
|
2285
|
+
if (resumedFromApprovalId.length === 0 || resumedFromApprovalId.startsWith('--')) {
|
|
2286
|
+
process.stderr.write('run: --resumed-from-approval needs an approval id\n');
|
|
2287
|
+
return 1;
|
|
2288
|
+
}
|
|
2289
|
+
if (goalId === undefined) {
|
|
2290
|
+
process.stderr.write('run: --resumed-from-approval needs a --goal-id\n');
|
|
2291
|
+
return 1;
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
/**
|
|
2295
|
+
* The approved action this Start was accepted to carry out.
|
|
2296
|
+
*
|
|
2297
|
+
* PARSED HERE AND PROVEN NOWHERE HERE. An id on a command line is something
|
|
2298
|
+
* anybody can type, and this flag buys exactly nothing on its own: the run
|
|
2299
|
+
* refuses it unless a durable reservation, written by the Start gate before
|
|
2300
|
+
* this process existed, names this run's own `--command-id`. That check lives
|
|
2301
|
+
* in `governed-run`, where the rest of the governed vocabulary lives.
|
|
2302
|
+
*
|
|
2303
|
+
* The goal requirement is the same one the resume flag has, for the same
|
|
2304
|
+
* reason: an approval belongs to a pursuit at a version, and a run that names
|
|
2305
|
+
* none has nothing to check it against.
|
|
2306
|
+
*/
|
|
2307
|
+
const governedAt = argv.indexOf('--governed-approval');
|
|
2308
|
+
const governedApprovalId = governedAt >= 0 ? argv[governedAt + 1] : undefined;
|
|
2309
|
+
if (governedApprovalId !== undefined) {
|
|
2310
|
+
if (governedApprovalId.length === 0 || governedApprovalId.startsWith('--')) {
|
|
2311
|
+
process.stderr.write('run: --governed-approval needs an approval id\n');
|
|
2312
|
+
return 1;
|
|
2313
|
+
}
|
|
2314
|
+
if (goalId === undefined) {
|
|
2315
|
+
process.stderr.write('run: --governed-approval needs a --goal-id\n');
|
|
2316
|
+
return 1;
|
|
2317
|
+
}
|
|
2318
|
+
if (resumedFromApprovalId !== undefined) {
|
|
2319
|
+
// A run picks up an answer or carries one out. Both at once would be a
|
|
2320
|
+
// run acting on a refusal and an approval in the same breath.
|
|
2321
|
+
process.stderr.write('run: a run cannot both resume a decision and carry one out\n');
|
|
2322
|
+
return 1;
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
// An empty value is not an empty id: `--item-ids ''` means "adopt nothing
|
|
2326
|
+
// new", which is what a second run of the same goal asks for. Splitting it
|
|
2327
|
+
// naively would produce one item whose id is the empty string.
|
|
2328
|
+
const itemIds = rawItems === undefined ? undefined : rawItems === '' ? [] : rawItems.split(',');
|
|
2329
|
+
if (itemIds?.some((i) => i.length === 0)) {
|
|
2330
|
+
process.stderr.write('run: --item-ids has an empty id\n');
|
|
2331
|
+
return 1;
|
|
2332
|
+
}
|
|
2333
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
2334
|
+
// THE ADMISSION SEAM.
|
|
2335
|
+
//
|
|
2336
|
+
// Before the config is read, before the goal is resolved, before the lease is
|
|
2337
|
+
// taken, before a single agent call — this process states that it is the one
|
|
2338
|
+
// executing this attempt, and the filesystem decides whether it is.
|
|
2339
|
+
//
|
|
2340
|
+
// It is HERE, in the child, rather than in the daemon that spawned it,
|
|
2341
|
+
// because children are detached and outlive their parents. A claim written by
|
|
2342
|
+
// the daemon would record the identity of a process whose death proves
|
|
2343
|
+
// nothing about whether the run is still going; a claim written here records
|
|
2344
|
+
// the identity of the only process that can go on to do the work.
|
|
2345
|
+
//
|
|
2346
|
+
// The loser exits by its own door. It has decided nothing about the goal, so
|
|
2347
|
+
// it writes no refusal mark and no start mark — there is nothing to say.
|
|
2348
|
+
let autonomous = false;
|
|
2349
|
+
if (requestAttempt !== undefined && commandId !== undefined) {
|
|
2350
|
+
/**
|
|
2351
|
+
* 🔴 THE AUTHORITY TRAIL IS THE DAEMON'S AND THE PROJECT'S, NOT THIS
|
|
2352
|
+
* CHECKOUT'S — AND READING IT FROM THE WORKSPACE REFUSED EVERY PLACED RUN.
|
|
2353
|
+
*
|
|
2354
|
+
* This was `join(workspace ?? process.cwd(), STATE_DIR, 'state')`, which is
|
|
2355
|
+
* EXECUTION-scoped. Every file it looks for is written by the DAEMON: the
|
|
2356
|
+
* adopted controller, the validated ordinal reservation under it, and the
|
|
2357
|
+
* launch receipt for this attempt. The daemon writes them in its own
|
|
2358
|
+
* checkout; a run the coordinator placed in a workspace looked in the
|
|
2359
|
+
* workspace, found an empty directory, and exited with
|
|
2360
|
+
*
|
|
2361
|
+
* run: no grant on this machine reserved a Start by that name
|
|
2362
|
+
*
|
|
2363
|
+
* MEASURED end to end, and reachable no other way: every unit test on this
|
|
2364
|
+
* path runs in a single checkout, where the project and execution state
|
|
2365
|
+
* directories are THE SAME PATH, so the distinction is invisible. It only
|
|
2366
|
+
* appears the moment a run is placed somewhere else — which is the entire
|
|
2367
|
+
* point of a coordinator.
|
|
2368
|
+
*
|
|
2369
|
+
* `projectStateDir` resolves the repository's identity, so it answers the
|
|
2370
|
+
* same path from the main checkout and from any linked worktree of it. In
|
|
2371
|
+
* an ordinary single-checkout project that path is exactly what this line
|
|
2372
|
+
* produced before, so nothing about Simple Mode changes.
|
|
2373
|
+
*/
|
|
2374
|
+
const stateDir = (0, git_driver_1.projectStateDir)(workspace ?? process.cwd());
|
|
2375
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
2376
|
+
// AUTHORITY BEFORE EXCLUSIVITY.
|
|
2377
|
+
//
|
|
2378
|
+
// The exclusive claim below proves UNIQUENESS: one process owns this
|
|
2379
|
+
// attempt. It was being read as permission, and it is not. A certification
|
|
2380
|
+
// pass showed the gap in one line — invent a request id, claim attempt 1
|
|
2381
|
+
// of it uncontested, and the autonomous policy was granted, with no grant,
|
|
2382
|
+
// no controller and no ordinal anywhere in existence.
|
|
2383
|
+
//
|
|
2384
|
+
// So this asks first, and asks about durable facts the DAEMON wrote: an
|
|
2385
|
+
// adopted controller, a validated ordinal reservation under it, a request
|
|
2386
|
+
// id that pair derives, a launch receipt for this attempt, and a spawn
|
|
2387
|
+
// record that — if it has been written yet — describes this process.
|
|
2388
|
+
//
|
|
2389
|
+
// The child asks the server nothing. It does not need to: the daemon
|
|
2390
|
+
// reconciled with the server before it reserved anything, and every step
|
|
2391
|
+
// after that left a file. This is that trail, read back.
|
|
2392
|
+
const authority = await (0, autonomy_authority_1.checkAutonomyAuthority)({ stateDir, fs: start_attempts_1.nodeAttemptFs, pid: process.pid, host: (0, node_os_1.hostname)() }, commandId, requestAttempt);
|
|
2393
|
+
if (!authority.ok) {
|
|
2394
|
+
// NOT a refusal about the goal. Nothing here has looked at a goal, a
|
|
2395
|
+
// queue or a repository — this process simply was not asked for. It
|
|
2396
|
+
// leaves by the non-semantic door, so nothing downstream may conclude
|
|
2397
|
+
// anything about the work.
|
|
2398
|
+
process.stderr.write(`run: ${authority.because}\n`);
|
|
2399
|
+
return run_cmd_1.EXIT_ATTEMPT_CLAIMED;
|
|
2400
|
+
}
|
|
2401
|
+
const attempts = new start_attempts_1.StartAttempts(stateDir, start_attempts_1.nodeAttemptFs);
|
|
2402
|
+
const claimed = await attempts.claim(commandId, requestAttempt);
|
|
2403
|
+
if (!claimed.ok) {
|
|
2404
|
+
process.stderr.write(`run: ${claimed.because}\n`);
|
|
2405
|
+
return run_cmd_1.EXIT_ATTEMPT_CLAIMED;
|
|
2406
|
+
}
|
|
2407
|
+
// EARNED TWICE OVER. The daemon authorized this exact execution, and this
|
|
2408
|
+
// process is the one that got in.
|
|
2409
|
+
autonomous = announcedAutonomous;
|
|
2410
|
+
}
|
|
2411
|
+
/**
|
|
2412
|
+
* RESOLVED INSIDE THE WORKSPACE when there is one. The default
|
|
2413
|
+
* `.looply/config.yaml` is relative, and a run in another checkout must read
|
|
2414
|
+
* THAT checkout's config — otherwise two workspaces would run whichever gates
|
|
2415
|
+
* belonged to the directory the operator happened to be standing in. An
|
|
2416
|
+
* explicit absolute `--config` still wins, because `resolve` leaves it alone.
|
|
2417
|
+
*/
|
|
2418
|
+
const cfg = loadConfig(workspace === undefined ? configPath : (0, path_1.resolve)(workspace, configPath));
|
|
2419
|
+
for (const w of (0, config_1.configWarnings)(cfg))
|
|
2420
|
+
process.stdout.write(`warning : ${w}\n`);
|
|
2421
|
+
const log = (line) => {
|
|
2422
|
+
process.stdout.write(`${line}\n`);
|
|
2423
|
+
};
|
|
2424
|
+
let summary;
|
|
2425
|
+
try {
|
|
2426
|
+
summary = await (0, run_cmd_1.runLoop)({
|
|
2427
|
+
cwd: workspace ?? process.cwd(),
|
|
2428
|
+
config: cfg,
|
|
2429
|
+
/**
|
|
2430
|
+
* ⚠️ ASKED PER PICK, NOT ONCE. A sibling publishes while this run is
|
|
2431
|
+
* working, and an item blocked at round 3 should be pickable at round 9 —
|
|
2432
|
+
* a context captured at startup would keep it waiting all night.
|
|
2433
|
+
*/
|
|
2434
|
+
crossRepo: () => crossRepoContext(workspace ?? process.cwd(), cfg),
|
|
2435
|
+
...(hours !== undefined ? { hours } : {}),
|
|
2436
|
+
// `!== undefined`, like every field beside it. Under a truthiness test an
|
|
2437
|
+
// EMPTY `--command-id` was silently dropped, and a run with no request
|
|
2438
|
+
// identity writes no lease field, no start mark and no refusal mark — it
|
|
2439
|
+
// is invisible to whatever asked for it, for ever. Unreachable for a
|
|
2440
|
+
// server command, which always has an id; reachable the moment a
|
|
2441
|
+
// controller composes one.
|
|
2442
|
+
...(commandId !== undefined ? { commandId } : {}),
|
|
2443
|
+
...(autonomous ? { autonomous: true } : {}),
|
|
2444
|
+
...(goalId === undefined ? {} : { goalId }),
|
|
2445
|
+
...(goalVersion === undefined ? {} : { goalVersion }),
|
|
2446
|
+
...(itemIds === undefined ? {} : { itemIds }),
|
|
2447
|
+
...(resumedFromApprovalId === undefined ? {} : { resumedFromApprovalId }),
|
|
2448
|
+
...(governedApprovalId === undefined ? {} : { governedApprovalId }),
|
|
2449
|
+
dryRun: argv.includes('--dry-run'),
|
|
2450
|
+
/**
|
|
2451
|
+
* 🔴 THE ONLY WAY A PERSON AT A TERMINAL CAN CLEAR A SAFETY HOLD, and
|
|
2452
|
+
* until now there was none. A hold has no expiry by design; the one
|
|
2453
|
+
* retry it carries was spendable only by a paired phone's Start. A
|
|
2454
|
+
* `looply run --goal-id G --goal-version 1` that hit a hold was told to
|
|
2455
|
+
* "start it again", and starting it again hit the same fence for ever.
|
|
2456
|
+
*
|
|
2457
|
+
* ⚠️ IT SPENDS, IT DOES NOT DISABLE. Same exclusive create the daemon
|
|
2458
|
+
* uses, so the second invocation is refused rather than granted.
|
|
2459
|
+
*/
|
|
2460
|
+
tryOnceMore: argv.includes('--try-once-more'),
|
|
2461
|
+
log,
|
|
2462
|
+
});
|
|
2463
|
+
}
|
|
2464
|
+
catch (e) {
|
|
2465
|
+
// A refusal retrying cannot fix leaves by its own door. The daemon that
|
|
2466
|
+
// spawned this reads the exit code and stops offering the command, rather
|
|
2467
|
+
// than handing it back every few seconds for the rest of the redelivery
|
|
2468
|
+
// window — which is noise, not resilience. Everything else keeps exiting 1
|
|
2469
|
+
// and stays retryable.
|
|
2470
|
+
if (!(e instanceof run_cmd_1.RunRefused))
|
|
2471
|
+
throw e;
|
|
2472
|
+
// Two audiences, one decision. A person watching a terminal reads this;
|
|
2473
|
+
// the daemon that spawned this cannot, because it spawns with
|
|
2474
|
+
// `stdio: 'ignore'` on a detached child, so the words go nowhere. Both are
|
|
2475
|
+
// told, by the channel each can actually hear.
|
|
2476
|
+
process.stderr.write(`${e.message}
|
|
2477
|
+
`);
|
|
2478
|
+
// Only when something asked. A hand-typed `looply run` has no request to
|
|
2479
|
+
// answer, and writing a mark nobody will ever look up would make this file
|
|
2480
|
+
// a log of every refusal rather than an index of the outstanding one.
|
|
2481
|
+
//
|
|
2482
|
+
// `!== undefined`, matching the forwarding guard above: the two must agree
|
|
2483
|
+
// about what counts as a request, or a run could be attributed on the lease
|
|
2484
|
+
// and unattributed in its refusal. An empty id never reaches here anyway —
|
|
2485
|
+
// it is refused at the parse — and this says so rather than relying on it.
|
|
2486
|
+
//
|
|
2487
|
+
// ONLY the RunRefused message. Not the stack, not anything else this
|
|
2488
|
+
// process printed — `Command.reason` is read by a client, and a refusal is
|
|
2489
|
+
// a decision the machine chose to report, not whatever happened to be on
|
|
2490
|
+
// its way out.
|
|
2491
|
+
if (commandId !== undefined) {
|
|
2492
|
+
try {
|
|
2493
|
+
// The SAME expression the run process uses, not a relative one that
|
|
2494
|
+
// happens to agree: a mark written anywhere else is a mark nobody reads.
|
|
2495
|
+
//
|
|
2496
|
+
// 🔴 IT SAID `process.cwd()` AND MEANT IT, and `--in` made that false.
|
|
2497
|
+
// `runLoop` is given `cwd: workspace ?? process.cwd()` and the admission
|
|
2498
|
+
// seam claims its attempt under the same expression, so a run executing
|
|
2499
|
+
// in a workspace wrote its StartAttempts claim in one directory and its
|
|
2500
|
+
// refusal in another. The phone would then wait out the full retry
|
|
2501
|
+
// window for a reason that was already on disk, one directory over.
|
|
2502
|
+
//
|
|
2503
|
+
// ⚠️ RESIDUAL, STATED RATHER THAN PAPERED OVER: the DAEMON reads marks
|
|
2504
|
+
// from its own state directory. That still meets this one, because
|
|
2505
|
+
// nothing passes `--in` to a daemon-launched run — `--command-id` and
|
|
2506
|
+
// `--in` never appear together today. A later phase that launches into
|
|
2507
|
+
// workspaces must move this channel to project scope AND make it
|
|
2508
|
+
// append-only first: it is a whole-document rewrite, so sharing it
|
|
2509
|
+
// between concurrent runs without that is the lost-update class.
|
|
2510
|
+
/**
|
|
2511
|
+
* 🔴 PROJECT-SCOPED, AS THE COMMENT ABOVE PREDICTED IT WOULD HAVE TO
|
|
2512
|
+
* BE. "A later phase that launches into workspaces must move this
|
|
2513
|
+
* channel to project scope AND make it append-only first" — this is
|
|
2514
|
+
* that phase, and `RunRefusalMarks` now appends beside its index for
|
|
2515
|
+
* exactly the lost-update reason named there.
|
|
2516
|
+
*
|
|
2517
|
+
* The daemon reads this from the PROJECT's state, so a placed run
|
|
2518
|
+
* writing it beside its workspace left every refusal unreadable: the
|
|
2519
|
+
* launcher fell back to "the run exited — exit 3" with no code, and
|
|
2520
|
+
* nothing downstream could conclude anything about the goal.
|
|
2521
|
+
*/
|
|
2522
|
+
const marks = new run_refusal_marks_1.RunRefusalMarks((0, git_driver_1.projectStateDir)(workspace ?? process.cwd()), ledger_store_1.nodeLedgerFs);
|
|
2523
|
+
await marks.load();
|
|
2524
|
+
// The CODE as well as the words. `e` is a `RunRefused` — the line above
|
|
2525
|
+
// rethrows anything else — so its code is in scope and typed here, and
|
|
2526
|
+
// until 6D2A it was simply dropped on the floor at this one line.
|
|
2527
|
+
await marks.record({
|
|
2528
|
+
commandId,
|
|
2529
|
+
reason: e.message,
|
|
2530
|
+
code: e.code,
|
|
2531
|
+
// M5-2: and the branch, when the run had one — the lane's key to the grant.
|
|
2532
|
+
...(e.branch === undefined ? {} : { branch: e.branch }),
|
|
2533
|
+
// L4b: and what key 2 confirmed, on a met goal -- the facts the settle carries.
|
|
2534
|
+
...(e.delivery === undefined ? {} : { delivery: e.delivery }),
|
|
2535
|
+
at: new Date().toISOString(),
|
|
2536
|
+
});
|
|
2537
|
+
}
|
|
2538
|
+
catch {
|
|
2539
|
+
// Never fatal, and never changes the answer. Failing to write the
|
|
2540
|
+
// reason costs the phone a sentence; failing to exit 3 would cost it
|
|
2541
|
+
// the refusal itself, and be retried every three seconds for an hour.
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
return run_cmd_1.EXIT_REFUSED;
|
|
2545
|
+
}
|
|
2546
|
+
const { rounds, committed, discarded, abandoned } = summary.totals;
|
|
2547
|
+
log('');
|
|
2548
|
+
log(`stopped : ${summary.stopReason}`);
|
|
2549
|
+
log(`rounds : ${rounds} — ${committed} kept, ${discarded} discarded, ${abandoned} abandoned`);
|
|
2550
|
+
// A run that never discarded anything is not a triumph, it is a signal: the
|
|
2551
|
+
// prover is the only thing that can fail a round on quality, and one that
|
|
2552
|
+
// never fires is one that is not working.
|
|
2553
|
+
if (rounds >= 10 && discarded === 0 && cfg.proof.enabled) {
|
|
2554
|
+
log('');
|
|
2555
|
+
log('note : nothing was discarded across the whole run. A prover that never');
|
|
2556
|
+
log(' rejects is usually misconfigured rather than lucky — check it.');
|
|
2557
|
+
}
|
|
2558
|
+
return 0;
|
|
2559
|
+
}
|
|
2560
|
+
/**
|
|
2561
|
+
* Read enough of the world to render the welcome or the agent snapshot.
|
|
2562
|
+
*
|
|
2563
|
+
* Every read is optional. A folder with no looply in it must still get a useful
|
|
2564
|
+
* screen, so a missing file is a fact about the state rather than an error.
|
|
2565
|
+
*/
|
|
2566
|
+
function readWorld() {
|
|
2567
|
+
let config = null;
|
|
2568
|
+
try {
|
|
2569
|
+
config = (0, config_1.parseConfig)((0, fs_1.readFileSync)((0, path_1.resolve)('.looply/config.yaml'), 'utf8'));
|
|
2570
|
+
}
|
|
2571
|
+
catch {
|
|
2572
|
+
config = null;
|
|
2573
|
+
}
|
|
2574
|
+
let status = null;
|
|
2575
|
+
try {
|
|
2576
|
+
const parsed = JSON.parse((0, fs_1.readFileSync)((0, path_1.resolve)('.looply/state/status.json'), 'utf8'));
|
|
2577
|
+
status = parsed;
|
|
2578
|
+
}
|
|
2579
|
+
catch {
|
|
2580
|
+
status = null;
|
|
2581
|
+
}
|
|
2582
|
+
let items = [];
|
|
2583
|
+
try {
|
|
2584
|
+
const parsed = JSON.parse((0, fs_1.readFileSync)((0, path_1.resolve)('.looply/state/queue.json'), 'utf8'));
|
|
2585
|
+
if (Array.isArray(parsed))
|
|
2586
|
+
items = parsed;
|
|
2587
|
+
else if (parsed && typeof parsed === 'object' && Array.isArray(parsed.items)) {
|
|
2588
|
+
items = parsed.items;
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
catch {
|
|
2592
|
+
items = [];
|
|
2593
|
+
}
|
|
2594
|
+
let machine = { paired: false, id: null, serverUrl: null };
|
|
2595
|
+
try {
|
|
2596
|
+
const identity = new machine_store_1.MachineStore((0, machine_store_1.machineFilePath)()).load();
|
|
2597
|
+
if (identity)
|
|
2598
|
+
machine = { paired: true, id: identity.machineId, serverUrl: identity.serverUrl };
|
|
2599
|
+
}
|
|
2600
|
+
catch {
|
|
2601
|
+
// A corrupt identity file is reported by `pair` and `daemon`, which can act
|
|
2602
|
+
// on it. Here it just means "not paired" for display purposes.
|
|
2603
|
+
}
|
|
2604
|
+
return { config, status, items, machine };
|
|
2605
|
+
}
|
|
2606
|
+
/** The bare `looply` screen: where you are, what you can do, what to do next. */
|
|
2607
|
+
function welcome() {
|
|
2608
|
+
const world = readWorld();
|
|
2609
|
+
const status = world.status;
|
|
2610
|
+
process.stdout.write((0, welcome_1.renderWelcome)({
|
|
2611
|
+
paired: world.machine.paired,
|
|
2612
|
+
...(world.machine.id ? { machineName: world.machine.id } : {}),
|
|
2613
|
+
...(world.machine.serverUrl ? { serverUrl: world.machine.serverUrl } : {}),
|
|
2614
|
+
configured: world.config !== null,
|
|
2615
|
+
...(world.config ? { projectName: world.config.project.name } : {}),
|
|
2616
|
+
readyItems: world.items.filter((i) => i.state === 'ready').length,
|
|
2617
|
+
draftItems: world.items.filter((i) => i.state === 'draft' || i.state === 'verifying').length,
|
|
2618
|
+
runLive: status?.live === true,
|
|
2619
|
+
currentItem: status?.round?.title ?? null,
|
|
2620
|
+
}, daemon_cmd_1.CLI_VERSION));
|
|
2621
|
+
return 0;
|
|
2622
|
+
}
|
|
2623
|
+
/**
|
|
2624
|
+
* `looply agent` — state for another program.
|
|
2625
|
+
*
|
|
2626
|
+
* Exits 0 whenever it could answer, including "nothing is running". A non-zero
|
|
2627
|
+
* exit means the question could not be answered, never that the answer was dull.
|
|
2628
|
+
*/
|
|
2629
|
+
function agent(argv) {
|
|
2630
|
+
const world = readWorld();
|
|
2631
|
+
const snapshot = (0, agent_cmd_1.buildSnapshot)({
|
|
2632
|
+
cliVersion: daemon_cmd_1.CLI_VERSION,
|
|
2633
|
+
config: world.config,
|
|
2634
|
+
status: world.status,
|
|
2635
|
+
items: world.items,
|
|
2636
|
+
machine: world.machine,
|
|
2637
|
+
now: Date.now(),
|
|
2638
|
+
});
|
|
2639
|
+
const json = argv.includes('--json') || !argv.includes('--text');
|
|
2640
|
+
process.stdout.write(json ? (0, agent_cmd_1.renderAgentJson)(snapshot) : (0, agent_cmd_1.renderAgentText)(snapshot));
|
|
2641
|
+
return 0;
|
|
2642
|
+
}
|
|
2643
|
+
async function main(argv) {
|
|
2644
|
+
// Honoured before anything prints, so a piped or redirected run is clean.
|
|
2645
|
+
if (argv.includes('--no-color'))
|
|
2646
|
+
(0, ui_1.setColor)(false);
|
|
2647
|
+
const [cmd, ...rest] = argv.filter((a) => a !== '--no-color');
|
|
2648
|
+
switch (cmd) {
|
|
2649
|
+
case 'init':
|
|
2650
|
+
return init(rest);
|
|
2651
|
+
case 'doctor':
|
|
2652
|
+
return doctor(rest);
|
|
2653
|
+
case 'pair':
|
|
2654
|
+
return pair(rest);
|
|
2655
|
+
case 'daemon':
|
|
2656
|
+
return daemon(rest);
|
|
2657
|
+
case 'scan':
|
|
2658
|
+
return scan(rest);
|
|
2659
|
+
case 'verify':
|
|
2660
|
+
return verify(rest);
|
|
2661
|
+
case 'queue':
|
|
2662
|
+
return queue(rest);
|
|
2663
|
+
case 'integrate':
|
|
2664
|
+
return integrate(rest);
|
|
2665
|
+
case 'runs':
|
|
2666
|
+
return runs(rest);
|
|
2667
|
+
case 'source':
|
|
2668
|
+
return (0, source_cmd_1.sourceCommand)(rest, (0, source_cmd_1.nodeSourceDeps)());
|
|
2669
|
+
case 'stop':
|
|
2670
|
+
return stop(rest);
|
|
2671
|
+
case 'pause':
|
|
2672
|
+
return pauseOrResume('pause', rest);
|
|
2673
|
+
case 'resume':
|
|
2674
|
+
return pauseOrResume('resume', rest);
|
|
2675
|
+
case 'run':
|
|
2676
|
+
return run(rest);
|
|
2677
|
+
case 'status':
|
|
2678
|
+
return status();
|
|
2679
|
+
case 'report':
|
|
2680
|
+
return report();
|
|
2681
|
+
case 'goal':
|
|
2682
|
+
return goal(rest);
|
|
2683
|
+
case 'goals':
|
|
2684
|
+
return goals();
|
|
2685
|
+
case 'repos':
|
|
2686
|
+
return (0, repos_cmd_1.reposCommand)(rest, nodeReposDeps());
|
|
2687
|
+
case 'sessions':
|
|
2688
|
+
return (0, sessions_cmd_1.sessionsCommand)(rest, nodeSessionsDeps());
|
|
2689
|
+
case 'evidence':
|
|
2690
|
+
return (0, evidence_cmd_1.evidenceCommand)(rest, nodeEvidenceDeps());
|
|
2691
|
+
case 'version':
|
|
2692
|
+
case '--version':
|
|
2693
|
+
case '-v':
|
|
2694
|
+
process.stdout.write('0.1.0\n');
|
|
2695
|
+
return 0;
|
|
2696
|
+
case undefined:
|
|
2697
|
+
return welcome();
|
|
2698
|
+
case 'agent':
|
|
2699
|
+
return agent(rest);
|
|
2700
|
+
case 'help':
|
|
2701
|
+
case '--help':
|
|
2702
|
+
case '-h':
|
|
2703
|
+
process.stdout.write(USAGE);
|
|
2704
|
+
return 0;
|
|
2705
|
+
default:
|
|
2706
|
+
process.stderr.write(`looply: unknown command ${JSON.stringify(cmd)}\n\n${USAGE}`);
|
|
2707
|
+
return 64;
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
/* istanbul ignore next — process wiring, exercised by running the binary */
|
|
2711
|
+
if (require.main === module) {
|
|
2712
|
+
main(process.argv.slice(2)).then((code) => {
|
|
2713
|
+
process.exitCode = code;
|
|
2714
|
+
}, (e) => {
|
|
2715
|
+
process.stderr.write(`${e.message}\n`);
|
|
2716
|
+
process.exitCode = 1;
|
|
2717
|
+
});
|
|
2718
|
+
}
|
|
2719
|
+
//# sourceMappingURL=bin.js.map
|