@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/config.ts
ADDED
|
@@ -0,0 +1,2050 @@
|
|
|
1
|
+
// Typed access to config.toml — the single source of every runtime knob.
|
|
2
|
+
// The review sections (their types, defaults and parsers) live in
|
|
3
|
+
// packages/pi-plugin/src/reviewconfig.ts, the core's half; this module adds every other
|
|
4
|
+
// surface's, the install homes and the live config's loader, and re-exports
|
|
5
|
+
// the review half so its importers need not know where a knob lives.
|
|
6
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { resolve, dirname, isAbsolute, join } from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { parse } from "smol-toml";
|
|
11
|
+
import {
|
|
12
|
+
locateConfig,
|
|
13
|
+
locateSecrets,
|
|
14
|
+
missingConfigMessage,
|
|
15
|
+
strandedCheckoutConfig,
|
|
16
|
+
} from "./install.js";
|
|
17
|
+
import { packageRootFrom } from "@quickchatai/pi-quickreview/src/assetroot.js";
|
|
18
|
+
import { repoCloneDirName } from "./clonedir.js";
|
|
19
|
+
import { MissingConfigError } from "./missingconfig.js";
|
|
20
|
+
import { DEFAULT_LENS_GRANT_STAGGER_MS, DEFAULT_LENS_PERMITS } from "./ipc/permits.js";
|
|
21
|
+
import {
|
|
22
|
+
configDeprecations,
|
|
23
|
+
foldLegacyOwnerSlackId,
|
|
24
|
+
MEMORY_DELIVERY_MODES,
|
|
25
|
+
parseEngine,
|
|
26
|
+
parseEscalation,
|
|
27
|
+
parseGithubSection,
|
|
28
|
+
parseGuardsSection,
|
|
29
|
+
parseModels,
|
|
30
|
+
parseOwnerSection,
|
|
31
|
+
parsePiSection,
|
|
32
|
+
parseReviewSection,
|
|
33
|
+
parseScan,
|
|
34
|
+
presetOf,
|
|
35
|
+
req,
|
|
36
|
+
requireGithubKeys,
|
|
37
|
+
REVIEW_PRESETS,
|
|
38
|
+
validateReviewSection,
|
|
39
|
+
type CoreConfig,
|
|
40
|
+
type GithubConfig,
|
|
41
|
+
type MemoryDelivery,
|
|
42
|
+
type OwnerConfig,
|
|
43
|
+
} from "@quickchatai/pi-quickreview/src/reviewconfig.js";
|
|
44
|
+
|
|
45
|
+
export * from "@quickchatai/pi-quickreview/src/reviewconfig.js";
|
|
46
|
+
|
|
47
|
+
/** Directory name of the base clone: `[github] clone_dir` when set, else the
|
|
48
|
+
* repo slug's basename ("owner/name" → "name"; a URL's trailing path segment,
|
|
49
|
+
* `.git` stripped), e.g. "acme/webapp" ⇒ "webapp". */
|
|
50
|
+
export function githubCloneDir(g: Pick<GithubConfig, "clone_dir" | "repo">): string {
|
|
51
|
+
// ?? "": hand-rolled configs (tests) may omit both keys.
|
|
52
|
+
const explicit = (g.clone_dir ?? "").trim();
|
|
53
|
+
if (explicit) return explicit;
|
|
54
|
+
return repoCloneDirName(g.repo ?? "");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Where the daemon container lives relative to the TUI. `"ssh"` is
|
|
58
|
+
* the VM topology (every remote op rides `ssh <ssh_host> …`); `"local"` is the
|
|
59
|
+
* laptop topology (the SAME commands run against local docker directly, and no
|
|
60
|
+
* ssh process is ever spawned). Always explicit — never inferred from a magic
|
|
61
|
+
* ssh_host value. */
|
|
62
|
+
export type TuiTarget = "ssh" | "local";
|
|
63
|
+
|
|
64
|
+
export interface TuiConfig {
|
|
65
|
+
/** Transport for every TUI remote op (see TuiTarget). Default "ssh". */
|
|
66
|
+
target: TuiTarget;
|
|
67
|
+
/** ssh alias of the box running the container. Ignored when target = "local". */
|
|
68
|
+
ssh_host: string;
|
|
69
|
+
container: string;
|
|
70
|
+
/** The container the memory forward relays into, when OpenViking does not
|
|
71
|
+
* live in `container`: the memory sidecar (`[openviking] endpoint =
|
|
72
|
+
* "remote"`, docker/memory/). Empty = `container` (the in-container
|
|
73
|
+
* server). See `tuiMemoryContainer`. */
|
|
74
|
+
memory_container: string;
|
|
75
|
+
remote_state_dir: string;
|
|
76
|
+
/** The app root inside the daemon container. "" (the default) = resolved
|
|
77
|
+
* from the running container's `qcr.layout` label (src/tui/layout.ts): the
|
|
78
|
+
* release image is /opt/qcr, the legacy container (no label)
|
|
79
|
+
* /opt/quickreview/sandbox. A set value wins and is never probed. */
|
|
80
|
+
remote_sandbox_root: string;
|
|
81
|
+
sudo_docker: boolean;
|
|
82
|
+
poll_interval_s: number;
|
|
83
|
+
/** DATA STALE banner when status.json's written_at ages past this — sized
|
|
84
|
+
* to the daemon's ~30s write cadence, not the TUI's fetch cadence. */
|
|
85
|
+
stale_after_s: number;
|
|
86
|
+
msg_timeout_s: number;
|
|
87
|
+
/** Per-remote-read budget for the finished-round evidence copier.
|
|
88
|
+
* Every artefact fetch is killed at this age, so a wedged ssh control master
|
|
89
|
+
* costs one sweep and nothing else — never the poll, never the paint loop. */
|
|
90
|
+
evidence_timeout_s: number;
|
|
91
|
+
/** The evidence sweep's pace — the minimum gap between the end of
|
|
92
|
+
* one `qcr why` exec and the start of the next (each costs the container
|
|
93
|
+
* ~0.5–1 s of CPU), whatever the poll cadence. Clamped to 5–600 s
|
|
94
|
+
* (`evidenceWhyIntervalMs`, src/tui/evidence.ts); the pacing cannot be
|
|
95
|
+
* switched off. */
|
|
96
|
+
evidence_why_interval_s: number;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The local companion container. Host paths are expanded; empty
|
|
100
|
+
* strings mean "derive from the sandbox root" at parse time. */
|
|
101
|
+
export interface CompanionConfig {
|
|
102
|
+
/** `qcr tui` starts the companion unless this is off or --no-companion. */
|
|
103
|
+
enabled: boolean;
|
|
104
|
+
container_name: string;
|
|
105
|
+
image: string;
|
|
106
|
+
/** Named volume for the companion's own pihome (OAuth, sessions, memory). */
|
|
107
|
+
pihome_volume: string;
|
|
108
|
+
/** tmux session name inside the container. */
|
|
109
|
+
session: string;
|
|
110
|
+
model: string;
|
|
111
|
+
/** Host path of the review-sandbox repo → /workspace (rw). */
|
|
112
|
+
repo_dir: string;
|
|
113
|
+
/** Host path of the issue tracker → /tracker (rw, actor `companion`). */
|
|
114
|
+
tracker_dir: string;
|
|
115
|
+
/** Host path of the tracker's protocol docs → /tracker-docs (READ-ONLY).
|
|
116
|
+
* Sits outside `tracker_dir`, so it needs its own mount: without it the
|
|
117
|
+
* companion drives wayfinder from a summary instead of the real adapter. */
|
|
118
|
+
tracker_docs_dir: string;
|
|
119
|
+
/** Host path of the REVIEWED repo → /reviewed-repo, READ-ONLY. The
|
|
120
|
+
* companion reads the code a review is about; it can never write to it, and
|
|
121
|
+
* rule 8's boundary is otherwise unchanged (no GitHub write, no gate, no
|
|
122
|
+
* deploy.sh, no VM credential). A missing checkout simply drops the mount —
|
|
123
|
+
* under-equipped, never broken. */
|
|
124
|
+
reviewed_repo_dir: string;
|
|
125
|
+
/** Host dir the TUI copies finished-round evidence into → /evidence,
|
|
126
|
+
* READ-ONLY. The companion reads transcripts, ledgers, prompts and
|
|
127
|
+
* verdicts as plain files; it gains NO ssh, no credential and no new reach —
|
|
128
|
+
* the TUI holds the VM access and does all the fetching. Read-only because
|
|
129
|
+
* this is a RECORD: the companion reads history, it never rewrites it.
|
|
130
|
+
* Empty ⇒ `<sandbox root>/evidence` (gitignored); the default lives outside
|
|
131
|
+
* the repo because the store grows without bound and is kept indefinitely. */
|
|
132
|
+
evidence_dir: string;
|
|
133
|
+
/** Host dir the TUI writes ambient snapshots into → /feed. */
|
|
134
|
+
feed_dir: string;
|
|
135
|
+
/** Host dir the companion writes alert files into → /alerts. */
|
|
136
|
+
alerts_dir: string;
|
|
137
|
+
/** Accepted so an older config still loads, and read by nothing: the
|
|
138
|
+
* companion never mounts `~/.ssh` (no ssh config, no agent socket). */
|
|
139
|
+
ssh_dir: string;
|
|
140
|
+
/** Docker build context for the companion image (auto-built when
|
|
141
|
+
* missing): the app root. The image is the release image, built from
|
|
142
|
+
* `docker/Dockerfile` under it, which compiles the app itself. */
|
|
143
|
+
build_context: string;
|
|
144
|
+
// --- watcher knobs ---
|
|
145
|
+
/** A round overdue by more than this is worth waking the operator for. */
|
|
146
|
+
overdue_grace_min: number;
|
|
147
|
+
/** status.json older than this counts as a stale-daemon alert. */
|
|
148
|
+
stale_alert_s: number;
|
|
149
|
+
/** A PR sitting in awaiting_author longer than this is nag-worthy. */
|
|
150
|
+
awaiting_age_h: number;
|
|
151
|
+
/** Minimum gap between two alerts about the same subject. */
|
|
152
|
+
cooldown_min: number;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** [backfill] — knobs for the one-shot GitHub history importer.
|
|
156
|
+
* Every default is derived from [github] at parse time; the section exists so
|
|
157
|
+
* a re-run against a renamed label/identity needs no code change. */
|
|
158
|
+
export interface BackfillConfig {
|
|
159
|
+
/** Review author to import (GraphQL `reviews(author:)`, with the [bot] suffix). */
|
|
160
|
+
bot_login: string;
|
|
161
|
+
/** The same identity as a timeline *actor* — GraphQL drops the [bot] suffix. */
|
|
162
|
+
bot_actor: string;
|
|
163
|
+
/** Label whose presence marks a PR as part of the bot-review era. */
|
|
164
|
+
reviewed_label: string;
|
|
165
|
+
/** Label a human adds to request a review — a trigger candidate. */
|
|
166
|
+
trigger_label: string;
|
|
167
|
+
/** Label the bot adds when a round starts — the fallback trigger candidate. */
|
|
168
|
+
in_progress_label: string;
|
|
169
|
+
/** Team slug whose review request is a trigger (bare slug, no org prefix). */
|
|
170
|
+
review_team_slug: string;
|
|
171
|
+
/** Per-PR detail queries in flight at once (kept small on purpose). */
|
|
172
|
+
concurrency: number;
|
|
173
|
+
/** Search pages per enumeration query (100 hits each; search caps at 1,000). */
|
|
174
|
+
max_search_pages: number;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ---- [slack] — the Slack concierge ----
|
|
178
|
+
// Tokens are never in config. `bot_token_file` / `app_token_file` name flat
|
|
179
|
+
// files under [github].secrets_dir, read the way every other secret is
|
|
180
|
+
// (PiSessionManager.secret / TokenMinter): the config carries the file NAME.
|
|
181
|
+
|
|
182
|
+
/** One announcement cause: may it speak at all, and how often. */
|
|
183
|
+
export interface SlackCausePolicy {
|
|
184
|
+
enabled: boolean;
|
|
185
|
+
/** 0 = no cooldown (every occurrence fires). */
|
|
186
|
+
cooldown_min: number;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export interface SlackPolicyConfig {
|
|
190
|
+
/** Channel-wide model turns per policy day. */
|
|
191
|
+
daily_turns: number;
|
|
192
|
+
/** Per-person model turns per policy day. */
|
|
193
|
+
per_user_daily_turns: number;
|
|
194
|
+
/** Daemon posts into any ONE thread per policy day. */
|
|
195
|
+
thread_replies_per_day: number;
|
|
196
|
+
/** Accepted so an older config still loads, and read by nothing: the
|
|
197
|
+
* policy's turn caps are the daily and per-person ones above. */
|
|
198
|
+
soft_exchange_limit: number;
|
|
199
|
+
/** Rolling-hour ceiling on announcements (they bypass turn caps, not this). */
|
|
200
|
+
announcements_per_hour: number;
|
|
201
|
+
/** Cooldown for a cause with none of its own: one outside the built-in
|
|
202
|
+
* table that is unlisted, or listed in [slack.policy.causes] without a
|
|
203
|
+
* `cooldown_min`. A built-in cause keeps its own unless that table sets one. */
|
|
204
|
+
default_cooldown_min: number;
|
|
205
|
+
/** The day boundary for every cap here — DST-correct via src/eod.ts. */
|
|
206
|
+
timezone: string;
|
|
207
|
+
pickup_grace_min: number;
|
|
208
|
+
downtime_notice_min: number;
|
|
209
|
+
heartbeat_interval_min: number;
|
|
210
|
+
/** Accepted so an older config still loads, and read by nothing: no
|
|
211
|
+
* code raises the `reconcile.storm` announcement this would threshold. */
|
|
212
|
+
storm_corrections: number;
|
|
213
|
+
/** Accepted so an older config still loads, and read by nothing: no
|
|
214
|
+
* code raises the `pr.awaiting_author_stale` announcement this would time. */
|
|
215
|
+
awaiting_author_stale_h: number;
|
|
216
|
+
/** Accepted so an older config still loads, and read by nothing: a
|
|
217
|
+
* verdict always stays in-thread, never `reply_broadcast`, whatever this
|
|
218
|
+
* says — the announcer has no broadcast parameter. */
|
|
219
|
+
broadcast_verdicts: "never" | "always";
|
|
220
|
+
causes: Record<string, SlackCausePolicy>;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export interface SlackConfig {
|
|
224
|
+
enabled: boolean;
|
|
225
|
+
/** The ONE channel id the concierge lives in; empty ⇒ cannot enable. */
|
|
226
|
+
channel: string;
|
|
227
|
+
channel_kind: "public" | "private";
|
|
228
|
+
/** File name under [github].secrets_dir holding the xoxb- bot token. */
|
|
229
|
+
bot_token_file: string;
|
|
230
|
+
/** File name under [github].secrets_dir holding the xapp- socket token. */
|
|
231
|
+
app_token_file: string;
|
|
232
|
+
post_min_interval_ms: number;
|
|
233
|
+
reconnect_backoff_ms: number[];
|
|
234
|
+
server_ping_timeout_ms: number;
|
|
235
|
+
/** Accepted so an older config still loads, and read by nothing: Slack
|
|
236
|
+
* inbound turns are retired, so no backlog message is ever replayed or aged. */
|
|
237
|
+
catchup_max_age_s: number;
|
|
238
|
+
max_reply_chars: number;
|
|
239
|
+
name_cache_ttl_s: number;
|
|
240
|
+
/** Slack member id → GitHub login. The ONLY source of own-PR rights. */
|
|
241
|
+
users: Record<string, string>;
|
|
242
|
+
policy: SlackPolicyConfig;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Per-cause defaults. Absent causes fall back to `default_cooldown_min`. */
|
|
246
|
+
export const SLACK_CAUSE_DEFAULTS: Record<string, SlackCausePolicy> = {
|
|
247
|
+
"round.overdue": { enabled: true, cooldown_min: 60 },
|
|
248
|
+
"round.malformed_verdict": { enabled: true, cooldown_min: 60 },
|
|
249
|
+
"review.failed_loud": { enabled: true, cooldown_min: 60 },
|
|
250
|
+
"pr.not_picked_up": { enabled: true, cooldown_min: 120 },
|
|
251
|
+
"daemon.up": { enabled: true, cooldown_min: 0 },
|
|
252
|
+
"daemon.stopping": { enabled: true, cooldown_min: 0 },
|
|
253
|
+
"slack.cap_reached": { enabled: true, cooldown_min: 0 },
|
|
254
|
+
"reconcile.storm": { enabled: false, cooldown_min: 180 },
|
|
255
|
+
"digest.eod": { enabled: false, cooldown_min: 0 },
|
|
256
|
+
"pr.awaiting_author_stale": { enabled: false, cooldown_min: 1440 },
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
export const SLACK_POLICY_DEFAULTS: SlackPolicyConfig = {
|
|
260
|
+
daily_turns: 80,
|
|
261
|
+
per_user_daily_turns: 20,
|
|
262
|
+
thread_replies_per_day: 6,
|
|
263
|
+
soft_exchange_limit: 3,
|
|
264
|
+
announcements_per_hour: 20,
|
|
265
|
+
default_cooldown_min: 60,
|
|
266
|
+
// Effective value comes from [slack.policy] timezone, else [ops] timezone
|
|
267
|
+
// (parseSlack) — this static default (= OPS_DEFAULTS.timezone, declared
|
|
268
|
+
// below) only reaches direct constructions that bypass parseConfig.
|
|
269
|
+
timezone: "UTC",
|
|
270
|
+
pickup_grace_min: 20,
|
|
271
|
+
downtime_notice_min: 5,
|
|
272
|
+
heartbeat_interval_min: 5,
|
|
273
|
+
storm_corrections: 5,
|
|
274
|
+
awaiting_author_stale_h: 48,
|
|
275
|
+
broadcast_verdicts: "never",
|
|
276
|
+
causes: SLACK_CAUSE_DEFAULTS,
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
/** [slack] is optional in config.toml, exactly like [tui] / [companion]. */
|
|
280
|
+
export const SLACK_DEFAULTS: SlackConfig = {
|
|
281
|
+
enabled: false,
|
|
282
|
+
channel: "",
|
|
283
|
+
channel_kind: "public",
|
|
284
|
+
bot_token_file: "slack_bot_token",
|
|
285
|
+
app_token_file: "slack_app_token",
|
|
286
|
+
post_min_interval_ms: 1100,
|
|
287
|
+
reconnect_backoff_ms: [1000, 2000, 5000, 15000, 30000],
|
|
288
|
+
server_ping_timeout_ms: 30000,
|
|
289
|
+
catchup_max_age_s: 300,
|
|
290
|
+
max_reply_chars: 3000,
|
|
291
|
+
name_cache_ttl_s: 86400,
|
|
292
|
+
users: {},
|
|
293
|
+
policy: SLACK_POLICY_DEFAULTS,
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* [recheck] — the post-approval delta recheck. One knob per trigger,
|
|
298
|
+
* so a noisy trigger can be taken out of the path deliberately, in one place.
|
|
299
|
+
* All on: the uphold-by-default design is what makes the broad set affordable.
|
|
300
|
+
*/
|
|
301
|
+
export interface RecheckConfig {
|
|
302
|
+
enabled: boolean;
|
|
303
|
+
/** A push landing after an approve — the core case. */
|
|
304
|
+
on_push: boolean;
|
|
305
|
+
/** CI completing at the approved head; an adverse conclusion is evidence. */
|
|
306
|
+
on_ci: boolean;
|
|
307
|
+
/** The base branch moving under an approved PR (the noisiest by far). */
|
|
308
|
+
on_base_advance: boolean;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export const RECHECK_DEFAULTS: RecheckConfig = {
|
|
312
|
+
enabled: true,
|
|
313
|
+
on_push: true,
|
|
314
|
+
on_ci: true,
|
|
315
|
+
on_base_advance: true,
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* OpenViking (github.com/volcengine/OpenViking) — the context store the daemon
|
|
320
|
+
* runs as a SECOND process inside the container, on loopback :1933.
|
|
321
|
+
*
|
|
322
|
+
* Three invariants live in this block rather than in prose:
|
|
323
|
+
*
|
|
324
|
+
* * `version` + `llama_cpp_python_version` are the TRIPLE-MIRRORED pins —
|
|
325
|
+
* this block == `deploy/ansible/group_vars/all.yml` == the Dockerfile venv
|
|
326
|
+
* layer, exactly like the `[pi]` pin. `tests/openviking.test.ts` fails on
|
|
327
|
+
* drift, and the running server's self-reported version is drift-checked
|
|
328
|
+
* against `version` in the heartbeat.
|
|
329
|
+
* * `host` is loopback and the port is NEVER published out of the container
|
|
330
|
+
* (the playbook's `published_ports` carries only the webhook port).
|
|
331
|
+
* * every path here is a CONTAINER path; on a dev PC the binary is absent,
|
|
332
|
+
* which the supervisor treats as "unavailable", not as a crash loop.
|
|
333
|
+
*/
|
|
334
|
+
export interface OpenVikingConfig {
|
|
335
|
+
/** Off ⇒ the supervisor is never constructed and status.json says so. */
|
|
336
|
+
enabled: boolean;
|
|
337
|
+
/** Who runs the server. `local` (the default, and what every live daemon
|
|
338
|
+
* does) supervises it, and the local embedder, as children of this process.
|
|
339
|
+
* `remote` is the memory sidecar (docker/memory/): `host`:`port` names a
|
|
340
|
+
* server some other container runs, the daemon only probes its health, and
|
|
341
|
+
* the sidecar owns the processes, the boot provisioning and the re-embed
|
|
342
|
+
* migration. */
|
|
343
|
+
endpoint: OpenVikingEndpoint;
|
|
344
|
+
/** The pinned server version (mirror 1 of 3). */
|
|
345
|
+
version: string;
|
|
346
|
+
/** Pinned llama-cpp-python: an OPTIONAL extra openviking does not declare,
|
|
347
|
+
* without which the default local dense embedder raises
|
|
348
|
+
* EmbeddingConfigurationError at startup (verified against 0.4.15). */
|
|
349
|
+
llama_cpp_python_version: string;
|
|
350
|
+
/** Upstream base commit of the vendored pi extension. Pinned beside the
|
|
351
|
+
* server version so `tests/openviking-fork.test.ts` gates drift instead of
|
|
352
|
+
* letting a bump rebase silently onto new upstream behaviour. */
|
|
353
|
+
extension_commit: string;
|
|
354
|
+
/** OpenViking user the daemon and review sessions act as. Their key is
|
|
355
|
+
* confined to this user server-side — that confinement is what keeps
|
|
356
|
+
* companion memory out of a posted review. */
|
|
357
|
+
user: string;
|
|
358
|
+
/** Tenant account; empty ⇒ derived from the key. */
|
|
359
|
+
account: string;
|
|
360
|
+
/** Secret FILE NAME under [github].secrets_dir holding the reviewer USER
|
|
361
|
+
* key. Never an ADMIN or ROOT key: ADMIN grants no cross-user data access
|
|
362
|
+
* and can list every user key in plaintext. */
|
|
363
|
+
reviewer_key_file: string;
|
|
364
|
+
/** ov.conf `server.auth_mode`. `api_key` is the required posture:
|
|
365
|
+
* every request carries a USER key the server CONFINES to its own tree, and
|
|
366
|
+
* that confinement is the only thing keeping companion memory out of a
|
|
367
|
+
* posted review. `dev` bypasses authentication entirely — it is a local
|
|
368
|
+
* development escape hatch, never the deployed value. MIRRORED into
|
|
369
|
+
* `deploy/ansible/group_vars/all.yml` (`openviking_auth_mode`), which the
|
|
370
|
+
* entrypoint template renders; `tests/openviking.test.ts` and the
|
|
371
|
+
* `scripts/deploy.sh` preflight both fail on drift. */
|
|
372
|
+
auth_mode: string;
|
|
373
|
+
/** Secret FILE NAME under the container's secrets mount holding the
|
|
374
|
+
* server's OWN root key — the credential ov.conf needs to run in `api_key`
|
|
375
|
+
* mode and to mint users at boot. It is the server's, never a client's: no
|
|
376
|
+
* daemon surface, no review session and no companion process ever reads it
|
|
377
|
+
* (`writeOvcliConf` refuses it as a session credential and the bash guard
|
|
378
|
+
* refuses to read the files that hold it). Mirrored as
|
|
379
|
+
* `openviking_root_key_file`. */
|
|
380
|
+
root_key_file: string;
|
|
381
|
+
/** The OpenViking users boot provisioning ENSURES exist — created if
|
|
382
|
+
* absent, left exactly as they are if present. Never rotated: a rotation
|
|
383
|
+
* would orphan the keys already distributed to the companion's PC. Mirrored
|
|
384
|
+
* as `openviking_provision_users` (comma-joined). */
|
|
385
|
+
provision_users: string[];
|
|
386
|
+
request_timeout_ms: number;
|
|
387
|
+
/** Tighter than `request_timeout_ms`: a search sits on the round-build
|
|
388
|
+
* path, where the deadline in [memory] is the real guard. It must exceed
|
|
389
|
+
* one document embedding on the live VM. Clamped to
|
|
390
|
+
* `SEARCH_TIMEOUT_BOUNDS_MS`; a seed that sent its search late passes its
|
|
391
|
+
* time left, and the search takes the shorter. */
|
|
392
|
+
search_timeout_ms: number;
|
|
393
|
+
/** Where an entry lands when the writer names no category. */
|
|
394
|
+
default_category: string;
|
|
395
|
+
read_cache_ttl_ms: number;
|
|
396
|
+
host: string;
|
|
397
|
+
port: number;
|
|
398
|
+
/** `openviking-server` inside the image's venv. */
|
|
399
|
+
bin: string;
|
|
400
|
+
/** `~/.openviking` is symlinked here at boot — it must sit on the state
|
|
401
|
+
* volume so memory survives an image rebuild. */
|
|
402
|
+
home: string;
|
|
403
|
+
/** Empty ⇒ `<home>/ov.conf` (the path the boot template writes). */
|
|
404
|
+
config_file: string;
|
|
405
|
+
/** Empty ⇒ `<home>/workspace` (ov.conf `storage.workspace`). */
|
|
406
|
+
workspace: string;
|
|
407
|
+
/** ov.conf `server.workers` — one worker on a 2-CPU cap. */
|
|
408
|
+
workers: number;
|
|
409
|
+
/** Accepted, and read by nothing in the daemon: the entrypoint that
|
|
410
|
+
* templates ov.conf takes its queue concurrency from the deploy
|
|
411
|
+
* (`openviking_queue_concurrency` in group_vars, or
|
|
412
|
+
* `QCR_OPENVIKING_QUEUE_CONCURRENCY` for the plain image), never from here. */
|
|
413
|
+
queue_concurrency: number;
|
|
414
|
+
/** Health-poll cadence; the heartbeat reports the CACHED sample, so
|
|
415
|
+
* status.json writing never waits on the network. */
|
|
416
|
+
health_interval_s: number;
|
|
417
|
+
/** The supervisor's `/health` + `/ready` probe and the local embedder's
|
|
418
|
+
* `/health` probe. `/ready` runs an embedding, so it queues behind a case
|
|
419
|
+
* batch's embeddings — a shorter timeout makes `/ready` flap and can tip
|
|
420
|
+
* memory health to degraded. It is the timeout behind memory health's
|
|
421
|
+
* "serving" half (`memoryServing`). Clamped to `HEALTH_TIMEOUT_BOUNDS_MS`. */
|
|
422
|
+
health_timeout_ms: number;
|
|
423
|
+
/** Restart backoff ladder; the last entry is the ceiling. The supervisor
|
|
424
|
+
* never gives up — a memory engine that stays down is worse than a slow
|
|
425
|
+
* retry, and every call site already degrades memory-less. */
|
|
426
|
+
restart_backoff_ms: number[];
|
|
427
|
+
/** A child that stayed up this long resets the backoff ladder. */
|
|
428
|
+
restart_reset_after_s: number;
|
|
429
|
+
/** Which embedder memory runs. `local` is the shipped
|
|
430
|
+
* default: the LOCAL ENGLISH EMBEDDER (`LOCAL_EMBEDDING`) — a loopback
|
|
431
|
+
* service inside the container, so no memory text leaves the machine.
|
|
432
|
+
* `openai` is the explicit hosted opt-in and needs `embedding_key_file` in
|
|
433
|
+
* the secrets dir. Mirrored as `openviking_embedding_provider`. This is the
|
|
434
|
+
* operator's word, not ov.conf's: under `local` the entrypoint renders
|
|
435
|
+
* ov.conf's OpenAI-COMPATIBLE provider with `api_base` on loopback
|
|
436
|
+
* (`ovConfEmbedder`). */
|
|
437
|
+
embedding_provider: EmbeddingProvider;
|
|
438
|
+
/** ov.conf `embedding.dense.model` / `.dimension`. Under `local` they are
|
|
439
|
+
* the local embedder's model and its fixed dimension (`LOCAL_EMBEDDING`);
|
|
440
|
+
* under `openai` the operator names the hosted model and its dimension. */
|
|
441
|
+
embedding_model: string;
|
|
442
|
+
embedding_dimension: number;
|
|
443
|
+
/** Secrets-dir file name of the HOSTED embeddings credential — deliberately
|
|
444
|
+
* NOT `openai_api_key` (pi's model credential), so the two can be set
|
|
445
|
+
* independently. Read only by the ov.conf render, only under `openai`. */
|
|
446
|
+
embedding_key_file: string;
|
|
447
|
+
/** The local embedder's MODEL FILE pin (the version-pinning rule): the GGUF the
|
|
448
|
+
* image downloads at BUILD time from `local_embedder_url`, verified against
|
|
449
|
+
* `local_embedder_sha256` there and again by the service at every start.
|
|
450
|
+
* TRIPLE-MIRRORED like `version`: this block == group_vars
|
|
451
|
+
* (`openviking_local_embedder_*`) == the memory Dockerfile's model layer —
|
|
452
|
+
* `tests/openviking.test.ts`, `tests/memory-image.test.ts` and the
|
|
453
|
+
* `scripts/deploy.sh` preflight refuse on drift. The file's basename is
|
|
454
|
+
* `<LOCAL_EMBEDDING.model>.gguf`; the image carries the file whatever the
|
|
455
|
+
* provider, so a hosted opt-in can come back to local without a rebuild. */
|
|
456
|
+
local_embedder_file: string;
|
|
457
|
+
local_embedder_url: string;
|
|
458
|
+
local_embedder_sha256: string;
|
|
459
|
+
/** The service listens on 127.0.0.1 at this port — never published; ov.conf's
|
|
460
|
+
* `api_base` names it. Mirrored as `openviking_local_embedder_port`. */
|
|
461
|
+
local_embedder_port: number;
|
|
462
|
+
/** llama.cpp threads for the service. One is the budget the model was chosen
|
|
463
|
+
* on (~25 ms for a round's context query): the container has two CPUs and a
|
|
464
|
+
* review round to run. Mirrored as `openviking_local_embedder_threads`. */
|
|
465
|
+
local_embedder_threads: number;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export const OPENVIKING_ENDPOINTS = ["local", "remote"] as const;
|
|
469
|
+
export type OpenVikingEndpoint = (typeof OPENVIKING_ENDPOINTS)[number];
|
|
470
|
+
|
|
471
|
+
export const EMBEDDING_PROVIDERS = ["local", "openai"] as const;
|
|
472
|
+
export type EmbeddingProvider = (typeof EMBEDDING_PROVIDERS)[number];
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* The LOCAL ENGLISH EMBEDDER: the model
|
|
476
|
+
* `embedding_provider = "local"` runs, served on loopback by
|
|
477
|
+
* `scripts/openviking-embedder.py` from the GGUF the image pins. Chosen by an
|
|
478
|
+
* offline retrieval evaluation over the reviewer's own memory
|
|
479
|
+
* (docs/memory-embeddings.md: MRR@10 0.56 against 0.34 for the engine's
|
|
480
|
+
* built-in Chinese-first model; ~25 ms per round query on one thread). The
|
|
481
|
+
* service's model table (`--describe`) carries the same name and dimension
|
|
482
|
+
* plus the query instruction; `tests/openviking-embedder.test.ts` holds the
|
|
483
|
+
* two together. Mirrored by `LOCAL_EMBEDDER` in scripts/openviking-reembed.mjs.
|
|
484
|
+
*/
|
|
485
|
+
export const LOCAL_EMBEDDING = Object.freeze({ model: "bge-small-en-v1.5-f16", dimension: 384 });
|
|
486
|
+
/** The engine's BUILT-IN local embedder — the retired meaning of `local`, and
|
|
487
|
+
* what an older store was embedded under. A config.toml that still spells it
|
|
488
|
+
* under `local` is read as the current local embedder rather than refused: the
|
|
489
|
+
* daemon must not stop booting over a value an earlier release required. The
|
|
490
|
+
* re-embed migration carries the store across. */
|
|
491
|
+
export const RETIRED_LOCAL_EMBEDDING = Object.freeze({
|
|
492
|
+
model: "bge-small-zh-v1.5-f16",
|
|
493
|
+
dimension: 512,
|
|
494
|
+
});
|
|
495
|
+
/** What an `openai` opt-in runs unless the operator names another model. */
|
|
496
|
+
export const HOSTED_EMBEDDING = Object.freeze({ model: "text-embedding-3-small", dimension: 1536 });
|
|
497
|
+
|
|
498
|
+
/** ov.conf's `embedding.dense.provider` under `local`: the engine's OpenAI-
|
|
499
|
+
* COMPATIBLE client — its async embedder — pointed at the loopback service.
|
|
500
|
+
* The wire format, never a destination. */
|
|
501
|
+
export const LOCAL_EMBEDDER_WIRE_PROVIDER = "openai";
|
|
502
|
+
|
|
503
|
+
/** `http://127.0.0.1:<port>/v1` — ov.conf's `api_base` under `local`. */
|
|
504
|
+
export function localEmbedderBaseUrl(c: Pick<OpenVikingConfig, "local_embedder_port">): string {
|
|
505
|
+
return `http://127.0.0.1:${c.local_embedder_port}/v1`;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/** What ov.conf's `embedding.dense` must say for this config: provider,
|
|
509
|
+
* model and dimension as the ENGINE spells them (the record its vector
|
|
510
|
+
* collection stores), plus the loopback `api_base` under `local`. The
|
|
511
|
+
* daemon's drift check holds the rendered ov.conf to it. */
|
|
512
|
+
export interface OvConfEmbedder {
|
|
513
|
+
provider: string;
|
|
514
|
+
model: string;
|
|
515
|
+
dimension: number;
|
|
516
|
+
api_base?: string;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
export function ovConfEmbedder(
|
|
520
|
+
c: Pick<
|
|
521
|
+
OpenVikingConfig,
|
|
522
|
+
"embedding_provider" | "embedding_model" | "embedding_dimension" | "local_embedder_port"
|
|
523
|
+
>,
|
|
524
|
+
): OvConfEmbedder {
|
|
525
|
+
if (c.embedding_provider === "local")
|
|
526
|
+
return {
|
|
527
|
+
provider: LOCAL_EMBEDDER_WIRE_PROVIDER,
|
|
528
|
+
model: c.embedding_model,
|
|
529
|
+
dimension: c.embedding_dimension,
|
|
530
|
+
api_base: localEmbedderBaseUrl(c),
|
|
531
|
+
};
|
|
532
|
+
return {
|
|
533
|
+
provider: c.embedding_provider,
|
|
534
|
+
model: c.embedding_model,
|
|
535
|
+
dimension: c.embedding_dimension,
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/** Round-seeding knobs: how much memory rides each round prompt and
|
|
540
|
+
* how long the daemon is willing to wait for it. Every value is defaulted —
|
|
541
|
+
* an absent `[memory]` block is a supported configuration, and a store that
|
|
542
|
+
* misses the deadline costs the round its context, never the round. */
|
|
543
|
+
export interface MemoryConfig {
|
|
544
|
+
/** Token budget for the assembled context block. */
|
|
545
|
+
context_tokens: number;
|
|
546
|
+
/** Deadline for the head + block assembly, after which the round proceeds
|
|
547
|
+
* memory-less. It must exceed one document embedding on the live VM; the
|
|
548
|
+
* seed's wait for the case writer's
|
|
549
|
+
* embedding tail is inside it. Clamped to `CONTEXT_TIMEOUT_BOUNDS_MS`. */
|
|
550
|
+
context_timeout_ms: number;
|
|
551
|
+
/** The embedding tail of one written finding case (ms): after
|
|
552
|
+
* a case batch's write returns the server goes on embedding it for about
|
|
553
|
+
* this long a case, and a round seed starting inside that waits for it —
|
|
554
|
+
* within its own `context_timeout_ms`. Clamped to 0…30000; 0 turns the
|
|
555
|
+
* tail off. */
|
|
556
|
+
case_embed_tail_ms: number;
|
|
557
|
+
/** Memory health (`src/memory/health.ts`): how often the daemon's
|
|
558
|
+
* AUTHENTICATED probe lists the reviewer memories root with each credential
|
|
559
|
+
* in use (its key file and the sessions' ovcli.conf). A leftover
|
|
560
|
+
* `alert_cooldown_min` key in a live config.toml is ignored. */
|
|
561
|
+
probe_interval_s: number;
|
|
562
|
+
/** No authenticated answer for this long ⇒ `down` (wakes `memory_down`). */
|
|
563
|
+
down_after_min: number;
|
|
564
|
+
/** After boot, a server still warming up reads `unknown`, not degraded,
|
|
565
|
+
* for this long. Never applies to an authentication failure. */
|
|
566
|
+
boot_grace_min: number;
|
|
567
|
+
/** An outage that lasts re-wakes the overseer this often. */
|
|
568
|
+
rewake_h: number;
|
|
569
|
+
/** The TUI files ONE tracker ticket for a memory incident older than this
|
|
570
|
+
* (sourced from status.json, never from memory). */
|
|
571
|
+
incident_ticket_after_min: number;
|
|
572
|
+
/** Writes that stick: how often the backfill pump writes the
|
|
573
|
+
* [feedback] signals memory still owes (at most 20 a tick, only while
|
|
574
|
+
* memory health reads healthy). */
|
|
575
|
+
backfill_interval_s: number;
|
|
576
|
+
/** An owed signal older than this wakes `memory_write_backlog` (and again
|
|
577
|
+
* every `rewake_h` while the backlog lasts). */
|
|
578
|
+
write_backlog_after_min: number;
|
|
579
|
+
/** Finding cases (src/cases.ts): how often the case writer syncs the cases
|
|
580
|
+
* state.db says are owed and, only while memory health reads healthy,
|
|
581
|
+
* writes up to 2 of them (one batch-write, never started while a round
|
|
582
|
+
* reads memory) and backfills two more PRs. */
|
|
583
|
+
cases_interval_s: number;
|
|
584
|
+
/** How far back the case backfill reaches: PRs with a finding posted in
|
|
585
|
+
* the last this-many days get their cases written (resumable, a few PRs a
|
|
586
|
+
* tick, only while memory reads healthy). 0 turns the backfill off. */
|
|
587
|
+
cases_backfill_days: number;
|
|
588
|
+
/** The learner (src/learner/learner.ts). The miner runs this
|
|
589
|
+
* many minutes before the EOD sweep (and on `qcr lessons mine`). */
|
|
590
|
+
lessons_mine_lead_min: number;
|
|
591
|
+
/** How far back a mine reads human outcomes (fates, the feedback ledger,
|
|
592
|
+
* recall sources). */
|
|
593
|
+
lessons_mine_days: number;
|
|
594
|
+
/** No `eod-curation-receipt` this long after the EOD sweep was delivered
|
|
595
|
+
* wakes `eod_curation_missing`. */
|
|
596
|
+
lessons_receipt_wait_min: number;
|
|
597
|
+
/** An offered candidate still undecided this many ops days later EXPIRES —
|
|
598
|
+
* recorded on the row, in an event and in the packet. */
|
|
599
|
+
lessons_expiry_ops_days: number;
|
|
600
|
+
/** At most this many candidates are offered for the FIRST time per EOD
|
|
601
|
+
* packet (misses first); the rest wait for a later one. Carried candidates
|
|
602
|
+
* are never capped. */
|
|
603
|
+
lessons_packet_new_max: number;
|
|
604
|
+
/** A bot-approved merged PR's later commits are read this long after the
|
|
605
|
+
* merge (post-merge recall, read-only git on the base clone). */
|
|
606
|
+
lessons_recall_window_days: number;
|
|
607
|
+
/** The window a cluster's recurrence and counterweight count over. */
|
|
608
|
+
lessons_recurrence_days: number;
|
|
609
|
+
/** The digests whose "Learnings" never reached memory — the one-time
|
|
610
|
+
* backfill into candidates (inclusive, YYYY-MM-DD). */
|
|
611
|
+
lessons_digest_backfill_from: string;
|
|
612
|
+
lessons_digest_backfill_to: string;
|
|
613
|
+
/** How many items the round's context search may draw from each area it
|
|
614
|
+
* searches. A round searches `experiences/` ONLY
|
|
615
|
+
* (`ROUND_SEARCH_AREAS`, src/memory/layout.ts): any other key here is
|
|
616
|
+
* ignored with one log line (src/openviking/context.ts
|
|
617
|
+
* `roundSearchQuotas`), and a missing or non-positive `experiences` count
|
|
618
|
+
* takes the default — the search is never left to the server's own balance
|
|
619
|
+
* across every scope (see the comment in config.toml). */
|
|
620
|
+
context_quotas: Record<string, number>;
|
|
621
|
+
/**
|
|
622
|
+
* How curated lessons and past finding cases reach a review:
|
|
623
|
+
* `off` — nothing runs: no selection, no shadow ledger;
|
|
624
|
+
* `shadow` — the SHIPPED value: every round selects lessons and cases
|
|
625
|
+
* through OpenViking (one bounded prefetch, never delaying the
|
|
626
|
+
* round) and the shadow ledger records what WOULD have been
|
|
627
|
+
* delivered and which posted findings a delivered dont-raise
|
|
628
|
+
* lesson would have touched — and NO prompt, task file or
|
|
629
|
+
* agent changes;
|
|
630
|
+
* `on` — delivery: the same selection, AWAITED beside the
|
|
631
|
+
* round's memory seed within the same budget, and rendered —
|
|
632
|
+
* each lens's task file, the lessons files of the validator
|
|
633
|
+
* and the lenses a session adds, the main session's "Lessons
|
|
634
|
+
* in force" index and past-review cases (the bleed rule
|
|
635
|
+
* reworded), the validator's `lesson-shape` addendum, and the
|
|
636
|
+
* repo facts for the paths a recheck or an author reply
|
|
637
|
+
* touches. A round that selects nothing renders as `off`.
|
|
638
|
+
* The owner switches it to `on` after reading `qcr lessons shadow-report`.
|
|
639
|
+
*/
|
|
640
|
+
delivery: MemoryDelivery;
|
|
641
|
+
/** The round's ONE lesson prefetch may take at most this long (ms) when the
|
|
642
|
+
* round WAITS for it — under `on` — then whatever arrived is used and every
|
|
643
|
+
* find still out is a recorded timeout. Clamped to 1…2000: the contract's
|
|
644
|
+
* hard ceiling is 2 s. */
|
|
645
|
+
delivery_budget_ms: number;
|
|
646
|
+
/** The same prefetch's budget (ms) when NOTHING waits for it — under
|
|
647
|
+
* `shadow`, where it starts after the round's handoff: looser,
|
|
648
|
+
* so a shadow selection behind a busy embedder still records what it would
|
|
649
|
+
* have delivered. Clamped to 1…20000 — it holds the selector's two places
|
|
650
|
+
* in the embedder queue for at most that long. */
|
|
651
|
+
shadow_budget_ms: number;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/** The three memory timeouts are BOUNDED — memory must never
|
|
655
|
+
* hold a round past a budget an operator can read off the config, and a
|
|
656
|
+
* mistyped 0 must not turn every seed memory-less. Inclusive ms. */
|
|
657
|
+
export const CONTEXT_TIMEOUT_BOUNDS_MS = { min: 100, max: 15_000 } as const;
|
|
658
|
+
export const SEARCH_TIMEOUT_BOUNDS_MS = { min: 100, max: 15_000 } as const;
|
|
659
|
+
export const HEALTH_TIMEOUT_BOUNDS_MS = { min: 100, max: 20_000 } as const;
|
|
660
|
+
export const CASE_EMBED_TAIL_MAX_MS = 30_000;
|
|
661
|
+
|
|
662
|
+
/** `v` as an integer inside `bounds`, or `fallback` when it is not a finite
|
|
663
|
+
* number at all. */
|
|
664
|
+
function clampMs(v: unknown, bounds: { min: number; max: number }, fallback: number): number {
|
|
665
|
+
const n = typeof v === "number" ? v : Number.NaN;
|
|
666
|
+
if (!Number.isFinite(n)) return fallback;
|
|
667
|
+
return Math.max(bounds.min, Math.min(bounds.max, Math.floor(n)));
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/** The contract's hard ceiling on the lesson prefetch. */
|
|
671
|
+
export const LESSON_PREFETCH_MAX_MS = 2000;
|
|
672
|
+
/** The shadow prefetch's ceiling and default. The default covers
|
|
673
|
+
* the live VM's measured worst case — a find's 2.5 s p90 behind a case batch
|
|
674
|
+
* already on the embedder (two ~3.4 s embeddings, src/cases.ts). */
|
|
675
|
+
export const LESSON_SHADOW_PREFETCH_MAX_MS = 20_000;
|
|
676
|
+
export const LESSON_SHADOW_PREFETCH_DEFAULT_MS = 10_000;
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* The [memory] block, defaults filled and the delivery switch checked: an
|
|
680
|
+
* unknown `delivery` value REFUSES to load (a misspelt `"onn"` must not
|
|
681
|
+
* quietly mean shadow, or off), and the prefetch budget is clamped to the
|
|
682
|
+
* contract's ceiling.
|
|
683
|
+
*/
|
|
684
|
+
export function parseMemory(raw: Record<string, unknown>): MemoryConfig {
|
|
685
|
+
const m = { ...MEMORY_DEFAULTS, ...(raw as Partial<MemoryConfig>) };
|
|
686
|
+
if (!(MEMORY_DELIVERY_MODES as readonly unknown[]).includes(m.delivery))
|
|
687
|
+
throw new Error(
|
|
688
|
+
`[memory] delivery must be one of ${MEMORY_DELIVERY_MODES.map((v) => `"${v}"`).join(", ")} (got ${JSON.stringify(m.delivery)})`,
|
|
689
|
+
);
|
|
690
|
+
const budget = Number(m.delivery_budget_ms);
|
|
691
|
+
m.delivery_budget_ms = Number.isFinite(budget)
|
|
692
|
+
? Math.max(1, Math.min(LESSON_PREFETCH_MAX_MS, Math.floor(budget)))
|
|
693
|
+
: LESSON_PREFETCH_MAX_MS;
|
|
694
|
+
const shadow = Number(m.shadow_budget_ms);
|
|
695
|
+
m.shadow_budget_ms = Number.isFinite(shadow)
|
|
696
|
+
? Math.max(1, Math.min(LESSON_SHADOW_PREFETCH_MAX_MS, Math.floor(shadow)))
|
|
697
|
+
: LESSON_SHADOW_PREFETCH_DEFAULT_MS;
|
|
698
|
+
m.context_timeout_ms = clampMs(
|
|
699
|
+
m.context_timeout_ms,
|
|
700
|
+
CONTEXT_TIMEOUT_BOUNDS_MS,
|
|
701
|
+
MEMORY_DEFAULTS.context_timeout_ms,
|
|
702
|
+
);
|
|
703
|
+
m.case_embed_tail_ms = clampMs(
|
|
704
|
+
m.case_embed_tail_ms,
|
|
705
|
+
{ min: 0, max: CASE_EMBED_TAIL_MAX_MS },
|
|
706
|
+
MEMORY_DEFAULTS.case_embed_tail_ms,
|
|
707
|
+
);
|
|
708
|
+
return m;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/** The quota a round's context search gives each area it searches when
|
|
712
|
+
* `[memory] context_quotas` names none (the round context assembler reads
|
|
713
|
+
* this same constant). */
|
|
714
|
+
export const DEFAULT_ROUND_QUOTA = 24;
|
|
715
|
+
|
|
716
|
+
export const MEMORY_DEFAULTS: MemoryConfig = {
|
|
717
|
+
context_tokens: 2000,
|
|
718
|
+
context_timeout_ms: 9000,
|
|
719
|
+
case_embed_tail_ms: 4000,
|
|
720
|
+
probe_interval_s: 120,
|
|
721
|
+
down_after_min: 10,
|
|
722
|
+
boot_grace_min: 5,
|
|
723
|
+
rewake_h: 6,
|
|
724
|
+
incident_ticket_after_min: 120,
|
|
725
|
+
backfill_interval_s: 60,
|
|
726
|
+
write_backlog_after_min: 120,
|
|
727
|
+
cases_interval_s: 120,
|
|
728
|
+
cases_backfill_days: 60,
|
|
729
|
+
lessons_mine_lead_min: 15,
|
|
730
|
+
lessons_mine_days: 45,
|
|
731
|
+
lessons_receipt_wait_min: 180,
|
|
732
|
+
lessons_expiry_ops_days: 7,
|
|
733
|
+
lessons_packet_new_max: 40,
|
|
734
|
+
lessons_recall_window_days: 14,
|
|
735
|
+
lessons_recurrence_days: 90,
|
|
736
|
+
lessons_digest_backfill_from: "2026-08-31",
|
|
737
|
+
lessons_digest_backfill_to: "2026-10-02",
|
|
738
|
+
context_quotas: { experiences: DEFAULT_ROUND_QUOTA },
|
|
739
|
+
delivery: "shadow",
|
|
740
|
+
delivery_budget_ms: LESSON_PREFETCH_MAX_MS,
|
|
741
|
+
shadow_budget_ms: LESSON_SHADOW_PREFETCH_DEFAULT_MS,
|
|
742
|
+
};
|
|
743
|
+
|
|
744
|
+
/** [openviking] is optional in config.toml — the defaults ARE the shipped
|
|
745
|
+
* configuration, exactly like [tui]/[companion]. */
|
|
746
|
+
export const OPENVIKING_DEFAULTS: OpenVikingConfig = {
|
|
747
|
+
enabled: true,
|
|
748
|
+
endpoint: "local",
|
|
749
|
+
version: "0.4.15",
|
|
750
|
+
llama_cpp_python_version: "0.3.35",
|
|
751
|
+
extension_commit: "d436baa23c83938b22bb0aab08b0ce48568b5ff4",
|
|
752
|
+
user: "reviewer",
|
|
753
|
+
account: "",
|
|
754
|
+
reviewer_key_file: "openviking_reviewer_key",
|
|
755
|
+
auth_mode: "api_key",
|
|
756
|
+
root_key_file: "openviking_root_key",
|
|
757
|
+
provision_users: ["reviewer", "companion"],
|
|
758
|
+
request_timeout_ms: 10000,
|
|
759
|
+
search_timeout_ms: 8000,
|
|
760
|
+
default_category: "experiences",
|
|
761
|
+
read_cache_ttl_ms: 15000,
|
|
762
|
+
host: "127.0.0.1",
|
|
763
|
+
port: 1933,
|
|
764
|
+
bin: "/opt/openviking/venv/bin/openviking-server",
|
|
765
|
+
home: "/opt/quickreview/state/openviking",
|
|
766
|
+
config_file: "", // → <home>/ov.conf
|
|
767
|
+
workspace: "", // → <home>/workspace
|
|
768
|
+
workers: 1,
|
|
769
|
+
queue_concurrency: 2,
|
|
770
|
+
health_interval_s: 30,
|
|
771
|
+
health_timeout_ms: 6000,
|
|
772
|
+
restart_backoff_ms: [1000, 2000, 5000, 15000, 30000, 60000],
|
|
773
|
+
restart_reset_after_s: 120,
|
|
774
|
+
embedding_provider: "local",
|
|
775
|
+
embedding_model: LOCAL_EMBEDDING.model,
|
|
776
|
+
embedding_dimension: LOCAL_EMBEDDING.dimension,
|
|
777
|
+
embedding_key_file: "openai_embeddings_api_key",
|
|
778
|
+
local_embedder_file: `/opt/openviking/models/${LOCAL_EMBEDDING.model}.gguf`,
|
|
779
|
+
local_embedder_url: `https://huggingface.co/CompendiumLabs/bge-small-en-v1.5-gguf/resolve/d32f8c040ea3b516330eeb75b72bcc2d3a780ab7/${LOCAL_EMBEDDING.model}.gguf`,
|
|
780
|
+
local_embedder_sha256: "f0b2fef971e8366438bfd2d9aefea1b0115919389448806d290237f638bae999",
|
|
781
|
+
local_embedder_port: 1934,
|
|
782
|
+
local_embedder_threads: 1,
|
|
783
|
+
};
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* The [openviking] block, defaults filled and the embedder checked.
|
|
787
|
+
*
|
|
788
|
+
* The embedder is validated at LOAD time because the boot validates it too,
|
|
789
|
+
* and a memory engine that refuses to start is a silent outage: an unknown
|
|
790
|
+
* provider, a non-positive dimension, a `local` model/dimension that is not
|
|
791
|
+
* the local embedder's, or a local model-file pin that does not name that
|
|
792
|
+
* model are all config errors here.
|
|
793
|
+
*/
|
|
794
|
+
export function parseOpenViking(raw: Record<string, unknown>): OpenVikingConfig {
|
|
795
|
+
const merged = { ...OPENVIKING_DEFAULTS, ...(raw as Partial<OpenVikingConfig>) };
|
|
796
|
+
if (!(OPENVIKING_ENDPOINTS as readonly string[]).includes(merged.endpoint)) {
|
|
797
|
+
throw new Error(
|
|
798
|
+
`config.toml: [openviking] endpoint must be one of ${OPENVIKING_ENDPOINTS.join(", ")} (got '${String(merged.endpoint)}')`,
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
// Bounded, never a tight loop or an unbounded wait.
|
|
802
|
+
merged.search_timeout_ms = clampMs(
|
|
803
|
+
merged.search_timeout_ms,
|
|
804
|
+
SEARCH_TIMEOUT_BOUNDS_MS,
|
|
805
|
+
OPENVIKING_DEFAULTS.search_timeout_ms,
|
|
806
|
+
);
|
|
807
|
+
merged.health_timeout_ms = clampMs(
|
|
808
|
+
merged.health_timeout_ms,
|
|
809
|
+
HEALTH_TIMEOUT_BOUNDS_MS,
|
|
810
|
+
OPENVIKING_DEFAULTS.health_timeout_ms,
|
|
811
|
+
);
|
|
812
|
+
// The defaults are the LOCAL embedder: a hosted opt-in that omits the model
|
|
813
|
+
// runs HOSTED_EMBEDDING, never the local model (which no hosted API serves).
|
|
814
|
+
// A named hosted model with no dimension has no default to fall back on.
|
|
815
|
+
if (merged.embedding_provider !== "local") {
|
|
816
|
+
if (raw.embedding_model === undefined) {
|
|
817
|
+
merged.embedding_model = HOSTED_EMBEDDING.model;
|
|
818
|
+
if (raw.embedding_dimension === undefined)
|
|
819
|
+
merged.embedding_dimension = HOSTED_EMBEDDING.dimension;
|
|
820
|
+
} else if (raw.embedding_dimension === undefined) {
|
|
821
|
+
if (merged.embedding_model !== HOSTED_EMBEDDING.model)
|
|
822
|
+
throw new Error(
|
|
823
|
+
`config.toml: [openviking] embedding_model = "${String(merged.embedding_model)}" needs its embedding_dimension set too`,
|
|
824
|
+
);
|
|
825
|
+
merged.embedding_dimension = HOSTED_EMBEDDING.dimension;
|
|
826
|
+
}
|
|
827
|
+
} else if (
|
|
828
|
+
merged.embedding_model === RETIRED_LOCAL_EMBEDDING.model &&
|
|
829
|
+
(raw.embedding_dimension === undefined ||
|
|
830
|
+
merged.embedding_dimension === RETIRED_LOCAL_EMBEDDING.dimension)
|
|
831
|
+
) {
|
|
832
|
+
// An earlier release required this spelling under `local`. It means the
|
|
833
|
+
// current local embedder — the boot renders the same, and the re-embed
|
|
834
|
+
// migration moves the store across.
|
|
835
|
+
merged.embedding_model = LOCAL_EMBEDDING.model;
|
|
836
|
+
merged.embedding_dimension = LOCAL_EMBEDDING.dimension;
|
|
837
|
+
}
|
|
838
|
+
if (!(EMBEDDING_PROVIDERS as readonly string[]).includes(merged.embedding_provider)) {
|
|
839
|
+
throw new Error(
|
|
840
|
+
`config.toml: [openviking] embedding_provider must be one of ${EMBEDDING_PROVIDERS.join(", ")} (got '${String(merged.embedding_provider)}')`,
|
|
841
|
+
);
|
|
842
|
+
}
|
|
843
|
+
if (!Number.isInteger(merged.embedding_dimension) || merged.embedding_dimension <= 0) {
|
|
844
|
+
throw new Error(
|
|
845
|
+
`config.toml: [openviking] embedding_dimension must be a positive integer (got '${String(merged.embedding_dimension)}')`,
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
if (
|
|
849
|
+
merged.embedding_provider === "local" &&
|
|
850
|
+
(merged.embedding_model !== LOCAL_EMBEDDING.model ||
|
|
851
|
+
merged.embedding_dimension !== LOCAL_EMBEDDING.dimension)
|
|
852
|
+
) {
|
|
853
|
+
throw new Error(
|
|
854
|
+
`config.toml: [openviking] embedding_provider = "local" runs the local English embedder, ${LOCAL_EMBEDDING.model} at ${LOCAL_EMBEDDING.dimension} dims — ` +
|
|
855
|
+
`the only local model the image pins; set embedding_model/embedding_dimension to it, or opt into a hosted provider`,
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
if (
|
|
859
|
+
typeof merged.embedding_key_file !== "string" ||
|
|
860
|
+
merged.embedding_key_file.trim() === "" ||
|
|
861
|
+
merged.embedding_key_file.includes("/")
|
|
862
|
+
) {
|
|
863
|
+
throw new Error(
|
|
864
|
+
"config.toml: [openviking] embedding_key_file must be a bare file name inside the secrets dir",
|
|
865
|
+
);
|
|
866
|
+
}
|
|
867
|
+
checkLocalEmbedderPin(merged);
|
|
868
|
+
return resolveOpenVikingPaths(merged);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
/** The local embedder's model-file pin and service knobs. The
|
|
872
|
+
* file and the URL must both name `LOCAL_EMBEDDING.model`: a pin for another
|
|
873
|
+
* model would put a file the service's model table does not describe under a
|
|
874
|
+
* name the vector store trusts. */
|
|
875
|
+
function checkLocalEmbedderPin(c: OpenVikingConfig): void {
|
|
876
|
+
const gguf = `${LOCAL_EMBEDDING.model}.gguf`;
|
|
877
|
+
const file = c.local_embedder_file;
|
|
878
|
+
if (typeof file !== "string" || !file.startsWith("/") || !file.endsWith(`/${gguf}`)) {
|
|
879
|
+
throw new Error(
|
|
880
|
+
`config.toml: [openviking] local_embedder_file must be an absolute container path ending in /${gguf} (got '${String(file)}')`,
|
|
881
|
+
);
|
|
882
|
+
}
|
|
883
|
+
const url = c.local_embedder_url;
|
|
884
|
+
if (typeof url !== "string" || !url.startsWith("https://") || !url.endsWith(`/${gguf}`)) {
|
|
885
|
+
throw new Error(
|
|
886
|
+
`config.toml: [openviking] local_embedder_url must be an https URL ending in /${gguf} (got '${String(url)}')`,
|
|
887
|
+
);
|
|
888
|
+
}
|
|
889
|
+
if (
|
|
890
|
+
typeof c.local_embedder_sha256 !== "string" ||
|
|
891
|
+
!/^[0-9a-f]{64}$/.test(c.local_embedder_sha256)
|
|
892
|
+
) {
|
|
893
|
+
throw new Error(
|
|
894
|
+
"config.toml: [openviking] local_embedder_sha256 must be the file's sha256, 64 lowercase hex digits",
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
const port = c.local_embedder_port;
|
|
898
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535 || port === c.port) {
|
|
899
|
+
throw new Error(
|
|
900
|
+
`config.toml: [openviking] local_embedder_port must be a TCP port other than the server's own ${c.port} (got '${String(port)}')`,
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
const threads = c.local_embedder_threads;
|
|
904
|
+
if (!Number.isInteger(threads) || threads < 1 || threads > 16) {
|
|
905
|
+
throw new Error(
|
|
906
|
+
`config.toml: [openviking] local_embedder_threads must be an integer in 1..16 (got '${String(threads)}')`,
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/** Derived paths: `config_file`/`workspace` default off `home`, so the three
|
|
912
|
+
* never drift apart in a hand-edited config. */
|
|
913
|
+
export function resolveOpenVikingPaths(c: OpenVikingConfig): OpenVikingConfig {
|
|
914
|
+
const home = expandHome(c.home);
|
|
915
|
+
return {
|
|
916
|
+
...c,
|
|
917
|
+
home,
|
|
918
|
+
config_file: c.config_file ? expandHome(c.config_file) : join(home, "ov.conf"),
|
|
919
|
+
workspace: c.workspace ? expandHome(c.workspace) : join(home, "workspace"),
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/** `http://host:port` — the one place the base URL is spelled. */
|
|
924
|
+
export function openVikingBaseUrl(c: OpenVikingConfig): string {
|
|
925
|
+
return `http://${c.host}:${c.port}`;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/**
|
|
929
|
+
* [ops] — the operations timezone: the ONE knob that defines whose
|
|
930
|
+
* calendar day the system runs on. Every day-scoped feature derives its day
|
|
931
|
+
* from here — the [feedback] same-day addendum + agent-rebuild rollover,
|
|
932
|
+
* metrics day buckets, TUI alert fold memory and companion day rollover —
|
|
933
|
+
* and the two sections that carry their own zone ([eod] and [slack.policy]
|
|
934
|
+
* `timezone`) DEFAULT from it, each still individually overridable in its own
|
|
935
|
+
* section.
|
|
936
|
+
*/
|
|
937
|
+
export interface OpsConfig {
|
|
938
|
+
/** IANA zone (e.g. "Europe/Warsaw") of the operator's day. */
|
|
939
|
+
timezone: string;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/** `[eod] pinned_watchdog_window_min`'s default, and the watchdog's own. */
|
|
943
|
+
export const EOD_PINNED_WATCHDOG_WINDOW_MIN = 90;
|
|
944
|
+
|
|
945
|
+
/** Neutral default — a config that never names a zone runs on UTC days. */
|
|
946
|
+
export const OPS_DEFAULTS: OpsConfig = { timezone: "UTC" };
|
|
947
|
+
|
|
948
|
+
/** The same Intl-based check the EOD machinery relies on (src/eod.ts builds
|
|
949
|
+
* an Intl.DateTimeFormat per zone): an unknown zone throws RangeError at
|
|
950
|
+
* formatter construction, so validate at parse time rather than at 18:00. */
|
|
951
|
+
export function assertIanaTimeZone(tz: string, where: string): string {
|
|
952
|
+
try {
|
|
953
|
+
new Intl.DateTimeFormat("en-US", { timeZone: tz });
|
|
954
|
+
} catch {
|
|
955
|
+
throw new Error(`config.toml: ${where} '${tz}' is not a valid IANA timezone`);
|
|
956
|
+
}
|
|
957
|
+
return tz;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
/** A section carrying its own `timezone` (defaulted from [ops]), returned
|
|
961
|
+
* once that zone passes `assertIanaTimeZone`. */
|
|
962
|
+
function checkSectionTimeZone<T extends { timezone: string }>(section: T, where: string): T {
|
|
963
|
+
assertIanaTimeZone(String(section.timezone), where);
|
|
964
|
+
return section;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
/**
|
|
968
|
+
* [ingress] — the webhook ingress shape. `tunnel-file` turns on the
|
|
969
|
+
* hostname-file watcher: a tunnel sidecar writes its assigned bare
|
|
970
|
+
* public hostname (e.g. `abc.trycloudflare.com`, trailing newline tolerated)
|
|
971
|
+
* to `hostname_file`, and the daemon re-points the App's own webhook URL when
|
|
972
|
+
* it changes — through the app-self-config gate class, which executes even in
|
|
973
|
+
* dry-run (AGENTS.md invariant 6). `none` (the default, and
|
|
974
|
+
* what an absent section means) starts nothing at all.
|
|
975
|
+
*/
|
|
976
|
+
export interface IngressConfig {
|
|
977
|
+
/** "none" ⇒ no watcher is ever constructed; "tunnel-file" ⇒ self-heal on. */
|
|
978
|
+
shape: "none" | "tunnel-file";
|
|
979
|
+
/** File the tunnel sidecar writes the bare public hostname into. Empty ⇒
|
|
980
|
+
* `<root>/state/ingress/hostname` (see ingressHostnameFile). */
|
|
981
|
+
hostname_file: string;
|
|
982
|
+
/** Settle time after a file change before the re-point runs — quick-tunnel
|
|
983
|
+
* sidecars write, then sometimes rewrite, within a second or two. */
|
|
984
|
+
debounce_ms: number;
|
|
985
|
+
/** The reliable path: a periodic sweep backs up fs.watch (the fast path),
|
|
986
|
+
* exactly like the verdict watcher. */
|
|
987
|
+
sweep_interval_s: number;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
export const INGRESS_DEFAULTS: IngressConfig = {
|
|
991
|
+
shape: "none",
|
|
992
|
+
hostname_file: "",
|
|
993
|
+
debounce_ms: 3000,
|
|
994
|
+
sweep_interval_s: 300,
|
|
995
|
+
};
|
|
996
|
+
|
|
997
|
+
/** The hostname file's resolved path — the one place the default is spelled. */
|
|
998
|
+
export function ingressHostnameFile(cfg: Config, root: string): string {
|
|
999
|
+
const configured = cfg.ingress.hostname_file.trim();
|
|
1000
|
+
return configured ? expandHome(configured) : join(root, "state", "ingress", "hostname");
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
/** The whole config.toml: the review sections (`CoreConfig`) and every
|
|
1004
|
+
* other surface's. */
|
|
1005
|
+
export interface Config extends CoreConfig {
|
|
1006
|
+
ops: OpsConfig;
|
|
1007
|
+
ingress: IngressConfig;
|
|
1008
|
+
server: { hostname: string; port: number };
|
|
1009
|
+
daemon: {
|
|
1010
|
+
poll_interval_s: number;
|
|
1011
|
+
max_concurrent_rounds: number;
|
|
1012
|
+
round_deadline_min: number;
|
|
1013
|
+
/** How many times the daemon may self-requeue a stuck round (overdue +
|
|
1014
|
+
* no verdict + session dead) per PR head. Past the cap the round
|
|
1015
|
+
* terminalizes through the overdue-anomaly path. Optional
|
|
1016
|
+
* so hand-rolled test configs keep compiling; readers default to
|
|
1017
|
+
* DEFAULT_MAX_SELF_REQUEUES. */
|
|
1018
|
+
max_self_requeues?: number;
|
|
1019
|
+
/** Minutes after dispatch before a running round with no prompt receipt
|
|
1020
|
+
* counts as lost and is recovered. Optional; readers
|
|
1021
|
+
* default to DEFAULT_ROUND_STALL_GRACE_MIN. */
|
|
1022
|
+
round_stall_grace_min?: number;
|
|
1023
|
+
/** How many times a round's prompt may be RE-DELIVERED before the
|
|
1024
|
+
* expensive recovery takes over. Separate from `max_self_requeues` on
|
|
1025
|
+
* purpose — redelivery costs one file write and keeps the round, the
|
|
1026
|
+
* session and the conversation, where a self-requeue burns a round and
|
|
1027
|
+
* (for a live session) kills it. Optional; readers default to
|
|
1028
|
+
* DEFAULT_MAX_PROMPT_REDELIVERIES. */
|
|
1029
|
+
max_prompt_redeliveries?: number;
|
|
1030
|
+
/** Daemon→pi stage bounds. Optional only for old configs; the
|
|
1031
|
+
* parser supplies the exported defaults so production always has finite
|
|
1032
|
+
* values. Progress telemetry never changes any of them. */
|
|
1033
|
+
session_command_start_lease_s?: number;
|
|
1034
|
+
/** Maximum wall time from tmux birth to a generation-matched inject-bridge
|
|
1035
|
+
* startup receipt. Tmux existence alone is never delivery readiness. */
|
|
1036
|
+
session_bridge_ready_timeout_s?: number;
|
|
1037
|
+
session_command_running_max_min?: number;
|
|
1038
|
+
/** Round execution milestones, all anchored to durable command
|
|
1039
|
+
* start. They never renew from heartbeat/activity telemetry. */
|
|
1040
|
+
round_first_pass_max_min?: number;
|
|
1041
|
+
round_passes_max_min?: number;
|
|
1042
|
+
round_finalize_grace_min?: number;
|
|
1043
|
+
/** Once a round's review is CONFIRMED posted, a stage limit
|
|
1044
|
+
* no longer cancels it — the session's tail (ledger, findings record,
|
|
1045
|
+
* verdict file) gets this long after the posting to finish, and a round
|
|
1046
|
+
* still without a verdict then is settled from the posting receipt. Never
|
|
1047
|
+
* a self-requeue, so never a second posting. Readers default to
|
|
1048
|
+
* DEFAULT_ROUND_POSTED_TAIL_GRACE_MIN. */
|
|
1049
|
+
round_posted_tail_grace_min?: number;
|
|
1050
|
+
/** The extra minutes a follow-up round whose pre-approval sweep
|
|
1051
|
+
* is OWED gets for its passes and its hard cap — the sweep is a whole
|
|
1052
|
+
* round-1 plan run after the delta's. Readers default to
|
|
1053
|
+
* DEFAULT_FOLLOW_UP_SWEEP_EXTRA_MIN. */
|
|
1054
|
+
follow_up_sweep_extra_min?: number;
|
|
1055
|
+
posting_receipt_grace_s?: number;
|
|
1056
|
+
/** A review POST GitHub answered 5xx (or not at all) is handed
|
|
1057
|
+
* to the daemon as a `deferred` receipt and retried at these offsets, in
|
|
1058
|
+
* minutes from the first failure — one attempt per entry, each through
|
|
1059
|
+
* the MutationGate after a marker read. Readers default to
|
|
1060
|
+
* DEFAULT_POSTING_RETRY_OFFSETS_MIN. */
|
|
1061
|
+
posting_retry_offsets_min?: number[];
|
|
1062
|
+
/** Every retry lies inside this many minutes of the first failure; past
|
|
1063
|
+
* it, nothing is POSTed (one last marker read) and the PR goes
|
|
1064
|
+
* FAILED_LOUD. Readers default to DEFAULT_POSTING_RETRY_WINDOW_MIN. */
|
|
1065
|
+
posting_retry_window_min?: number;
|
|
1066
|
+
session_command_unknown_grace_s?: number;
|
|
1067
|
+
session_command_unknown_max_checks?: number;
|
|
1068
|
+
session_command_max_start_retries?: number;
|
|
1069
|
+
/** How long a compaction the session's inject bridge reports
|
|
1070
|
+
* running may hold a waiting command's start lease open. pi refuses every
|
|
1071
|
+
* prompt while it compacts, so a command queued behind one is late, not
|
|
1072
|
+
* lost; past this bound a compaction still "running" is no reason to wait,
|
|
1073
|
+
* and the bounded redelivery → kill-and-replay recovery takes over.
|
|
1074
|
+
* Readers default to DEFAULT_SESSION_COMPACTION_MAX_S. */
|
|
1075
|
+
session_compaction_max_s?: number;
|
|
1076
|
+
/** The central lens permit ceiling: the most lens sessions alive at
|
|
1077
|
+
* once across every worker. Readers default to DEFAULT_LENS_PERMITS (20,
|
|
1078
|
+
* ruled for the VM: the CPU gate measured 20 sessions fine on 2 cores). */
|
|
1079
|
+
lens_permits?: number;
|
|
1080
|
+
/** The least gap, in ms, between two permit grants, so a wave never creates
|
|
1081
|
+
* its sessions in one burst (the gate measured a 0.7 to 2.5 s loop stall for
|
|
1082
|
+
* 8 to 20 at once). 0 turns it off. Default DEFAULT_LENS_GRANT_STAGGER_MS (250). */
|
|
1083
|
+
lens_grant_stagger_ms?: number;
|
|
1084
|
+
/** The idle-PR answer worker's wall-clock cap, in seconds: the
|
|
1085
|
+
* operator's message to a PR with no running worker is answered by a short
|
|
1086
|
+
* worker that is ended at this age. Readers default to DEFAULT_ANSWER_TIMEOUT_S. */
|
|
1087
|
+
answer_timeout_s?: number;
|
|
1088
|
+
/** The answer worker's dollar cap per message (the session's own spend, in
|
|
1089
|
+
* USD). Readers default to DEFAULT_ANSWER_CAP_USD. */
|
|
1090
|
+
answer_cap_usd?: number;
|
|
1091
|
+
round_stage_supervisor_stale_s?: number;
|
|
1092
|
+
/** The daemon's event-loop watch (src/eventloop.ts). All
|
|
1093
|
+
* optional — `eventLoopPolicy` defaults each (300 s window, a 10 s single
|
|
1094
|
+
* delay or a 2 s p99 over a full window is a stall, one wake per 60 min,
|
|
1095
|
+
* "CPU starved" from 20% of CFS periods throttled) and clamps a number
|
|
1096
|
+
* into range; a non-number falls back to the default. */
|
|
1097
|
+
event_loop_window_s?: number;
|
|
1098
|
+
event_loop_stall_max_s?: number;
|
|
1099
|
+
event_loop_stall_p99_s?: number;
|
|
1100
|
+
event_loop_wake_cooldown_min?: number;
|
|
1101
|
+
event_loop_starved_throttle_ratio?: number;
|
|
1102
|
+
/** Zombie-round expiry horizon: a round stuck `overdue` for longer than
|
|
1103
|
+
* this — and whose PR has since verdicted or closed, so a self-requeue
|
|
1104
|
+
* would never touch it — moves to terminal `expired`.
|
|
1105
|
+
* 0 disables the sweep. */
|
|
1106
|
+
expire_overdue_after_h: number;
|
|
1107
|
+
};
|
|
1108
|
+
eod: {
|
|
1109
|
+
time: string;
|
|
1110
|
+
timezone: string;
|
|
1111
|
+
/** Daily overseer heartbeat wall time; same zone as `time`. */
|
|
1112
|
+
heartbeat_time: string;
|
|
1113
|
+
/** Watch the pinned area across the sweep and raise an anomaly if
|
|
1114
|
+
* curation touched it. Off ⇒ no watch. */
|
|
1115
|
+
pinned_watchdog: boolean;
|
|
1116
|
+
/** How long after the inject the watchdog keeps looking, when the day's
|
|
1117
|
+
* digest never appears to close the window early. */
|
|
1118
|
+
pinned_watchdog_window_min: number;
|
|
1119
|
+
};
|
|
1120
|
+
caps: {
|
|
1121
|
+
improvement_pr_max_lines: number;
|
|
1122
|
+
improvement_pr_max_files: number;
|
|
1123
|
+
improvement_pr_per_day: number;
|
|
1124
|
+
improvement_pr_per_week: number;
|
|
1125
|
+
issues_per_day: number;
|
|
1126
|
+
/** Path prefixes a bot improvement PR may never touch. A configured
|
|
1127
|
+
* value REPLACES the list (it is repo-shape, not an invariant —
|
|
1128
|
+
* `migrations/` is Django's name for the concept), and `.env`-named files
|
|
1129
|
+
* are refused unconditionally regardless of this knob. */
|
|
1130
|
+
improvement_pr_deny_prefixes: string[];
|
|
1131
|
+
};
|
|
1132
|
+
openviking: OpenVikingConfig;
|
|
1133
|
+
memory: MemoryConfig;
|
|
1134
|
+
tui: TuiConfig;
|
|
1135
|
+
companion: CompanionConfig;
|
|
1136
|
+
sink: SinkConfig;
|
|
1137
|
+
backfill: BackfillConfig;
|
|
1138
|
+
recheck: RecheckConfig;
|
|
1139
|
+
levers: LeversConfig;
|
|
1140
|
+
slack: SlackConfig;
|
|
1141
|
+
/** Where `qcr scan start` drives a run: `[scan] sandbox`,
|
|
1142
|
+
* `pihome_volume` and `image`, read from the same table and kept out of
|
|
1143
|
+
* the scan settings (a profile cannot set them). */
|
|
1144
|
+
scan_sandbox: ScanSandboxConfig;
|
|
1145
|
+
/** The closed-PR archiver (`src/archive/`). Optional in the file;
|
|
1146
|
+
* the parser fills every key. */
|
|
1147
|
+
archive: ArchiveConfig;
|
|
1148
|
+
/** The bot login's single refresher (`src/loginrefresh/`). Optional
|
|
1149
|
+
* in the file; off unless the cutover's config turns it on. */
|
|
1150
|
+
login_refresh: LoginRefreshConfig;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
/**
|
|
1154
|
+
* `[login_refresh]`: the VM-only exception to the login-refresh rule (packages/pi-plugin/src/engine/auth.ts). After
|
|
1155
|
+
* the cutover ONE refresher process, spawned and supervised by the daemon,
|
|
1156
|
+
* owns pihome/agent/auth.json and every worker borrows a refresh-blanked copy.
|
|
1157
|
+
* Off by default: while the bot's tmux pi sessions exist they refresh the file
|
|
1158
|
+
* themselves, and a second refresher would rotate the refresh token under them.
|
|
1159
|
+
*/
|
|
1160
|
+
export interface LoginRefreshConfig {
|
|
1161
|
+
enabled: boolean;
|
|
1162
|
+
/** Refresh once the token has less than pi's own refresh window plus this
|
|
1163
|
+
* left. Must exceed `[daemon] round_deadline_min` (the longest worker cap), so
|
|
1164
|
+
* a worker that borrowed the file can finish on it. */
|
|
1165
|
+
margin_min: number;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
export const LOGIN_REFRESH_DEFAULTS: LoginRefreshConfig = Object.freeze({
|
|
1169
|
+
enabled: false,
|
|
1170
|
+
margin_min: 90,
|
|
1171
|
+
});
|
|
1172
|
+
|
|
1173
|
+
/** Parse `[login_refresh]`; with the refresher enabled, a margin that does not
|
|
1174
|
+
* outlast the longest worker cap REFUSES to load (a worker would then outlive
|
|
1175
|
+
* the token it borrowed). Disabled, nothing borrows, so the rule is not checked. */
|
|
1176
|
+
export function parseLoginRefresh(
|
|
1177
|
+
raw: Record<string, unknown>,
|
|
1178
|
+
roundDeadlineMin: number,
|
|
1179
|
+
): LoginRefreshConfig {
|
|
1180
|
+
const enabled = raw.enabled ?? LOGIN_REFRESH_DEFAULTS.enabled;
|
|
1181
|
+
if (typeof enabled !== "boolean")
|
|
1182
|
+
throw new Error(`config.toml [login_refresh]: enabled must be true or false`);
|
|
1183
|
+
const m = raw.margin_min ?? LOGIN_REFRESH_DEFAULTS.margin_min;
|
|
1184
|
+
const margin = typeof m === "bigint" ? Number(m) : m;
|
|
1185
|
+
if (typeof margin !== "number" || !Number.isFinite(margin) || margin > 1440)
|
|
1186
|
+
throw new Error(
|
|
1187
|
+
`config.toml [login_refresh]: margin_min must be a number of minutes up to 1440 (got ${JSON.stringify(typeof m === "bigint" ? String(m) : m)})`,
|
|
1188
|
+
);
|
|
1189
|
+
if (enabled && margin <= roundDeadlineMin)
|
|
1190
|
+
throw new Error(
|
|
1191
|
+
`config.toml [login_refresh]: margin_min (${margin}) must exceed [daemon] round_deadline_min (${roundDeadlineMin}), the longest worker cap`,
|
|
1192
|
+
);
|
|
1193
|
+
return { enabled, margin_min: margin };
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
/** Where a `qcr scan start` driver runs. `docker`: in a throwaway
|
|
1197
|
+
* container from the scan sandbox image that mounts only the reviewed repo
|
|
1198
|
+
* (read-only), the state home, the scan's own pi home and the config
|
|
1199
|
+
* (src/scanbox.ts). `host`: a detached process on this machine. */
|
|
1200
|
+
export const SCAN_SANDBOXES = ["docker", "host"] as const;
|
|
1201
|
+
export type ScanSandbox = (typeof SCAN_SANDBOXES)[number];
|
|
1202
|
+
|
|
1203
|
+
export interface ScanSandboxConfig {
|
|
1204
|
+
sandbox: ScanSandbox;
|
|
1205
|
+
/** Whether config.toml names `sandbox`: only the default gives way to
|
|
1206
|
+
* where the CLI runs (inside the companion container it is `host`). */
|
|
1207
|
+
sandbox_set: boolean;
|
|
1208
|
+
/** The pi home volume whose login the sandbox reads — read-only, a
|
|
1209
|
+
* refresh-disabled copy per case; by default the companion's own
|
|
1210
|
+
* (`[companion] pihome_volume`), which alone refreshes it. */
|
|
1211
|
+
pihome_volume: string;
|
|
1212
|
+
/** The image the sandbox runs; `SCAN_IMAGE_DEFAULT` when the key is omitted (an empty value is refused). */
|
|
1213
|
+
image: string;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
/** The scan sandbox's own image (docker/companion/Dockerfile, built by
|
|
1217
|
+
* `qcr scan rebuild`). Not the companion's: now that is the release
|
|
1218
|
+
* image in `companion` mode, which the sandbox's one-shot entrypoint does not
|
|
1219
|
+
* serve. */
|
|
1220
|
+
export const SCAN_IMAGE_DEFAULT = "qcr-scan";
|
|
1221
|
+
|
|
1222
|
+
export const SCAN_SANDBOX_DEFAULTS: Omit<ScanSandboxConfig, "image" | "pihome_volume"> = {
|
|
1223
|
+
sandbox: "docker",
|
|
1224
|
+
sandbox_set: false,
|
|
1225
|
+
};
|
|
1226
|
+
|
|
1227
|
+
/** `[scan] sandbox / pihome_volume / image`; the pi home volume defaults to
|
|
1228
|
+
* the companion's, the image to `SCAN_IMAGE_DEFAULT`. */
|
|
1229
|
+
export function parseScanSandbox(
|
|
1230
|
+
raw: unknown,
|
|
1231
|
+
companion: { pihome_volume: string },
|
|
1232
|
+
): ScanSandboxConfig {
|
|
1233
|
+
const t = (typeof raw === "object" && raw !== null ? raw : {}) as Record<string, unknown>;
|
|
1234
|
+
const name = (key: string, fallback: string): string => {
|
|
1235
|
+
const v = t[key];
|
|
1236
|
+
if (v === undefined) return fallback;
|
|
1237
|
+
if (typeof v !== "string" || v.trim() === "")
|
|
1238
|
+
throw new Error(
|
|
1239
|
+
`config.toml [scan] ${key} must be a non-empty name (got ${JSON.stringify(v)})`,
|
|
1240
|
+
);
|
|
1241
|
+
return v.trim();
|
|
1242
|
+
};
|
|
1243
|
+
const sandbox = t["sandbox"] ?? SCAN_SANDBOX_DEFAULTS.sandbox;
|
|
1244
|
+
if (typeof sandbox !== "string" || !(SCAN_SANDBOXES as readonly string[]).includes(sandbox))
|
|
1245
|
+
throw new Error(
|
|
1246
|
+
`config.toml [scan] sandbox must be "docker" or "host" (got ${JSON.stringify(sandbox)})`,
|
|
1247
|
+
);
|
|
1248
|
+
return {
|
|
1249
|
+
sandbox: sandbox as ScanSandbox,
|
|
1250
|
+
sandbox_set: t["sandbox"] !== undefined,
|
|
1251
|
+
pihome_volume: name("pihome_volume", companion.pihome_volume),
|
|
1252
|
+
image: name("image", SCAN_IMAGE_DEFAULT),
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
/**
|
|
1257
|
+
* `[levers]`: the bounds on a SESSION-issued `drain` / `pause-pr`.
|
|
1258
|
+
* The operator's own levers (`qcr drain on`, the deploy tab) are not bounded
|
|
1259
|
+
* by anything here — the deploy flow's drain stands until the new boot's
|
|
1260
|
+
* first healthy reconcile lifts it. Every number is minutes; the budgets are
|
|
1261
|
+
* counted per `[ops] timezone` day.
|
|
1262
|
+
*/
|
|
1263
|
+
export interface LeversConfig {
|
|
1264
|
+
/** Longest single session drain. */
|
|
1265
|
+
session_drain_max_min: number;
|
|
1266
|
+
/** Total session-drain minutes grantable per day; must be ≥ the cap. */
|
|
1267
|
+
session_drain_budget_min_per_day: number;
|
|
1268
|
+
/** Longest single session pause of one PR. */
|
|
1269
|
+
session_pause_max_min: number;
|
|
1270
|
+
/** Total session-pause minutes grantable per PR per day; must be ≥ the cap. */
|
|
1271
|
+
session_pause_budget_min_per_day: number;
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
export const LEVERS_DEFAULTS: LeversConfig = {
|
|
1275
|
+
session_drain_max_min: 30,
|
|
1276
|
+
session_drain_budget_min_per_day: 90,
|
|
1277
|
+
session_pause_max_min: 240,
|
|
1278
|
+
session_pause_budget_min_per_day: 480,
|
|
1279
|
+
};
|
|
1280
|
+
|
|
1281
|
+
/** Parse `[levers]`; a bound that is not a positive integer, or a budget
|
|
1282
|
+
* smaller than its cap, refuses to load — a lever bound of zero or a budget
|
|
1283
|
+
* that can never fund one full grant is a misconfiguration, not a setting. */
|
|
1284
|
+
export function parseLevers(raw: Record<string, any>): LeversConfig {
|
|
1285
|
+
const cfg: LeversConfig = { ...LEVERS_DEFAULTS, ...(raw as Partial<LeversConfig>) };
|
|
1286
|
+
for (const key of Object.keys(LEVERS_DEFAULTS) as Array<keyof LeversConfig>) {
|
|
1287
|
+
const v = cfg[key];
|
|
1288
|
+
if (!Number.isInteger(v) || v < 1) {
|
|
1289
|
+
throw new Error(
|
|
1290
|
+
`config.toml [levers]: ${key} must be a positive integer number of minutes (got ${JSON.stringify(v)})`,
|
|
1291
|
+
);
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
if (cfg.session_drain_budget_min_per_day < cfg.session_drain_max_min) {
|
|
1295
|
+
throw new Error(
|
|
1296
|
+
"config.toml [levers]: session_drain_budget_min_per_day must be at least session_drain_max_min",
|
|
1297
|
+
);
|
|
1298
|
+
}
|
|
1299
|
+
if (cfg.session_pause_budget_min_per_day < cfg.session_pause_max_min) {
|
|
1300
|
+
throw new Error(
|
|
1301
|
+
"config.toml [levers]: session_pause_budget_min_per_day must be at least session_pause_max_min",
|
|
1302
|
+
);
|
|
1303
|
+
}
|
|
1304
|
+
return cfg;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
/** The values `[archive] mode` takes (docs/archiving.md §12). */
|
|
1308
|
+
export const ARCHIVE_MODES = ["off", "plan", "on"] as const;
|
|
1309
|
+
export type ArchiveMode = (typeof ARCHIVE_MODES)[number];
|
|
1310
|
+
|
|
1311
|
+
/**
|
|
1312
|
+
* `[archive]` (docs/archiving.md): closed-PR history out of every
|
|
1313
|
+
* live path into a compressed archive under `<root>/archive/`, nothing of
|
|
1314
|
+
* analysis value deleted. Read by `src/archive/` (the archiver's policy) and
|
|
1315
|
+
* `qcr archive status`.
|
|
1316
|
+
*/
|
|
1317
|
+
export interface ArchiveConfig {
|
|
1318
|
+
/**
|
|
1319
|
+
* `off` — the archiver arms no timer at all;
|
|
1320
|
+
* `plan` — the SHIPPED value: it computes what it would move (status.json
|
|
1321
|
+
* `archive`, `qcr status`, `qcr archive status --plan`) and
|
|
1322
|
+
* writes nothing;
|
|
1323
|
+
* `on` — it MOVES, every tick: it restores reopened PRs' transcripts
|
|
1324
|
+
* and payloads first; then the old reader-less event rows and
|
|
1325
|
+
* webhook deliveries into `archive/db/`, sealing finished months;
|
|
1326
|
+
* then each eligible closed PR's files into `archive/pr-<n>/`,
|
|
1327
|
+
* removing each from the live tree once its copy is verified;
|
|
1328
|
+
* then the overseer's non-current transcripts and pi's day-old
|
|
1329
|
+
* global session files.
|
|
1330
|
+
* Nothing is deleted that is not verified in the archive or a
|
|
1331
|
+
* proven duplicate.
|
|
1332
|
+
*/
|
|
1333
|
+
mode: ArchiveMode;
|
|
1334
|
+
/** Closed PRs' files and webhook deliveries leave the live tree this many
|
|
1335
|
+
* days after the close (clamped 7..3650). */
|
|
1336
|
+
after_days: number;
|
|
1337
|
+
/** The reader-less event kinds leave this many days after their row was
|
|
1338
|
+
* written (clamped 31..3650, so `qcr metrics` d30 stays exact). */
|
|
1339
|
+
events_after_days: number;
|
|
1340
|
+
/** The archiver's own timer (clamped 10..3600 s). */
|
|
1341
|
+
interval_s: number;
|
|
1342
|
+
/** No tick before the daemon's first healthy reconcile plus this (clamped
|
|
1343
|
+
* 0..1440 min). */
|
|
1344
|
+
start_delay_min: number;
|
|
1345
|
+
/** No new file is started after this much of a tick (clamped 1..300 s). */
|
|
1346
|
+
tick_budget_s: number;
|
|
1347
|
+
/** At most this many PRs a tick (clamped 1..100). */
|
|
1348
|
+
max_prs_per_tick: number;
|
|
1349
|
+
/** Input rate cap for compress + verify (clamped 1..200 MB/s). */
|
|
1350
|
+
max_mb_per_s: number;
|
|
1351
|
+
/** Free space kept on both the archive and the state mount (clamped
|
|
1352
|
+
* 0.5..1024 GB). */
|
|
1353
|
+
disk_headroom_gb: number;
|
|
1354
|
+
/** The DB mover's starting batch (clamped 100..2000 rows). */
|
|
1355
|
+
db_batch_rows: number;
|
|
1356
|
+
/** gzip level (clamped 1..9). */
|
|
1357
|
+
gzip_level: number;
|
|
1358
|
+
/** A worker round's logs under `state/live/` and `state/lens/`
|
|
1359
|
+
* are removed from the live tree this many days after the round's last write,
|
|
1360
|
+
* and only once the PR's archive holds a verified copy (clamped 7..3650). A
|
|
1361
|
+
* log nothing archived is never removed. */
|
|
1362
|
+
live_retention_days: number;
|
|
1363
|
+
/** The disk cap on `state/live/` plus `state/lens/` together, in GB (clamped
|
|
1364
|
+
* 0..4096; 0 turns the check off). Past it the archiver raises the
|
|
1365
|
+
* `archive_live_cap` wake once per incident and every 6 h; it never deletes
|
|
1366
|
+
* to get under the cap. */
|
|
1367
|
+
live_cap_gb: number;
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
export const ARCHIVE_DEFAULTS: Readonly<ArchiveConfig> = Object.freeze({
|
|
1371
|
+
mode: "plan",
|
|
1372
|
+
after_days: 14,
|
|
1373
|
+
events_after_days: 35,
|
|
1374
|
+
interval_s: 60,
|
|
1375
|
+
start_delay_min: 10,
|
|
1376
|
+
tick_budget_s: 20,
|
|
1377
|
+
max_prs_per_tick: 5,
|
|
1378
|
+
max_mb_per_s: 8,
|
|
1379
|
+
disk_headroom_gb: 2,
|
|
1380
|
+
db_batch_rows: 500,
|
|
1381
|
+
gzip_level: 6,
|
|
1382
|
+
live_retention_days: 30,
|
|
1383
|
+
live_cap_gb: 20,
|
|
1384
|
+
});
|
|
1385
|
+
|
|
1386
|
+
/** Each numeric `[archive]` key's range, and whether it is a whole number. */
|
|
1387
|
+
export const ARCHIVE_LIMITS: Readonly<
|
|
1388
|
+
Record<Exclude<keyof ArchiveConfig, "mode">, readonly [min: number, max: number, int: boolean]>
|
|
1389
|
+
> = Object.freeze({
|
|
1390
|
+
after_days: [7, 3650, true],
|
|
1391
|
+
events_after_days: [31, 3650, true],
|
|
1392
|
+
interval_s: [10, 3600, true],
|
|
1393
|
+
start_delay_min: [0, 1440, true],
|
|
1394
|
+
tick_budget_s: [1, 300, true],
|
|
1395
|
+
max_prs_per_tick: [1, 100, true],
|
|
1396
|
+
max_mb_per_s: [1, 200, false],
|
|
1397
|
+
disk_headroom_gb: [0.5, 1024, false],
|
|
1398
|
+
db_batch_rows: [100, 2000, true],
|
|
1399
|
+
gzip_level: [1, 9, true],
|
|
1400
|
+
live_retention_days: [7, 3650, true],
|
|
1401
|
+
live_cap_gb: [0, 4096, false],
|
|
1402
|
+
});
|
|
1403
|
+
|
|
1404
|
+
/**
|
|
1405
|
+
* Parse `[archive]`. An unknown `mode` REFUSES to load (a misspelt `"onn"`
|
|
1406
|
+
* must not quietly mean plan, or off), and so does a key that is not a
|
|
1407
|
+
* number; a number out of its range is CLAMPED into it (`ARCHIVE_LIMITS`).
|
|
1408
|
+
*/
|
|
1409
|
+
export function parseArchive(raw: Record<string, unknown>): ArchiveConfig {
|
|
1410
|
+
const mode = raw.mode ?? ARCHIVE_DEFAULTS.mode;
|
|
1411
|
+
if (!(ARCHIVE_MODES as readonly unknown[]).includes(mode))
|
|
1412
|
+
throw new Error(
|
|
1413
|
+
`config.toml [archive]: mode must be one of ${ARCHIVE_MODES.map((v) => `"${v}"`).join(", ")} (got ${JSON.stringify(mode)})`,
|
|
1414
|
+
);
|
|
1415
|
+
const out: ArchiveConfig = { ...ARCHIVE_DEFAULTS, mode: mode as ArchiveMode };
|
|
1416
|
+
for (const [key, [min, max, int]] of Object.entries(ARCHIVE_LIMITS) as Array<
|
|
1417
|
+
[Exclude<keyof ArchiveConfig, "mode">, readonly [number, number, boolean]]
|
|
1418
|
+
>) {
|
|
1419
|
+
const v = raw[key];
|
|
1420
|
+
if (v === undefined) continue;
|
|
1421
|
+
const n = typeof v === "bigint" ? Number(v) : v;
|
|
1422
|
+
if (typeof n !== "number" || !Number.isFinite(n))
|
|
1423
|
+
throw new Error(
|
|
1424
|
+
`config.toml [archive]: ${key} must be a number (got ${JSON.stringify(typeof v === "bigint" ? String(v) : v)})`,
|
|
1425
|
+
);
|
|
1426
|
+
out[key] = Math.min(max, Math.max(min, int ? Math.floor(n) : n));
|
|
1427
|
+
}
|
|
1428
|
+
return out;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
/** The team slug a configured `org/team` review team is requested as — the
|
|
1432
|
+
* one rule the webhook's trigger and withdrawal, the trigger consumption and
|
|
1433
|
+
* the backfill all match on. A bare slug is its own slug. */
|
|
1434
|
+
export function reviewTeamSlug(reviewTeam: string): string {
|
|
1435
|
+
return reviewTeam.split("/")[1] ?? reviewTeam;
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
/** Defaults for [backfill], derived from the already-parsed [github] block. */
|
|
1439
|
+
export function backfillDefaults(github: GithubConfig): BackfillConfig {
|
|
1440
|
+
return {
|
|
1441
|
+
bot_login: github.bot_login,
|
|
1442
|
+
bot_actor: github.app_slug,
|
|
1443
|
+
reviewed_label: github.reviewed_label,
|
|
1444
|
+
trigger_label: github.trigger_label,
|
|
1445
|
+
in_progress_label: github.in_progress_label,
|
|
1446
|
+
review_team_slug: reviewTeamSlug(github.review_team),
|
|
1447
|
+
concurrency: 2,
|
|
1448
|
+
max_search_pages: 10,
|
|
1449
|
+
};
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
/** [tui] is optional in config.toml — the TUI is an operator convenience and
|
|
1453
|
+
* older configs must keep parsing. */
|
|
1454
|
+
export const TUI_DEFAULTS: TuiConfig = {
|
|
1455
|
+
target: "ssh",
|
|
1456
|
+
ssh_host: "my-vm",
|
|
1457
|
+
container: "quickreview-sandbox",
|
|
1458
|
+
memory_container: "",
|
|
1459
|
+
remote_state_dir: "/opt/quickreview/state",
|
|
1460
|
+
remote_sandbox_root: "",
|
|
1461
|
+
sudo_docker: true,
|
|
1462
|
+
poll_interval_s: 3,
|
|
1463
|
+
stale_after_s: 90,
|
|
1464
|
+
msg_timeout_s: 30,
|
|
1465
|
+
evidence_timeout_s: 60,
|
|
1466
|
+
evidence_why_interval_s: 10,
|
|
1467
|
+
};
|
|
1468
|
+
|
|
1469
|
+
/** The local companion image's tag: the release image (docker/Dockerfile) in
|
|
1470
|
+
* `companion` mode, built on the host. */
|
|
1471
|
+
export const COMPANION_IMAGE = "qcr-companion:release";
|
|
1472
|
+
|
|
1473
|
+
/** [companion] is optional in config.toml, exactly like [tui]. */
|
|
1474
|
+
export const COMPANION_DEFAULTS: CompanionConfig = {
|
|
1475
|
+
enabled: true,
|
|
1476
|
+
container_name: "qcr-companion",
|
|
1477
|
+
// The release image, locally built (`qcr companion rebuild`). A tag of its own:
|
|
1478
|
+
// the name `qcr-companion` is the retired companion image on an existing
|
|
1479
|
+
// PC, which the new container's entrypoint modes do not serve, so a
|
|
1480
|
+
// recreate must find no image here and build the right one.
|
|
1481
|
+
image: COMPANION_IMAGE,
|
|
1482
|
+
pihome_volume: "companion-pihome",
|
|
1483
|
+
session: "companion",
|
|
1484
|
+
model: "openai-codex/gpt-6.1-sol:medium",
|
|
1485
|
+
repo_dir: "", // → sandboxRoot()
|
|
1486
|
+
tracker_dir: "", // → /tracker (rw) when set; empty = no tracker mount
|
|
1487
|
+
tracker_docs_dir: "", // → /tracker-docs (ro) when set
|
|
1488
|
+
reviewed_repo_dir: "", // → /reviewed-repo (ro) when set and present
|
|
1489
|
+
evidence_dir: "~/.local/share/qcr/evidence", // → /evidence (ro)
|
|
1490
|
+
feed_dir: "", // → <root>/state/companion/feed
|
|
1491
|
+
alerts_dir: "", // → <root>/state/companion/alerts
|
|
1492
|
+
ssh_dir: "~/.ssh",
|
|
1493
|
+
build_context: "", // → <root> (the release image is built from it; Dockerfile at docker/Dockerfile)
|
|
1494
|
+
overdue_grace_min: 30,
|
|
1495
|
+
stale_alert_s: 300,
|
|
1496
|
+
awaiting_age_h: 24,
|
|
1497
|
+
cooldown_min: 20,
|
|
1498
|
+
};
|
|
1499
|
+
|
|
1500
|
+
/**
|
|
1501
|
+
* The issue sink — where auto-filed defect flags (`#bug` memory entries)
|
|
1502
|
+
* land. Three kinds:
|
|
1503
|
+
*
|
|
1504
|
+
* `wayfinder` — tickets written into a filesystem wayfinder tracker on the
|
|
1505
|
+
* operator's PC, into the effort and under the parent this
|
|
1506
|
+
* block names.
|
|
1507
|
+
* `github` — defects filed as issues on the reviewed repo. The TUI never
|
|
1508
|
+
* talks to GitHub: it writes a `file-issue` COMMAND into the
|
|
1509
|
+
* daemon's commands/ channel, so the daemon's MutationGate,
|
|
1510
|
+
* day caps and title dedup enforce the filing.
|
|
1511
|
+
* `none` — auto-filing off. The default when `[sink]` is absent:
|
|
1512
|
+
* unconfigured = off.
|
|
1513
|
+
*/
|
|
1514
|
+
export type IssueSinkKind = "wayfinder" | "github" | "none";
|
|
1515
|
+
|
|
1516
|
+
export interface SinkConfig {
|
|
1517
|
+
kind: IssueSinkKind;
|
|
1518
|
+
/** Wayfinder tracker root (also the ticket projection's root). Empty ⇒
|
|
1519
|
+
* defaults from `[companion] tracker_dir`. */
|
|
1520
|
+
root: string;
|
|
1521
|
+
/** Wayfinder: the effort auto-filed tickets are written into. */
|
|
1522
|
+
effort: string;
|
|
1523
|
+
/** Wayfinder: the `Parent:` map id stamped into each filed ticket. */
|
|
1524
|
+
parent: string;
|
|
1525
|
+
/** The TUI's read-only ticket projection's effort. Deliberately a
|
|
1526
|
+
* SEPARATE key: the board may read a different effort from the one the
|
|
1527
|
+
* filer writes into. Empty ⇒ same as `effort`. */
|
|
1528
|
+
tickets_effort: string;
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
export const SINK_DEFAULTS: SinkConfig = {
|
|
1532
|
+
kind: "none",
|
|
1533
|
+
root: "",
|
|
1534
|
+
effort: "",
|
|
1535
|
+
parent: "",
|
|
1536
|
+
tickets_effort: "",
|
|
1537
|
+
};
|
|
1538
|
+
|
|
1539
|
+
/** [sink] parse + validation. Misconfig is loud at boot, not a quiet no-op:
|
|
1540
|
+
* a `wayfinder` sink missing its effort or parent would otherwise silently
|
|
1541
|
+
* file nothing (or file into a nameless effort) forever. */
|
|
1542
|
+
function parseSink(raw: Record<string, any>, companion: CompanionConfig): SinkConfig {
|
|
1543
|
+
const sink: SinkConfig = { ...SINK_DEFAULTS, ...(raw as Partial<SinkConfig>) };
|
|
1544
|
+
if (!["wayfinder", "github", "none"].includes(sink.kind)) {
|
|
1545
|
+
throw new Error(
|
|
1546
|
+
`config.toml [sink]: kind must be "wayfinder", "github" or "none" (got ${JSON.stringify(sink.kind)})`,
|
|
1547
|
+
);
|
|
1548
|
+
}
|
|
1549
|
+
sink.root = sink.root ? expandHome(sink.root) : companion.tracker_dir;
|
|
1550
|
+
if (sink.kind === "wayfinder") {
|
|
1551
|
+
if (!sink.root.trim())
|
|
1552
|
+
throw new Error(
|
|
1553
|
+
'config.toml [sink]: kind = "wayfinder" requires a `root` (or a [companion] tracker_dir to default from)',
|
|
1554
|
+
);
|
|
1555
|
+
if (!sink.effort.trim())
|
|
1556
|
+
throw new Error('config.toml [sink]: kind = "wayfinder" requires a non-empty `effort`');
|
|
1557
|
+
if (!sink.parent.trim())
|
|
1558
|
+
throw new Error('config.toml [sink]: kind = "wayfinder" requires a non-empty `parent`');
|
|
1559
|
+
}
|
|
1560
|
+
return sink;
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
/** The ticket projection's effort — its own key when set, else the filer's.
|
|
1564
|
+
* Empty ⇒ the projection has nothing configured to read. */
|
|
1565
|
+
export function sinkTicketsEffort(cfg: Config): string {
|
|
1566
|
+
return cfg.sink.tickets_effort.trim() || cfg.sink.effort.trim();
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
/** Default lost-prompt grace when [daemon] carries no
|
|
1570
|
+
* `round_stall_grace_min`: long enough that a slow first tool call is never
|
|
1571
|
+
* mistaken for a dropped prompt, far shorter than the round deadline. */
|
|
1572
|
+
export const DEFAULT_ROUND_STALL_GRACE_MIN = 10;
|
|
1573
|
+
|
|
1574
|
+
/** Default self-requeue budget per PR head when [daemon] carries no
|
|
1575
|
+
* `max_self_requeues`. */
|
|
1576
|
+
export const DEFAULT_MAX_SELF_REQUEUES = 2;
|
|
1577
|
+
|
|
1578
|
+
/** Default prompt-redelivery budget per round. Two is enough to cover
|
|
1579
|
+
* a delivery that raced a busy session and one that raced a session restart;
|
|
1580
|
+
* a third failure is no longer a delivery problem, so escalating beats
|
|
1581
|
+
* re-sending the same file forever. */
|
|
1582
|
+
export const DEFAULT_MAX_PROMPT_REDELIVERIES = 2;
|
|
1583
|
+
|
|
1584
|
+
/** Finite daemon→pi stage bounds. The 45-minute hard run cap is above the
|
|
1585
|
+
* measured live maximum (~23 minutes) yet materially below the 75-minute
|
|
1586
|
+
* defense-in-depth round deadline. */
|
|
1587
|
+
export const DEFAULT_SESSION_COMMAND_START_LEASE_S = 120;
|
|
1588
|
+
export const DEFAULT_SESSION_BRIDGE_READY_TIMEOUT_S = 30;
|
|
1589
|
+
export const DEFAULT_SESSION_COMMAND_RUNNING_MAX_MIN = 45;
|
|
1590
|
+
export const DEFAULT_ROUND_FIRST_PASS_MAX_MIN = 5;
|
|
1591
|
+
export const DEFAULT_ROUND_PASSES_MAX_MIN = 35;
|
|
1592
|
+
export const DEFAULT_ROUND_FINALIZE_GRACE_MIN = 5;
|
|
1593
|
+
/** Measured posting→verdict tails run to ~6.5 min; 15 is over twice that and
|
|
1594
|
+
* still well inside the 75-minute round deadline. */
|
|
1595
|
+
export const DEFAULT_ROUND_POSTED_TAIL_GRACE_MIN = 15;
|
|
1596
|
+
/** `[daemon] follow_up_sweep_extra_min`'s default. */
|
|
1597
|
+
export const DEFAULT_FOLLOW_UP_SWEEP_EXTRA_MIN = 20;
|
|
1598
|
+
/** The round stage bounds in minutes, each with its code default: the one
|
|
1599
|
+
* resolution status publication and the stage supervisor both use. */
|
|
1600
|
+
export function resolveRoundStageBounds(daemon: Config["daemon"]): {
|
|
1601
|
+
first_pass_max_min: number;
|
|
1602
|
+
passes_max_min: number;
|
|
1603
|
+
running_max_min: number;
|
|
1604
|
+
follow_up_sweep_extra_min: number;
|
|
1605
|
+
} {
|
|
1606
|
+
return {
|
|
1607
|
+
first_pass_max_min: daemon.round_first_pass_max_min ?? DEFAULT_ROUND_FIRST_PASS_MAX_MIN,
|
|
1608
|
+
passes_max_min: daemon.round_passes_max_min ?? DEFAULT_ROUND_PASSES_MAX_MIN,
|
|
1609
|
+
running_max_min:
|
|
1610
|
+
daemon.session_command_running_max_min ?? DEFAULT_SESSION_COMMAND_RUNNING_MAX_MIN,
|
|
1611
|
+
follow_up_sweep_extra_min:
|
|
1612
|
+
daemon.follow_up_sweep_extra_min ?? DEFAULT_FOLLOW_UP_SWEEP_EXTRA_MIN,
|
|
1613
|
+
};
|
|
1614
|
+
}
|
|
1615
|
+
export const DEFAULT_POSTING_RECEIPT_GRACE_S = 120;
|
|
1616
|
+
/** Three retries, exponential,
|
|
1617
|
+
* all inside an hour of the first failure. */
|
|
1618
|
+
export const DEFAULT_POSTING_RETRY_OFFSETS_MIN: readonly number[] = [4, 12, 36];
|
|
1619
|
+
export const DEFAULT_POSTING_RETRY_WINDOW_MIN = 60;
|
|
1620
|
+
export const DEFAULT_SESSION_COMMAND_UNKNOWN_GRACE_S = 60;
|
|
1621
|
+
export const DEFAULT_SESSION_COMMAND_UNKNOWN_MAX_CHECKS = 1;
|
|
1622
|
+
export const DEFAULT_SESSION_COMMAND_MAX_START_RETRIES = 2;
|
|
1623
|
+
/** A post-verdict compaction can take over a minute with the container
|
|
1624
|
+
* CPU-starved; ten minutes is several times the worst seen and still well
|
|
1625
|
+
* inside the 75-minute round deadline. */
|
|
1626
|
+
export const DEFAULT_SESSION_COMPACTION_MAX_S = 600;
|
|
1627
|
+
/** The idle-PR answer worker: ten minutes of wall clock, two dollars. */
|
|
1628
|
+
export const DEFAULT_ANSWER_TIMEOUT_S = 600;
|
|
1629
|
+
export const DEFAULT_ANSWER_CAP_USD = 2;
|
|
1630
|
+
export const DEFAULT_ROUND_STAGE_SUPERVISOR_STALE_S = 180;
|
|
1631
|
+
|
|
1632
|
+
/** The app's root: the checkout (or image tree) this module runs from. The
|
|
1633
|
+
* review core's shipped data is found through `assetRoot` (packages/pi-plugin/src/assetroot.ts). */
|
|
1634
|
+
export function sandboxRoot(): string {
|
|
1635
|
+
return packageRootFrom(dirname(fileURLToPath(import.meta.url)), "sandbox root");
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
export function expandHome(p: string): string {
|
|
1639
|
+
return p.startsWith("~/") || p === "~" ? resolve(homedir(), p.slice(2)) : p;
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
/** Host paths for every companion mount: a set path gets its `~` expanded (a relative one stays relative); "" means derive from root. */
|
|
1643
|
+
export function resolveCompanionPaths(
|
|
1644
|
+
c: CompanionConfig,
|
|
1645
|
+
root: string = sandboxRoot(),
|
|
1646
|
+
): CompanionConfig {
|
|
1647
|
+
return {
|
|
1648
|
+
...c,
|
|
1649
|
+
repo_dir: c.repo_dir ? expandHome(c.repo_dir) : root,
|
|
1650
|
+
tracker_dir: c.tracker_dir ? expandHome(c.tracker_dir) : "",
|
|
1651
|
+
tracker_docs_dir: c.tracker_docs_dir ? expandHome(c.tracker_docs_dir) : "",
|
|
1652
|
+
reviewed_repo_dir: expandHome(c.reviewed_repo_dir),
|
|
1653
|
+
evidence_dir: c.evidence_dir ? expandHome(c.evidence_dir) : resolve(root, "evidence"),
|
|
1654
|
+
feed_dir: c.feed_dir ? expandHome(c.feed_dir) : resolve(root, "state", "companion", "feed"),
|
|
1655
|
+
alerts_dir: c.alerts_dir
|
|
1656
|
+
? expandHome(c.alerts_dir)
|
|
1657
|
+
: resolve(root, "state", "companion", "alerts"),
|
|
1658
|
+
ssh_dir: expandHome(c.ssh_dir),
|
|
1659
|
+
build_context: c.build_context ? expandHome(c.build_context) : root,
|
|
1660
|
+
};
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
/**
|
|
1664
|
+
* `[caps]` parsed with each numeric cap CHECKED, not cast: a key missing
|
|
1665
|
+
* from an operator's file must never reach `checkDiffstat` as `undefined`,
|
|
1666
|
+
* where `files > undefined` is `false` and the cap would pass. A cap the
|
|
1667
|
+
* daemon cannot compare against is a startup refusal: the improvement
|
|
1668
|
+
* channel is the one place this system pushes code, and it must never run uncapped.
|
|
1669
|
+
*/
|
|
1670
|
+
export function parseCaps(raw: Record<string, unknown>): Config["caps"] {
|
|
1671
|
+
const cap = (key: string): number => {
|
|
1672
|
+
const v = raw[key];
|
|
1673
|
+
if (typeof v !== "number" || !Number.isFinite(v) || v < 0) {
|
|
1674
|
+
throw new Error(
|
|
1675
|
+
`config.toml [caps]: ${key} must be a non-negative number (got ${JSON.stringify(v)})`,
|
|
1676
|
+
);
|
|
1677
|
+
}
|
|
1678
|
+
return v;
|
|
1679
|
+
};
|
|
1680
|
+
const isStrings = (v: unknown): v is string[] =>
|
|
1681
|
+
Array.isArray(v) && v.every((x) => typeof x === "string");
|
|
1682
|
+
const denyPrefixes = (): string[] => {
|
|
1683
|
+
const prefixes = raw.improvement_pr_deny_prefixes;
|
|
1684
|
+
if (prefixes === undefined) return [...IMPROVEMENT_DENY_PREFIXES_DEFAULT];
|
|
1685
|
+
if (isStrings(prefixes)) return [...prefixes];
|
|
1686
|
+
throw new Error(
|
|
1687
|
+
`config.toml [caps]: improvement_pr_deny_prefixes must be an array of strings (got ${JSON.stringify(prefixes)})`,
|
|
1688
|
+
);
|
|
1689
|
+
};
|
|
1690
|
+
return {
|
|
1691
|
+
improvement_pr_max_lines: cap("improvement_pr_max_lines"),
|
|
1692
|
+
improvement_pr_max_files: cap("improvement_pr_max_files"),
|
|
1693
|
+
improvement_pr_per_day: cap("improvement_pr_per_day"),
|
|
1694
|
+
improvement_pr_per_week: cap("improvement_pr_per_week"),
|
|
1695
|
+
issues_per_day: cap("issues_per_day"),
|
|
1696
|
+
improvement_pr_deny_prefixes: denyPrefixes(),
|
|
1697
|
+
};
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
/** The shipped improvement-PR path denylist. Lives here (not in
|
|
1701
|
+
* improvements.ts) so config carries the default without a runtime import cycle. */
|
|
1702
|
+
export const IMPROVEMENT_DENY_PREFIXES_DEFAULT: readonly string[] = [
|
|
1703
|
+
"migrations/",
|
|
1704
|
+
"infra/",
|
|
1705
|
+
"ansible/",
|
|
1706
|
+
".github/workflows/",
|
|
1707
|
+
];
|
|
1708
|
+
|
|
1709
|
+
export function parseConfig(toml: string): Config {
|
|
1710
|
+
const raw = parse(toml) as Record<string, any>;
|
|
1711
|
+
for (const line of configDeprecations(raw)) console.warn(`[quickreviewd] ${line}`);
|
|
1712
|
+
const github = parseGithubSection(raw);
|
|
1713
|
+
const preset = presetOf(raw);
|
|
1714
|
+
const engine = parseEngine((raw.engine ?? {}) as Record<string, unknown>);
|
|
1715
|
+
const companion = resolveCompanionPaths({
|
|
1716
|
+
...COMPANION_DEFAULTS,
|
|
1717
|
+
...((raw.companion ?? {}) as Partial<CompanionConfig>),
|
|
1718
|
+
});
|
|
1719
|
+
// [ops] is optional so older configs keep parsing; [eod] and
|
|
1720
|
+
// [slack.policy] timezones default from it below, own-section value wins.
|
|
1721
|
+
const ops: OpsConfig = {
|
|
1722
|
+
timezone: assertIanaTimeZone(
|
|
1723
|
+
String(((raw.ops ?? {}) as Partial<OpsConfig>).timezone ?? OPS_DEFAULTS.timezone),
|
|
1724
|
+
"[ops] timezone",
|
|
1725
|
+
),
|
|
1726
|
+
};
|
|
1727
|
+
// [ingress] is optional: absent ⇒ shape "none", nothing starts.
|
|
1728
|
+
const ingress: IngressConfig = {
|
|
1729
|
+
...INGRESS_DEFAULTS,
|
|
1730
|
+
...((raw.ingress ?? {}) as Partial<IngressConfig>),
|
|
1731
|
+
};
|
|
1732
|
+
if (!["none", "tunnel-file"].includes(ingress.shape)) {
|
|
1733
|
+
throw new Error(
|
|
1734
|
+
`config.toml [ingress]: shape must be "none" or "tunnel-file" (got ${JSON.stringify(ingress.shape)})`,
|
|
1735
|
+
);
|
|
1736
|
+
}
|
|
1737
|
+
const cfg: Config = {
|
|
1738
|
+
dry_run: Boolean(req(raw, "dry_run")),
|
|
1739
|
+
ops,
|
|
1740
|
+
ingress,
|
|
1741
|
+
github,
|
|
1742
|
+
server: { ...(req(raw, "server") as Config["server"]) },
|
|
1743
|
+
// Every optional knob defaults so older configs keep parsing.
|
|
1744
|
+
daemon: {
|
|
1745
|
+
max_self_requeues: DEFAULT_MAX_SELF_REQUEUES,
|
|
1746
|
+
expire_overdue_after_h: 24,
|
|
1747
|
+
session_command_start_lease_s: DEFAULT_SESSION_COMMAND_START_LEASE_S,
|
|
1748
|
+
session_bridge_ready_timeout_s: DEFAULT_SESSION_BRIDGE_READY_TIMEOUT_S,
|
|
1749
|
+
session_command_running_max_min: DEFAULT_SESSION_COMMAND_RUNNING_MAX_MIN,
|
|
1750
|
+
round_first_pass_max_min: DEFAULT_ROUND_FIRST_PASS_MAX_MIN,
|
|
1751
|
+
round_passes_max_min: DEFAULT_ROUND_PASSES_MAX_MIN,
|
|
1752
|
+
round_finalize_grace_min: DEFAULT_ROUND_FINALIZE_GRACE_MIN,
|
|
1753
|
+
round_posted_tail_grace_min: DEFAULT_ROUND_POSTED_TAIL_GRACE_MIN,
|
|
1754
|
+
follow_up_sweep_extra_min: DEFAULT_FOLLOW_UP_SWEEP_EXTRA_MIN,
|
|
1755
|
+
posting_receipt_grace_s: DEFAULT_POSTING_RECEIPT_GRACE_S,
|
|
1756
|
+
posting_retry_offsets_min: [...DEFAULT_POSTING_RETRY_OFFSETS_MIN],
|
|
1757
|
+
posting_retry_window_min: DEFAULT_POSTING_RETRY_WINDOW_MIN,
|
|
1758
|
+
session_command_unknown_grace_s: DEFAULT_SESSION_COMMAND_UNKNOWN_GRACE_S,
|
|
1759
|
+
session_command_unknown_max_checks: DEFAULT_SESSION_COMMAND_UNKNOWN_MAX_CHECKS,
|
|
1760
|
+
session_command_max_start_retries: DEFAULT_SESSION_COMMAND_MAX_START_RETRIES,
|
|
1761
|
+
session_compaction_max_s: DEFAULT_SESSION_COMPACTION_MAX_S,
|
|
1762
|
+
lens_permits: DEFAULT_LENS_PERMITS,
|
|
1763
|
+
lens_grant_stagger_ms: DEFAULT_LENS_GRANT_STAGGER_MS,
|
|
1764
|
+
answer_timeout_s: DEFAULT_ANSWER_TIMEOUT_S,
|
|
1765
|
+
answer_cap_usd: DEFAULT_ANSWER_CAP_USD,
|
|
1766
|
+
round_stage_supervisor_stale_s: DEFAULT_ROUND_STAGE_SUPERVISOR_STALE_S,
|
|
1767
|
+
...(req(raw, "daemon") as Partial<Config["daemon"]>),
|
|
1768
|
+
} as Config["daemon"],
|
|
1769
|
+
// heartbeat_time defaults so older configs keep parsing; the timezone
|
|
1770
|
+
// defaults from [ops], own [eod] value wins — and is checked as [ops] is.
|
|
1771
|
+
eod: checkSectionTimeZone(
|
|
1772
|
+
{
|
|
1773
|
+
heartbeat_time: "09:00",
|
|
1774
|
+
pinned_watchdog: true,
|
|
1775
|
+
pinned_watchdog_window_min: EOD_PINNED_WATCHDOG_WINDOW_MIN,
|
|
1776
|
+
timezone: ops.timezone,
|
|
1777
|
+
...(req(raw, "eod") as Partial<Config["eod"]>),
|
|
1778
|
+
} as Config["eod"],
|
|
1779
|
+
"[eod] timezone",
|
|
1780
|
+
),
|
|
1781
|
+
caps: parseCaps(req(raw, "caps") as Record<string, unknown>),
|
|
1782
|
+
models: parseModels(req(raw, "models") as Record<string, any>, REVIEW_PRESETS[preset].effort),
|
|
1783
|
+
engine,
|
|
1784
|
+
pi: parsePiSection(raw),
|
|
1785
|
+
openviking: parseOpenViking((raw.openviking ?? {}) as Record<string, unknown>),
|
|
1786
|
+
memory: parseMemory((raw.memory ?? {}) as Record<string, unknown>),
|
|
1787
|
+
tui: { ...TUI_DEFAULTS, ...((raw.tui ?? {}) as Partial<TuiConfig>) },
|
|
1788
|
+
companion,
|
|
1789
|
+
sink: parseSink((raw.sink ?? {}) as Record<string, any>, companion),
|
|
1790
|
+
backfill: { ...backfillDefaults(github), ...((raw.backfill ?? {}) as Partial<BackfillConfig>) },
|
|
1791
|
+
guards: parseGuardsSection(raw),
|
|
1792
|
+
recheck: { ...RECHECK_DEFAULTS, ...((raw.recheck ?? {}) as Partial<RecheckConfig>) },
|
|
1793
|
+
review: parseReviewSection(raw, preset),
|
|
1794
|
+
escalation: parseEscalation((raw.escalation ?? {}) as Record<string, any>),
|
|
1795
|
+
levers: parseLevers((raw.levers ?? {}) as Record<string, any>),
|
|
1796
|
+
slack: parseSlack((raw.slack ?? {}) as Record<string, any>, ops.timezone),
|
|
1797
|
+
owner: parseOwnerSection(raw),
|
|
1798
|
+
scan: parseScan(raw.scan),
|
|
1799
|
+
scan_sandbox: parseScanSandbox(raw.scan, companion),
|
|
1800
|
+
archive: parseArchive((raw.archive ?? {}) as Record<string, unknown>),
|
|
1801
|
+
login_refresh: parseLoginRefresh(
|
|
1802
|
+
(raw.login_refresh ?? {}) as Record<string, unknown>,
|
|
1803
|
+
Number((req(raw, "daemon") as { round_deadline_min?: number }).round_deadline_min ?? 75),
|
|
1804
|
+
),
|
|
1805
|
+
};
|
|
1806
|
+
validateReviewSection(cfg);
|
|
1807
|
+
foldLegacyOwnerSlackId(raw, cfg.owner);
|
|
1808
|
+
requireGithubKeys(cfg.github);
|
|
1809
|
+
// Every secrets reader in the daemon reads THIS field, so the
|
|
1810
|
+
// install locator decides it here, once, for all of them.
|
|
1811
|
+
cfg.github.secrets_dir = locateSecrets({
|
|
1812
|
+
configured: typeof cfg.github.secrets_dir === "string" ? cfg.github.secrets_dir : "",
|
|
1813
|
+
}).path;
|
|
1814
|
+
if (cfg.tui.target !== "ssh" && cfg.tui.target !== "local") {
|
|
1815
|
+
throw new Error(
|
|
1816
|
+
`config.toml [tui]: target must be "ssh" or "local" (got ${JSON.stringify(cfg.tui.target)})`,
|
|
1817
|
+
);
|
|
1818
|
+
}
|
|
1819
|
+
const firstPass = cfg.daemon.round_first_pass_max_min ?? DEFAULT_ROUND_FIRST_PASS_MAX_MIN;
|
|
1820
|
+
const passes = cfg.daemon.round_passes_max_min ?? DEFAULT_ROUND_PASSES_MAX_MIN;
|
|
1821
|
+
const running =
|
|
1822
|
+
cfg.daemon.session_command_running_max_min ?? DEFAULT_SESSION_COMMAND_RUNNING_MAX_MIN;
|
|
1823
|
+
const finalize = cfg.daemon.round_finalize_grace_min ?? DEFAULT_ROUND_FINALIZE_GRACE_MIN;
|
|
1824
|
+
const stageBounds = [firstPass, passes, running, finalize] as unknown[];
|
|
1825
|
+
if (!(
|
|
1826
|
+
stageBounds.every((v) => typeof v === "number" && Number.isFinite(v)) &&
|
|
1827
|
+
firstPass > 0 &&
|
|
1828
|
+
firstPass < passes &&
|
|
1829
|
+
passes < running &&
|
|
1830
|
+
finalize > 0 &&
|
|
1831
|
+
finalize < running
|
|
1832
|
+
)) {
|
|
1833
|
+
throw new Error(
|
|
1834
|
+
"config.toml: require finite numbers 0 < round_first_pass_max_min < round_passes_max_min < session_command_running_max_min and 0 < round_finalize_grace_min < session_command_running_max_min",
|
|
1835
|
+
);
|
|
1836
|
+
}
|
|
1837
|
+
// The two second-scale daemon→pi bounds become command deadlines
|
|
1838
|
+
// (`toISOString`), which a non-finite value would throw at the first command.
|
|
1839
|
+
for (const key of ["session_command_start_lease_s", "session_bridge_ready_timeout_s"] as const) {
|
|
1840
|
+
const v: unknown = cfg.daemon[key];
|
|
1841
|
+
if (!(typeof v === "number" && Number.isFinite(v) && v > 0))
|
|
1842
|
+
throw new Error(`config.toml: require a finite number [daemon] ${key} > 0`);
|
|
1843
|
+
}
|
|
1844
|
+
const postedTail = cfg.daemon.round_posted_tail_grace_min ?? DEFAULT_ROUND_POSTED_TAIL_GRACE_MIN;
|
|
1845
|
+
if (!(typeof postedTail === "number" && Number.isFinite(postedTail) && postedTail > 0)) {
|
|
1846
|
+
throw new Error("config.toml: require round_posted_tail_grace_min > 0");
|
|
1847
|
+
}
|
|
1848
|
+
const sweepExtra = cfg.daemon.follow_up_sweep_extra_min ?? DEFAULT_FOLLOW_UP_SWEEP_EXTRA_MIN;
|
|
1849
|
+
if (!(typeof sweepExtra === "number" && Number.isFinite(sweepExtra) && sweepExtra >= 0)) {
|
|
1850
|
+
throw new Error("config.toml: require follow_up_sweep_extra_min >= 0");
|
|
1851
|
+
}
|
|
1852
|
+
const retryWindow = cfg.daemon.posting_retry_window_min ?? DEFAULT_POSTING_RETRY_WINDOW_MIN;
|
|
1853
|
+
const retryOffsets = cfg.daemon.posting_retry_offsets_min ?? DEFAULT_POSTING_RETRY_OFFSETS_MIN;
|
|
1854
|
+
if (
|
|
1855
|
+
!(typeof retryWindow === "number" && Number.isFinite(retryWindow) && retryWindow > 0) ||
|
|
1856
|
+
!Array.isArray(retryOffsets) ||
|
|
1857
|
+
retryOffsets.length === 0 ||
|
|
1858
|
+
!retryOffsets.every(
|
|
1859
|
+
(m, i) =>
|
|
1860
|
+
typeof m === "number" &&
|
|
1861
|
+
Number.isFinite(m) &&
|
|
1862
|
+
m > 0 &&
|
|
1863
|
+
m < retryWindow &&
|
|
1864
|
+
(i === 0 || m > (retryOffsets[i - 1] as number)),
|
|
1865
|
+
)
|
|
1866
|
+
) {
|
|
1867
|
+
throw new Error(
|
|
1868
|
+
"config.toml: require posting_retry_offsets_min = a non-empty, strictly increasing list of minutes, each > 0 and < posting_retry_window_min (> 0)",
|
|
1869
|
+
);
|
|
1870
|
+
}
|
|
1871
|
+
const lensPermits = cfg.daemon.lens_permits ?? DEFAULT_LENS_PERMITS;
|
|
1872
|
+
if (!(Number.isSafeInteger(lensPermits) && lensPermits >= 1)) {
|
|
1873
|
+
throw new Error("config.toml: require an integer [daemon] lens_permits >= 1");
|
|
1874
|
+
}
|
|
1875
|
+
const lensStagger = cfg.daemon.lens_grant_stagger_ms ?? DEFAULT_LENS_GRANT_STAGGER_MS;
|
|
1876
|
+
if (!(typeof lensStagger === "number" && Number.isFinite(lensStagger) && lensStagger >= 0)) {
|
|
1877
|
+
throw new Error("config.toml: require a finite number [daemon] lens_grant_stagger_ms >= 0");
|
|
1878
|
+
}
|
|
1879
|
+
const answerTimeout = cfg.daemon.answer_timeout_s ?? DEFAULT_ANSWER_TIMEOUT_S;
|
|
1880
|
+
if (!(typeof answerTimeout === "number" && Number.isFinite(answerTimeout) && answerTimeout > 0)) {
|
|
1881
|
+
throw new Error("config.toml: require a finite number [daemon] answer_timeout_s > 0");
|
|
1882
|
+
}
|
|
1883
|
+
const answerCap = cfg.daemon.answer_cap_usd ?? DEFAULT_ANSWER_CAP_USD;
|
|
1884
|
+
if (!(typeof answerCap === "number" && Number.isFinite(answerCap) && answerCap > 0)) {
|
|
1885
|
+
throw new Error("config.toml: require a finite number [daemon] answer_cap_usd > 0");
|
|
1886
|
+
}
|
|
1887
|
+
const compactionMax = cfg.daemon.session_compaction_max_s ?? DEFAULT_SESSION_COMPACTION_MAX_S;
|
|
1888
|
+
if (!(typeof compactionMax === "number" && Number.isFinite(compactionMax) && compactionMax > 0)) {
|
|
1889
|
+
throw new Error("config.toml: require session_compaction_max_s > 0");
|
|
1890
|
+
}
|
|
1891
|
+
// Shape checks are pure, so they fire on every parse (tests included);
|
|
1892
|
+
// the token-file check is I/O and lives in validateSlackConfig.
|
|
1893
|
+
checkSlackShape(cfg.slack, cfg.owner);
|
|
1894
|
+
delete (cfg.daemon as Record<string, unknown>)["overseer_rotation_days"];
|
|
1895
|
+
return cfg;
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
function parseSlack(raw: Record<string, any>, defaultTimezone: string): SlackConfig {
|
|
1899
|
+
// Inbound turns went with the overseer: nothing routes a Slack message to a
|
|
1900
|
+
// model any more. A config that still asks for it is refused, not ignored,
|
|
1901
|
+
// so nobody believes the bot is listening.
|
|
1902
|
+
if (raw.inbound !== undefined && raw.inbound !== false) {
|
|
1903
|
+
throw new Error(
|
|
1904
|
+
"config.toml [slack]: `inbound` is retired — Slack inbound turns went with the overseer " +
|
|
1905
|
+
"(the router refuses them: dropped_no_overseer); outbound announcements are unchanged. Remove the key.",
|
|
1906
|
+
);
|
|
1907
|
+
}
|
|
1908
|
+
const rawPolicy = (raw.policy ?? {}) as Record<string, any>;
|
|
1909
|
+
// A cause with no cooldown of its own takes the configured default — the
|
|
1910
|
+
// same one an unlisted cause gets (`causePolicy`).
|
|
1911
|
+
const defaultCooldown =
|
|
1912
|
+
rawPolicy.default_cooldown_min ?? SLACK_POLICY_DEFAULTS.default_cooldown_min;
|
|
1913
|
+
const causes: Record<string, SlackCausePolicy> = { ...SLACK_CAUSE_DEFAULTS };
|
|
1914
|
+
for (const [cause, v] of Object.entries(
|
|
1915
|
+
(rawPolicy.causes ?? {}) as Record<string, Partial<SlackCausePolicy>>,
|
|
1916
|
+
)) {
|
|
1917
|
+
causes[cause] = {
|
|
1918
|
+
enabled: v.enabled ?? causes[cause]?.enabled ?? true,
|
|
1919
|
+
cooldown_min: Number(v.cooldown_min ?? causes[cause]?.cooldown_min ?? defaultCooldown),
|
|
1920
|
+
};
|
|
1921
|
+
}
|
|
1922
|
+
const { causes: _drop, ...policyScalars } = rawPolicy;
|
|
1923
|
+
// `legacy_owner_slack_id` is the legacy alias for [owner] slack_id —
|
|
1924
|
+
// consumed by parseConfig's fold, never carried on the parsed shape.
|
|
1925
|
+
const { legacy_owner_slack_id: _legacyOwnerId, inbound: _retiredInbound, ...rawScalars } = raw;
|
|
1926
|
+
const slack: SlackConfig = {
|
|
1927
|
+
...SLACK_DEFAULTS,
|
|
1928
|
+
...(rawScalars as Partial<SlackConfig>),
|
|
1929
|
+
users: { ...((raw.users ?? {}) as Record<string, string>) },
|
|
1930
|
+
// The day boundary defaults from [ops]; an explicit
|
|
1931
|
+
// [slack.policy] timezone still wins.
|
|
1932
|
+
policy: {
|
|
1933
|
+
...SLACK_POLICY_DEFAULTS,
|
|
1934
|
+
timezone: defaultTimezone,
|
|
1935
|
+
...(policyScalars as Partial<SlackPolicyConfig>),
|
|
1936
|
+
causes,
|
|
1937
|
+
},
|
|
1938
|
+
};
|
|
1939
|
+
// An explicit zone is checked as [ops]'s is — when the concierge is on;
|
|
1940
|
+
// a switched-off section is a draft, and nothing reads its clock.
|
|
1941
|
+
if (slack.enabled) checkSectionTimeZone(slack.policy, "[slack.policy] timezone");
|
|
1942
|
+
// Token *files* are names under secrets_dir, but an absolute or ~-rooted
|
|
1943
|
+
// override must still work — expand here so every reader sees one shape.
|
|
1944
|
+
slack.bot_token_file = expandHome(slack.bot_token_file);
|
|
1945
|
+
slack.app_token_file = expandHome(slack.app_token_file);
|
|
1946
|
+
return slack;
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
/** Absolute path of a Slack token file: a bare name resolves under
|
|
1950
|
+
* [github].secrets_dir, an absolute/~ path is taken as given. */
|
|
1951
|
+
export function slackTokenPath(cfg: Config, which: "bot" | "app"): string {
|
|
1952
|
+
const name = which === "bot" ? cfg.slack.bot_token_file : cfg.slack.app_token_file;
|
|
1953
|
+
return isAbsolute(name) ? name : join(cfg.github.secrets_dir, name);
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
/** Pure (I/O-free) part of Slack boot validation. When disabled, nothing at
|
|
1957
|
+
* all is required — the concierge is simply not there. When enabled, the
|
|
1958
|
+
* owner's Slack id (`[owner] slack_id`, legacy alias
|
|
1959
|
+
* `[slack] legacy_owner_slack_id`) must be set and be a key of [slack.users]. */
|
|
1960
|
+
function checkSlackShape(slack: SlackConfig, owner: OwnerConfig): void {
|
|
1961
|
+
if (!slack.enabled) return;
|
|
1962
|
+
const fail = (msg: string): never => {
|
|
1963
|
+
throw new Error(`config.toml [slack]: ${msg}`);
|
|
1964
|
+
};
|
|
1965
|
+
if (!slack.channel.trim()) fail("enabled = true requires a non-empty `channel`");
|
|
1966
|
+
if (slack.channel_kind !== "public" && slack.channel_kind !== "private") {
|
|
1967
|
+
fail(`channel_kind must be "public" or "private" (got ${JSON.stringify(slack.channel_kind)})`);
|
|
1968
|
+
}
|
|
1969
|
+
if (!owner.slack_id.trim()) fail("enabled = true requires a non-empty `[owner] slack_id`");
|
|
1970
|
+
if (!(owner.slack_id in slack.users)) {
|
|
1971
|
+
fail(
|
|
1972
|
+
`[owner] slack_id '${owner.slack_id}' must appear as a key in [slack.users] (it is the only source of own-PR rights)`,
|
|
1973
|
+
);
|
|
1974
|
+
}
|
|
1975
|
+
for (const key of [
|
|
1976
|
+
"daily_turns",
|
|
1977
|
+
"per_user_daily_turns",
|
|
1978
|
+
"thread_replies_per_day",
|
|
1979
|
+
"announcements_per_hour",
|
|
1980
|
+
] as const) {
|
|
1981
|
+
const cap = slack.policy[key];
|
|
1982
|
+
if (typeof cap !== "number" || !Number.isFinite(cap) || cap < 0)
|
|
1983
|
+
fail(`policy.${key} must be a finite non-negative number`);
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
/** How validation reads a secret file. Injectable so tests never need real
|
|
1988
|
+
* files; the default is the same read-and-trim every other secret gets.
|
|
1989
|
+
* Undefined means absent or empty; a file that is there but cannot be read
|
|
1990
|
+
* (permissions, a directory) throws with its cause, so a mount problem is
|
|
1991
|
+
* never reported as a missing token. */
|
|
1992
|
+
export type SecretReader = (path: string) => string | undefined;
|
|
1993
|
+
|
|
1994
|
+
export const fsSecretReader: SecretReader = (path) => {
|
|
1995
|
+
let v: string;
|
|
1996
|
+
try {
|
|
1997
|
+
v = readFileSync(path, "utf8").trim();
|
|
1998
|
+
} catch (err) {
|
|
1999
|
+
if ((err as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
2000
|
+
throw new Error(`cannot read secret file '${path}': ${(err as Error).message}`, { cause: err });
|
|
2001
|
+
}
|
|
2002
|
+
return v.length > 0 ? v : undefined;
|
|
2003
|
+
};
|
|
2004
|
+
|
|
2005
|
+
/**
|
|
2006
|
+
* Full Slack boot validation — shape plus token-file presence. A missing or
|
|
2007
|
+
* empty token is a HARD startup error: the concierge never degrades silently
|
|
2008
|
+
* into a read-only half-presence in the channel. Disabled ⇒ nothing is read.
|
|
2009
|
+
*/
|
|
2010
|
+
export function validateSlackConfig(cfg: Config, deps: { readSecret?: SecretReader } = {}): void {
|
|
2011
|
+
checkSlackShape(cfg.slack, cfg.owner);
|
|
2012
|
+
if (!cfg.slack.enabled) return;
|
|
2013
|
+
const read = deps.readSecret ?? fsSecretReader;
|
|
2014
|
+
for (const which of ["bot", "app"] as const) {
|
|
2015
|
+
const path = slackTokenPath(cfg, which);
|
|
2016
|
+
if (!read(path)) {
|
|
2017
|
+
throw new Error(
|
|
2018
|
+
`config.toml [slack]: ${which} token file '${path}' is missing or empty — Slack cannot start`,
|
|
2019
|
+
);
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
let cached: Config | null = null;
|
|
2025
|
+
|
|
2026
|
+
/**
|
|
2027
|
+
* The live config's path for this process, decided by the install locator:
|
|
2028
|
+
* `$QCR_CONFIG`, else the install's config home — never the checkout's
|
|
2029
|
+
* `config.toml`. The ONE answer every no-argument
|
|
2030
|
+
* `loadConfig()` reads, and the one the daemon hands its sessions.
|
|
2031
|
+
*/
|
|
2032
|
+
export function liveConfigPath(): string {
|
|
2033
|
+
return locateConfig().path;
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
export function loadConfig(path?: string): Config {
|
|
2037
|
+
if (cached && !path) return cached;
|
|
2038
|
+
const p = path ?? liveConfigPath();
|
|
2039
|
+
if (!path && !existsSync(p)) {
|
|
2040
|
+
// A missing live config says where it was looked for, and — when the
|
|
2041
|
+
// checkout still strands one — how to move it.
|
|
2042
|
+
throw new MissingConfigError(
|
|
2043
|
+
missingConfigMessage(locateConfig(), strandedCheckoutConfig({ root: sandboxRoot() })),
|
|
2044
|
+
);
|
|
2045
|
+
}
|
|
2046
|
+
const cfg = parseConfig(readFileSync(p, "utf8"));
|
|
2047
|
+
validateSlackConfig(cfg);
|
|
2048
|
+
if (!path) cached = cfg;
|
|
2049
|
+
return cfg;
|
|
2050
|
+
}
|