@quickchatai/quickreview 0.0.0-stage → 1.1.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 +23 -0
- package/README.md +385 -2
- package/THIRD-PARTY-NOTICES.md +134 -0
- package/agents/pass-abstraction.md +64 -0
- package/agents/pass-ask-human.md +134 -0
- package/agents/pass-backcompat.md +67 -0
- package/agents/pass-baseline.md +61 -0
- package/agents/pass-behaviour-delta.md +64 -0
- package/agents/pass-bounds.md +36 -0
- package/agents/pass-claims.md +31 -0
- package/agents/pass-concurrency.md +168 -0
- package/agents/pass-consistency.md +57 -0
- package/agents/pass-contracts.md +30 -0
- package/agents/pass-data-safety.md +54 -0
- package/agents/pass-deferred.md +35 -0
- package/agents/pass-delivery.md +37 -0
- package/agents/pass-duplication.md +60 -0
- package/agents/pass-intent.md +63 -0
- package/agents/pass-lint-conformance.md +46 -0
- package/agents/pass-maintainability.md +67 -0
- package/agents/pass-placement.md +68 -0
- package/agents/pass-prod-risk.md +24 -0
- package/agents/pass-rollout.md +32 -0
- package/agents/pass-roundtrip.md +46 -0
- package/agents/pass-security.md +31 -0
- package/agents/pass-simplicity.md +68 -0
- package/agents/pass-statelessness.md +27 -0
- package/agents/pass-style.md +37 -0
- package/agents/pass-sweep.md +57 -0
- package/agents/pass-tests.md +58 -0
- package/agents/pass-ui.md +23 -0
- package/agents/pass-validate.md +50 -0
- package/agents/pass-widened.md +46 -0
- package/agents/scan/pass-scan.md +86 -0
- package/bin/qcr.js +11 -0
- package/config.example.toml +1067 -0
- package/deploy/ansible/README.md +356 -0
- package/deploy/ansible/group_vars/all.yml +212 -0
- package/deploy/ansible/inventory.example.ini +25 -0
- package/deploy/ansible/playbook.yml +360 -0
- package/deploy/collect-secrets.sh +289 -0
- package/deploy/collect-slack-secrets.mjs +1190 -0
- package/deploy/github-app-manifest.json +31 -0
- package/deploy/manifest.schema.json +75 -0
- package/deploy/qcr-update.sh +803 -0
- package/deploy/reviewer/AGENTS.override.md +12 -0
- package/dist/extensions/cli/bash-guard-ambient.js +42 -0
- package/dist/extensions/cli/bash-guard-ambient.js.map +1 -0
- package/dist/extensions/cli/bash-guard.js +5 -0
- package/dist/extensions/cli/bash-guard.js.map +1 -0
- package/dist/extensions/cli/companion-guard.js +5 -0
- package/dist/extensions/cli/companion-guard.js.map +1 -0
- package/dist/extensions/cli/debug-round.js +8 -0
- package/dist/extensions/cli/debug-round.js.map +1 -0
- package/dist/extensions/cli/inject-bridge.js +5 -0
- package/dist/extensions/cli/inject-bridge.js.map +1 -0
- package/dist/extensions/cli/ledger-pin.js +5 -0
- package/dist/extensions/cli/ledger-pin.js.map +1 -0
- package/dist/extensions/cli/lens.js +13 -0
- package/dist/extensions/cli/lens.js.map +1 -0
- package/dist/extensions/cli/muted-theme.js +5 -0
- package/dist/extensions/cli/muted-theme.js.map +1 -0
- package/dist/extensions/cli/onboarding.js +5 -0
- package/dist/extensions/cli/onboarding.js.map +1 -0
- package/dist/extensions/cli/setup.js +5 -0
- package/dist/extensions/cli/setup.js.map +1 -0
- package/dist/extensions/cli/tui-view.js +5 -0
- package/dist/extensions/cli/tui-view.js.map +1 -0
- package/dist/extensions/companion-guard.js +361 -0
- package/dist/extensions/companion-guard.js.map +1 -0
- package/dist/extensions/debug-round.js +204 -0
- package/dist/extensions/debug-round.js.map +1 -0
- package/dist/extensions/inject-bridge.js +603 -0
- package/dist/extensions/inject-bridge.js.map +1 -0
- package/dist/extensions/muted-theme.js +12 -0
- package/dist/extensions/muted-theme.js.map +1 -0
- package/dist/extensions/onboarding.js +175 -0
- package/dist/extensions/onboarding.js.map +1 -0
- package/dist/extensions/setup-replay.js +269 -0
- package/dist/extensions/setup-replay.js.map +1 -0
- package/dist/extensions/setup-scan.js +998 -0
- package/dist/extensions/setup-scan.js.map +1 -0
- package/dist/extensions/setup-triage.js +175 -0
- package/dist/extensions/setup-triage.js.map +1 -0
- package/dist/extensions/setup.js +1160 -0
- package/dist/extensions/setup.js.map +1 -0
- package/dist/extensions/tui-view.js +389 -0
- package/dist/extensions/tui-view.js.map +1 -0
- package/dist/extensions/vendor/openviking-pi/client.js +376 -0
- package/dist/extensions/vendor/openviking-pi/client.js.map +1 -0
- package/dist/extensions/vendor/openviking-pi/config.js +189 -0
- package/dist/extensions/vendor/openviking-pi/config.js.map +1 -0
- package/dist/extensions/vendor/openviking-pi/index.js +421 -0
- package/dist/extensions/vendor/openviking-pi/index.js.map +1 -0
- package/dist/extensions/vendor/openviking-pi/lib/capture-adapter.mjs +86 -0
- package/dist/extensions/vendor/openviking-pi/lib/capture-adapter.mjs.map +1 -0
- package/dist/extensions/vendor/openviking-pi/lib/takeover-core.mjs +408 -0
- package/dist/extensions/vendor/openviking-pi/lib/takeover-core.mjs.map +1 -0
- package/dist/extensions/vendor/openviking-pi/lib/uri-guard-adapter.mjs +41 -0
- package/dist/extensions/vendor/openviking-pi/lib/uri-guard-adapter.mjs.map +1 -0
- package/dist/extensions/vendor/openviking-pi/qcr-hint.js +290 -0
- package/dist/extensions/vendor/openviking-pi/qcr-hint.js.map +1 -0
- package/dist/extensions/vendor/openviking-pi/qcr-honest.js +148 -0
- package/dist/extensions/vendor/openviking-pi/qcr-honest.js.map +1 -0
- package/dist/extensions/vendor/openviking-pi/qcr-hooks.js +68 -0
- package/dist/extensions/vendor/openviking-pi/qcr-hooks.js.map +1 -0
- package/dist/extensions/vendor/openviking-pi/qcr-leaf.js +77 -0
- package/dist/extensions/vendor/openviking-pi/qcr-leaf.js.map +1 -0
- package/dist/extensions/vendor/openviking-pi/qcr-lesson.js +216 -0
- package/dist/extensions/vendor/openviking-pi/qcr-lesson.js.map +1 -0
- package/dist/extensions/vendor/openviking-pi/qcr-namespaces.js +154 -0
- package/dist/extensions/vendor/openviking-pi/qcr-namespaces.js.map +1 -0
- package/dist/extensions/vendor/openviking-pi/qcr-recall.js +254 -0
- package/dist/extensions/vendor/openviking-pi/qcr-recall.js.map +1 -0
- package/dist/extensions/vendor/openviking-pi/qcr-shim.js +94 -0
- package/dist/extensions/vendor/openviking-pi/qcr-shim.js.map +1 -0
- package/dist/extensions/vendor/openviking-pi/qcr-write.js +333 -0
- package/dist/extensions/vendor/openviking-pi/qcr-write.js.map +1 -0
- package/dist/extensions/vendor/openviking-pi/recall.js +108 -0
- package/dist/extensions/vendor/openviking-pi/recall.js.map +1 -0
- package/dist/extensions/vendor/openviking-pi/shared/capture-utils.mjs +529 -0
- package/dist/extensions/vendor/openviking-pi/shared/capture-utils.mjs.map +1 -0
- package/dist/extensions/vendor/openviking-pi/shared/credentials.mjs +205 -0
- package/dist/extensions/vendor/openviking-pi/shared/credentials.mjs.map +1 -0
- package/dist/extensions/vendor/openviking-pi/shared/pending-queue.mjs +442 -0
- package/dist/extensions/vendor/openviking-pi/shared/pending-queue.mjs.map +1 -0
- package/dist/extensions/vendor/openviking-pi/shared/profile-inject.mjs +286 -0
- package/dist/extensions/vendor/openviking-pi/shared/profile-inject.mjs.map +1 -0
- package/dist/extensions/vendor/openviking-pi/shared/recall-compress-core.mjs +189 -0
- package/dist/extensions/vendor/openviking-pi/shared/recall-compress-core.mjs.map +1 -0
- package/dist/extensions/vendor/openviking-pi/shared/recall-core.mjs +580 -0
- package/dist/extensions/vendor/openviking-pi/shared/recall-core.mjs.map +1 -0
- package/dist/extensions/vendor/openviking-pi/shared/retryable.mjs +11 -0
- package/dist/extensions/vendor/openviking-pi/shared/retryable.mjs.map +1 -0
- package/dist/extensions/vendor/openviking-pi/shared/session-model.mjs +63 -0
- package/dist/extensions/vendor/openviking-pi/shared/session-model.mjs.map +1 -0
- package/dist/extensions/vendor/openviking-pi/shared/uri-guard.mjs +58 -0
- package/dist/extensions/vendor/openviking-pi/shared/uri-guard.mjs.map +1 -0
- package/dist/extensions/vendor/openviking-pi/shared/workspace-peer.mjs +16 -0
- package/dist/extensions/vendor/openviking-pi/shared/workspace-peer.mjs.map +1 -0
- package/dist/extensions/vendor/openviking-pi/sync.js +127 -0
- package/dist/extensions/vendor/openviking-pi/sync.js.map +1 -0
- package/dist/extensions/vendor/openviking-pi/takeover.js +25 -0
- package/dist/extensions/vendor/openviking-pi/takeover.js.map +1 -0
- package/dist/extensions/vendor/openviking-pi/tools.js +417 -0
- package/dist/extensions/vendor/openviking-pi/tools.js.map +1 -0
- package/dist/src/anomalies.js +483 -0
- package/dist/src/anomalies.js.map +1 -0
- package/dist/src/answerworker.js +180 -0
- package/dist/src/answerworker.js.map +1 -0
- package/dist/src/appstamp.js +68 -0
- package/dist/src/appstamp.js.map +1 -0
- package/dist/src/archive/admission.js +3 -0
- package/dist/src/archive/admission.js.map +1 -0
- package/dist/src/archive/archivedb.js +474 -0
- package/dist/src/archive/archivedb.js.map +1 -0
- package/dist/src/archive/archiver.js +1770 -0
- package/dist/src/archive/archiver.js.map +1 -0
- package/dist/src/archive/archivewrite.js +198 -0
- package/dist/src/archive/archivewrite.js.map +1 -0
- package/dist/src/archive/bundle.js +266 -0
- package/dist/src/archive/bundle.js.map +1 -0
- package/dist/src/archive/dbmove.js +638 -0
- package/dist/src/archive/dbmove.js.map +1 -0
- package/dist/src/archive/duplicatecounts.js +80 -0
- package/dist/src/archive/duplicatecounts.js.map +1 -0
- package/dist/src/archive/eligibility.js +113 -0
- package/dist/src/archive/eligibility.js.map +1 -0
- package/dist/src/archive/extract.js +288 -0
- package/dist/src/archive/extract.js.map +1 -0
- package/dist/src/archive/fsutil.js +330 -0
- package/dist/src/archive/fsutil.js.map +1 -0
- package/dist/src/archive/gzfile.js +277 -0
- package/dist/src/archive/gzfile.js.map +1 -0
- package/dist/src/archive/kinds.js +22 -0
- package/dist/src/archive/kinds.js.map +1 -0
- package/dist/src/archive/livecheckpoint.js +178 -0
- package/dist/src/archive/livecheckpoint.js.map +1 -0
- package/dist/src/archive/liveretention.js +145 -0
- package/dist/src/archive/liveretention.js.map +1 -0
- package/dist/src/archive/manifest.js +284 -0
- package/dist/src/archive/manifest.js.map +1 -0
- package/dist/src/archive/overseer.js +283 -0
- package/dist/src/archive/overseer.js.map +1 -0
- package/dist/src/archive/piglobal.js +129 -0
- package/dist/src/archive/piglobal.js.map +1 -0
- package/dist/src/archive/plan.js +398 -0
- package/dist/src/archive/plan.js.map +1 -0
- package/dist/src/archive/prarchive.js +831 -0
- package/dist/src/archive/prarchive.js.map +1 -0
- package/dist/src/archive/prfiles.js +303 -0
- package/dist/src/archive/prfiles.js.map +1 -0
- package/dist/src/archive/report.js +169 -0
- package/dist/src/archive/report.js.map +1 -0
- package/dist/src/archive/restore.js +276 -0
- package/dist/src/archive/restore.js.map +1 -0
- package/dist/src/archive/rows.js +106 -0
- package/dist/src/archive/rows.js.map +1 -0
- package/dist/src/archive/seal.js +480 -0
- package/dist/src/archive/seal.js.map +1 -0
- package/dist/src/archive/status.js +528 -0
- package/dist/src/archive/status.js.map +1 -0
- package/dist/src/archive/storepass.js +409 -0
- package/dist/src/archive/storepass.js.map +1 -0
- package/dist/src/backfill.js +217 -0
- package/dist/src/backfill.js.map +1 -0
- package/dist/src/bench/cost.js +87 -0
- package/dist/src/bench/exec.js +76 -0
- package/dist/src/bench/values.js +14 -0
- package/dist/src/botcontrol.js +127 -0
- package/dist/src/botcontrol.js.map +1 -0
- package/dist/src/botworker.js +805 -0
- package/dist/src/botworker.js.map +1 -0
- package/dist/src/capture.js +234 -0
- package/dist/src/capture.js.map +1 -0
- package/dist/src/cases.js +1491 -0
- package/dist/src/cases.js.map +1 -0
- package/dist/src/channelfs.js +27 -0
- package/dist/src/channelfs.js.map +1 -0
- package/dist/src/checklist.js +312 -0
- package/dist/src/checklist.js.map +1 -0
- package/dist/src/ciretry.js +91 -0
- package/dist/src/ciretry.js.map +1 -0
- package/dist/src/cli.js +1916 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/cliops.js +333 -0
- package/dist/src/cliops.js.map +1 -0
- package/dist/src/clonedir.js +15 -0
- package/dist/src/clonedir.js.map +1 -0
- package/dist/src/commandeffects.js +43 -0
- package/dist/src/commandeffects.js.map +1 -0
- package/dist/src/commands.js +1312 -0
- package/dist/src/commands.js.map +1 -0
- package/dist/src/companioninbox.js +203 -0
- package/dist/src/companioninbox.js.map +1 -0
- package/dist/src/compare.js +673 -0
- package/dist/src/compare.js.map +1 -0
- package/dist/src/config.js +1027 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/configcheck.js +111 -0
- package/dist/src/configcheck.js.map +1 -0
- package/dist/src/coreext.js +43 -0
- package/dist/src/coreext.js.map +1 -0
- package/dist/src/daemon.js +929 -0
- package/dist/src/daemon.js.map +1 -0
- package/dist/src/debuground.js +731 -0
- package/dist/src/debuground.js.map +1 -0
- package/dist/src/debugroundpi.js +219 -0
- package/dist/src/debugroundpi.js.map +1 -0
- package/dist/src/diagnostics.js +14 -0
- package/dist/src/diagnostics.js.map +1 -0
- package/dist/src/digest.js +139 -0
- package/dist/src/digest.js.map +1 -0
- package/dist/src/dispatch.js +1726 -0
- package/dist/src/dispatch.js.map +1 -0
- package/dist/src/engine/answerguard.js +18 -0
- package/dist/src/engine/answerguard.js.map +1 -0
- package/dist/src/engine/botsdk.js +262 -0
- package/dist/src/engine/botsdk.js.map +1 -0
- package/dist/src/engine/caserunner-sdk.js +332 -0
- package/dist/src/engine/caserunner-sdk.js.map +1 -0
- package/dist/src/engine/debugpi.js +194 -0
- package/dist/src/engine/debugpi.js.map +1 -0
- package/dist/src/eod.js +473 -0
- package/dist/src/eod.js.map +1 -0
- package/dist/src/eodpinwatch.js +391 -0
- package/dist/src/eodpinwatch.js.map +1 -0
- package/dist/src/escalation.js +1009 -0
- package/dist/src/escalation.js.map +1 -0
- package/dist/src/eventloop.js +779 -0
- package/dist/src/eventloop.js.map +1 -0
- package/dist/src/execboundary.js +430 -0
- package/dist/src/execboundary.js.map +1 -0
- package/dist/src/feedback.js +915 -0
- package/dist/src/feedback.js.map +1 -0
- package/dist/src/findingscheck.js +200 -0
- package/dist/src/findingscheck.js.map +1 -0
- package/dist/src/fsdenied.js +26 -0
- package/dist/src/fsdenied.js.map +1 -0
- package/dist/src/ghread.js +181 -0
- package/dist/src/ghread.js.map +1 -0
- package/dist/src/github.js +1063 -0
- package/dist/src/github.js.map +1 -0
- package/dist/src/homes.js +103 -0
- package/dist/src/homes.js.map +1 -0
- package/dist/src/hostroute.js +24 -0
- package/dist/src/hostroute.js.map +1 -0
- package/dist/src/improvements.js +625 -0
- package/dist/src/improvements.js.map +1 -0
- package/dist/src/install.js +946 -0
- package/dist/src/install.js.map +1 -0
- package/dist/src/installkind.js +40 -0
- package/dist/src/installkind.js.map +1 -0
- package/dist/src/ipc/capability.js +286 -0
- package/dist/src/ipc/capability.js.map +1 -0
- package/dist/src/ipc/client.js +226 -0
- package/dist/src/ipc/client.js.map +1 -0
- package/dist/src/ipc/control.js +70 -0
- package/dist/src/ipc/control.js.map +1 -0
- package/dist/src/ipc/ghreadshim.js +77 -0
- package/dist/src/ipc/ghreadshim.js.map +1 -0
- package/dist/src/ipc/launcher.js +65 -0
- package/dist/src/ipc/launcher.js.map +1 -0
- package/dist/src/ipc/permits.js +293 -0
- package/dist/src/ipc/permits.js.map +1 -0
- package/dist/src/ipc/postshim.js +137 -0
- package/dist/src/ipc/postshim.js.map +1 -0
- package/dist/src/ipc/protocol.js +239 -0
- package/dist/src/ipc/protocol.js.map +1 -0
- package/dist/src/ipc/server.js +312 -0
- package/dist/src/ipc/server.js.map +1 -0
- package/dist/src/ipc/workerspec.js +148 -0
- package/dist/src/ipc/workerspec.js.map +1 -0
- package/dist/src/learner/learner.js +1272 -0
- package/dist/src/learner/learner.js.map +1 -0
- package/dist/src/learner/miner.js +948 -0
- package/dist/src/learner/miner.js.map +1 -0
- package/dist/src/learner/packet.js +216 -0
- package/dist/src/learner/packet.js.map +1 -0
- package/dist/src/learner/receipt.js +135 -0
- package/dist/src/learner/receipt.js.map +1 -0
- package/dist/src/learner/status.js +38 -0
- package/dist/src/learner/status.js.map +1 -0
- package/dist/src/ledgercheck.js +120 -0
- package/dist/src/ledgercheck.js.map +1 -0
- package/dist/src/lessonledger.js +451 -0
- package/dist/src/lessonledger.js.map +1 -0
- package/dist/src/lessonselect.js +639 -0
- package/dist/src/lessonselect.js.map +1 -0
- package/dist/src/lessonshadow.js +540 -0
- package/dist/src/lessonshadow.js.map +1 -0
- package/dist/src/levers.js +302 -0
- package/dist/src/levers.js.map +1 -0
- package/dist/src/live/projection.js +450 -0
- package/dist/src/live/projection.js.map +1 -0
- package/dist/src/live/publish.js +83 -0
- package/dist/src/live/publish.js.map +1 -0
- package/dist/src/live/read.js +60 -0
- package/dist/src/live/read.js.map +1 -0
- package/dist/src/livecli.js +288 -0
- package/dist/src/livecli.js.map +1 -0
- package/dist/src/loginhint.js +42 -0
- package/dist/src/loginhint.js.map +1 -0
- package/dist/src/loginrefresh/atomic.js +66 -0
- package/dist/src/loginrefresh/atomic.js.map +1 -0
- package/dist/src/loginrefresh/lock.js +93 -0
- package/dist/src/loginrefresh/lock.js.map +1 -0
- package/dist/src/loginrefresh/main.js +58 -0
- package/dist/src/loginrefresh/main.js.map +1 -0
- package/dist/src/loginrefresh/pirefresh.js +49 -0
- package/dist/src/loginrefresh/pirefresh.js.map +1 -0
- package/dist/src/loginrefresh/refresher.js +286 -0
- package/dist/src/loginrefresh/refresher.js.map +1 -0
- package/dist/src/loginrefresh/supervisor.js +253 -0
- package/dist/src/loginrefresh/supervisor.js.map +1 -0
- package/dist/src/loginrefresh/types.js +4 -0
- package/dist/src/loginrefresh/types.js.map +1 -0
- package/dist/src/memory/context.js +251 -0
- package/dist/src/memory/context.js.map +1 -0
- package/dist/src/memory/datafence.js +5 -0
- package/dist/src/memory/datafence.js.map +1 -0
- package/dist/src/memory/delivery.js +131 -0
- package/dist/src/memory/delivery.js.map +1 -0
- package/dist/src/memory/eventtags.js +81 -0
- package/dist/src/memory/eventtags.js.map +1 -0
- package/dist/src/memory/health.js +713 -0
- package/dist/src/memory/health.js.map +1 -0
- package/dist/src/memory/import.js +504 -0
- package/dist/src/memory/import.js.map +1 -0
- package/dist/src/memory/layout.js +252 -0
- package/dist/src/memory/layout.js.map +1 -0
- package/dist/src/memory/legacy-file.js +100 -0
- package/dist/src/memory/legacy-file.js.map +1 -0
- package/dist/src/memory/lessons.js +370 -0
- package/dist/src/memory/lessons.js.map +1 -0
- package/dist/src/memory/ovsession.js +449 -0
- package/dist/src/memory/ovsession.js.map +1 -0
- package/dist/src/memory/relayout.js +259 -0
- package/dist/src/memory/relayout.js.map +1 -0
- package/dist/src/memory/roundactivity.js +180 -0
- package/dist/src/memory/roundactivity.js.map +1 -0
- package/dist/src/memory/selection.js +690 -0
- package/dist/src/memory/selection.js.map +1 -0
- package/dist/src/memory/shadowstatus.js +69 -0
- package/dist/src/memory/shadowstatus.js.map +1 -0
- package/dist/src/memory/store.js +83 -0
- package/dist/src/memory/store.js.map +1 -0
- package/dist/src/memory/textidentity.js +8 -0
- package/dist/src/memory/textidentity.js.map +1 -0
- package/dist/src/metrics.js +773 -0
- package/dist/src/metrics.js.map +1 -0
- package/dist/src/missingconfig.js +8 -0
- package/dist/src/missingconfig.js.map +1 -0
- package/dist/src/npmverbs.js +226 -0
- package/dist/src/npmverbs.js.map +1 -0
- package/dist/src/onboard/choices.js +191 -0
- package/dist/src/onboard/choices.js.map +1 -0
- package/dist/src/onboard/engine.js +344 -0
- package/dist/src/onboard/engine.js.map +1 -0
- package/dist/src/onboard/launch.js +83 -0
- package/dist/src/onboard/launch.js.map +1 -0
- package/dist/src/onboard/steps/appslug.js +211 -0
- package/dist/src/onboard/steps/appslug.js.map +1 -0
- package/dist/src/onboard/steps/bringup.js +408 -0
- package/dist/src/onboard/steps/bringup.js.map +1 -0
- package/dist/src/onboard/steps/compose.js +343 -0
- package/dist/src/onboard/steps/compose.js.map +1 -0
- package/dist/src/onboard/steps/configdelivery.js +50 -0
- package/dist/src/onboard/steps/configdelivery.js.map +1 -0
- package/dist/src/onboard/steps/configfill.js +1148 -0
- package/dist/src/onboard/steps/configfill.js.map +1 -0
- package/dist/src/onboard/steps/deploysh.js +80 -0
- package/dist/src/onboard/steps/deploysh.js.map +1 -0
- package/dist/src/onboard/steps/embeddings.js +472 -0
- package/dist/src/onboard/steps/embeddings.js.map +1 -0
- package/dist/src/onboard/steps/escalation.js +571 -0
- package/dist/src/onboard/steps/escalation.js.map +1 -0
- package/dist/src/onboard/steps/githubapp.js +1092 -0
- package/dist/src/onboard/steps/githubapp.js.map +1 -0
- package/dist/src/onboard/steps/golive.js +506 -0
- package/dist/src/onboard/steps/golive.js.map +1 -0
- package/dist/src/onboard/steps/index.js +67 -0
- package/dist/src/onboard/steps/index.js.map +1 -0
- package/dist/src/onboard/steps/ingress.js +762 -0
- package/dist/src/onboard/steps/ingress.js.map +1 -0
- package/dist/src/onboard/steps/inventory.js +420 -0
- package/dist/src/onboard/steps/inventory.js.map +1 -0
- package/dist/src/onboard/steps/llmauth.js +893 -0
- package/dist/src/onboard/steps/llmauth.js.map +1 -0
- package/dist/src/onboard/steps/memory.js +1060 -0
- package/dist/src/onboard/steps/memory.js.map +1 -0
- package/dist/src/onboard/steps/memorykeys.js +43 -0
- package/dist/src/onboard/steps/memorykeys.js.map +1 -0
- package/dist/src/onboard/steps/preflight.js +172 -0
- package/dist/src/onboard/steps/preflight.js.map +1 -0
- package/dist/src/onboard/steps/proof.js +502 -0
- package/dist/src/onboard/steps/proof.js.map +1 -0
- package/dist/src/onboard/steps/ssh.js +300 -0
- package/dist/src/onboard/steps/ssh.js.map +1 -0
- package/dist/src/onboard/steps/topology.js +291 -0
- package/dist/src/onboard/steps/topology.js.map +1 -0
- package/dist/src/onboard/steps/transport.js +511 -0
- package/dist/src/onboard/steps/transport.js.map +1 -0
- package/dist/src/onboard/steps/verifyup.js +362 -0
- package/dist/src/onboard/steps/verifyup.js.map +1 -0
- package/dist/src/onboard/steps/vmdeploy.js +368 -0
- package/dist/src/onboard/steps/vmdeploy.js.map +1 -0
- package/dist/src/onboard/toml.js +53 -0
- package/dist/src/onboard/toml.js.map +1 -0
- package/dist/src/opencli.js +508 -0
- package/dist/src/opencli.js.map +1 -0
- package/dist/src/openpr.js +457 -0
- package/dist/src/openpr.js.map +1 -0
- package/dist/src/openviking/client.js +821 -0
- package/dist/src/openviking/client.js.map +1 -0
- package/dist/src/openviking/context.js +218 -0
- package/dist/src/openviking/context.js.map +1 -0
- package/dist/src/openviking/embedder.js +457 -0
- package/dist/src/openviking/embedder.js.map +1 -0
- package/dist/src/openviking/gapsink.js +38 -0
- package/dist/src/openviking/gapsink.js.map +1 -0
- package/dist/src/openviking/health.js +69 -0
- package/dist/src/openviking/health.js.map +1 -0
- package/dist/src/openviking/index.js +24 -0
- package/dist/src/openviking/index.js.map +1 -0
- package/dist/src/openviking/lessonfind.js +54 -0
- package/dist/src/openviking/lessonfind.js.map +1 -0
- package/dist/src/openviking/probe.js +89 -0
- package/dist/src/openviking/probe.js.map +1 -0
- package/dist/src/openviking/proc.js +95 -0
- package/dist/src/openviking/proc.js.map +1 -0
- package/dist/src/openviking/provisionreport.js +144 -0
- package/dist/src/openviking/provisionreport.js.map +1 -0
- package/dist/src/openviking/reembed.js +317 -0
- package/dist/src/openviking/reembed.js.map +1 -0
- package/dist/src/openviking/relayout.js +153 -0
- package/dist/src/openviking/relayout.js.map +1 -0
- package/dist/src/openviking/sidecar-main.js +39 -0
- package/dist/src/openviking/sidecar-main.js.map +1 -0
- package/dist/src/openviking/sidecar.js +79 -0
- package/dist/src/openviking/sidecar.js.map +1 -0
- package/dist/src/openviking/store.js +1758 -0
- package/dist/src/openviking/store.js.map +1 -0
- package/dist/src/openviking/supervisor.js +422 -0
- package/dist/src/openviking/supervisor.js.map +1 -0
- package/dist/src/openviking/types.js +5 -0
- package/dist/src/openviking/types.js.map +1 -0
- package/dist/src/operator-recovery.js +172 -0
- package/dist/src/operator-recovery.js.map +1 -0
- package/dist/src/operatormessage.js +44 -0
- package/dist/src/operatormessage.js.map +1 -0
- package/dist/src/outcomes.js +1361 -0
- package/dist/src/outcomes.js.map +1 -0
- package/dist/src/piauthcopy.js +5 -0
- package/dist/src/piauthcopy.js.map +1 -0
- package/dist/src/picli.js +45 -0
- package/dist/src/picli.js.map +1 -0
- package/dist/src/picwd.js +25 -0
- package/dist/src/picwd.js.map +1 -0
- package/dist/src/pilogin.js +35 -0
- package/dist/src/pilogin.js.map +1 -0
- package/dist/src/pingpong.js +1028 -0
- package/dist/src/pingpong.js.map +1 -0
- package/dist/src/posting-journal.js +441 -0
- package/dist/src/posting-journal.js.map +1 -0
- package/dist/src/posting-outbox.js +662 -0
- package/dist/src/posting-outbox.js.map +1 -0
- package/dist/src/posting-receipt.js +203 -0
- package/dist/src/posting-receipt.js.map +1 -0
- package/dist/src/posting.js +348 -0
- package/dist/src/posting.js.map +1 -0
- package/dist/src/postops.js +637 -0
- package/dist/src/postops.js.map +1 -0
- package/dist/src/pr-activity.js +11 -0
- package/dist/src/pr-activity.js.map +1 -0
- package/dist/src/pr-author.js +5 -0
- package/dist/src/pr-author.js.map +1 -0
- package/dist/src/pr-conversation.js +355 -0
- package/dist/src/pr-conversation.js.map +1 -0
- package/dist/src/pr-session-lifecycle.js +542 -0
- package/dist/src/pr-session-lifecycle.js.map +1 -0
- package/dist/src/preflight.js +341 -0
- package/dist/src/preflight.js.map +1 -0
- package/dist/src/priorresolve.js +275 -0
- package/dist/src/priorresolve.js.map +1 -0
- package/dist/src/qcrbin.js +180 -0
- package/dist/src/qcrbin.js.map +1 -0
- package/dist/src/recheck.js +1398 -0
- package/dist/src/recheck.js.map +1 -0
- package/dist/src/reconcile.js +820 -0
- package/dist/src/reconcile.js.map +1 -0
- package/dist/src/recordedfindings.js +15 -0
- package/dist/src/recordedfindings.js.map +1 -0
- package/dist/src/regression.js +546 -0
- package/dist/src/regression.js.map +1 -0
- package/dist/src/replay.js +598 -0
- package/dist/src/replay.js.map +1 -0
- package/dist/src/requiredpasses.js +113 -0
- package/dist/src/requiredpasses.js.map +1 -0
- package/dist/src/resumecheck.js +168 -0
- package/dist/src/resumecheck.js.map +1 -0
- package/dist/src/reviewcli.js +671 -0
- package/dist/src/reviewcli.js.map +1 -0
- package/dist/src/reviewcontext.js +22 -0
- package/dist/src/reviewcontext.js.map +1 -0
- package/dist/src/reviewdetach.js +574 -0
- package/dist/src/reviewdetach.js.map +1 -0
- package/dist/src/reviewhistory.js +284 -0
- package/dist/src/reviewhistory.js.map +1 -0
- package/dist/src/reviewhome.js +26 -0
- package/dist/src/reviewhome.js.map +1 -0
- package/dist/src/reviewmemory.js +110 -0
- package/dist/src/reviewmemory.js.map +1 -0
- package/dist/src/reviewrendercli.js +104 -0
- package/dist/src/reviewrendercli.js.map +1 -0
- package/dist/src/round-work.js +20 -0
- package/dist/src/round-work.js.map +1 -0
- package/dist/src/runhost.js +72 -0
- package/dist/src/runhost.js.map +1 -0
- package/dist/src/scanauth.js +25 -0
- package/dist/src/scanauth.js.map +1 -0
- package/dist/src/scanbox.js +846 -0
- package/dist/src/scanbox.js.map +1 -0
- package/dist/src/scancli.js +1074 -0
- package/dist/src/scancli.js.map +1 -0
- package/dist/src/scandriverlog.js +30 -0
- package/dist/src/scandriverlog.js.map +1 -0
- package/dist/src/session-bridge.js +75 -0
- package/dist/src/session-bridge.js.map +1 -0
- package/dist/src/session-command-payload.js +29 -0
- package/dist/src/session-command-payload.js.map +1 -0
- package/dist/src/session-command-supervisor.js +939 -0
- package/dist/src/session-command-supervisor.js.map +1 -0
- package/dist/src/sessionjsonl.js +85 -0
- package/dist/src/sessionjsonl.js.map +1 -0
- package/dist/src/sessions.js +541 -0
- package/dist/src/sessions.js.map +1 -0
- package/dist/src/setup/adapters.js +330 -0
- package/dist/src/setup/adapters.js.map +1 -0
- package/dist/src/setup/baselines.js +64 -0
- package/dist/src/setup/baselines.js.map +1 -0
- package/dist/src/setup/caserunner.js +554 -0
- package/dist/src/setup/caserunner.js.map +1 -0
- package/dist/src/setup/configcommit.js +358 -0
- package/dist/src/setup/configcommit.js.map +1 -0
- package/dist/src/setup/engine.js +672 -0
- package/dist/src/setup/engine.js.map +1 -0
- package/dist/src/setup/facts.js +593 -0
- package/dist/src/setup/facts.js.map +1 -0
- package/dist/src/setup/findings.js +948 -0
- package/dist/src/setup/findings.js.map +1 -0
- package/dist/src/setup/history.js +338 -0
- package/dist/src/setup/history.js.map +1 -0
- package/dist/src/setup/interview.js +185 -0
- package/dist/src/setup/interview.js.map +1 -0
- package/dist/src/setup/ledger.js +207 -0
- package/dist/src/setup/ledger.js.map +1 -0
- package/dist/src/setup/pins.js +43 -0
- package/dist/src/setup/pins.js.map +1 -0
- package/dist/src/setup/portrait.js +432 -0
- package/dist/src/setup/portrait.js.map +1 -0
- package/dist/src/setup/presentation.js +9 -0
- package/dist/src/setup/presentation.js.map +1 -0
- package/dist/src/setup/profile.js +417 -0
- package/dist/src/setup/profile.js.map +1 -0
- package/dist/src/setup/replay.js +240 -0
- package/dist/src/setup/replay.js.map +1 -0
- package/dist/src/setup/replaybuild.js +311 -0
- package/dist/src/setup/replaybuild.js.map +1 -0
- package/dist/src/setup/replayrun.js +806 -0
- package/dist/src/setup/replayrun.js.map +1 -0
- package/dist/src/setup/replaysource.js +169 -0
- package/dist/src/setup/replaysource.js.map +1 -0
- package/dist/src/setup/retune.js +333 -0
- package/dist/src/setup/retune.js.map +1 -0
- package/dist/src/setup/retunelog.js +51 -0
- package/dist/src/setup/retunelog.js.map +1 -0
- package/dist/src/setup/scan.js +725 -0
- package/dist/src/setup/scan.js.map +1 -0
- package/dist/src/setup/scanbuild.js +133 -0
- package/dist/src/setup/scanbuild.js.map +1 -0
- package/dist/src/setup/scanfacts.js +96 -0
- package/dist/src/setup/scanfacts.js.map +1 -0
- package/dist/src/setup/scanlease.js +251 -0
- package/dist/src/setup/scanlease.js.map +1 -0
- package/dist/src/setup/scanreport.js +289 -0
- package/dist/src/setup/scanreport.js.map +1 -0
- package/dist/src/setup/scanrun.js +1733 -0
- package/dist/src/setup/scanrun.js.map +1 -0
- package/dist/src/setup/scantree.js +257 -0
- package/dist/src/setup/scantree.js.map +1 -0
- package/dist/src/setup/stage2.js +617 -0
- package/dist/src/setup/stage2.js.map +1 -0
- package/dist/src/setup/stage2request.js +259 -0
- package/dist/src/setup/stage2request.js.map +1 -0
- package/dist/src/setup/survey.js +307 -0
- package/dist/src/setup/survey.js.map +1 -0
- package/dist/src/setup/surveyrun.js +549 -0
- package/dist/src/setup/surveyrun.js.map +1 -0
- package/dist/src/setup/textsniff.js +89 -0
- package/dist/src/setup/textsniff.js.map +1 -0
- package/dist/src/setup/trackersink.js +395 -0
- package/dist/src/setup/trackersink.js.map +1 -0
- package/dist/src/setup/triage.js +636 -0
- package/dist/src/setup/triage.js.map +1 -0
- package/dist/src/setup/tuning.js +585 -0
- package/dist/src/setup/tuning.js.map +1 -0
- package/dist/src/setup/unitcut.js +437 -0
- package/dist/src/setup/unitcut.js.map +1 -0
- package/dist/src/singleflight.js +72 -0
- package/dist/src/singleflight.js.map +1 -0
- package/dist/src/slack/announce.js +725 -0
- package/dist/src/slack/announce.js.map +1 -0
- package/dist/src/slack/api.js +227 -0
- package/dist/src/slack/api.js.map +1 -0
- package/dist/src/slack/messages.js +158 -0
- package/dist/src/slack/messages.js.map +1 -0
- package/dist/src/slack/origin.js +550 -0
- package/dist/src/slack/origin.js.map +1 -0
- package/dist/src/slack/policy.js +225 -0
- package/dist/src/slack/policy.js.map +1 -0
- package/dist/src/slack/reply.js +153 -0
- package/dist/src/slack/reply.js.map +1 -0
- package/dist/src/slack/router.js +189 -0
- package/dist/src/slack/router.js.map +1 -0
- package/dist/src/slack/socket.js +323 -0
- package/dist/src/slack/socket.js.map +1 -0
- package/dist/src/slack/types.js +11 -0
- package/dist/src/slack/types.js.map +1 -0
- package/dist/src/sqlitehint.js +30 -0
- package/dist/src/sqlitehint.js.map +1 -0
- package/dist/src/ssh-observation.js +25 -0
- package/dist/src/ssh-observation.js.map +1 -0
- package/dist/src/state.js +5869 -0
- package/dist/src/state.js.map +1 -0
- package/dist/src/statemachine.js +220 -0
- package/dist/src/statemachine.js.map +1 -0
- package/dist/src/stubs.js +52 -0
- package/dist/src/stubs.js.map +1 -0
- package/dist/src/takeover.js +506 -0
- package/dist/src/takeover.js.map +1 -0
- package/dist/src/takeoverpi.js +249 -0
- package/dist/src/takeoverpi.js.map +1 -0
- package/dist/src/terminal-failure.js +26 -0
- package/dist/src/terminal-failure.js.map +1 -0
- package/dist/src/thread-resolution-refusal.js +4 -0
- package/dist/src/thread-resolution-refusal.js.map +1 -0
- package/dist/src/tmux.js +126 -0
- package/dist/src/tmux.js.map +1 -0
- package/dist/src/trackerid.js +43 -0
- package/dist/src/trackerid.js.map +1 -0
- package/dist/src/transcriptindex.js +512 -0
- package/dist/src/transcriptindex.js.map +1 -0
- package/dist/src/tui/actions.js +404 -0
- package/dist/src/tui/actions.js.map +1 -0
- package/dist/src/tui/alerts.js +523 -0
- package/dist/src/tui/alerts.js.map +1 -0
- package/dist/src/tui/app.js +1835 -0
- package/dist/src/tui/app.js.map +1 -0
- package/dist/src/tui/archiveops.js +164 -0
- package/dist/src/tui/archiveops.js.map +1 -0
- package/dist/src/tui/autofile.js +191 -0
- package/dist/src/tui/autofile.js.map +1 -0
- package/dist/src/tui/board.js +1143 -0
- package/dist/src/tui/board.js.map +1 -0
- package/dist/src/tui/candidates.js +699 -0
- package/dist/src/tui/candidates.js.map +1 -0
- package/dist/src/tui/cellwidth.js +151 -0
- package/dist/src/tui/cellwidth.js.map +1 -0
- package/dist/src/tui/changelog-store.js +404 -0
- package/dist/src/tui/changelog-store.js.map +1 -0
- package/dist/src/tui/chord.js +26 -0
- package/dist/src/tui/chord.js.map +1 -0
- package/dist/src/tui/clipboard.js +81 -0
- package/dist/src/tui/clipboard.js.map +1 -0
- package/dist/src/tui/cmdbar.js +253 -0
- package/dist/src/tui/cmdbar.js.map +1 -0
- package/dist/src/tui/companion.js +1275 -0
- package/dist/src/tui/companion.js.map +1 -0
- package/dist/src/tui/companionplan.js +200 -0
- package/dist/src/tui/companionplan.js.map +1 -0
- package/dist/src/tui/companiontab.js +61 -0
- package/dist/src/tui/companiontab.js.map +1 -0
- package/dist/src/tui/confighome.js +105 -0
- package/dist/src/tui/confighome.js.map +1 -0
- package/dist/src/tui/connect.js +44 -0
- package/dist/src/tui/connect.js.map +1 -0
- package/dist/src/tui/deploy-ceremony.js +75 -0
- package/dist/src/tui/deploy-ceremony.js.map +1 -0
- package/dist/src/tui/deploy-drain.js +93 -0
- package/dist/src/tui/deploy-drain.js.map +1 -0
- package/dist/src/tui/deploy-process.js +234 -0
- package/dist/src/tui/deploy-process.js.map +1 -0
- package/dist/src/tui/deploytab.js +1263 -0
- package/dist/src/tui/deploytab.js.map +1 -0
- package/dist/src/tui/design.js +111 -0
- package/dist/src/tui/design.js.map +1 -0
- package/dist/src/tui/digesttab.js +148 -0
- package/dist/src/tui/digesttab.js.map +1 -0
- package/dist/src/tui/emptystate.js +32 -0
- package/dist/src/tui/emptystate.js.map +1 -0
- package/dist/src/tui/evidence.js +790 -0
- package/dist/src/tui/evidence.js.map +1 -0
- package/dist/src/tui/feed.js +415 -0
- package/dist/src/tui/feed.js.map +1 -0
- package/dist/src/tui/feedbacktab.js +907 -0
- package/dist/src/tui/feedbacktab.js.map +1 -0
- package/dist/src/tui/filters.js +72 -0
- package/dist/src/tui/filters.js.map +1 -0
- package/dist/src/tui/ghread.js +272 -0
- package/dist/src/tui/ghread.js.map +1 -0
- package/dist/src/tui/historypull.js +61 -0
- package/dist/src/tui/historypull.js.map +1 -0
- package/dist/src/tui/inboxpull.js +197 -0
- package/dist/src/tui/inboxpull.js.map +1 -0
- package/dist/src/tui/index.js +1425 -0
- package/dist/src/tui/index.js.map +1 -0
- package/dist/src/tui/issuesink.js +206 -0
- package/dist/src/tui/issuesink.js.map +1 -0
- package/dist/src/tui/jsonfile.js +32 -0
- package/dist/src/tui/jsonfile.js.map +1 -0
- package/dist/src/tui/keys.js +97 -0
- package/dist/src/tui/keys.js.map +1 -0
- package/dist/src/tui/layout.js +70 -0
- package/dist/src/tui/layout.js.map +1 -0
- package/dist/src/tui/learning.js +469 -0
- package/dist/src/tui/learning.js.map +1 -0
- package/dist/src/tui/lessonswitch.js +103 -0
- package/dist/src/tui/lessonswitch.js.map +1 -0
- package/dist/src/tui/livecontrol.js +167 -0
- package/dist/src/tui/livecontrol.js.map +1 -0
- package/dist/src/tui/memforward.js +310 -0
- package/dist/src/tui/memforward.js.map +1 -0
- package/dist/src/tui/memincident.js +287 -0
- package/dist/src/tui/memincident.js.map +1 -0
- package/dist/src/tui/memory.js +485 -0
- package/dist/src/tui/memory.js.map +1 -0
- package/dist/src/tui/memtab.js +513 -0
- package/dist/src/tui/memtab.js.map +1 -0
- package/dist/src/tui/metricstab.js +979 -0
- package/dist/src/tui/metricstab.js.map +1 -0
- package/dist/src/tui/model.js +104 -0
- package/dist/src/tui/model.js.map +1 -0
- package/dist/src/tui/mouse.js +83 -0
- package/dist/src/tui/mouse.js.map +1 -0
- package/dist/src/tui/msg.js +196 -0
- package/dist/src/tui/msg.js.map +1 -0
- package/dist/src/tui/nativecompanion.js +321 -0
- package/dist/src/tui/nativecompanion.js.map +1 -0
- package/dist/src/tui/openurl.js +17 -0
- package/dist/src/tui/openurl.js.map +1 -0
- package/dist/src/tui/paint.js +501 -0
- package/dist/src/tui/paint.js.map +1 -0
- package/dist/src/tui/panes.js +74 -0
- package/dist/src/tui/panes.js.map +1 -0
- package/dist/src/tui/pipane.js +166 -0
- package/dist/src/tui/pipane.js.map +1 -0
- package/dist/src/tui/poll-hooks.js +11 -0
- package/dist/src/tui/poll-hooks.js.map +1 -0
- package/dist/src/tui/poll.js +113 -0
- package/dist/src/tui/poll.js.map +1 -0
- package/dist/src/tui/proposals.js +715 -0
- package/dist/src/tui/proposals.js.map +1 -0
- package/dist/src/tui/prpause.js +132 -0
- package/dist/src/tui/prpause.js.map +1 -0
- package/dist/src/tui/reload.js +351 -0
- package/dist/src/tui/reload.js.map +1 -0
- package/dist/src/tui/remote.js +312 -0
- package/dist/src/tui/remote.js.map +1 -0
- package/dist/src/tui/rollover.js +767 -0
- package/dist/src/tui/rollover.js.map +1 -0
- package/dist/src/tui/scantab.js +1053 -0
- package/dist/src/tui/scantab.js.map +1 -0
- package/dist/src/tui/setupentry.js +81 -0
- package/dist/src/tui/setupentry.js.map +1 -0
- package/dist/src/tui/sidebar.js +602 -0
- package/dist/src/tui/sidebar.js.map +1 -0
- package/dist/src/tui/sshtriage.js +393 -0
- package/dist/src/tui/sshtriage.js.map +1 -0
- package/dist/src/tui/startup.js +172 -0
- package/dist/src/tui/startup.js.map +1 -0
- package/dist/src/tui/tabs.js +41 -0
- package/dist/src/tui/tabs.js.map +1 -0
- package/dist/src/tui/termmirror.js +217 -0
- package/dist/src/tui/termmirror.js.map +1 -0
- package/dist/src/tui/theme.js +95 -0
- package/dist/src/tui/theme.js.map +1 -0
- package/dist/src/tui/ticketstab.js +224 -0
- package/dist/src/tui/ticketstab.js.map +1 -0
- package/dist/src/tui/tmuxsocket.js +101 -0
- package/dist/src/tui/tmuxsocket.js.map +1 -0
- package/dist/src/tui/trackertickets.js +77 -0
- package/dist/src/tui/trackertickets.js.map +1 -0
- package/dist/src/tui/unwedge.js +245 -0
- package/dist/src/tui/unwedge.js.map +1 -0
- package/dist/src/tui/viewer.js +479 -0
- package/dist/src/tui/viewer.js.map +1 -0
- package/dist/src/tui/viewerinput.js +125 -0
- package/dist/src/tui/viewerinput.js.map +1 -0
- package/dist/src/updatenotice.js +143 -0
- package/dist/src/updatenotice.js.map +1 -0
- package/dist/src/usage.js +1007 -0
- package/dist/src/usage.js.map +1 -0
- package/dist/src/verdicts.js +3848 -0
- package/dist/src/verdicts.js.map +1 -0
- package/dist/src/version.js +58 -0
- package/dist/src/version.js.map +1 -0
- package/dist/src/wakes.js +411 -0
- package/dist/src/wakes.js.map +1 -0
- package/dist/src/webhook.js +725 -0
- package/dist/src/webhook.js.map +1 -0
- package/dist/src/webhookheal.js +238 -0
- package/dist/src/webhookheal.js.map +1 -0
- package/dist/src/wire.js +1801 -0
- package/dist/src/wire.js.map +1 -0
- package/dist/src/worker-session-host.js +531 -0
- package/dist/src/worker-session-host.js.map +1 -0
- package/dist/src/workerbreaker.js +112 -0
- package/dist/src/workerbreaker.js.map +1 -0
- package/dist/src/workercontrol.js +49 -0
- package/dist/src/workercontrol.js.map +1 -0
- package/dist/src/workerlease.js +351 -0
- package/dist/src/workerlease.js.map +1 -0
- package/dist/src/workerliveness.js +84 -0
- package/dist/src/workerliveness.js.map +1 -0
- package/dist/src/workerlog.js +65 -0
- package/dist/src/workerlog.js.map +1 -0
- package/dist/src/workerruntime.js +10 -0
- package/dist/src/workerruntime.js.map +1 -0
- package/dist/src/workersupervisor.js +1206 -0
- package/dist/src/workersupervisor.js.map +1 -0
- package/dist/src/workerusage.js +23 -0
- package/dist/src/workerusage.js.map +1 -0
- package/dist/src/workerworld.js +332 -0
- package/dist/src/workerworld.js.map +1 -0
- package/dist/src/worktrees.js +984 -0
- package/dist/src/worktrees.js.map +1 -0
- package/docker/Dockerfile +284 -0
- package/docker/Dockerfile.dockerignore +35 -0
- package/docker/README.md +221 -0
- package/docker/companion/Dockerfile +139 -0
- package/docker/companion/agents/evidence.md +34 -0
- package/docker/companion/agents/explore.md +26 -0
- package/docker/companion/agents/pass-survey.md +49 -0
- package/docker/companion/agents/study.md +26 -0
- package/docker/companion/entrypoint.sh +343 -0
- package/docker/companion/memory-health.mjs +269 -0
- package/docker/entrypoint.sh +782 -0
- package/docker/memory/Dockerfile +191 -0
- package/docker/memory/Dockerfile.dockerignore +20 -0
- package/docker/memory/README.md +60 -0
- package/docker/memory/entrypoint.sh +163 -0
- package/docker/setid/qcr-setid.c +109 -0
- package/docker/tunnel/Dockerfile.cloudflared +51 -0
- package/docker/tunnel/Dockerfile.ngrok +40 -0
- package/docker/tunnel/sidecar.mjs +411 -0
- package/docker/vm/tmux.conf +26 -0
- package/docker-compose.yml +348 -0
- package/extensions/cli/bash-guard-ambient.ts +71 -0
- package/extensions/cli/bash-guard.ts +12 -0
- package/extensions/cli/companion-guard.ts +8 -0
- package/extensions/cli/debug-round.ts +9 -0
- package/extensions/cli/inject-bridge.ts +8 -0
- package/extensions/cli/ledger-pin.ts +13 -0
- package/extensions/cli/lens.ts +22 -0
- package/extensions/cli/muted-theme.ts +9 -0
- package/extensions/cli/onboarding.ts +8 -0
- package/extensions/cli/setup.ts +8 -0
- package/extensions/cli/tui-view.ts +8 -0
- package/extensions/companion-guard.ts +411 -0
- package/extensions/debug-round.ts +281 -0
- package/extensions/inject-bridge.ts +630 -0
- package/extensions/inline-bash.ts +102 -0
- package/extensions/muted-theme.json +68 -0
- package/extensions/muted-theme.ts +23 -0
- package/extensions/onboarding.ts +233 -0
- package/extensions/setup-replay.ts +336 -0
- package/extensions/setup-scan.ts +1236 -0
- package/extensions/setup-triage.ts +190 -0
- package/extensions/setup.ts +1428 -0
- package/extensions/tui-view.ts +489 -0
- package/extensions/vendor/openviking-pi/LICENSE +201 -0
- package/extensions/vendor/openviking-pi/NOTICE +68 -0
- package/extensions/vendor/openviking-pi/README.md +550 -0
- package/extensions/vendor/openviking-pi/client.ts +588 -0
- package/extensions/vendor/openviking-pi/config.json +28 -0
- package/extensions/vendor/openviking-pi/config.ts +271 -0
- package/extensions/vendor/openviking-pi/index.ts +523 -0
- package/extensions/vendor/openviking-pi/lib/capture-adapter.d.mts +10 -0
- package/extensions/vendor/openviking-pi/lib/capture-adapter.mjs +88 -0
- package/extensions/vendor/openviking-pi/lib/takeover-core.d.mts +80 -0
- package/extensions/vendor/openviking-pi/lib/takeover-core.mjs +406 -0
- package/extensions/vendor/openviking-pi/lib/uri-guard-adapter.d.mts +1 -0
- package/extensions/vendor/openviking-pi/lib/uri-guard-adapter.mjs +42 -0
- package/extensions/vendor/openviking-pi/patches/0000-vendoring-and-build-conformance.patch +726 -0
- package/extensions/vendor/openviking-pi/patches/0001-remove-session-before-compact-handler.patch +101 -0
- package/extensions/vendor/openviking-pi/patches/0002-no-ambient-work-in-subagent-leaves.patch +202 -0
- package/extensions/vendor/openviking-pi/patches/0003-never-auto-create-openviking-sessions.patch +120 -0
- package/extensions/vendor/openviking-pi/patches/0004-viking-write-durable-batch-write.patch +521 -0
- package/extensions/vendor/openviking-pi/patches/0005-second-memory-namespace.patch +786 -0
- package/extensions/vendor/openviking-pi/patches/0006-writes-that-stick-and-honest-errors.patch +996 -0
- package/extensions/vendor/openviking-pi/patches/0007-bounded-recall-no-commit.patch +794 -0
- package/extensions/vendor/openviking-pi/patches/0008-pr-derived-recall.patch +379 -0
- package/extensions/vendor/openviking-pi/patches/0009-pr-session-tools-in-round-areas.patch +375 -0
- package/extensions/vendor/openviking-pi/patches/0010-typed-lessons.patch +448 -0
- package/extensions/vendor/openviking-pi/patches/0011-review-session-memory-boundary.patch +382 -0
- package/extensions/vendor/openviking-pi/patches/0012-typecheck-against-the-real-pi.patch +129 -0
- package/extensions/vendor/openviking-pi/patches/0013-createopenviking-factory.patch +1168 -0
- package/extensions/vendor/openviking-pi/qcr-hint.ts +313 -0
- package/extensions/vendor/openviking-pi/qcr-honest.ts +183 -0
- package/extensions/vendor/openviking-pi/qcr-hooks.ts +87 -0
- package/extensions/vendor/openviking-pi/qcr-leaf.ts +78 -0
- package/extensions/vendor/openviking-pi/qcr-lesson.ts +231 -0
- package/extensions/vendor/openviking-pi/qcr-namespaces.ts +198 -0
- package/extensions/vendor/openviking-pi/qcr-recall.ts +288 -0
- package/extensions/vendor/openviking-pi/qcr-shim.ts +142 -0
- package/extensions/vendor/openviking-pi/qcr-write.ts +427 -0
- package/extensions/vendor/openviking-pi/recall.ts +149 -0
- package/extensions/vendor/openviking-pi/shared/capture-utils.d.mts +9 -0
- package/extensions/vendor/openviking-pi/shared/capture-utils.mjs +515 -0
- package/extensions/vendor/openviking-pi/shared/credentials.d.mts +14 -0
- package/extensions/vendor/openviking-pi/shared/credentials.mjs +228 -0
- package/extensions/vendor/openviking-pi/shared/pending-queue.d.mts +19 -0
- package/extensions/vendor/openviking-pi/shared/pending-queue.mjs +458 -0
- package/extensions/vendor/openviking-pi/shared/profile-inject.d.mts +17 -0
- package/extensions/vendor/openviking-pi/shared/profile-inject.mjs +294 -0
- package/extensions/vendor/openviking-pi/shared/recall-compress-core.mjs +195 -0
- package/extensions/vendor/openviking-pi/shared/recall-core.d.mts +13 -0
- package/extensions/vendor/openviking-pi/shared/recall-core.mjs +609 -0
- package/extensions/vendor/openviking-pi/shared/retryable.mjs +9 -0
- package/extensions/vendor/openviking-pi/shared/session-model.d.mts +3 -0
- package/extensions/vendor/openviking-pi/shared/session-model.mjs +57 -0
- package/extensions/vendor/openviking-pi/shared/uri-guard.mjs +56 -0
- package/extensions/vendor/openviking-pi/shared/workspace-peer.d.mts +5 -0
- package/extensions/vendor/openviking-pi/shared/workspace-peer.mjs +16 -0
- package/extensions/vendor/openviking-pi/sync.ts +163 -0
- package/extensions/vendor/openviking-pi/takeover.ts +36 -0
- package/extensions/vendor/openviking-pi/tools.ts +479 -0
- package/extensions/vendor/openviking-pi/upstream.json +48 -0
- package/package.json +138 -4
- package/profiles/default/README.md +21 -0
- package/profiles/default/addenda/.gitkeep +0 -0
- package/profiles/default/profile.toml +17 -0
- package/profiles/example-django/README.md +21 -0
- package/profiles/example-django/addenda/10-migration-discipline.md +13 -0
- package/profiles/example-django/addenda/20-new-column-nullability.md +12 -0
- package/profiles/example-django/addenda/30-test-doctrine.md +12 -0
- package/profiles/example-django/addenda/40-orm-usage.md +19 -0
- package/profiles/example-django/addenda/50-declared-shape.md +10 -0
- package/profiles/example-django/addenda/60-config-placement.md +14 -0
- package/profiles/example-django/heuristics.toml +24 -0
- package/profiles/example-django/profile.toml +9 -0
- package/profiles/fast/README.md +24 -0
- package/profiles/fast/profile.toml +59 -0
- package/profiles/minimal/README.md +17 -0
- package/profiles/minimal/addenda/.gitkeep +0 -0
- package/profiles/minimal/profile.toml +8 -0
- package/profiles/snappy/README.md +24 -0
- package/profiles/snappy/addenda/10-race-as-question.md +6 -0
- package/profiles/snappy/profile.toml +46 -0
- package/prompts/companion-day-open.md +46 -0
- package/prompts/companion-first-run.md +104 -0
- package/prompts/companion.md +821 -0
- package/prompts/conversation-system.md +117 -0
- package/prompts/eod-sweep.md +120 -0
- package/prompts/onboarding.md +74 -0
- package/prompts/pass-common.md +52 -0
- package/prompts/pass-quality-common.md +22 -0
- package/prompts/round-first.md +874 -0
- package/prompts/round-follow-up.md +991 -0
- package/prompts/round-re-review.md +889 -0
- package/prompts/round-recheck.md +196 -0
- package/prompts/round-staleness.md +856 -0
- package/schemas/review-result.v1.json +391 -0
- package/schemas/scan-result.v1.json +421 -0
- package/schemas/version.v1.json +57 -0
- package/scripts/approve-pr.sh +16 -0
- package/scripts/build-stamp.mjs +67 -0
- package/scripts/commit-verdict.sh +155 -0
- package/scripts/companion-e2e.sh +250 -0
- package/scripts/companion-memory-forward.mjs +1966 -0
- package/scripts/config-fingerprint.sh +28 -0
- package/scripts/deploy-manifest.mjs +141 -0
- package/scripts/deploy.sh +1435 -0
- package/scripts/directory-lock.mjs +193 -0
- package/scripts/heartbeat-fresh.mjs +54 -0
- package/scripts/install-locator.sh +328 -0
- package/scripts/install-wrapper.sh +76 -0
- package/scripts/memory-search-check.mjs +98 -0
- package/scripts/mint-token.sh +212 -0
- package/scripts/openviking-embedder.py +392 -0
- package/scripts/openviking-ov-conf.mjs +161 -0
- package/scripts/openviking-provision.mjs +1122 -0
- package/scripts/openviking-reembed.mjs +1154 -0
- package/scripts/post-review.sh +16 -0
- package/scripts/pr-checks.sh +256 -0
- package/scripts/pr-context.py +174 -0
- package/scripts/pr-context.sh +63 -0
- package/scripts/pr-self.sh +144 -0
- package/scripts/pr-threads.sh +145 -0
- package/scripts/qcr +15 -0
- package/scripts/qcr-host.sh +769 -0
- package/scripts/render-review.sh +24 -0
- package/scripts/reply-thread.sh +16 -0
- package/scripts/required-passes.py +112 -0
- package/scripts/smoke-companion.sh +211 -0
- package/scripts/tui-deploy-runner.mjs +61 -0
- package/scripts/verify-dist.mjs +201 -0
- package/scripts/vm-ghcr-login.sh +188 -0
- package/seeds/agents/pass-baseline.md +61 -0
- package/seeds/agents/pass-contracts.md +30 -0
- package/seeds/agents/pass-data-safety.md +54 -0
- package/seeds/agents/pass-delivery.md +37 -0
- package/seeds/agents/pass-prod-risk.md +24 -0
- package/seeds/agents/pass-security.md +31 -0
- package/seeds/agents/pass-statelessness.md +27 -0
- package/seeds/agents/pass-ui.md +23 -0
- package/seeds/agents/pass-validate.md +50 -0
- package/seeds/prompts/conversation-system.md +117 -0
- package/seeds/prompts/eod-sweep.md +120 -0
- package/seeds/prompts/pass-common.md +52 -0
- package/skills/companion/adjudicate/SKILL.md +247 -0
- package/skills/companion/evidence/SKILL.md +150 -0
- package/skills/companion/land/SKILL.md +200 -0
- package/skills/companion/replay/SKILL.md +138 -0
- package/skills/companion/setup/SKILL.md +167 -0
- package/skills/companion/survey/SKILL.md +160 -0
- package/skills/selfimprove/README.md +34 -0
- package/skills/selfimprove/SKILL.md +127 -0
- package/skills/selfimprove/agents/openai.yaml +2 -0
- package/skills/selfimprove/commands/selfimprove.md +6 -0
- package/skills/selfimprove/references/fix-loop.md +49 -0
- package/skills/selfimprove/references/ledger.md +20 -0
- package/skills/selfimprove/references/setup.md +40 -0
- package/src/anomalies.ts +566 -0
- package/src/answerworker.ts +251 -0
- package/src/appstamp.ts +65 -0
- package/src/archive/admission.ts +3 -0
- package/src/archive/archivedb.ts +548 -0
- package/src/archive/archiver.ts +1963 -0
- package/src/archive/archivewrite.ts +279 -0
- package/src/archive/bundle.ts +333 -0
- package/src/archive/dbmove.ts +911 -0
- package/src/archive/duplicatecounts.ts +98 -0
- package/src/archive/eligibility.ts +181 -0
- package/src/archive/extract.ts +342 -0
- package/src/archive/fsutil.ts +415 -0
- package/src/archive/gzfile.ts +370 -0
- package/src/archive/kinds.ts +21 -0
- package/src/archive/livecheckpoint.ts +217 -0
- package/src/archive/liveretention.ts +180 -0
- package/src/archive/manifest.ts +519 -0
- package/src/archive/overseer.ts +336 -0
- package/src/archive/piglobal.ts +174 -0
- package/src/archive/plan.ts +612 -0
- package/src/archive/prarchive.ts +984 -0
- package/src/archive/prfiles.ts +406 -0
- package/src/archive/report.ts +252 -0
- package/src/archive/restore.ts +330 -0
- package/src/archive/rows.ts +187 -0
- package/src/archive/seal.ts +630 -0
- package/src/archive/status.ts +719 -0
- package/src/archive/storepass.ts +581 -0
- package/src/backfill.ts +362 -0
- package/src/bench/cost.ts +113 -0
- package/src/bench/exec.ts +90 -0
- package/src/bench/values.ts +13 -0
- package/src/botcontrol.ts +176 -0
- package/src/botworker.ts +953 -0
- package/src/capture.ts +321 -0
- package/src/cases.ts +1789 -0
- package/src/channelfs.ts +26 -0
- package/src/checklist.ts +506 -0
- package/src/ciretry.ts +141 -0
- package/src/cli.ts +2271 -0
- package/src/cliops.ts +384 -0
- package/src/clonedir.ts +15 -0
- package/src/commandeffects.ts +57 -0
- package/src/commands.ts +1678 -0
- package/src/companioninbox.ts +239 -0
- package/src/compare.ts +957 -0
- package/src/config.ts +2050 -0
- package/src/configcheck.ts +133 -0
- package/src/coreext.ts +47 -0
- package/src/daemon.ts +1302 -0
- package/src/debuground.ts +927 -0
- package/src/debugroundpi.ts +262 -0
- package/src/diagnostics.ts +14 -0
- package/src/digest.ts +183 -0
- package/src/dispatch.ts +2250 -0
- package/src/engine/answerguard.ts +27 -0
- package/src/engine/botsdk.ts +336 -0
- package/src/engine/caserunner-sdk.ts +423 -0
- package/src/engine/debugpi.ts +235 -0
- package/src/eod.ts +669 -0
- package/src/eodpinwatch.ts +556 -0
- package/src/escalation.ts +1253 -0
- package/src/eventloop.ts +1045 -0
- package/src/execboundary.ts +633 -0
- package/src/feedback.ts +1162 -0
- package/src/findingscheck.ts +298 -0
- package/src/fsdenied.ts +24 -0
- package/src/ghread.ts +242 -0
- package/src/github.ts +1497 -0
- package/src/homes.ts +175 -0
- package/src/hostroute.ts +53 -0
- package/src/improvements.ts +787 -0
- package/src/install.ts +1103 -0
- package/src/installkind.ts +50 -0
- package/src/ipc/capability.ts +333 -0
- package/src/ipc/client.ts +269 -0
- package/src/ipc/control.ts +90 -0
- package/src/ipc/ghreadshim.ts +94 -0
- package/src/ipc/launcher.ts +65 -0
- package/src/ipc/permits.ts +362 -0
- package/src/ipc/postshim.ts +164 -0
- package/src/ipc/protocol.ts +342 -0
- package/src/ipc/server.ts +371 -0
- package/src/ipc/workerspec.ts +218 -0
- package/src/learner/learner.ts +1655 -0
- package/src/learner/miner.ts +1290 -0
- package/src/learner/packet.ts +340 -0
- package/src/learner/receipt.ts +168 -0
- package/src/learner/status.ts +107 -0
- package/src/ledgercheck.ts +157 -0
- package/src/lessonledger.ts +703 -0
- package/src/lessonselect.ts +888 -0
- package/src/lessonshadow.ts +810 -0
- package/src/levers.ts +393 -0
- package/src/live/projection.ts +491 -0
- package/src/live/publish.ts +92 -0
- package/src/live/read.ts +86 -0
- package/src/livecli.ts +336 -0
- package/src/loginhint.ts +56 -0
- package/src/loginrefresh/atomic.ts +76 -0
- package/src/loginrefresh/lock.ts +104 -0
- package/src/loginrefresh/main.ts +59 -0
- package/src/loginrefresh/pirefresh.ts +57 -0
- package/src/loginrefresh/refresher.ts +326 -0
- package/src/loginrefresh/supervisor.ts +273 -0
- package/src/loginrefresh/types.ts +35 -0
- package/src/memory/context.ts +385 -0
- package/src/memory/datafence.ts +9 -0
- package/src/memory/delivery.ts +171 -0
- package/src/memory/eventtags.ts +80 -0
- package/src/memory/health.ts +953 -0
- package/src/memory/import.ts +664 -0
- package/src/memory/layout.ts +261 -0
- package/src/memory/legacy-file.ts +104 -0
- package/src/memory/lessons.ts +447 -0
- package/src/memory/ovsession.ts +601 -0
- package/src/memory/relayout.ts +335 -0
- package/src/memory/roundactivity.ts +209 -0
- package/src/memory/selection.ts +989 -0
- package/src/memory/shadowstatus.ts +125 -0
- package/src/memory/store.ts +275 -0
- package/src/memory/textidentity.ts +8 -0
- package/src/metrics.ts +1056 -0
- package/src/missingconfig.ts +7 -0
- package/src/npmverbs.ts +264 -0
- package/src/onboard/choices.ts +222 -0
- package/src/onboard/engine.ts +518 -0
- package/src/onboard/launch.ts +115 -0
- package/src/onboard/steps/appslug.ts +265 -0
- package/src/onboard/steps/bringup.ts +482 -0
- package/src/onboard/steps/compose.ts +459 -0
- package/src/onboard/steps/configdelivery.ts +58 -0
- package/src/onboard/steps/configfill.ts +1352 -0
- package/src/onboard/steps/deploysh.ts +105 -0
- package/src/onboard/steps/embeddings.ts +550 -0
- package/src/onboard/steps/escalation.ts +640 -0
- package/src/onboard/steps/githubapp.ts +1414 -0
- package/src/onboard/steps/golive.ts +577 -0
- package/src/onboard/steps/index.ts +71 -0
- package/src/onboard/steps/ingress.ts +907 -0
- package/src/onboard/steps/inventory.ts +526 -0
- package/src/onboard/steps/llmauth.ts +1095 -0
- package/src/onboard/steps/memory.ts +1290 -0
- package/src/onboard/steps/memorykeys.ts +85 -0
- package/src/onboard/steps/preflight.ts +191 -0
- package/src/onboard/steps/proof.ts +624 -0
- package/src/onboard/steps/ssh.ts +377 -0
- package/src/onboard/steps/topology.ts +337 -0
- package/src/onboard/steps/transport.ts +665 -0
- package/src/onboard/steps/verifyup.ts +448 -0
- package/src/onboard/steps/vmdeploy.ts +442 -0
- package/src/onboard/toml.ts +50 -0
- package/src/opencli.ts +591 -0
- package/src/openpr.ts +568 -0
- package/src/openviking/client.ts +1131 -0
- package/src/openviking/context.ts +263 -0
- package/src/openviking/embedder.ts +516 -0
- package/src/openviking/gapsink.ts +63 -0
- package/src/openviking/health.ts +83 -0
- package/src/openviking/index.ts +119 -0
- package/src/openviking/lessonfind.ts +92 -0
- package/src/openviking/probe.ts +129 -0
- package/src/openviking/proc.ts +110 -0
- package/src/openviking/provisionreport.ts +160 -0
- package/src/openviking/reembed.ts +412 -0
- package/src/openviking/relayout.ts +177 -0
- package/src/openviking/sidecar-main.ts +38 -0
- package/src/openviking/sidecar.ts +106 -0
- package/src/openviking/store.ts +2025 -0
- package/src/openviking/supervisor.ts +474 -0
- package/src/openviking/types.ts +174 -0
- package/src/operator-recovery.ts +237 -0
- package/src/operatormessage.ts +52 -0
- package/src/outcomes.ts +1670 -0
- package/src/piauthcopy.ts +12 -0
- package/src/picli.ts +49 -0
- package/src/picwd.ts +25 -0
- package/src/pilogin.ts +37 -0
- package/src/pingpong.ts +1339 -0
- package/src/posting-journal.ts +549 -0
- package/src/posting-outbox.ts +802 -0
- package/src/posting-receipt.ts +324 -0
- package/src/posting.ts +603 -0
- package/src/postops.ts +832 -0
- package/src/pr-activity.ts +20 -0
- package/src/pr-author.ts +6 -0
- package/src/pr-conversation.ts +432 -0
- package/src/pr-session-lifecycle.ts +631 -0
- package/src/preflight.ts +391 -0
- package/src/priorresolve.ts +343 -0
- package/src/qcrbin.ts +186 -0
- package/src/recheck.ts +1775 -0
- package/src/reconcile.ts +1030 -0
- package/src/recordedfindings.ts +27 -0
- package/src/regression.ts +833 -0
- package/src/replay.ts +816 -0
- package/src/requiredpasses.ts +148 -0
- package/src/resumecheck.ts +246 -0
- package/src/reviewcli.ts +787 -0
- package/src/reviewcontext.ts +35 -0
- package/src/reviewdetach.ts +734 -0
- package/src/reviewhistory.ts +427 -0
- package/src/reviewhome.ts +36 -0
- package/src/reviewmemory.ts +133 -0
- package/src/reviewrendercli.ts +139 -0
- package/src/round-work.ts +42 -0
- package/src/runhost.ts +108 -0
- package/src/scanauth.ts +36 -0
- package/src/scanbox.ts +1081 -0
- package/src/scancli.ts +1296 -0
- package/src/scandriverlog.ts +32 -0
- package/src/session-bridge.ts +139 -0
- package/src/session-command-payload.ts +35 -0
- package/src/session-command-supervisor.ts +1142 -0
- package/src/sessionjsonl.ts +88 -0
- package/src/sessions.ts +659 -0
- package/src/setup/adapters.ts +335 -0
- package/src/setup/baselines.ts +110 -0
- package/src/setup/caserunner.ts +611 -0
- package/src/setup/configcommit.ts +443 -0
- package/src/setup/engine.ts +1001 -0
- package/src/setup/facts.ts +627 -0
- package/src/setup/findings.ts +1264 -0
- package/src/setup/history.ts +443 -0
- package/src/setup/interview.ts +243 -0
- package/src/setup/ledger.ts +324 -0
- package/src/setup/pins.ts +66 -0
- package/src/setup/portrait.ts +635 -0
- package/src/setup/presentation.ts +18 -0
- package/src/setup/profile.ts +508 -0
- package/src/setup/replay.ts +370 -0
- package/src/setup/replaybuild.ts +392 -0
- package/src/setup/replayrun.ts +1096 -0
- package/src/setup/replaysource.ts +198 -0
- package/src/setup/retune.ts +415 -0
- package/src/setup/retunelog.ts +95 -0
- package/src/setup/scan.ts +1033 -0
- package/src/setup/scanbuild.ts +168 -0
- package/src/setup/scanfacts.ts +107 -0
- package/src/setup/scanlease.ts +297 -0
- package/src/setup/scanreport.ts +373 -0
- package/src/setup/scanrun.ts +2268 -0
- package/src/setup/scantree.ts +259 -0
- package/src/setup/stage2.ts +926 -0
- package/src/setup/stage2request.ts +347 -0
- package/src/setup/survey.ts +458 -0
- package/src/setup/surveyrun.ts +758 -0
- package/src/setup/textsniff.ts +95 -0
- package/src/setup/trackersink.ts +433 -0
- package/src/setup/triage.ts +768 -0
- package/src/setup/tuning.ts +766 -0
- package/src/setup/unitcut.ts +523 -0
- package/src/singleflight.ts +86 -0
- package/src/slack/announce.ts +911 -0
- package/src/slack/api.ts +303 -0
- package/src/slack/messages.ts +288 -0
- package/src/slack/origin.ts +599 -0
- package/src/slack/policy.ts +354 -0
- package/src/slack/reply.ts +201 -0
- package/src/slack/router.ts +285 -0
- package/src/slack/socket.ts +384 -0
- package/src/slack/types.ts +108 -0
- package/src/sqlitehint.ts +32 -0
- package/src/ssh-observation.ts +30 -0
- package/src/state.ts +8243 -0
- package/src/statemachine.ts +369 -0
- package/src/stubs.ts +68 -0
- package/src/takeover.ts +634 -0
- package/src/takeoverpi.ts +293 -0
- package/src/terminal-failure.ts +41 -0
- package/src/thread-resolution-refusal.ts +2 -0
- package/src/tmux.ts +153 -0
- package/src/trackerid.ts +41 -0
- package/src/transcriptindex.ts +665 -0
- package/src/tui/actions.ts +484 -0
- package/src/tui/alerts.ts +666 -0
- package/src/tui/app.ts +1979 -0
- package/src/tui/archiveops.ts +192 -0
- package/src/tui/autofile.ts +233 -0
- package/src/tui/board.ts +1432 -0
- package/src/tui/candidates.ts +889 -0
- package/src/tui/cellwidth.ts +148 -0
- package/src/tui/changelog-store.ts +503 -0
- package/src/tui/chord.ts +25 -0
- package/src/tui/clipboard.ts +92 -0
- package/src/tui/cmdbar.ts +296 -0
- package/src/tui/companion.ts +1427 -0
- package/src/tui/companionplan.ts +247 -0
- package/src/tui/companiontab.ts +83 -0
- package/src/tui/confighome.ts +155 -0
- package/src/tui/connect.ts +68 -0
- package/src/tui/deploy-ceremony.ts +104 -0
- package/src/tui/deploy-drain.ts +129 -0
- package/src/tui/deploy-process.ts +265 -0
- package/src/tui/deploytab.ts +1523 -0
- package/src/tui/design.ts +125 -0
- package/src/tui/digesttab.ts +214 -0
- package/src/tui/emptystate.ts +39 -0
- package/src/tui/evidence.ts +999 -0
- package/src/tui/feed.ts +604 -0
- package/src/tui/feedbacktab.ts +1077 -0
- package/src/tui/filters.ts +90 -0
- package/src/tui/ghread.ts +321 -0
- package/src/tui/historypull.ts +81 -0
- package/src/tui/inboxpull.ts +236 -0
- package/src/tui/index.ts +1565 -0
- package/src/tui/issuesink.ts +238 -0
- package/src/tui/jsonfile.ts +42 -0
- package/src/tui/keys.ts +109 -0
- package/src/tui/layout.ts +108 -0
- package/src/tui/learning.ts +627 -0
- package/src/tui/lessonswitch.ts +118 -0
- package/src/tui/livecontrol.ts +228 -0
- package/src/tui/memforward.ts +443 -0
- package/src/tui/memincident.ts +363 -0
- package/src/tui/memory.ts +620 -0
- package/src/tui/memtab.ts +606 -0
- package/src/tui/metricstab.ts +1177 -0
- package/src/tui/model.ts +131 -0
- package/src/tui/mouse.ts +98 -0
- package/src/tui/msg.ts +268 -0
- package/src/tui/nativecompanion.ts +417 -0
- package/src/tui/openurl.ts +21 -0
- package/src/tui/paint.ts +511 -0
- package/src/tui/panes.ts +98 -0
- package/src/tui/pipane.ts +219 -0
- package/src/tui/poll-hooks.ts +12 -0
- package/src/tui/poll.ts +147 -0
- package/src/tui/proposals.ts +838 -0
- package/src/tui/prpause.ts +150 -0
- package/src/tui/reload.ts +436 -0
- package/src/tui/remote.ts +413 -0
- package/src/tui/rollover.ts +969 -0
- package/src/tui/scantab.ts +1290 -0
- package/src/tui/setupentry.ts +93 -0
- package/src/tui/sidebar.ts +749 -0
- package/src/tui/sshtriage.ts +510 -0
- package/src/tui/startup.ts +231 -0
- package/src/tui/tabs.ts +45 -0
- package/src/tui/termmirror.ts +224 -0
- package/src/tui/theme.ts +95 -0
- package/src/tui/ticketstab.ts +310 -0
- package/src/tui/tmuxsocket.ts +129 -0
- package/src/tui/trackertickets.ts +84 -0
- package/src/tui/unwedge.ts +286 -0
- package/src/tui/viewer.ts +575 -0
- package/src/tui/viewerinput.ts +173 -0
- package/src/updatenotice.ts +178 -0
- package/src/usage.ts +1154 -0
- package/src/verdicts.ts +4689 -0
- package/src/version.ts +80 -0
- package/src/wakes.ts +476 -0
- package/src/webhook.ts +942 -0
- package/src/webhookheal.ts +276 -0
- package/src/wire.ts +2153 -0
- package/src/worker-session-host.ts +602 -0
- package/src/workerbreaker.ts +156 -0
- package/src/workercontrol.ts +104 -0
- package/src/workerlease.ts +441 -0
- package/src/workerliveness.ts +103 -0
- package/src/workerlog.ts +79 -0
- package/src/workerruntime.ts +30 -0
- package/src/workersupervisor.ts +1538 -0
- package/src/workerusage.ts +42 -0
- package/src/workerworld.ts +427 -0
- package/src/worktrees.ts +1138 -0
- package/tests/fixtures/regression-cases.json +795 -0
- package/tests/fixtures/seed-manifest.json +19 -0
package/src/commands.ts
ADDED
|
@@ -0,0 +1,1678 @@
|
|
|
1
|
+
// The overseer's action space: command files in commands/*.json, validated
|
|
2
|
+
// against a closed whitelist and executed by the daemon. There is deliberately
|
|
3
|
+
// no generic escape hatch — anything not on the whitelist is rejected, logged
|
|
4
|
+
// loudly, and answered with a result file the overseer can read.
|
|
5
|
+
//
|
|
6
|
+
// Envelope: {"command": "<name>", ...payload}. Schemas are hand-rolled and
|
|
7
|
+
// STRICT — unknown fields are a rejection, not a warning.
|
|
8
|
+
//
|
|
9
|
+
// Two issuers, decided by directory. The same vocabulary is served
|
|
10
|
+
// from the daemon-owned `commands/` (and `commands/slack/`), where every
|
|
11
|
+
// writer is a pi SESSION, and from `commands/operator/`, which the operator's
|
|
12
|
+
// tools (`qcr drain`, the board's `u`, the deploy tab) create and write as
|
|
13
|
+
// ROOT so that the daemon's uid — the uid every session runs as — cannot write
|
|
14
|
+
// into it. Nothing in a command file says who wrote it; the watcher stamps
|
|
15
|
+
// the issuer from the directory and the seal, and the lever handlers
|
|
16
|
+
// (`src/levers.ts`) bound a session's `drain`/`pause-pr` and never an
|
|
17
|
+
// operator's. A file in the operator directory without the seal is served as
|
|
18
|
+
// a session's, loudly.
|
|
19
|
+
import { createHash } from "node:crypto";
|
|
20
|
+
import {
|
|
21
|
+
closeSync,
|
|
22
|
+
constants as fsConstants,
|
|
23
|
+
existsSync,
|
|
24
|
+
fstatSync,
|
|
25
|
+
lstatSync,
|
|
26
|
+
mkdirSync,
|
|
27
|
+
openSync,
|
|
28
|
+
readFileSync,
|
|
29
|
+
readdirSync,
|
|
30
|
+
renameSync,
|
|
31
|
+
statSync,
|
|
32
|
+
watch,
|
|
33
|
+
writeFileSync,
|
|
34
|
+
type FSWatcher,
|
|
35
|
+
} from "node:fs";
|
|
36
|
+
import { basename, dirname, join } from "node:path";
|
|
37
|
+
import type { StateStore } from "./state.js";
|
|
38
|
+
import type { FileOutcome, ImprovementPayload, IssuePayload } from "./improvements.js";
|
|
39
|
+
import { BRANCH_MAX, BRANCH_RE, isBranchName, SLUG_RE } from "./improvements.js";
|
|
40
|
+
import type { LeverIssuer } from "./levers.js";
|
|
41
|
+
import type { LessonSwitchChange } from "./lessonledger.js";
|
|
42
|
+
import { validateReceipt, type CurationReceipt } from "./learner/receipt.js";
|
|
43
|
+
import { beltRestricts, type OriginRecord } from "./slack/origin.js";
|
|
44
|
+
import { SLACK_TS_RE } from "./slack/reply.js";
|
|
45
|
+
|
|
46
|
+
/** The session-writable command vocabulary — the one list `CommandName` is
|
|
47
|
+
* read from, so a name cannot be typed without being admitted. */
|
|
48
|
+
export const COMMAND_WHITELIST = [
|
|
49
|
+
"rerun-round",
|
|
50
|
+
"restart-session",
|
|
51
|
+
"resolve-terminal-failure",
|
|
52
|
+
"pause-pr",
|
|
53
|
+
"resume-pr",
|
|
54
|
+
"flag-owner",
|
|
55
|
+
"file-improvement-pr",
|
|
56
|
+
"file-issue",
|
|
57
|
+
"slack_reply",
|
|
58
|
+
"drain",
|
|
59
|
+
"retry-thread-decision",
|
|
60
|
+
"eod-curation-receipt",
|
|
61
|
+
] as const;
|
|
62
|
+
|
|
63
|
+
export type CommandName = (typeof COMMAND_WHITELIST)[number];
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The operator's round controls: what the viewer's input line, `msg.ts` and
|
|
67
|
+
* the companion send to a PR's running (or idle) session. `steer` goes straight
|
|
68
|
+
* to the running session (rule 9's one exception), `follow-up` queues behind
|
|
69
|
+
* the turn, `abort-round` stops the round, and `message` is the idle PR's line
|
|
70
|
+
* (`may_post` marks one the operator allows to post). `takeover`, `release` and
|
|
71
|
+
* `debug-round` take, free and replace the PR's host. They are operator
|
|
72
|
+
* commands, so they are valid only from the root-sealed channel, and no session
|
|
73
|
+
* or Slack turn can spell them. The daemon acts on them through the optional
|
|
74
|
+
* `operatorControl` dep; a daemon without it refuses with a reason.
|
|
75
|
+
*/
|
|
76
|
+
export const CONTROL_COMMANDS = [
|
|
77
|
+
"steer",
|
|
78
|
+
"follow-up",
|
|
79
|
+
"abort-round",
|
|
80
|
+
"message",
|
|
81
|
+
"takeover",
|
|
82
|
+
"release",
|
|
83
|
+
"debug-round",
|
|
84
|
+
] as const;
|
|
85
|
+
|
|
86
|
+
export type ControlCommandName = (typeof CONTROL_COMMANDS)[number];
|
|
87
|
+
|
|
88
|
+
/** The longest `text` a steer, follow-up or message carries, in characters. */
|
|
89
|
+
export const CONTROL_TEXT_MAX = 8000;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The operator's commands: the lesson kill switch and the archiver's two
|
|
93
|
+
* verbs (`archive-run`, one immediate bounded archiver tick, and
|
|
94
|
+
* `archive-restore`, one PR's archive back to its live paths, both run under
|
|
95
|
+
* the archiver's strict mutex). They are not in `COMMAND_WHITELIST` — the
|
|
96
|
+
* vocabulary a session (the overseer, the EOD curator, a Slack turn) may
|
|
97
|
+
* write — and they validate only for a file the watcher found in the
|
|
98
|
+
* root-sealed `commands/operator/` directory (`operatorSealVerdict`). A
|
|
99
|
+
* session's command file cannot spell them: in the session directories, and
|
|
100
|
+
* in an UNSEALED operator directory (served as a session's), they are refused
|
|
101
|
+
* at validation, and `executeCommand` refuses
|
|
102
|
+
* them again for any issuer but the operator. The kill switch's state lives
|
|
103
|
+
* in state.db (src/lessonledger.ts), never as a memory tag anyone holding a
|
|
104
|
+
* key could write.
|
|
105
|
+
*/
|
|
106
|
+
export const OPERATOR_COMMANDS = [
|
|
107
|
+
"lessons-off",
|
|
108
|
+
"lessons-on",
|
|
109
|
+
"suppress-lesson",
|
|
110
|
+
"unsuppress-lesson",
|
|
111
|
+
"archive-run",
|
|
112
|
+
"archive-restore",
|
|
113
|
+
...CONTROL_COMMANDS,
|
|
114
|
+
] as const;
|
|
115
|
+
|
|
116
|
+
export type OperatorCommandName = (typeof OPERATOR_COMMANDS)[number];
|
|
117
|
+
|
|
118
|
+
/** A lesson the operator names: the full memory URI of one entry
|
|
119
|
+
* (`viking://user/<user>/memories/<area>/<file>.md`) — `qcr lessons
|
|
120
|
+
* suppress` expands a bare `experiences/<file>.md` to it. */
|
|
121
|
+
export const LESSON_URI_RE =
|
|
122
|
+
/^viking:\/\/user\/[A-Za-z0-9_-]{1,64}\/memories\/(?:[a-z][a-z0-9_-]*\/){1,2}[A-Za-z0-9._-]{1,200}\.md$/;
|
|
123
|
+
|
|
124
|
+
function isOperatorCommand(name: string): name is OperatorCommandName {
|
|
125
|
+
return (OPERATOR_COMMANDS as readonly string[]).includes(name);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Retired command spellings, folded to their current name at validate time —
|
|
130
|
+
* the codebase's ESCALATE→NEEDS_HUMAN pattern: accepted on input (sessions on
|
|
131
|
+
* older prompts may still emit them), never emitted by us — the parsed
|
|
132
|
+
* `Command`, the result file and the event ledger all carry the current name.
|
|
133
|
+
* Pinned by tests/legacy-acceptance.test.ts; an entry here is a permanent
|
|
134
|
+
* belt, not a cleanup candidate.
|
|
135
|
+
*/
|
|
136
|
+
const LEGACY_COMMAND_NAMES: Readonly<Record<string, CommandName>> = {
|
|
137
|
+
"flag-owner-legacy": "flag-owner",
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export interface TerminalFailurePayload {
|
|
141
|
+
pr: number;
|
|
142
|
+
incident_id: string;
|
|
143
|
+
command_id: string;
|
|
144
|
+
head_sha: string;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export type Command =
|
|
148
|
+
| { command: "rerun-round"; pr: number }
|
|
149
|
+
| { command: "restart-session"; pr: number }
|
|
150
|
+
| ({ command: "resolve-terminal-failure" } & TerminalFailurePayload)
|
|
151
|
+
/** `for_min`: how long the hold is asked for. Optional — a
|
|
152
|
+
* session that omits it gets the largest bounded grant the cap and the
|
|
153
|
+
* day's budget allow, never "until resumed"; an operator's omission IS
|
|
154
|
+
* "until resumed". Which of the two applies comes from the channel. */
|
|
155
|
+
| { command: "pause-pr"; pr: number; reason: string; for_min?: number }
|
|
156
|
+
| { command: "resume-pr"; pr: number }
|
|
157
|
+
| { command: "flag-owner"; message: string }
|
|
158
|
+
| ({ command: "file-improvement-pr" } & ImprovementPayload)
|
|
159
|
+
| ({ command: "file-issue" } & IssuePayload)
|
|
160
|
+
| { command: "slack_reply"; thread: string; text: string }
|
|
161
|
+
/** Deploy-window drain: a hard webhook stop + dispatch/recheck/ping-pong/EOD
|
|
162
|
+
* hold while in-flight rounds finish naturally. `for_min` as on `pause-pr`;
|
|
163
|
+
* only meaningful with `on: true`. `hold` asks for the operator's
|
|
164
|
+
* no-auto-lift drain: only with `on: true`, never with `for_min`.
|
|
165
|
+
* The field says WHAT is asked, never who asks: the lever handler refuses a
|
|
166
|
+
* hold to any issuer but the sealed operator directory's. */
|
|
167
|
+
| { command: "drain"; on: boolean; for_min?: number; hold?: true }
|
|
168
|
+
| { command: "retry-thread-decision"; comment_id: number }
|
|
169
|
+
/** The EOD curator's answer to the day's outcomes packet — per
|
|
170
|
+
* candidate, adopted / declined (with a reason) / refused (the write
|
|
171
|
+
* failed). Schema in src/learner/receipt.ts. */
|
|
172
|
+
| ({ command: "eod-curation-receipt" } & CurationReceipt)
|
|
173
|
+
/** The operator's lesson kill switch (`OPERATOR_COMMANDS`):
|
|
174
|
+
* never valid in a session's file. */
|
|
175
|
+
| { command: "lessons-off" }
|
|
176
|
+
| { command: "lessons-on" }
|
|
177
|
+
| { command: "suppress-lesson"; uri: string }
|
|
178
|
+
| { command: "unsuppress-lesson"; uri: string }
|
|
179
|
+
/** The operator's archive verbs (`OPERATOR_COMMANDS`): never
|
|
180
|
+
* valid in a session's file. */
|
|
181
|
+
| { command: "archive-run" }
|
|
182
|
+
| { command: "archive-restore"; pr: number }
|
|
183
|
+
/** The operator's round controls (`CONTROL_COMMANDS`): never valid in a
|
|
184
|
+
* session's file. */
|
|
185
|
+
| { command: "steer"; pr: number; text: string }
|
|
186
|
+
| { command: "follow-up"; pr: number; text: string }
|
|
187
|
+
| { command: "abort-round"; pr: number }
|
|
188
|
+
| { command: "message"; pr: number; text: string; may_post?: boolean }
|
|
189
|
+
| { command: "takeover"; pr: number }
|
|
190
|
+
| { command: "release"; pr: number }
|
|
191
|
+
| { command: "debug-round"; pr: number };
|
|
192
|
+
|
|
193
|
+
/** A round control as the daemon's `operatorControl` handler receives it. */
|
|
194
|
+
export type ControlCommand = Extract<Command, { command: ControlCommandName }>;
|
|
195
|
+
|
|
196
|
+
/** Who a command directory's writer is. Decided by which directory a file
|
|
197
|
+
* arrived in and whether the operator seal held — never by a field in the
|
|
198
|
+
* file. `session` is the default everywhere: it is the bounded side. */
|
|
199
|
+
export type CommandIssuer = LeverIssuer;
|
|
200
|
+
|
|
201
|
+
/** Context a command file carries with it (its own name, for idempotence,
|
|
202
|
+
* and the issuer the watcher stamped from the channel). */
|
|
203
|
+
export interface CommandContext {
|
|
204
|
+
/** The command file's name without `.json` — `<scope>/<name>` when the
|
|
205
|
+
* watcher serving it has a `scope` — or "" for a direct executeCommand
|
|
206
|
+
* call. It names one delivery, stable across that file's retries. */
|
|
207
|
+
file: string;
|
|
208
|
+
/** Exact parsed file version; replacements are fresh deliveries. */
|
|
209
|
+
delivery_key?: string;
|
|
210
|
+
issuer: CommandIssuer;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Handlers the daemon wires; each may reject by returning {ok:false, reason}. */
|
|
214
|
+
export interface CommandDeps {
|
|
215
|
+
rerunRound(pr: number): Promise<FileOutcome | void> | FileOutcome | void;
|
|
216
|
+
restartSession(
|
|
217
|
+
target: number,
|
|
218
|
+
deliveryKey?: string,
|
|
219
|
+
): Promise<FileOutcome | void> | FileOutcome | void;
|
|
220
|
+
resolveTerminalFailure(
|
|
221
|
+
payload: TerminalFailurePayload,
|
|
222
|
+
): Promise<FileOutcome | void> | FileOutcome | void;
|
|
223
|
+
/** The two bounded levers receive the issuer the watcher stamped
|
|
224
|
+
* from the channel, plus the optional asked-for duration. */
|
|
225
|
+
pausePr(
|
|
226
|
+
pr: number,
|
|
227
|
+
reason: string,
|
|
228
|
+
issuer: CommandIssuer,
|
|
229
|
+
forMin: number | undefined,
|
|
230
|
+
deliveryKey?: string,
|
|
231
|
+
): Promise<FileOutcome | void> | FileOutcome | void;
|
|
232
|
+
resumePr(pr: number, issuer: CommandIssuer): Promise<FileOutcome | void> | FileOutcome | void;
|
|
233
|
+
flagOwner(message: string): Promise<FileOutcome | void> | FileOutcome | void;
|
|
234
|
+
fileImprovementPr(
|
|
235
|
+
payload: ImprovementPayload,
|
|
236
|
+
deliveryKey?: string,
|
|
237
|
+
): Promise<FileOutcome | void> | FileOutcome | void;
|
|
238
|
+
fileIssue(
|
|
239
|
+
payload: IssuePayload,
|
|
240
|
+
deliveryKey?: string,
|
|
241
|
+
): Promise<FileOutcome | void> | FileOutcome | void;
|
|
242
|
+
/** Deploy-window drain valve. Optional like `slackReply`: a daemon wired
|
|
243
|
+
* without it rejects the command with a reason instead of pretending. */
|
|
244
|
+
drain?(
|
|
245
|
+
on: boolean,
|
|
246
|
+
issuer: CommandIssuer,
|
|
247
|
+
forMin: number | undefined,
|
|
248
|
+
deliveryKey?: string,
|
|
249
|
+
hold?: boolean,
|
|
250
|
+
): Promise<FileOutcome | void> | FileOutcome | void;
|
|
251
|
+
retryThreadDecision?(commentId: number): Promise<FileOutcome | void> | FileOutcome | void;
|
|
252
|
+
/** The learner's receipt handler. Optional like `drain`: a
|
|
253
|
+
* daemon wired without the learner rejects the command with a reason. */
|
|
254
|
+
curationReceipt?(receipt: CurationReceipt): Promise<FileOutcome | void> | FileOutcome | void;
|
|
255
|
+
/** The operator's lesson kill switch (state.db). Reached only for an
|
|
256
|
+
* operator-issued command; optional like `drain`. */
|
|
257
|
+
lessonSwitch?(
|
|
258
|
+
change: LessonSwitchChange,
|
|
259
|
+
file: string,
|
|
260
|
+
): Promise<FileOutcome | void> | FileOutcome | void;
|
|
261
|
+
/** The operator's archive verbs — one immediate bounded tick of the
|
|
262
|
+
* archiver, and the restore of one PR's archive to its live paths. Reached
|
|
263
|
+
* only for an operator-issued command; optional like `drain`. */
|
|
264
|
+
archiveRun?(file: string): Promise<FileOutcome | void> | FileOutcome | void;
|
|
265
|
+
archiveRestore?(pr: number, file: string): Promise<FileOutcome | void> | FileOutcome | void;
|
|
266
|
+
/** The operator's round controls (`CONTROL_COMMANDS`), forwarded to the PR's
|
|
267
|
+
* host. Reached only for an operator-issued command; optional like `drain`,
|
|
268
|
+
* so a daemon with no worker host refuses them with a reason. */
|
|
269
|
+
operatorControl?(
|
|
270
|
+
command: ControlCommand,
|
|
271
|
+
file: string,
|
|
272
|
+
): Promise<FileOutcome | void> | FileOutcome | void;
|
|
273
|
+
/** Optional: absent when Slack is disabled, in which case `slack_reply` is
|
|
274
|
+
* rejected with a reason instead of silently succeeding. */
|
|
275
|
+
slackReply?(payload: {
|
|
276
|
+
thread: string;
|
|
277
|
+
text: string;
|
|
278
|
+
file: string;
|
|
279
|
+
}): Promise<FileOutcome | void> | FileOutcome | void;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export type ValidationResult = { ok: true; command: Command } | { ok: false; reason: string };
|
|
283
|
+
|
|
284
|
+
// ---- hand-rolled schema validation ----
|
|
285
|
+
|
|
286
|
+
function fieldError(obj: Record<string, unknown>, allowed: string[]): string | null {
|
|
287
|
+
const unknown = Object.keys(obj).filter((k) => k !== "command" && !allowed.includes(k));
|
|
288
|
+
return unknown.length > 0 ? `unknown field(s): ${unknown.join(", ")}` : null;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function isPrNumber(v: unknown): v is number {
|
|
292
|
+
return typeof v === "number" && Number.isInteger(v) && v > 0;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function isGitHubId(v: unknown): v is number {
|
|
296
|
+
return typeof v === "number" && Number.isSafeInteger(v) && v > 0;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function isNonEmptyString(v: unknown): v is string {
|
|
300
|
+
return typeof v === "string" && v.trim().length > 0;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** The optional `for_min` of a lever command: absent, or a positive integer.
|
|
304
|
+
* The CAP is not checked here — the validator does not know the channel; the
|
|
305
|
+
* lever handler applies it with the issuer in hand. */
|
|
306
|
+
function forMinField(
|
|
307
|
+
obj: Record<string, unknown>,
|
|
308
|
+
): { ok: true; value: number | undefined } | { ok: false; reason: string } {
|
|
309
|
+
const v = obj["for_min"];
|
|
310
|
+
if (v === undefined) return { ok: true, value: undefined };
|
|
311
|
+
if (typeof v !== "number" || !Number.isInteger(v) || v < 1) {
|
|
312
|
+
return {
|
|
313
|
+
ok: false,
|
|
314
|
+
reason: "'for_min' must be a positive integer number of minutes when present",
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
return { ok: true, value: v };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Where a command file came from, as far as validation needs to know: the
|
|
321
|
+
* operator's commands (`OPERATOR_COMMANDS`) validate only from the sealed
|
|
322
|
+
* operator channel. The default is the bounded side. */
|
|
323
|
+
export interface ValidateOptions {
|
|
324
|
+
issuer?: CommandIssuer;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** Validate a parsed command file against the whitelist + per-command schema. */
|
|
328
|
+
export function validateCommand(raw: unknown, opts: ValidateOptions = {}): ValidationResult {
|
|
329
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
330
|
+
return { ok: false, reason: "command file must be a JSON object" };
|
|
331
|
+
}
|
|
332
|
+
const obj = raw as Record<string, unknown>;
|
|
333
|
+
const name = obj["command"];
|
|
334
|
+
if (typeof name !== "string") return { ok: false, reason: "missing 'command' field" };
|
|
335
|
+
if (isOperatorCommand(name)) {
|
|
336
|
+
if (opts.issuer !== "operator")
|
|
337
|
+
return {
|
|
338
|
+
ok: false,
|
|
339
|
+
reason:
|
|
340
|
+
`command '${name}' is the OPERATOR's alone: it is accepted only from the root-sealed commands/operator/ ` +
|
|
341
|
+
"channel (`qcr lessons off|on|suppress|unsuppress`, `qcr archive run|restore`), never from a session's command file",
|
|
342
|
+
};
|
|
343
|
+
return validateOperatorCommand(name, obj);
|
|
344
|
+
}
|
|
345
|
+
// Legacy-name fold FIRST: an aliased spelling validates against — and parses
|
|
346
|
+
// into — its current name, so downstream (tier table, dispatch, result
|
|
347
|
+
// files) never sees the retired spelling.
|
|
348
|
+
const canonical = LEGACY_COMMAND_NAMES[name] ?? name;
|
|
349
|
+
if (!(COMMAND_WHITELIST as readonly string[]).includes(canonical)) {
|
|
350
|
+
// A retired command gets its own reason: a session spawned on an older
|
|
351
|
+
// prompt can still emit it and should learn why, not just "unknown". The
|
|
352
|
+
// hint is read by a model, so it must not describe a NEEDS_HUMAN park
|
|
353
|
+
// either — no such park exists.
|
|
354
|
+
if (name === "mark-escalation-worthy") {
|
|
355
|
+
return {
|
|
356
|
+
ok: false,
|
|
357
|
+
reason:
|
|
358
|
+
"command 'mark-escalation-worthy' was RETIRED on 2026-08-18: escalation-worthiness no longer exists, " +
|
|
359
|
+
"and the review is never handed to a person — every round decides APPROVE or REQUEST_CHANGES on the merits. " +
|
|
360
|
+
"Use pause-pr to hold one, or flag-owner (formerly flag-owner-legacy — the old name is still accepted) to raise it.",
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
return {
|
|
364
|
+
ok: false,
|
|
365
|
+
reason: `command '${name}' is not whitelisted (allowed: ${COMMAND_WHITELIST.join(", ")})`,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
switch (canonical as CommandName) {
|
|
370
|
+
case "rerun-round": {
|
|
371
|
+
const err = fieldError(obj, ["pr"]);
|
|
372
|
+
if (err) return { ok: false, reason: err };
|
|
373
|
+
if (!isPrNumber(obj["pr"])) return { ok: false, reason: "'pr' must be a positive integer" };
|
|
374
|
+
return { ok: true, command: { command: "rerun-round", pr: obj["pr"] } };
|
|
375
|
+
}
|
|
376
|
+
case "restart-session": {
|
|
377
|
+
const err = fieldError(obj, ["pr"]);
|
|
378
|
+
if (err) return { ok: false, reason: err };
|
|
379
|
+
const pr = obj["pr"];
|
|
380
|
+
if (!isPrNumber(pr)) {
|
|
381
|
+
return { ok: false, reason: "'pr' must be a positive integer" };
|
|
382
|
+
}
|
|
383
|
+
return { ok: true, command: { command: "restart-session", pr: pr } };
|
|
384
|
+
}
|
|
385
|
+
case "resolve-terminal-failure": {
|
|
386
|
+
const err = fieldError(obj, ["pr", "incident_id", "command_id", "head_sha"]);
|
|
387
|
+
if (err) return { ok: false, reason: err };
|
|
388
|
+
if (!isPrNumber(obj["pr"])) return { ok: false, reason: "'pr' must be a positive integer" };
|
|
389
|
+
for (const field of ["incident_id", "command_id"]) {
|
|
390
|
+
if (!isNonEmptyString(obj[field]))
|
|
391
|
+
return { ok: false, reason: `'${field}' must be a non-empty string` };
|
|
392
|
+
}
|
|
393
|
+
// An authoritative GitHub 404 can legitimately correspond to a legacy
|
|
394
|
+
// synthetic debt whose stored head is the empty string. Closed PRs are
|
|
395
|
+
// still forced to a non-empty exact match by resolveTerminalFailure().
|
|
396
|
+
if (typeof obj["head_sha"] !== "string")
|
|
397
|
+
return { ok: false, reason: "'head_sha' must be a string" };
|
|
398
|
+
if (obj["incident_id"] !== `review-failure:${obj["command_id"] as string}`) {
|
|
399
|
+
return {
|
|
400
|
+
ok: false,
|
|
401
|
+
reason: "'incident_id' must exactly equal review-failure:<command_id>",
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
return {
|
|
405
|
+
ok: true,
|
|
406
|
+
command: {
|
|
407
|
+
command: "resolve-terminal-failure",
|
|
408
|
+
pr: obj["pr"],
|
|
409
|
+
incident_id: obj["incident_id"],
|
|
410
|
+
command_id: obj["command_id"] as string,
|
|
411
|
+
head_sha: obj["head_sha"],
|
|
412
|
+
},
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
case "pause-pr": {
|
|
416
|
+
const err = fieldError(obj, ["pr", "reason", "for_min"]);
|
|
417
|
+
if (err) return { ok: false, reason: err };
|
|
418
|
+
if (!isPrNumber(obj["pr"])) return { ok: false, reason: "'pr' must be a positive integer" };
|
|
419
|
+
if (!isNonEmptyString(obj["reason"]))
|
|
420
|
+
return { ok: false, reason: "'reason' must be a non-empty string" };
|
|
421
|
+
const forMin = forMinField(obj);
|
|
422
|
+
if (!forMin.ok) return forMin;
|
|
423
|
+
return {
|
|
424
|
+
ok: true,
|
|
425
|
+
command: {
|
|
426
|
+
command: "pause-pr",
|
|
427
|
+
pr: obj["pr"],
|
|
428
|
+
reason: obj["reason"],
|
|
429
|
+
...(forMin.value === undefined ? {} : { for_min: forMin.value }),
|
|
430
|
+
},
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
case "resume-pr": {
|
|
434
|
+
const err = fieldError(obj, ["pr"]);
|
|
435
|
+
if (err) return { ok: false, reason: err };
|
|
436
|
+
if (!isPrNumber(obj["pr"])) return { ok: false, reason: "'pr' must be a positive integer" };
|
|
437
|
+
return { ok: true, command: { command: "resume-pr", pr: obj["pr"] } };
|
|
438
|
+
}
|
|
439
|
+
case "flag-owner": {
|
|
440
|
+
const err = fieldError(obj, ["message"]);
|
|
441
|
+
if (err) return { ok: false, reason: err };
|
|
442
|
+
if (!isNonEmptyString(obj["message"]))
|
|
443
|
+
return { ok: false, reason: "'message' must be a non-empty string" };
|
|
444
|
+
return { ok: true, command: { command: "flag-owner", message: obj["message"] } };
|
|
445
|
+
}
|
|
446
|
+
case "file-improvement-pr": {
|
|
447
|
+
const err = fieldError(obj, ["branch", "title", "body", "slug"]);
|
|
448
|
+
if (err) return { ok: false, reason: err };
|
|
449
|
+
for (const f of ["branch", "title", "body", "slug"]) {
|
|
450
|
+
if (!isNonEmptyString(obj[f]))
|
|
451
|
+
return { ok: false, reason: `'${f}' must be a non-empty string` };
|
|
452
|
+
}
|
|
453
|
+
const slug = obj["slug"] as string;
|
|
454
|
+
if (!SLUG_RE.test(slug)) {
|
|
455
|
+
return {
|
|
456
|
+
ok: false,
|
|
457
|
+
reason: `'slug' must match ${SLUG_RE.source} (it becomes bot/improve-<slug>)`,
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
// The session writes `branch` and the daemon hands it to git: only a
|
|
461
|
+
// plain local branch name passes — never an option (`--quiet`), a force
|
|
462
|
+
// refspec (`+wip/x`), a refspec (`a:b`) or a revision expression (`a..b`).
|
|
463
|
+
const branch = obj["branch"] as string;
|
|
464
|
+
if (!isBranchName(branch)) {
|
|
465
|
+
return {
|
|
466
|
+
ok: false,
|
|
467
|
+
reason: `'branch' must be a plain local branch name matching ${BRANCH_RE.source} (at most ${BRANCH_MAX} chars)`,
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
return {
|
|
471
|
+
ok: true,
|
|
472
|
+
command: {
|
|
473
|
+
command: "file-improvement-pr",
|
|
474
|
+
branch,
|
|
475
|
+
title: obj["title"] as string,
|
|
476
|
+
body: obj["body"] as string,
|
|
477
|
+
slug,
|
|
478
|
+
},
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
case "file-issue": {
|
|
482
|
+
// `pr` is optional provenance — the PR a deferral's ticket was filed
|
|
483
|
+
// from. The review session files the ticket itself, in the round that
|
|
484
|
+
// deferred; `pr` attributes the filing — and a cap refusal, which then
|
|
485
|
+
// leaves a `deferral_ticket_refused` anomaly — to that PR, so the refusal
|
|
486
|
+
// is visible where the deferral happened. It grants no rights and lifts
|
|
487
|
+
// no cap.
|
|
488
|
+
const err = fieldError(obj, ["title", "body", "pr"]);
|
|
489
|
+
if (err) return { ok: false, reason: err };
|
|
490
|
+
for (const f of ["title", "body"]) {
|
|
491
|
+
if (!isNonEmptyString(obj[f]))
|
|
492
|
+
return { ok: false, reason: `'${f}' must be a non-empty string` };
|
|
493
|
+
}
|
|
494
|
+
if (obj["pr"] !== undefined && !isPrNumber(obj["pr"])) {
|
|
495
|
+
return { ok: false, reason: "'pr' must be a positive integer when present" };
|
|
496
|
+
}
|
|
497
|
+
return {
|
|
498
|
+
ok: true,
|
|
499
|
+
command: {
|
|
500
|
+
command: "file-issue",
|
|
501
|
+
title: obj["title"] as string,
|
|
502
|
+
body: obj["body"] as string,
|
|
503
|
+
...(obj["pr"] === undefined ? {} : { pr: obj["pr"] }),
|
|
504
|
+
},
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
case "drain": {
|
|
508
|
+
const err = fieldError(obj, ["on", "for_min", "hold"]);
|
|
509
|
+
if (err) return { ok: false, reason: err };
|
|
510
|
+
if (typeof obj["on"] !== "boolean") {
|
|
511
|
+
return {
|
|
512
|
+
ok: false,
|
|
513
|
+
reason: "'on' must be a boolean (true = engage the drain, false = lift it)",
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
const forMin = forMinField(obj);
|
|
517
|
+
if (!forMin.ok) return forMin;
|
|
518
|
+
if (forMin.value !== undefined && !obj["on"])
|
|
519
|
+
return { ok: false, reason: "'for_min' only applies with on = true" };
|
|
520
|
+
const hold = obj["hold"];
|
|
521
|
+
if (hold !== undefined && typeof hold !== "boolean")
|
|
522
|
+
return { ok: false, reason: "'hold' must be a boolean" };
|
|
523
|
+
if (hold === true && !obj["on"])
|
|
524
|
+
return { ok: false, reason: "'hold' only applies with on = true (drain off lifts a hold)" };
|
|
525
|
+
if (hold === true && forMin.value !== undefined)
|
|
526
|
+
return {
|
|
527
|
+
ok: false,
|
|
528
|
+
reason: "'hold' has no expiry; 'for_min' and 'hold' exclude each other",
|
|
529
|
+
};
|
|
530
|
+
return {
|
|
531
|
+
ok: true,
|
|
532
|
+
command: {
|
|
533
|
+
command: "drain",
|
|
534
|
+
on: obj["on"],
|
|
535
|
+
...(forMin.value === undefined ? {} : { for_min: forMin.value }),
|
|
536
|
+
...(hold === true ? { hold: true as const } : {}),
|
|
537
|
+
},
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
case "retry-thread-decision": {
|
|
541
|
+
const err = fieldError(obj, ["comment_id"]);
|
|
542
|
+
if (err) return { ok: false, reason: err };
|
|
543
|
+
if (!isGitHubId(obj["comment_id"]))
|
|
544
|
+
return { ok: false, reason: "'comment_id' must be a positive safe integer" };
|
|
545
|
+
return {
|
|
546
|
+
ok: true,
|
|
547
|
+
command: { command: "retry-thread-decision", comment_id: obj["comment_id"] },
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
case "eod-curation-receipt": {
|
|
551
|
+
const res = validateReceipt(obj);
|
|
552
|
+
if (!res.ok) return res;
|
|
553
|
+
return { ok: true, command: { command: "eod-curation-receipt", ...res.receipt } };
|
|
554
|
+
}
|
|
555
|
+
case "slack_reply": {
|
|
556
|
+
const err = fieldError(obj, ["thread", "text"]);
|
|
557
|
+
if (err) return { ok: false, reason: err };
|
|
558
|
+
// No `channel` field, by construction: the daemon binds every reply to
|
|
559
|
+
// cfg.slack.channel, so posting in the wrong place is unrepresentable.
|
|
560
|
+
if (!isNonEmptyString(obj["thread"]) || !SLACK_TS_RE.test(obj["thread"])) {
|
|
561
|
+
return {
|
|
562
|
+
ok: false,
|
|
563
|
+
reason: "'thread' must be a Slack message ts like \"1712345678.123456\"",
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
if (!isNonEmptyString(obj["text"]))
|
|
567
|
+
return { ok: false, reason: "'text' must be a non-empty string" };
|
|
568
|
+
return {
|
|
569
|
+
ok: true,
|
|
570
|
+
command: { command: "slack_reply", thread: obj["thread"], text: obj["text"] },
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/** The operator commands' strict schemas (the issuer was checked first). */
|
|
577
|
+
function validateOperatorCommand(
|
|
578
|
+
name: OperatorCommandName,
|
|
579
|
+
obj: Record<string, unknown>,
|
|
580
|
+
): ValidationResult {
|
|
581
|
+
switch (name) {
|
|
582
|
+
case "lessons-off":
|
|
583
|
+
case "lessons-on": {
|
|
584
|
+
const err = fieldError(obj, []);
|
|
585
|
+
if (err) return { ok: false, reason: err };
|
|
586
|
+
return { ok: true, command: { command: name } };
|
|
587
|
+
}
|
|
588
|
+
case "suppress-lesson":
|
|
589
|
+
case "unsuppress-lesson": {
|
|
590
|
+
const err = fieldError(obj, ["uri"]);
|
|
591
|
+
if (err) return { ok: false, reason: err };
|
|
592
|
+
const uri = obj["uri"];
|
|
593
|
+
if (typeof uri !== "string" || !LESSON_URI_RE.test(uri) || uri.includes(".."))
|
|
594
|
+
return {
|
|
595
|
+
ok: false,
|
|
596
|
+
reason:
|
|
597
|
+
"'uri' must be one memory entry's full URI (viking://user/<user>/memories/<area>/<file>.md)",
|
|
598
|
+
};
|
|
599
|
+
return { ok: true, command: { command: name, uri } };
|
|
600
|
+
}
|
|
601
|
+
case "archive-run": {
|
|
602
|
+
const err = fieldError(obj, []);
|
|
603
|
+
if (err) return { ok: false, reason: err };
|
|
604
|
+
return { ok: true, command: { command: "archive-run" } };
|
|
605
|
+
}
|
|
606
|
+
case "archive-restore": {
|
|
607
|
+
const err = fieldError(obj, ["pr"]);
|
|
608
|
+
if (err) return { ok: false, reason: err };
|
|
609
|
+
if (!isPrNumber(obj["pr"])) return { ok: false, reason: "'pr' must be a positive integer" };
|
|
610
|
+
return { ok: true, command: { command: "archive-restore", pr: obj["pr"] } };
|
|
611
|
+
}
|
|
612
|
+
case "steer":
|
|
613
|
+
case "follow-up":
|
|
614
|
+
case "message": {
|
|
615
|
+
const err = fieldError(obj, name === "message" ? ["pr", "text", "may_post"] : ["pr", "text"]);
|
|
616
|
+
if (err) return { ok: false, reason: err };
|
|
617
|
+
if (!isPrNumber(obj["pr"])) return { ok: false, reason: "'pr' must be a positive integer" };
|
|
618
|
+
const text = obj["text"];
|
|
619
|
+
if (!isNonEmptyString(text) || text.length > CONTROL_TEXT_MAX)
|
|
620
|
+
return {
|
|
621
|
+
ok: false,
|
|
622
|
+
reason: `'text' must be a non-empty string of at most ${CONTROL_TEXT_MAX} characters`,
|
|
623
|
+
};
|
|
624
|
+
if (name !== "message") return { ok: true, command: { command: name, pr: obj["pr"], text } };
|
|
625
|
+
const mayPost = obj["may_post"];
|
|
626
|
+
if (mayPost !== undefined && typeof mayPost !== "boolean")
|
|
627
|
+
return { ok: false, reason: "'may_post' must be a boolean when present" };
|
|
628
|
+
return {
|
|
629
|
+
ok: true,
|
|
630
|
+
command: {
|
|
631
|
+
command: "message",
|
|
632
|
+
pr: obj["pr"],
|
|
633
|
+
text,
|
|
634
|
+
...(mayPost === undefined ? {} : { may_post: mayPost }),
|
|
635
|
+
},
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
case "abort-round":
|
|
639
|
+
case "takeover":
|
|
640
|
+
case "release":
|
|
641
|
+
case "debug-round": {
|
|
642
|
+
const err = fieldError(obj, ["pr"]);
|
|
643
|
+
if (err) return { ok: false, reason: err };
|
|
644
|
+
if (!isPrNumber(obj["pr"])) return { ok: false, reason: "'pr' must be a positive integer" };
|
|
645
|
+
return { ok: true, command: { command: name, pr: obj["pr"] } };
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// ---- tier enforcement ----
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Commands a Slack turn may run, by tier. The table is data on purpose: adding
|
|
654
|
+
* a command without deciding its tier is then impossible to do by accident.
|
|
655
|
+
*
|
|
656
|
+
* `own` = allowed for tier `author` only when the target PR is in the origin's
|
|
657
|
+
* `authorized_prs` (recomputed by the router from `pr_state.author`, NEVER read
|
|
658
|
+
* out of the inject body).
|
|
659
|
+
*
|
|
660
|
+
* `owner` is the owner's tier (legacy persisted records with the retired tier
|
|
661
|
+
* spelling fold to it on read). The `companion` tier is not a column: it is the
|
|
662
|
+
* owner's column read again (`tierAllows`), so the two are one shared set and a
|
|
663
|
+
* verb added for the owner is the companion's in the same edit (by ruling:
|
|
664
|
+
* the companion can do everything the owner can). `system` is the
|
|
665
|
+
* daemon's own unattended turns (`OriginStore.writeLocal`): it is `true` on
|
|
666
|
+
* every row but the operator's round controls (`CONTROL_COMMANDS`, which a
|
|
667
|
+
* daemon turn has no standing to send) because a
|
|
668
|
+
* daemon turn with NO origin record already runs unrestricted —
|
|
669
|
+
* `authorizeCommand(_, null)` ⇒ ok, and that null-origin rule is unchanged.
|
|
670
|
+
* The column is the stamped, auditable form of that same
|
|
671
|
+
* standing authority; it never widens anything, and a Slack turn can never
|
|
672
|
+
* reach it (`computeTier` never returns `system`).
|
|
673
|
+
*
|
|
674
|
+
* Nothing here can reach the dry_run flag, a deploy, an approval or anything
|
|
675
|
+
* review-dismissing: those are not commands at all, and this feature adds no
|
|
676
|
+
* GitHub mutation — every write still goes through the MutationGate.
|
|
677
|
+
*/
|
|
678
|
+
const TIER_TABLE: Record<
|
|
679
|
+
CommandName | ControlCommandName,
|
|
680
|
+
{ anyone: boolean; author: "own" | boolean; owner: boolean; system: boolean }
|
|
681
|
+
> = {
|
|
682
|
+
"rerun-round": { anyone: false, author: "own", owner: true, system: true },
|
|
683
|
+
"pause-pr": { anyone: false, author: "own", owner: true, system: true },
|
|
684
|
+
"resume-pr": { anyone: false, author: "own", owner: true, system: true },
|
|
685
|
+
// The safety valve: anyone may always reach a human.
|
|
686
|
+
"flag-owner": { anyone: true, author: true, owner: true, system: true },
|
|
687
|
+
"restart-session": { anyone: false, author: false, owner: true, system: true },
|
|
688
|
+
// Terminal debt cleanup is destructive and never author-reachable. Its
|
|
689
|
+
// handler additionally requires exact identity + authoritative terminality.
|
|
690
|
+
"resolve-terminal-failure": { anyone: false, author: false, owner: true, system: true },
|
|
691
|
+
// improvements.ts caps still apply ON TOP — a tier never lifts a cap.
|
|
692
|
+
"file-improvement-pr": { anyone: false, author: false, owner: true, system: true },
|
|
693
|
+
// The review session files a deferral's ticket itself, in the round that
|
|
694
|
+
// deferred — there is no author confirmation step. Its command files land
|
|
695
|
+
// in the `commands/` dir with no Slack origin, which already runs at full
|
|
696
|
+
// (system-column) autonomy, so the deferral path needs no widening here. On
|
|
697
|
+
// Slack, `file-issue` is the owner's alone. The day cap, the dedup and the
|
|
698
|
+
// gate all still apply downstream — a tier never lifts a cap.
|
|
699
|
+
"file-issue": { anyone: false, author: false, owner: true, system: true },
|
|
700
|
+
// Not user-tiered: gated by channel binding, thread, caps, cooldown, length.
|
|
701
|
+
slack_reply: { anyone: true, author: true, owner: true, system: true },
|
|
702
|
+
// Deploy-window drain: an operator lever (same standing as restart-session)
|
|
703
|
+
// — it stops the whole board, so it is never author-reachable.
|
|
704
|
+
drain: { anyone: false, author: false, owner: true, system: true },
|
|
705
|
+
"retry-thread-decision": { anyone: false, author: false, owner: true, system: true },
|
|
706
|
+
// The EOD curator's receipt — a daemon (system) turn's, or the
|
|
707
|
+
// owner's; never a Slack author's or anyone's.
|
|
708
|
+
"eod-curation-receipt": { anyone: false, author: false, owner: true, system: true },
|
|
709
|
+
// The operator's round controls reach the daemon only from the sealed
|
|
710
|
+
// operator channel (`validateCommand`), so this row never admits a Slack
|
|
711
|
+
// turn; it is the verb set the owner and the companion share. A daemon turn
|
|
712
|
+
// (`system`) never steers, takes over or releases a PR.
|
|
713
|
+
steer: { anyone: false, author: false, owner: true, system: false },
|
|
714
|
+
"follow-up": { anyone: false, author: false, owner: true, system: false },
|
|
715
|
+
"abort-round": { anyone: false, author: false, owner: true, system: false },
|
|
716
|
+
message: { anyone: false, author: false, owner: true, system: false },
|
|
717
|
+
takeover: { anyone: false, author: false, owner: true, system: false },
|
|
718
|
+
release: { anyone: false, author: false, owner: true, system: false },
|
|
719
|
+
"debug-round": { anyone: false, author: false, owner: true, system: false },
|
|
720
|
+
};
|
|
721
|
+
|
|
722
|
+
/** A tier the table is read for besides a Slack turn's: the companion, whose
|
|
723
|
+
* every verb is the owner's. */
|
|
724
|
+
export type CommandTier = "owner" | "companion" | "system";
|
|
725
|
+
|
|
726
|
+
/** May `tier` send `name`? `companion` reads the owner's column: one set. */
|
|
727
|
+
export function tierAllows(tier: CommandTier, name: string): boolean {
|
|
728
|
+
const row = (TIER_TABLE as Record<string, (typeof TIER_TABLE)[CommandName] | undefined>)[name];
|
|
729
|
+
if (!row) return false;
|
|
730
|
+
return tier === "system" ? row.system : row.owner;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/** Every verb `tier` may send, in table order. */
|
|
734
|
+
export function tierVerbs(tier: CommandTier): string[] {
|
|
735
|
+
return Object.keys(TIER_TABLE).filter((name) => tierAllows(tier, name));
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/** Target PR of a command, when it has one. */
|
|
739
|
+
function targetPr(command: Command): number | null {
|
|
740
|
+
if (command.command === "restart-session")
|
|
741
|
+
return typeof command.pr === "number" ? command.pr : null;
|
|
742
|
+
return "pr" in command && typeof command.pr === "number" ? command.pr : null;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
export type AuthorizationResult = { ok: true } | { ok: false; reason: string };
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* May this command run on behalf of `origin`?
|
|
749
|
+
*
|
|
750
|
+
* `origin === null` means "no Slack turn is live" — the command came from the
|
|
751
|
+
* daemon itself (EOD sweep, anomaly wake) or from the owner at the tmux pane,
|
|
752
|
+
* and keeps full autonomy (the same rights the `system` tier stamps
|
|
753
|
+
* explicitly). A caller that must fail closed (the Slack command directory)
|
|
754
|
+
* checks for a missing origin BEFORE calling this.
|
|
755
|
+
*
|
|
756
|
+
* Every rejection reason starts with `tier: ` so the overseer — and the
|
|
757
|
+
* `command_rejected` event stream — can tell a rights refusal from a cap.
|
|
758
|
+
*/
|
|
759
|
+
export function authorizeCommand(
|
|
760
|
+
command: Command,
|
|
761
|
+
origin: OriginRecord | null,
|
|
762
|
+
): AuthorizationResult {
|
|
763
|
+
if (isOperatorCommand(command.command))
|
|
764
|
+
return {
|
|
765
|
+
ok: false,
|
|
766
|
+
reason: `tier: '${command.command}' is the operator's alone (commands/operator/), never a Slack turn's`,
|
|
767
|
+
};
|
|
768
|
+
if (origin === null) return { ok: true };
|
|
769
|
+
const row = TIER_TABLE[command.command];
|
|
770
|
+
const pr = targetPr(command);
|
|
771
|
+
switch (origin.tier) {
|
|
772
|
+
case "owner":
|
|
773
|
+
return row.owner
|
|
774
|
+
? { ok: true }
|
|
775
|
+
: { ok: false, reason: `tier: '${command.command}' is not available` };
|
|
776
|
+
case "system":
|
|
777
|
+
return row.system
|
|
778
|
+
? { ok: true }
|
|
779
|
+
: { ok: false, reason: `tier: '${command.command}' is not available` };
|
|
780
|
+
case "author": {
|
|
781
|
+
if (row.author === true) return { ok: true };
|
|
782
|
+
if (row.author === false) {
|
|
783
|
+
return {
|
|
784
|
+
ok: false,
|
|
785
|
+
reason: `tier: '${command.command}' requires tier owner (this turn is tier author)`,
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
if (pr === null)
|
|
789
|
+
return {
|
|
790
|
+
ok: false,
|
|
791
|
+
reason: `tier: '${command.command}' needs a PR number to check ownership`,
|
|
792
|
+
};
|
|
793
|
+
if (origin.authorized_prs.includes(pr)) return { ok: true };
|
|
794
|
+
return {
|
|
795
|
+
ok: false,
|
|
796
|
+
reason: `tier: PR #${pr} is not authored by this Slack user (authorized: ${
|
|
797
|
+
origin.authorized_prs.length ? origin.authorized_prs.join(", ") : "none"
|
|
798
|
+
})`,
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
default:
|
|
802
|
+
// `anyone` and `ignored` alike: read-only plus the two ungated commands.
|
|
803
|
+
return row.anyone
|
|
804
|
+
? { ok: true }
|
|
805
|
+
: {
|
|
806
|
+
ok: false,
|
|
807
|
+
reason: `tier: '${command.command}' is not available at tier ${origin.tier}`,
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* May the companion send this command? Not a Slack origin: the TUI forwards a
|
|
814
|
+
* companion's action as an operator command, and checks it here first.
|
|
815
|
+
* The verbs are the owner's, no more and no fewer (`tierAllows`).
|
|
816
|
+
*/
|
|
817
|
+
export function authorizeCompanionCommand(command: Command): AuthorizationResult {
|
|
818
|
+
return tierAllows("companion", command.command)
|
|
819
|
+
? { ok: true }
|
|
820
|
+
: { ok: false, reason: `tier: '${command.command}' is not available to the companion` };
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
function normalize(outcome: FileOutcome | void): FileOutcome {
|
|
824
|
+
return outcome === undefined || outcome === null ? { ok: true } : outcome;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/** Dispatch a validated command to the injected handlers. */
|
|
828
|
+
export async function executeCommand(
|
|
829
|
+
command: Command,
|
|
830
|
+
deps: CommandDeps,
|
|
831
|
+
// A direct call with no context is a SESSION's: the bounded side is the
|
|
832
|
+
// default, and only the operator watcher ever stamps the other one.
|
|
833
|
+
ctx: CommandContext = { file: "", issuer: "session" },
|
|
834
|
+
): Promise<FileOutcome> {
|
|
835
|
+
switch (command.command) {
|
|
836
|
+
case "rerun-round":
|
|
837
|
+
return normalize(await deps.rerunRound(command.pr));
|
|
838
|
+
case "restart-session":
|
|
839
|
+
return normalize(
|
|
840
|
+
await deps.restartSession(
|
|
841
|
+
command.pr,
|
|
842
|
+
...(ctx.delivery_key ? ([ctx.delivery_key] as [string]) : []),
|
|
843
|
+
),
|
|
844
|
+
);
|
|
845
|
+
case "resolve-terminal-failure":
|
|
846
|
+
return normalize(
|
|
847
|
+
await deps.resolveTerminalFailure({
|
|
848
|
+
pr: command.pr,
|
|
849
|
+
incident_id: command.incident_id,
|
|
850
|
+
command_id: command.command_id,
|
|
851
|
+
head_sha: command.head_sha,
|
|
852
|
+
}),
|
|
853
|
+
);
|
|
854
|
+
case "pause-pr":
|
|
855
|
+
return normalize(
|
|
856
|
+
await deps.pausePr(
|
|
857
|
+
command.pr,
|
|
858
|
+
command.reason,
|
|
859
|
+
ctx.issuer,
|
|
860
|
+
command.for_min,
|
|
861
|
+
...(ctx.delivery_key ? ([ctx.delivery_key] as [string]) : []),
|
|
862
|
+
),
|
|
863
|
+
);
|
|
864
|
+
case "resume-pr":
|
|
865
|
+
return normalize(await deps.resumePr(command.pr, ctx.issuer));
|
|
866
|
+
case "flag-owner":
|
|
867
|
+
return normalize(await deps.flagOwner(command.message));
|
|
868
|
+
case "file-improvement-pr":
|
|
869
|
+
return normalize(
|
|
870
|
+
await deps.fileImprovementPr(
|
|
871
|
+
{
|
|
872
|
+
branch: command.branch,
|
|
873
|
+
title: command.title,
|
|
874
|
+
body: command.body,
|
|
875
|
+
slug: command.slug,
|
|
876
|
+
},
|
|
877
|
+
...(ctx.delivery_key ? ([ctx.delivery_key] as [string]) : []),
|
|
878
|
+
),
|
|
879
|
+
);
|
|
880
|
+
case "file-issue":
|
|
881
|
+
return normalize(
|
|
882
|
+
await deps.fileIssue(
|
|
883
|
+
{
|
|
884
|
+
title: command.title,
|
|
885
|
+
body: command.body,
|
|
886
|
+
...(command.pr === undefined ? {} : { pr: command.pr }),
|
|
887
|
+
},
|
|
888
|
+
...(ctx.delivery_key ? ([ctx.delivery_key] as [string]) : []),
|
|
889
|
+
),
|
|
890
|
+
);
|
|
891
|
+
case "slack_reply": {
|
|
892
|
+
if (!deps.slackReply)
|
|
893
|
+
return { ok: false, reason: "slack_reply is not wired (Slack is disabled)" };
|
|
894
|
+
return normalize(
|
|
895
|
+
await deps.slackReply({
|
|
896
|
+
thread: command.thread,
|
|
897
|
+
text: command.text,
|
|
898
|
+
// Session filenames are stable replay keys, even after archival.
|
|
899
|
+
// Sealed operator rewrites are separate, versioned deliveries.
|
|
900
|
+
file: ctx.issuer === "operator" ? (ctx.delivery_key ?? ctx.file) : ctx.file,
|
|
901
|
+
}),
|
|
902
|
+
);
|
|
903
|
+
}
|
|
904
|
+
case "drain": {
|
|
905
|
+
if (!deps.drain) return { ok: false, reason: "drain is not wired on this daemon" };
|
|
906
|
+
if (command.hold === true)
|
|
907
|
+
return normalize(
|
|
908
|
+
await deps.drain(command.on, ctx.issuer, command.for_min, ctx.delivery_key, true),
|
|
909
|
+
);
|
|
910
|
+
return normalize(
|
|
911
|
+
await deps.drain(
|
|
912
|
+
command.on,
|
|
913
|
+
ctx.issuer,
|
|
914
|
+
command.for_min,
|
|
915
|
+
...(ctx.delivery_key ? ([ctx.delivery_key] as [string]) : []),
|
|
916
|
+
),
|
|
917
|
+
);
|
|
918
|
+
}
|
|
919
|
+
case "retry-thread-decision": {
|
|
920
|
+
if (!deps.retryThreadDecision)
|
|
921
|
+
return { ok: false, reason: "retry-thread-decision is not wired on this daemon" };
|
|
922
|
+
return normalize(await deps.retryThreadDecision(command.comment_id));
|
|
923
|
+
}
|
|
924
|
+
case "eod-curation-receipt": {
|
|
925
|
+
if (!deps.curationReceipt)
|
|
926
|
+
return {
|
|
927
|
+
ok: false,
|
|
928
|
+
reason: "eod-curation-receipt is not wired on this daemon (no learner)",
|
|
929
|
+
};
|
|
930
|
+
return normalize(
|
|
931
|
+
await deps.curationReceipt({ day: command.day, decisions: command.decisions }),
|
|
932
|
+
);
|
|
933
|
+
}
|
|
934
|
+
case "lessons-off":
|
|
935
|
+
case "lessons-on":
|
|
936
|
+
case "suppress-lesson":
|
|
937
|
+
case "unsuppress-lesson": {
|
|
938
|
+
// The belt behind validation's brace: whoever reached this function,
|
|
939
|
+
// only an operator-stamped context may flip the kill switch.
|
|
940
|
+
if (ctx.issuer !== "operator")
|
|
941
|
+
return {
|
|
942
|
+
ok: false,
|
|
943
|
+
reason: `'${command.command}' is the operator's alone — refused for issuer '${ctx.issuer}'`,
|
|
944
|
+
};
|
|
945
|
+
if (!deps.lessonSwitch)
|
|
946
|
+
return { ok: false, reason: `${command.command} is not wired on this daemon` };
|
|
947
|
+
const change: LessonSwitchChange =
|
|
948
|
+
command.command === "lessons-off"
|
|
949
|
+
? { action: "off" }
|
|
950
|
+
: command.command === "lessons-on"
|
|
951
|
+
? { action: "on" }
|
|
952
|
+
: command.command === "suppress-lesson"
|
|
953
|
+
? { action: "suppress", uri: command.uri }
|
|
954
|
+
: { action: "unsuppress", uri: command.uri };
|
|
955
|
+
return normalize(await deps.lessonSwitch(change, ctx.delivery_key ?? ctx.file));
|
|
956
|
+
}
|
|
957
|
+
case "archive-run":
|
|
958
|
+
case "archive-restore": {
|
|
959
|
+
// The belt behind validation's brace, as for the kill switch.
|
|
960
|
+
if (ctx.issuer !== "operator")
|
|
961
|
+
return {
|
|
962
|
+
ok: false,
|
|
963
|
+
reason: `'${command.command}' is the operator's alone — refused for issuer '${ctx.issuer}'`,
|
|
964
|
+
};
|
|
965
|
+
if (command.command === "archive-run") {
|
|
966
|
+
if (!deps.archiveRun)
|
|
967
|
+
return { ok: false, reason: "archive-run is not wired on this daemon" };
|
|
968
|
+
return normalize(await deps.archiveRun(ctx.delivery_key ?? ctx.file));
|
|
969
|
+
}
|
|
970
|
+
if (!deps.archiveRestore)
|
|
971
|
+
return { ok: false, reason: "archive-restore is not wired on this daemon" };
|
|
972
|
+
return normalize(await deps.archiveRestore(command.pr, ctx.delivery_key ?? ctx.file));
|
|
973
|
+
}
|
|
974
|
+
case "steer":
|
|
975
|
+
case "follow-up":
|
|
976
|
+
case "abort-round":
|
|
977
|
+
case "message":
|
|
978
|
+
case "takeover":
|
|
979
|
+
case "release":
|
|
980
|
+
case "debug-round": {
|
|
981
|
+
// The belt behind validation's brace, as for the kill switch.
|
|
982
|
+
if (ctx.issuer !== "operator")
|
|
983
|
+
return {
|
|
984
|
+
ok: false,
|
|
985
|
+
reason: `'${command.command}' is the operator's alone — refused for issuer '${ctx.issuer}'`,
|
|
986
|
+
};
|
|
987
|
+
if (!deps.operatorControl)
|
|
988
|
+
return { ok: false, reason: `${command.command} is not wired on this daemon` };
|
|
989
|
+
return normalize(await deps.operatorControl(command, ctx.delivery_key ?? ctx.file));
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// ---- the watcher ----
|
|
995
|
+
|
|
996
|
+
/**
|
|
997
|
+
* Which command directory this watcher serves, and how to find the live Slack
|
|
998
|
+
* origin — two directories, one gate.
|
|
999
|
+
*
|
|
1000
|
+
* `local` — the `commands/` dir. Untiered, so EOD sweeps, anomaly wakes and
|
|
1001
|
+
* the owner at the tmux pane keep full autonomy. The one BELT:
|
|
1002
|
+
* when the turn being served was CAUSED by a non-owner Slack
|
|
1003
|
+
* message, this dir refuses — the overseer
|
|
1004
|
+
* wrote to the wrong place mid-turn, and a tier-checked command
|
|
1005
|
+
* must not slip past by choosing a filename.
|
|
1006
|
+
* `slack` — the tier-checked `commands/slack/` dir. No live origin ⇒ DENY.
|
|
1007
|
+
*
|
|
1008
|
+
* Omit the gate entirely and the directory is served with no tier check.
|
|
1009
|
+
*/
|
|
1010
|
+
export interface CommandTierGate {
|
|
1011
|
+
dir: "local" | "slack";
|
|
1012
|
+
/**
|
|
1013
|
+
* What this gate reads, and it differs by dir — both directions fail closed:
|
|
1014
|
+
*
|
|
1015
|
+
* `slack` — the origin of the turn being served, while it is live
|
|
1016
|
+
* (`OriginStore.latest()`; never a queued wake's rights).
|
|
1017
|
+
* Null ⇒ deny: rights need a live turn.
|
|
1018
|
+
* `local` — the same origin REGARDLESS OF AGE
|
|
1019
|
+
* (`OriginStore.latestAny()`). Expiry is not an escalation: a
|
|
1020
|
+
* Slack turn that simply ran long must not gain owner rights
|
|
1021
|
+
* here, and only a daemon wake (a `local` record) pi has
|
|
1022
|
+
* accepted clears the belt.
|
|
1023
|
+
*/
|
|
1024
|
+
currentOrigin(): OriginRecord | null;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// ---- the operator seal ----
|
|
1028
|
+
|
|
1029
|
+
/** What a path IS, as `lstat` sees it — a symlink is reported as itself,
|
|
1030
|
+
* never as what it points at. */
|
|
1031
|
+
export type PathKind = "dir" | "file" | "symlink" | "other";
|
|
1032
|
+
|
|
1033
|
+
export interface PathIdentity {
|
|
1034
|
+
uid: number;
|
|
1035
|
+
gid: number;
|
|
1036
|
+
/** Permission bits only (`& 0o7777`). */
|
|
1037
|
+
mode: number;
|
|
1038
|
+
kind: PathKind;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
/** What the watcher can learn about a path's owner, mode and kind. Injected
|
|
1042
|
+
* so the sealed case — a ROOT-owned directory — is testable by a process that
|
|
1043
|
+
* is not root; the real one is `processOperatorSeal()`. */
|
|
1044
|
+
export interface OperatorSeal {
|
|
1045
|
+
/** The daemon's effective uid — the uid every pi session runs as too. */
|
|
1046
|
+
euid: number;
|
|
1047
|
+
/** The daemon's supplementary groups (for the group-write bit). */
|
|
1048
|
+
gids: readonly number[];
|
|
1049
|
+
/** `null` when the path does not exist. */
|
|
1050
|
+
stat(path: string): PathIdentity | null;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
export function processOperatorSeal(): OperatorSeal {
|
|
1054
|
+
return {
|
|
1055
|
+
euid: process.geteuid?.() ?? -1,
|
|
1056
|
+
gids: process.getgroups?.() ?? [],
|
|
1057
|
+
stat(path) {
|
|
1058
|
+
try {
|
|
1059
|
+
// lstat, not stat: the seal is a claim about the path the operator's
|
|
1060
|
+
// tools wrote, and a symlink planted at that path must show as one.
|
|
1061
|
+
const s = lstatSync(path);
|
|
1062
|
+
const kind: PathKind = s.isSymbolicLink()
|
|
1063
|
+
? "symlink"
|
|
1064
|
+
: s.isDirectory()
|
|
1065
|
+
? "dir"
|
|
1066
|
+
: s.isFile()
|
|
1067
|
+
? "file"
|
|
1068
|
+
: "other";
|
|
1069
|
+
return { uid: s.uid, gid: s.gid, mode: s.mode & 0o7777, kind };
|
|
1070
|
+
} catch {
|
|
1071
|
+
return null;
|
|
1072
|
+
}
|
|
1073
|
+
},
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
/** Could a process with `seal`'s identity write this path? */
|
|
1078
|
+
function writableBy(seal: OperatorSeal, s: PathIdentity): boolean {
|
|
1079
|
+
if (s.uid === seal.euid) return true;
|
|
1080
|
+
if ((s.mode & 0o002) !== 0) return true;
|
|
1081
|
+
return seal.gids.includes(s.gid) && (s.mode & 0o020) !== 0;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
export type SealVerdict = { sealed: true } | { sealed: false; reason: string };
|
|
1085
|
+
|
|
1086
|
+
/**
|
|
1087
|
+
* Is `file` in `dir` the OPERATOR's? It is exactly when the directory is a
|
|
1088
|
+
* real directory and the file a real file, and neither could have been
|
|
1089
|
+
* written by the daemon's own identity — the identity every session shares.
|
|
1090
|
+
* Anything else (a symlink standing in for the directory — the parent
|
|
1091
|
+
* `commands/` is the daemon's, so a session can plant one — the directory
|
|
1092
|
+
* made by the daemon's uid, a group- or world-writable mode, a file the
|
|
1093
|
+
* daemon could edit) is not a seal, and the verdict says which.
|
|
1094
|
+
*/
|
|
1095
|
+
export function operatorSealVerdict(seal: OperatorSeal, dir: string, file: string): SealVerdict {
|
|
1096
|
+
const d = seal.stat(dir);
|
|
1097
|
+
if (!d) return { sealed: false, reason: "operator directory missing" };
|
|
1098
|
+
if (d.kind !== "dir")
|
|
1099
|
+
return { sealed: false, reason: `operator directory is a ${d.kind}, not a directory` };
|
|
1100
|
+
if (writableBy(seal, d))
|
|
1101
|
+
return {
|
|
1102
|
+
sealed: false,
|
|
1103
|
+
reason: `operator directory is writable by uid ${seal.euid} (owner ${d.uid}, mode ${d.mode.toString(8)})`,
|
|
1104
|
+
};
|
|
1105
|
+
const f = seal.stat(file);
|
|
1106
|
+
if (!f) return { sealed: false, reason: "command file vanished" };
|
|
1107
|
+
if (f.kind !== "file")
|
|
1108
|
+
return { sealed: false, reason: `command file is a ${f.kind}, not a regular file` };
|
|
1109
|
+
if (writableBy(seal, f))
|
|
1110
|
+
return {
|
|
1111
|
+
sealed: false,
|
|
1112
|
+
reason: `command file is writable by uid ${seal.euid} (owner ${f.uid}, mode ${f.mode.toString(8)})`,
|
|
1113
|
+
};
|
|
1114
|
+
return { sealed: true };
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
/**
|
|
1118
|
+
* The identity of one version of an operator command file — the key the
|
|
1119
|
+
* handled-ledger is written under. The operator's directory is not the
|
|
1120
|
+
* daemon's to archive from, so "handled" has to be remembered; remembering the
|
|
1121
|
+
* name alone would make every re-used name a one-shot, silently ignoring a
|
|
1122
|
+
* second write under the same name. Inode + mtime + size change on every
|
|
1123
|
+
* rewrite that matters — the operator's tools land files by tmp+mv, a new
|
|
1124
|
+
* inode each time — and stay put across the sweeps that re-see an unchanged
|
|
1125
|
+
* file. `null` when the file is gone.
|
|
1126
|
+
*/
|
|
1127
|
+
export function operatorCommandVersion(path: string): string | null {
|
|
1128
|
+
try {
|
|
1129
|
+
return commandVersionOf(path, statSync(path, { bigint: true }));
|
|
1130
|
+
} catch {
|
|
1131
|
+
return null;
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
function commandVersionOf(path: string, s: { ino: bigint; mtimeNs: bigint; size: bigint }): string {
|
|
1136
|
+
return `${basename(path)}:${s.ino}:${s.mtimeNs}:${s.size}`;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
/** `dev:ino` of the object AT `path` (a symlink as itself), `null` when absent. */
|
|
1140
|
+
function fileIdentityAt(path: string): string | null {
|
|
1141
|
+
try {
|
|
1142
|
+
const s = lstatSync(path, { bigint: true });
|
|
1143
|
+
return `${s.dev}:${s.ino}`;
|
|
1144
|
+
} catch {
|
|
1145
|
+
return null;
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
/** The inode at `path` — what `fs.watch` binds to — or `null` when nothing is there. */
|
|
1150
|
+
function inodeAt(path: string): bigint | null {
|
|
1151
|
+
try {
|
|
1152
|
+
return statSync(path, { bigint: true }).ino;
|
|
1153
|
+
} catch {
|
|
1154
|
+
return null;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
export interface CommandWatcherOptions {
|
|
1159
|
+
/** The directory served. Session dirs: results land in <dir>/results and
|
|
1160
|
+
* handled files move to <dir>/processed. */
|
|
1161
|
+
dir: string;
|
|
1162
|
+
/** Tier enforcement for this directory. Absent ⇒ no tier check. */
|
|
1163
|
+
tier?: CommandTierGate;
|
|
1164
|
+
/**
|
|
1165
|
+
* Serve `dir` as the operator's directory. The watcher then never
|
|
1166
|
+
* creates it (a daemon-made directory would carry the daemon's uid and be
|
|
1167
|
+
* no seal), never renames out of it (it cannot), records handled files in
|
|
1168
|
+
* the store instead, stamps `issuer: "operator"` only on files the seal
|
|
1169
|
+
* vouches for, and writes results beside the session dir's — into
|
|
1170
|
+
* `<parent>/results` — so the operator's clients read them from one place.
|
|
1171
|
+
*/
|
|
1172
|
+
operator?: OperatorSeal;
|
|
1173
|
+
/** Names this directory in each command's context `file`
|
|
1174
|
+
* (`<scope>/<name>`), so a same-named file in another served directory is a
|
|
1175
|
+
* different delivery to a handler's idempotence key. Absent ⇒ the bare name. */
|
|
1176
|
+
scope?: string;
|
|
1177
|
+
deps: CommandDeps;
|
|
1178
|
+
store: StateStore;
|
|
1179
|
+
now?: () => Date;
|
|
1180
|
+
/** Partial-write tolerance: re-read a file that fails to parse. */
|
|
1181
|
+
parseAttempts?: number;
|
|
1182
|
+
parseRetryMs?: number;
|
|
1183
|
+
/** Operator dir only: re-sweep (and attach to a directory that appeared
|
|
1184
|
+
* after start) this often. 0 disables the timer. Default 15 s. */
|
|
1185
|
+
sweepIntervalMs?: number;
|
|
1186
|
+
log?: (line: string) => void;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
export interface CommandResultFile extends FileOutcome {
|
|
1190
|
+
command: string | null;
|
|
1191
|
+
file: string;
|
|
1192
|
+
handled_at: string;
|
|
1193
|
+
/** Who the watcher decided the writer was. */
|
|
1194
|
+
issuer: CommandIssuer;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
// These handlers own richer durable recovery (filing reconciles exact GitHub
|
|
1198
|
+
// markers; lever/process handlers journal their effects; the remaining handlers
|
|
1199
|
+
// use their own delivery key). Pending recovery must reach those handlers.
|
|
1200
|
+
const HANDLER_RECOVERY_COMMANDS = new Set<Command["command"]>([
|
|
1201
|
+
"file-improvement-pr",
|
|
1202
|
+
"file-issue",
|
|
1203
|
+
"pause-pr",
|
|
1204
|
+
"drain",
|
|
1205
|
+
"restart-session",
|
|
1206
|
+
"slack_reply",
|
|
1207
|
+
"lessons-off",
|
|
1208
|
+
"lessons-on",
|
|
1209
|
+
"suppress-lesson",
|
|
1210
|
+
"unsuppress-lesson",
|
|
1211
|
+
]);
|
|
1212
|
+
|
|
1213
|
+
export class CommandWatcher {
|
|
1214
|
+
private readonly dir: string;
|
|
1215
|
+
private readonly resultsDir: string;
|
|
1216
|
+
private readonly processedDir: string;
|
|
1217
|
+
private readonly now: () => Date;
|
|
1218
|
+
private readonly log: (line: string) => void;
|
|
1219
|
+
private watcher: FSWatcher | null = null;
|
|
1220
|
+
/** The inode `watcher` is bound to — `fs.watch` follows the inode, not the path. */
|
|
1221
|
+
private watchedIno: bigint | null = null;
|
|
1222
|
+
private parentWatcher: FSWatcher | null = null;
|
|
1223
|
+
private sweepTimer: NodeJS.Timeout | null = null;
|
|
1224
|
+
private readonly inFlight = new Set<string>();
|
|
1225
|
+
/** Diagnostics only: every sweep still reads/parses the source anew. */
|
|
1226
|
+
private readonly parseFailures = new Map<string, string>();
|
|
1227
|
+
/** Serialises handling so two file events can't race the same caps. */
|
|
1228
|
+
private queue: Promise<void> = Promise.resolve();
|
|
1229
|
+
|
|
1230
|
+
constructor(private readonly opts: CommandWatcherOptions) {
|
|
1231
|
+
this.dir = opts.dir;
|
|
1232
|
+
const resultsRoot = opts.operator ? dirname(opts.dir) : opts.dir;
|
|
1233
|
+
this.resultsDir = join(resultsRoot, "results");
|
|
1234
|
+
this.processedDir = join(opts.dir, "processed");
|
|
1235
|
+
this.now = opts.now ?? (() => new Date());
|
|
1236
|
+
this.log = opts.log ?? ((line) => console.error(line));
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
private ensureDirs(): void {
|
|
1240
|
+
mkdirSync(this.resultsDir, { recursive: true });
|
|
1241
|
+
if (this.opts.operator) return; // never ours to create — see `operator`
|
|
1242
|
+
mkdirSync(this.dir, { recursive: true });
|
|
1243
|
+
mkdirSync(this.processedDir, { recursive: true });
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
/** Startup sweep + fs.watch. Returns once the initial sweep has completed.
|
|
1247
|
+
* The operator directory may not exist yet (the operator's first write
|
|
1248
|
+
* creates it): then the parent is watched for it to appear, and the sweep
|
|
1249
|
+
* timer re-tries the attach as a belt. */
|
|
1250
|
+
async start(): Promise<void> {
|
|
1251
|
+
this.ensureDirs();
|
|
1252
|
+
// Watch FIRST, then sweep on the same queue the watch feeds: the sweep
|
|
1253
|
+
// snapshots the directory and then awaits each handler, so a file a live
|
|
1254
|
+
// session writes after the snapshot must already have a watcher to see
|
|
1255
|
+
// it — a session dir has no periodic sweep, so a file missed here would
|
|
1256
|
+
// wait for a restart. A file seen by both is handled once: the second
|
|
1257
|
+
// visit finds it archived.
|
|
1258
|
+
if (!this.opts.operator) this.attachDirWatch();
|
|
1259
|
+
const initial = this.queue.then(() => this.sweep());
|
|
1260
|
+
this.queue = initial.catch((err) => this.log(`[commands] sweep failed: ${String(err)}`));
|
|
1261
|
+
await initial;
|
|
1262
|
+
const interval = this.opts.sweepIntervalMs ?? 15_000;
|
|
1263
|
+
if (this.opts.operator && interval > 0) {
|
|
1264
|
+
this.sweepTimer = setInterval(() => void this.enqueueSweep(), interval);
|
|
1265
|
+
this.sweepTimer.unref?.();
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
/** Watch the served directory when it exists. True once watching. */
|
|
1270
|
+
private attachDirWatch(): boolean {
|
|
1271
|
+
if (this.watcher) return true;
|
|
1272
|
+
const ino = inodeAt(this.dir);
|
|
1273
|
+
if (ino === null) return false;
|
|
1274
|
+
this.watcher = watch(this.dir, (_event, filename) => {
|
|
1275
|
+
if (!filename) {
|
|
1276
|
+
void this.enqueueSweep();
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
const name = filename.toString();
|
|
1280
|
+
if (!name.endsWith(".json")) return;
|
|
1281
|
+
void this.enqueue(join(this.dir, name));
|
|
1282
|
+
});
|
|
1283
|
+
this.watchedIno = ino;
|
|
1284
|
+
this.parentWatcher?.close();
|
|
1285
|
+
this.parentWatcher = null;
|
|
1286
|
+
return true;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
private detachDirWatch(): void {
|
|
1290
|
+
this.watcher?.close();
|
|
1291
|
+
this.watcher = null;
|
|
1292
|
+
this.watchedIno = null;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
/**
|
|
1296
|
+
* Operator dir only: keep the watch on whatever directory is AT the path
|
|
1297
|
+
* now. `fs.watch` binds to an inode, and the parent `commands/` is the
|
|
1298
|
+
* daemon's — so a session can rename the sealed directory away and put
|
|
1299
|
+
* another in its place, and a watcher left on the old inode would see none
|
|
1300
|
+
* of the new one's files. Every sweep re-checks; a directory that is gone
|
|
1301
|
+
* or replaced drops the old watch, a present one is attached, and an absent
|
|
1302
|
+
* one leaves the parent watched for it to appear.
|
|
1303
|
+
*/
|
|
1304
|
+
private syncDirWatch(): void {
|
|
1305
|
+
if (this.watcher && inodeAt(this.dir) !== this.watchedIno) this.detachDirWatch();
|
|
1306
|
+
if (!this.attachDirWatch() && !this.parentWatcher) {
|
|
1307
|
+
this.parentWatcher = watch(dirname(this.dir), (_event, filename) => {
|
|
1308
|
+
if (filename?.toString() === basename(this.dir)) void this.enqueueSweep();
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
stop(): void {
|
|
1314
|
+
this.detachDirWatch();
|
|
1315
|
+
this.parentWatcher?.close();
|
|
1316
|
+
this.parentWatcher = null;
|
|
1317
|
+
if (this.sweepTimer) clearInterval(this.sweepTimer);
|
|
1318
|
+
this.sweepTimer = null;
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
/** Handle every pending <dir>/*.json once. */
|
|
1322
|
+
async sweep(): Promise<void> {
|
|
1323
|
+
this.ensureDirs();
|
|
1324
|
+
if (this.opts.operator) this.syncDirWatch();
|
|
1325
|
+
if (!existsSync(this.dir)) return;
|
|
1326
|
+
const names = readdirSync(this.dir, { withFileTypes: true })
|
|
1327
|
+
.filter((e) => e.isFile() && e.name.endsWith(".json"))
|
|
1328
|
+
.map((e) => e.name)
|
|
1329
|
+
.sort();
|
|
1330
|
+
for (const name of names) await this.handleFile(join(this.dir, name));
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
private enqueueSweep(): Promise<void> {
|
|
1334
|
+
this.queue = this.queue
|
|
1335
|
+
.then(() => this.sweep())
|
|
1336
|
+
.catch((err) => this.log(`[commands] sweep failed: ${String(err)}`));
|
|
1337
|
+
return this.queue;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
private enqueue(path: string): Promise<void> {
|
|
1341
|
+
this.queue = this.queue
|
|
1342
|
+
.then(async () => {
|
|
1343
|
+
await this.handleFile(path);
|
|
1344
|
+
})
|
|
1345
|
+
.catch((err) => this.log(`[commands] ${path}: ${String(err)}`));
|
|
1346
|
+
return this.queue;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
/**
|
|
1350
|
+
* Read and parse one command file. `identity` names the file object the
|
|
1351
|
+
* parsed bytes came from (`dev:ino` of the open descriptor) so the operator
|
|
1352
|
+
* seal can be checked against THOSE bytes rather than against whatever
|
|
1353
|
+
* stands at the path a moment earlier or later (the parent `commands/` is
|
|
1354
|
+
* the daemon's, so a session can swap the directory between two syscalls).
|
|
1355
|
+
* `version` is the handled-ledger key of those same bytes
|
|
1356
|
+
* (`operatorCommandVersion`'s), so a file replaced during a parse retry is
|
|
1357
|
+
* recorded as the version that ran, not the one first seen. The descriptor
|
|
1358
|
+
* is stat'ed before AND after the read, and a read the two disagree on (a
|
|
1359
|
+
* writer's write landed in between) is retried like a partial one — so the
|
|
1360
|
+
* version always names the bytes parsed. `text` is those bytes; a failed
|
|
1361
|
+
* read still returns the last text it read, when it read one. The
|
|
1362
|
+
* operator directory is opened without following a symlink.
|
|
1363
|
+
*/
|
|
1364
|
+
private async readCommandFile(
|
|
1365
|
+
path: string,
|
|
1366
|
+
): Promise<
|
|
1367
|
+
| { ok: true; value: unknown; identity: string; version: string; text: string }
|
|
1368
|
+
| { ok: false; reason: string; version: string | null; text: string | null }
|
|
1369
|
+
> {
|
|
1370
|
+
const attempts = this.opts.parseAttempts ?? 3;
|
|
1371
|
+
const delay = this.opts.parseRetryMs ?? 40;
|
|
1372
|
+
const flags = this.opts.operator
|
|
1373
|
+
? fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW
|
|
1374
|
+
: fsConstants.O_RDONLY;
|
|
1375
|
+
let lastError = "unreadable";
|
|
1376
|
+
let lastVersion: string | null = null;
|
|
1377
|
+
let lastText: string | null = null;
|
|
1378
|
+
for (let i = 0; i < attempts; i++) {
|
|
1379
|
+
if (!existsSync(path))
|
|
1380
|
+
return { ok: false, reason: "file disappeared", version: null, text: null };
|
|
1381
|
+
try {
|
|
1382
|
+
const fd = openSync(path, flags);
|
|
1383
|
+
let text: string;
|
|
1384
|
+
let identity: string;
|
|
1385
|
+
let version: string;
|
|
1386
|
+
let stable: boolean;
|
|
1387
|
+
try {
|
|
1388
|
+
const st = fstatSync(fd, { bigint: true });
|
|
1389
|
+
identity = `${st.dev}:${st.ino}`;
|
|
1390
|
+
text = readFileSync(fd, "utf8");
|
|
1391
|
+
version = commandVersionOf(path, fstatSync(fd, { bigint: true }));
|
|
1392
|
+
stable = version === commandVersionOf(path, st);
|
|
1393
|
+
} finally {
|
|
1394
|
+
closeSync(fd);
|
|
1395
|
+
}
|
|
1396
|
+
lastVersion = version;
|
|
1397
|
+
lastText = text;
|
|
1398
|
+
if (!stable) {
|
|
1399
|
+
lastError = "changed while it was read";
|
|
1400
|
+
} else if (text.trim().length === 0) {
|
|
1401
|
+
lastError = "empty file";
|
|
1402
|
+
} else {
|
|
1403
|
+
return { ok: true, value: JSON.parse(text) as unknown, identity, version, text };
|
|
1404
|
+
}
|
|
1405
|
+
} catch (err) {
|
|
1406
|
+
// A partially-written file is expected transiently — retry before rejecting.
|
|
1407
|
+
lastError = (err as Error).message;
|
|
1408
|
+
}
|
|
1409
|
+
if (i < attempts - 1) await new Promise((r) => setTimeout(r, delay));
|
|
1410
|
+
}
|
|
1411
|
+
return {
|
|
1412
|
+
ok: false,
|
|
1413
|
+
reason: `malformed JSON: ${lastError}`,
|
|
1414
|
+
version: lastVersion,
|
|
1415
|
+
text: lastText,
|
|
1416
|
+
};
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
/**
|
|
1420
|
+
* Who wrote this file, as far as the daemon can PROVE it. A session
|
|
1421
|
+
* directory's writer is a session. The operator directory's writer is the
|
|
1422
|
+
* operator only when the seal holds; a file there without it is served as a
|
|
1423
|
+
* session's — loudly, with the reason in the ledger — so an unsealed channel
|
|
1424
|
+
* degrades to the bounded side and never to the unbounded one.
|
|
1425
|
+
*/
|
|
1426
|
+
private issuerFor(path: string, readIdentity: string | null): CommandIssuer {
|
|
1427
|
+
const seal = this.opts.operator;
|
|
1428
|
+
if (!seal) return "session";
|
|
1429
|
+
// Checked AFTER the read, and then bound to it: the file at the path must
|
|
1430
|
+
// still be the very object whose bytes were parsed. Otherwise a directory
|
|
1431
|
+
// swapped in for the read and swapped back for the seal check would lend
|
|
1432
|
+
// its bytes the operator's authority.
|
|
1433
|
+
let verdict = operatorSealVerdict(seal, this.dir, path);
|
|
1434
|
+
if (verdict.sealed && readIdentity !== null && fileIdentityAt(path) !== readIdentity)
|
|
1435
|
+
verdict = { sealed: false, reason: "command file replaced while it was read" };
|
|
1436
|
+
if (verdict.sealed) return "operator";
|
|
1437
|
+
this.opts.store.logEvent("operator_channel_unsealed", null, {
|
|
1438
|
+
file: basename(path),
|
|
1439
|
+
reason: verdict.reason,
|
|
1440
|
+
});
|
|
1441
|
+
this.log(
|
|
1442
|
+
`[commands] ${basename(path)}: operator channel NOT sealed (${verdict.reason}) — served as a session's command`,
|
|
1443
|
+
);
|
|
1444
|
+
return "session";
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
/** Validate + execute one command file, writing its result file. */
|
|
1448
|
+
async handleFile(path: string): Promise<CommandResultFile | null> {
|
|
1449
|
+
const name = basename(path, ".json");
|
|
1450
|
+
if (this.inFlight.has(path)) return null;
|
|
1451
|
+
if (!existsSync(path)) return null;
|
|
1452
|
+
// An operator file cannot be archived away (the directory is not ours to
|
|
1453
|
+
// write), so the store remembers what was handled across restarts — by
|
|
1454
|
+
// file VERSION, so a name written again with new content is a new command
|
|
1455
|
+
// and an unchanged file re-seen by the sweep is not.
|
|
1456
|
+
const seen = this.opts.operator ? operatorCommandVersion(path) : null;
|
|
1457
|
+
if (this.opts.operator) {
|
|
1458
|
+
if (seen === null) return null;
|
|
1459
|
+
if (this.opts.store.operatorCommandHandled(seen)) return null;
|
|
1460
|
+
}
|
|
1461
|
+
this.inFlight.add(path);
|
|
1462
|
+
try {
|
|
1463
|
+
this.ensureDirs();
|
|
1464
|
+
const read = await this.readCommandFile(path);
|
|
1465
|
+
if (!read.ok && read.reason === "file disappeared") return null;
|
|
1466
|
+
// The ledger records the version whose bytes were parsed — a file
|
|
1467
|
+
// replaced during a parse retry is that new version.
|
|
1468
|
+
const version = this.opts.operator ? (read.version ?? seen) : null;
|
|
1469
|
+
if (version !== null && version !== seen && this.opts.store.operatorCommandHandled(version))
|
|
1470
|
+
return null;
|
|
1471
|
+
if (!read.ok) {
|
|
1472
|
+
const fingerprint = createHash("sha256")
|
|
1473
|
+
.update(JSON.stringify([read.version, read.text, read.reason]))
|
|
1474
|
+
.digest("hex");
|
|
1475
|
+
if (this.parseFailures.get(path) === fingerprint) return null;
|
|
1476
|
+
const result = this.finish(
|
|
1477
|
+
path,
|
|
1478
|
+
name,
|
|
1479
|
+
version,
|
|
1480
|
+
read.text,
|
|
1481
|
+
null,
|
|
1482
|
+
this.issuerFor(path, null),
|
|
1483
|
+
{ ok: false, reason: `${read.reason}; file retained for a later completed write` },
|
|
1484
|
+
true,
|
|
1485
|
+
true,
|
|
1486
|
+
);
|
|
1487
|
+
this.parseFailures.set(path, fingerprint);
|
|
1488
|
+
if (this.parseFailures.size > 256) {
|
|
1489
|
+
const oldest = this.parseFailures.keys().next().value;
|
|
1490
|
+
if (oldest !== undefined) this.parseFailures.delete(oldest);
|
|
1491
|
+
}
|
|
1492
|
+
return result;
|
|
1493
|
+
}
|
|
1494
|
+
this.parseFailures.delete(path);
|
|
1495
|
+
const issuer = this.issuerFor(path, read.identity);
|
|
1496
|
+
const validation = validateCommand(read.value, { issuer });
|
|
1497
|
+
if (!validation.ok) {
|
|
1498
|
+
const attempted = (read.value as { command?: unknown } | null)?.command;
|
|
1499
|
+
return this.finish(
|
|
1500
|
+
path,
|
|
1501
|
+
name,
|
|
1502
|
+
version,
|
|
1503
|
+
read.text,
|
|
1504
|
+
typeof attempted === "string" ? attempted : null,
|
|
1505
|
+
issuer,
|
|
1506
|
+
{ ok: false, reason: validation.reason },
|
|
1507
|
+
true,
|
|
1508
|
+
);
|
|
1509
|
+
}
|
|
1510
|
+
// The Slack belt stops a SESSION mis-filing; a sealed operator file is
|
|
1511
|
+
// the human at the terminal and never rides a Slack turn. An UNSEALED
|
|
1512
|
+
// operator file is a session's and gets the belt like any other.
|
|
1513
|
+
const denial = issuer === "operator" ? null : this.tierCheck(validation.command);
|
|
1514
|
+
if (denial)
|
|
1515
|
+
return this.finish(
|
|
1516
|
+
path,
|
|
1517
|
+
name,
|
|
1518
|
+
version,
|
|
1519
|
+
read.text,
|
|
1520
|
+
validation.command.command,
|
|
1521
|
+
issuer,
|
|
1522
|
+
{ ok: false, reason: denial },
|
|
1523
|
+
true,
|
|
1524
|
+
);
|
|
1525
|
+
const deliveryKey = `${this.opts.scope ?? "local"}/${read.identity}/${read.version}`;
|
|
1526
|
+
const receiptKey = createHash("sha256")
|
|
1527
|
+
.update(JSON.stringify([deliveryKey, validation.command]))
|
|
1528
|
+
.digest("hex");
|
|
1529
|
+
const journal = !HANDLER_RECOVERY_COMMANDS.has(validation.command.command);
|
|
1530
|
+
const reserved = !journal || this.opts.store.reserveCommandDelivery(receiptKey);
|
|
1531
|
+
let outcome: FileOutcome;
|
|
1532
|
+
if (!reserved) {
|
|
1533
|
+
const receipt = this.opts.store.commandDeliveryReceipt(receiptKey)!;
|
|
1534
|
+
outcome =
|
|
1535
|
+
receipt.result === null
|
|
1536
|
+
? {
|
|
1537
|
+
ok: false,
|
|
1538
|
+
reason:
|
|
1539
|
+
"command outcome unknown after interrupted delivery; submit a fresh command to retry",
|
|
1540
|
+
}
|
|
1541
|
+
: (JSON.parse(receipt.result) as FileOutcome);
|
|
1542
|
+
} else {
|
|
1543
|
+
try {
|
|
1544
|
+
outcome = await executeCommand(validation.command, this.opts.deps, {
|
|
1545
|
+
file: this.opts.scope ? `${this.opts.scope}/${name}` : name,
|
|
1546
|
+
delivery_key: deliveryKey,
|
|
1547
|
+
issuer,
|
|
1548
|
+
});
|
|
1549
|
+
if (journal) this.opts.store.settleCommandDelivery(receiptKey, JSON.stringify(outcome));
|
|
1550
|
+
} catch (err) {
|
|
1551
|
+
// The effect may already have happened. Leave its pending receipt
|
|
1552
|
+
// unknown rather than allowing a restart to repeat it.
|
|
1553
|
+
outcome = { ok: false, reason: `handler threw: ${(err as Error).message}` };
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
return this.finish(
|
|
1557
|
+
path,
|
|
1558
|
+
name,
|
|
1559
|
+
version,
|
|
1560
|
+
read.text,
|
|
1561
|
+
validation.command.command,
|
|
1562
|
+
issuer,
|
|
1563
|
+
outcome,
|
|
1564
|
+
!outcome.ok,
|
|
1565
|
+
);
|
|
1566
|
+
} finally {
|
|
1567
|
+
this.inFlight.delete(path);
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
/** Rights check between validation and execution. Returns a reason to deny. */
|
|
1572
|
+
private tierCheck(command: Command): string | null {
|
|
1573
|
+
const gate = this.opts.tier;
|
|
1574
|
+
if (!gate) return null;
|
|
1575
|
+
let origin: OriginRecord | null = null;
|
|
1576
|
+
try {
|
|
1577
|
+
origin = gate.currentOrigin();
|
|
1578
|
+
} catch (err) {
|
|
1579
|
+
// A gate that cannot answer denies — never fails open.
|
|
1580
|
+
return `tier: origin lookup failed (${(err as Error).message})`;
|
|
1581
|
+
}
|
|
1582
|
+
if (gate.dir === "slack") {
|
|
1583
|
+
if (!origin) {
|
|
1584
|
+
return "tier: no live Slack origin for this turn — a tier-checked command needs one (fail closed)";
|
|
1585
|
+
}
|
|
1586
|
+
const auth = authorizeCommand(command, origin);
|
|
1587
|
+
return auth.ok ? null : auth.reason;
|
|
1588
|
+
}
|
|
1589
|
+
// Local dir: only the belt applies. It fires on CAUSE, not on recency —
|
|
1590
|
+
// a daemon-caused turn (`source: "local"`) always passes, and a Slack-caused
|
|
1591
|
+
// one from a non-owner user always refuses, however old the record is.
|
|
1592
|
+
// (`parseOriginRecord` folds the retired tier spelling to "owner", so this
|
|
1593
|
+
// comparison only ever sees the current vocabulary; a Slack origin can
|
|
1594
|
+
// never be "system" — only `writeLocal` stamps that, with source "local".)
|
|
1595
|
+
if (origin && beltRestricts(origin)) {
|
|
1596
|
+
return (
|
|
1597
|
+
`tier: this turn was caused by a Slack message from ${origin.slack_user} (tier ${origin.tier}) — ` +
|
|
1598
|
+
`write '${command.command}' to commands/slack/ so it can be tier-checked`
|
|
1599
|
+
);
|
|
1600
|
+
}
|
|
1601
|
+
return null;
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
private finish(
|
|
1605
|
+
path: string,
|
|
1606
|
+
name: string,
|
|
1607
|
+
/** The operator file's version key (`operatorCommandVersion`); null for a session dir. */
|
|
1608
|
+
version: string | null,
|
|
1609
|
+
/** The bytes that were read; null when no file was read. */
|
|
1610
|
+
readText: string | null,
|
|
1611
|
+
command: string | null,
|
|
1612
|
+
issuer: CommandIssuer,
|
|
1613
|
+
outcome: FileOutcome,
|
|
1614
|
+
loud: boolean,
|
|
1615
|
+
unsettled = false,
|
|
1616
|
+
): CommandResultFile {
|
|
1617
|
+
const result: CommandResultFile = {
|
|
1618
|
+
...outcome,
|
|
1619
|
+
command,
|
|
1620
|
+
file: basename(path),
|
|
1621
|
+
handled_at: this.now().toISOString(),
|
|
1622
|
+
issuer,
|
|
1623
|
+
};
|
|
1624
|
+
writeFileSync(join(this.resultsDir, `${name}.result.json`), JSON.stringify(result, null, 2));
|
|
1625
|
+
if (outcome.ok) {
|
|
1626
|
+
this.opts.store.logEvent("command_executed", null, { command, file: result.file, issuer });
|
|
1627
|
+
} else {
|
|
1628
|
+
this.opts.store.logEvent("command_rejected", null, {
|
|
1629
|
+
command,
|
|
1630
|
+
file: result.file,
|
|
1631
|
+
issuer,
|
|
1632
|
+
reason: outcome.reason,
|
|
1633
|
+
});
|
|
1634
|
+
}
|
|
1635
|
+
if (loud)
|
|
1636
|
+
this.log(
|
|
1637
|
+
`[commands] REJECTED ${result.file} (${command ?? "?"}): ${outcome.reason ?? "unknown reason"}`,
|
|
1638
|
+
);
|
|
1639
|
+
// A parse failure is an observation, not a settled delivery: a slow
|
|
1640
|
+
// writer may complete after eager retries. Keep both session and operator
|
|
1641
|
+
// files eligible, even when metadata did not advance between observations.
|
|
1642
|
+
if (unsettled) return result;
|
|
1643
|
+
// Make sure a daemon restart cannot re-execute the file: session dirs
|
|
1644
|
+
// archive it into processed/; the operator dir is not writable by the
|
|
1645
|
+
// daemon, so the store keeps the ledger instead, keyed by this version.
|
|
1646
|
+
if (version !== null) {
|
|
1647
|
+
this.opts.store.markOperatorCommandHandled(version, result.file);
|
|
1648
|
+
} else {
|
|
1649
|
+
let atPath: string | null = null;
|
|
1650
|
+
try {
|
|
1651
|
+
atPath = readFileSync(path, "utf8");
|
|
1652
|
+
} catch {
|
|
1653
|
+
/* gone — nothing to archive or to run */
|
|
1654
|
+
}
|
|
1655
|
+
if (readText !== null && atPath !== null && atPath !== readText) {
|
|
1656
|
+
// A different command took this name while this one ran. It is not
|
|
1657
|
+
// the file this result describes, so it is not archived under it: it
|
|
1658
|
+
// is queued to run next (the watch event it raised found this one in
|
|
1659
|
+
// flight, and a session dir has no periodic sweep to find it later).
|
|
1660
|
+
// The same bytes written again are the same command, already run.
|
|
1661
|
+
this.log(`[commands] ${result.file} was replaced while it ran — running the new one next`);
|
|
1662
|
+
void this.enqueue(path);
|
|
1663
|
+
} else {
|
|
1664
|
+
try {
|
|
1665
|
+
renameSync(path, join(this.processedDir, `${name}.json`));
|
|
1666
|
+
} catch (err) {
|
|
1667
|
+
// A vanished file is fine; any other failure leaves the source
|
|
1668
|
+
// pending, so it is logged with the path and cause.
|
|
1669
|
+
if ((err as NodeJS.ErrnoException).code !== "ENOENT")
|
|
1670
|
+
this.log(
|
|
1671
|
+
`[commands] could not archive ${path}: ${(err as NodeJS.ErrnoException).code ?? (err as Error).message}`,
|
|
1672
|
+
);
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
return result;
|
|
1677
|
+
}
|
|
1678
|
+
}
|