@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
|
@@ -0,0 +1,1427 @@
|
|
|
1
|
+
// Local companion container lifecycle.
|
|
2
|
+
//
|
|
3
|
+
// The shape mirrors remote.ts: command construction is pure so tests can assert
|
|
4
|
+
// the exact argv, execution goes through one injectable runner, and nothing
|
|
5
|
+
// here ever kills anything the user did not explicitly ask to stop.
|
|
6
|
+
//
|
|
7
|
+
// Lifecycle contract:
|
|
8
|
+
// `qcr tui` start → adopt a running container, else `docker start`, else
|
|
9
|
+
// `docker run` it. Quit → `docker stop` (conversation, pihome auth and
|
|
10
|
+
// the pi home survive on volumes; next start is a fast resume). TUI crash →
|
|
11
|
+
// the container keeps running and the next start adopts it. A second
|
|
12
|
+
// concurrent TUI attaches to the same container, never spawns a twin:
|
|
13
|
+
// ensureUp() and spawnPi() are both idempotent. A container goes away only
|
|
14
|
+
// on the explicit `qcr companion down`, or when ensureUp() recreates one
|
|
15
|
+
// whose mounts or DNS drifted from runArgv() or that can no longer start —
|
|
16
|
+
// the conversation and the pi home survive both on their volumes.
|
|
17
|
+
import { spawn } from "node:child_process";
|
|
18
|
+
import {
|
|
19
|
+
cpSync,
|
|
20
|
+
existsSync,
|
|
21
|
+
mkdirSync,
|
|
22
|
+
mkdtempSync,
|
|
23
|
+
readFileSync,
|
|
24
|
+
rmSync,
|
|
25
|
+
statSync,
|
|
26
|
+
} from "node:fs";
|
|
27
|
+
import { homedir, tmpdir } from "node:os";
|
|
28
|
+
import { isAbsolute, join } from "node:path";
|
|
29
|
+
import {
|
|
30
|
+
companionConfigMirrorDir,
|
|
31
|
+
CONFIG_FILE,
|
|
32
|
+
companionGithubDir,
|
|
33
|
+
configHome,
|
|
34
|
+
KEYS_FILE,
|
|
35
|
+
homeProfileDirs,
|
|
36
|
+
legacySecretsDir,
|
|
37
|
+
locateConfig,
|
|
38
|
+
locateKeysFile,
|
|
39
|
+
PORTRAIT_DIR,
|
|
40
|
+
PROFILES_DIR,
|
|
41
|
+
stateHome,
|
|
42
|
+
type InstallEnv,
|
|
43
|
+
} from "../install.js";
|
|
44
|
+
import type { CompanionConfig } from "../config.js";
|
|
45
|
+
import { writeAtomic } from "../channelfs.js";
|
|
46
|
+
import { appStamp } from "../appstamp.js";
|
|
47
|
+
import { TOKEN_FILE } from "./ghread.js";
|
|
48
|
+
|
|
49
|
+
export interface DockerResult {
|
|
50
|
+
ok: boolean;
|
|
51
|
+
stdout: string;
|
|
52
|
+
stderr: string;
|
|
53
|
+
code: number | null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Runs `docker <argv…>`. Injectable — tests never spawn docker. */
|
|
57
|
+
export type DockerRunner = (argv: string[], opts?: { timeoutMs?: number }) => Promise<DockerResult>;
|
|
58
|
+
|
|
59
|
+
export interface ContainerStatus {
|
|
60
|
+
exists: boolean;
|
|
61
|
+
running: boolean;
|
|
62
|
+
/** `dest -> source` for every mount the container was created with; empty
|
|
63
|
+
* when it does not exist or docker could not be asked. */
|
|
64
|
+
mounts: Map<string, string>;
|
|
65
|
+
/** Set when docker could not be asked (daemon down, permission denied):
|
|
66
|
+
* `exists`/`running` are then false by default, not by observation. Absent
|
|
67
|
+
* when the inspect succeeded or docker said the container does not exist. */
|
|
68
|
+
unknown?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The one word `qcr companion status` prints — "unknown — <why>" when docker
|
|
72
|
+
* could not be asked, so a failed inspect never reads as a confirmed absence. */
|
|
73
|
+
export function companionStateWord(st: ContainerStatus): string {
|
|
74
|
+
if (st.unknown !== undefined) return `unknown — docker inspect failed: ${st.unknown}`;
|
|
75
|
+
return st.running ? "running" : st.exists ? "stopped" : "absent";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The companion's resolvers, pinned at `docker run`. Without this, docker's
|
|
80
|
+
* embedded DNS forwards to whatever upstream the host had at container
|
|
81
|
+
* creation — and that upstream is network-specific, so the first network
|
|
82
|
+
* change after a long-lived start leaves every lookup dead (pi: "Error: fetch
|
|
83
|
+
* failed" on all model calls). Public resolvers are identical on every
|
|
84
|
+
* network, and the companion needs no LAN names: it talks to public endpoints
|
|
85
|
+
* and to the memory bridge by IP.
|
|
86
|
+
*/
|
|
87
|
+
export const PUBLIC_DNS = ["1.1.1.1", "8.8.8.8"];
|
|
88
|
+
|
|
89
|
+
/** The filesystem facts the lifecycle needs. Injectable so argv tests stay
|
|
90
|
+
* hermetic; the real one is plain node:fs. */
|
|
91
|
+
export interface CompanionFs {
|
|
92
|
+
exists(path: string): boolean;
|
|
93
|
+
/** File text, or undefined when it cannot be read. */
|
|
94
|
+
read(path: string): string | undefined;
|
|
95
|
+
/** Unix mode bits of a file, or undefined. */
|
|
96
|
+
mode(path: string): number | undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export const realCompanionFs: CompanionFs = {
|
|
100
|
+
exists: (p) => existsSync(p),
|
|
101
|
+
read: (p) => {
|
|
102
|
+
try {
|
|
103
|
+
return readFileSync(p, "utf8");
|
|
104
|
+
} catch {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
mode: (p) => {
|
|
109
|
+
try {
|
|
110
|
+
return statSync(p).mode & 0o777;
|
|
111
|
+
} catch {
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/** In-container paths. Fixed by the image (docker/Dockerfile, run in its
|
|
118
|
+
* `companion` mode), so they are constants rather than knobs — the host side
|
|
119
|
+
* of each is configured. */
|
|
120
|
+
export const CONTAINER = {
|
|
121
|
+
/** The release image's fixed HOME (`ENV HOME` in docker/Dockerfile),
|
|
122
|
+
* writable by whatever uid the container runs as. */
|
|
123
|
+
home: "/home/qcr",
|
|
124
|
+
piHome: "/home/qcr/.pi",
|
|
125
|
+
/** The image's app root: the companion's extensions, its system prompt and
|
|
126
|
+
* its agent set are loaded from here, never from the /workspace
|
|
127
|
+
* mount, so a change to them lands with `qcr companion rebuild`. */
|
|
128
|
+
app: "/opt/qcr",
|
|
129
|
+
/** The owner's checkout, read-write: the repo the companion works on and
|
|
130
|
+
* commits to. It is the working directory, and the data it is about — not
|
|
131
|
+
* where its code comes from. */
|
|
132
|
+
workspace: "/workspace",
|
|
133
|
+
tracker: "/tracker",
|
|
134
|
+
/** The tracker's protocol docs, read-only. `/tracker` carries the
|
|
135
|
+
* issues; the doc that says how to write them lives one level up on the
|
|
136
|
+
* host, outside every other mount. */
|
|
137
|
+
trackerDocs: "/tracker-docs",
|
|
138
|
+
/** The reviewed repo, read-only. The companion reads the code a
|
|
139
|
+
* review is about; `:ro` is the whole guarantee that it can never touch that
|
|
140
|
+
* repo's `master`. Mounted only when the host checkout exists — a missing
|
|
141
|
+
* one leaves the companion under-equipped, never broken. A generic path:
|
|
142
|
+
* the container path is not the place a repo's name belongs. */
|
|
143
|
+
reviewedRepo: "/reviewed-repo",
|
|
144
|
+
/** The install's config home, in part: `profiles/` read-write — setup
|
|
145
|
+
* writes and re-tunes the repo profile there — `portrait/` read-write (the
|
|
146
|
+
* portrait narrative), and a mirror of the live `config.toml` read-only.
|
|
147
|
+
* Deliberately not the whole home: it holds
|
|
148
|
+
* `secrets/` (the App's private key, which can write to GitHub), and the
|
|
149
|
+
* companion's reach into GitHub is the read-only token below and nothing
|
|
150
|
+
* else. Mounting the two pieces rather than masking a subdirectory is what
|
|
151
|
+
* keeps a `secrets/` created later out as well. */
|
|
152
|
+
configHome: "/qcr-config",
|
|
153
|
+
/** Inside `configHome`: the profiles dir (`:rw`) and a read-only mirror of
|
|
154
|
+
* the live config (`configLive`, a directory mount). The live config is not
|
|
155
|
+
* bind-mounted as a file: a file bind pins the inode, and every writer of
|
|
156
|
+
* config.toml (the onboarding wizard, most editors) replaces it by rename —
|
|
157
|
+
* the companion would read the old text until the container was recreated,
|
|
158
|
+
* and no drift check can see it (the source path never changes). The host
|
|
159
|
+
* copies the located config into the mirror dir instead
|
|
160
|
+
* (`syncConfigMirror`), and a rename inside a mounted dir is visible.
|
|
161
|
+
* Spelled out so the in-container paths are literals — nothing here builds
|
|
162
|
+
* a host config path (the locator does). */
|
|
163
|
+
configLive: "/qcr-config/live",
|
|
164
|
+
configFile: "/qcr-config/live/config.toml",
|
|
165
|
+
profiles: "/qcr-config/profiles",
|
|
166
|
+
/** Inside `configHome`: the repo portrait's narrative (`:rw`) —
|
|
167
|
+
* setup writes it there, keeping the one it replaced. */
|
|
168
|
+
portrait: "/qcr-config/portrait",
|
|
169
|
+
/** The install's state home, read-write: the survey, the scan
|
|
170
|
+
* ledger, findings and the portrait facts — PC-only, never shipped. */
|
|
171
|
+
stateHome: "/qcr-state",
|
|
172
|
+
/** The down-scoped read-only GitHub token, as an env file the host re-mints
|
|
173
|
+
* before it expires (`src/tui/ghread.ts`). `:ro` — the file is the
|
|
174
|
+
* host's to write; the token itself cannot write to GitHub either way. */
|
|
175
|
+
githubRead: "/github-read",
|
|
176
|
+
/** Finished-round evidence, read-only. The TUI holds the VM access and
|
|
177
|
+
* copies each finished round's artefacts down here; the companion reads
|
|
178
|
+
* plain files. That is the whole of the transport — it grants no ssh, no
|
|
179
|
+
* credential and no network reach (the companion boundary).
|
|
180
|
+
* `:ro` because this is a record: read history, never rewrite it. */
|
|
181
|
+
evidence: "/evidence",
|
|
182
|
+
feed: "/feed",
|
|
183
|
+
alerts: "/alerts",
|
|
184
|
+
/** The shared tmux-socket volume (`tmux-socket` mode): the companion's
|
|
185
|
+
* tmux server listens inside it, and the containerised TUI mounts the same
|
|
186
|
+
* volume to drive that server with `tmux -S`. The image creates the dir
|
|
187
|
+
* 1777, so a fresh named volume is writable by whatever uid runs. */
|
|
188
|
+
tmuxDir: "/run/qcr-tmux",
|
|
189
|
+
} as const;
|
|
190
|
+
|
|
191
|
+
/** How the companion's tmux server is reached.
|
|
192
|
+
*
|
|
193
|
+
* docker `docker exec <container> tmux …`, the server on tmux's
|
|
194
|
+
* default socket inside the container. The clone route, where
|
|
195
|
+
* this process can run docker.
|
|
196
|
+
* tmux-socket the server listens on a socket in a named volume that the
|
|
197
|
+
* containerised TUI shares, so the TUI drives it with
|
|
198
|
+
* `tmux -S <socket>` and holds no docker.sock. The container
|
|
199
|
+
* itself is run by the host wrapper (scripts/qcr-host.sh),
|
|
200
|
+
* from the argv `qcr companion plan` prints. */
|
|
201
|
+
export type CompanionMode = "docker" | "tmux-socket";
|
|
202
|
+
|
|
203
|
+
/** The named volume holding the companion's tmux socket. */
|
|
204
|
+
export function tmuxVolume(cfg: Pick<CompanionConfig, "container_name">): string {
|
|
205
|
+
return `${cfg.container_name}-tmux`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** The companion's tmux server socket, the same path in both containers. */
|
|
209
|
+
export function tmuxSocketPath(cfg: Pick<CompanionConfig, "session">): string {
|
|
210
|
+
return `${CONTAINER.tmuxDir}/${cfg.session}.sock`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Set on the companion container in `tmux-socket` mode: the socket its
|
|
214
|
+
* entrypoint starts the tmux server on (docker/entrypoint.sh). */
|
|
215
|
+
export const TMUX_SOCKET_ENV = "QCR_TMUX_SOCKET";
|
|
216
|
+
|
|
217
|
+
/** The companion's ssh access to the VM (the companion boundary: full ssh, so
|
|
218
|
+
* it may deploy). Exactly three host
|
|
219
|
+
* sources, each read-only and each only when the host has it: the ssh agent's
|
|
220
|
+
* socket, the ssh config and `known_hosts` — the shape the containerised TUI
|
|
221
|
+
* gets. Never `~/.ssh` whole, never a private key file, never
|
|
222
|
+
* `docker.sock`: the keys stay in the host's agent. */
|
|
223
|
+
export const SSH = {
|
|
224
|
+
agent: "/run/qcr-ssh-agent",
|
|
225
|
+
config: `${CONTAINER.home}/.ssh/config`,
|
|
226
|
+
knownHosts: `${CONTAINER.home}/.ssh/known_hosts`,
|
|
227
|
+
} as const;
|
|
228
|
+
|
|
229
|
+
/** In `tmux-socket` mode this process sees none of the host's files, so the
|
|
230
|
+
* host wrapper checks the three ssh sources itself and names the present ones
|
|
231
|
+
* in these env vars (scripts/qcr-host.sh); the planner mounts what they name. */
|
|
232
|
+
export const HOST_SSH_ENV = {
|
|
233
|
+
agent: "QCR_HOST_SSH_AUTH_SOCK",
|
|
234
|
+
config: "QCR_HOST_SSH_CONFIG",
|
|
235
|
+
knownHosts: "QCR_HOST_SSH_KNOWN_HOSTS",
|
|
236
|
+
} as const;
|
|
237
|
+
|
|
238
|
+
/** The host's ssh sources the companion is given, present ones only. */
|
|
239
|
+
export interface SshSources {
|
|
240
|
+
agent?: string;
|
|
241
|
+
config?: string;
|
|
242
|
+
knownHosts?: string;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** An absolute single-line path, else undefined. */
|
|
246
|
+
function hostPath(v: string | undefined): string | undefined {
|
|
247
|
+
const t = (v ?? "").trim();
|
|
248
|
+
return t !== "" && isAbsolute(t) && !/[\n\r]/.test(t) ? t : undefined;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** How long a containerised TUI waits for the wrapper-run companion's tmux
|
|
252
|
+
* server (the wrapper starts the container just before it starts the TUI). */
|
|
253
|
+
const WRAPPER_START_WAIT_MS = 20_000;
|
|
254
|
+
|
|
255
|
+
export type EnsureOutcome = "adopted" | "started" | "created" | "recreated";
|
|
256
|
+
|
|
257
|
+
/** The vendored patched OpenViking pi extension, relative to the app root. */
|
|
258
|
+
const VENDOR_EXTENSION = "extensions/vendor/openviking-pi/index.ts";
|
|
259
|
+
|
|
260
|
+
// ---- the memory forward -----------------------------------------------------
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Constants of the host-owned memory path. Memory never rides ssh:
|
|
264
|
+
* a systemd --user forward on the owner's PC listens on a docker bridge the
|
|
265
|
+
* companion is attached to and relays to OpenViking inside the VM container.
|
|
266
|
+
*
|
|
267
|
+
* The subnet is fixed so the gateway address is knowable before the network
|
|
268
|
+
* exists; `resolveMemoryWiring()` still asks docker for the real gateway and
|
|
269
|
+
* only falls back to this constant.
|
|
270
|
+
*/
|
|
271
|
+
export const MEMORY = {
|
|
272
|
+
/** User-defined docker bridge shared by the forward and the companion. */
|
|
273
|
+
network: "qcr-memory",
|
|
274
|
+
subnet: "198.51.100.0/24",
|
|
275
|
+
gateway: "198.51.100.1",
|
|
276
|
+
/** Same port the server listens on in the VM container ([openviking] port). */
|
|
277
|
+
port: 1933,
|
|
278
|
+
/** The OpenViking user whose memory the companion owns — becomes the
|
|
279
|
+
* `X-OpenViking-User` header on every request it makes. */
|
|
280
|
+
user: "companion",
|
|
281
|
+
/** Peer id the patched extension reports (`X-OpenViking-Actor-Peer`);
|
|
282
|
+
* verified to override the path-derived default. Same
|
|
283
|
+
* identity as `user` for the companion — one surface, one actor. */
|
|
284
|
+
peerId: "companion",
|
|
285
|
+
/** The vendored patched OpenViking pi extension, inside the image. */
|
|
286
|
+
extension: `${CONTAINER.app}/${VENDOR_EXTENSION}`,
|
|
287
|
+
/** The companion namespace's retired URIs, relative to the state home.
|
|
288
|
+
* The TUI's memory store publishes them there on the host
|
|
289
|
+
* (no daemon sweeps the companion's tree); the extension's per-turn recall
|
|
290
|
+
* reads them from the mount (`QCR_OV_RETIRED_FILE`, set in `piEnvPrefix`)
|
|
291
|
+
* and sends them as `exclude_uris`, so a retired companion memory is never
|
|
292
|
+
* recalled — not only one whose own text still opens with the tag. */
|
|
293
|
+
retiredFile: "openviking/companion-retired-uris.json",
|
|
294
|
+
} as const;
|
|
295
|
+
|
|
296
|
+
/** The companion's lens tools (`lens`, `lens_wave`; packages/pi-plugin/extensions/lens.ts), which
|
|
297
|
+
* stand where pi's example subagent extension used to. They run a lens pass as
|
|
298
|
+
* an SDK session of the companion's own pi process, over its own login and
|
|
299
|
+
* reusing pi's HTTP dispatcher, and read their agents from the agents dir
|
|
300
|
+
* under pi's agent dir, which the entrypoint links at the companion agent set
|
|
301
|
+
* (explore/evidence/study, docker/companion/agents in the image). A lens
|
|
302
|
+
* session is never given a lens tool (leaves, rule 9), and there is no cwd or
|
|
303
|
+
* scope knob to pin: the tool takes `{agent, task}` and nothing else. */
|
|
304
|
+
export const LENS = {
|
|
305
|
+
extension: `${CONTAINER.app}/extensions/cli/lens.ts`,
|
|
306
|
+
/** The companion's process-wide pass cap: 8, because a survey or scan fans
|
|
307
|
+
* out one case per unit. Rides `QCR_SUBAGENT_MAX_CONCURRENCY` on the pi
|
|
308
|
+
* command line (the name the retired subagents glue gave it, which
|
|
309
|
+
* the lens tool and extensions/setup.ts still read). Pinned by
|
|
310
|
+
* tests/tui-companion.test.ts. */
|
|
311
|
+
maxConcurrency: 8,
|
|
312
|
+
} as const;
|
|
313
|
+
|
|
314
|
+
export type ConfigMirrorOutcome =
|
|
315
|
+
"copied" | "unchanged" | "removed" | "absent" | { failed: string };
|
|
316
|
+
|
|
317
|
+
/** Mirror `source` (the host's live config, or undefined when there is none)
|
|
318
|
+
* into `dir/config.toml`. Pure fs, total — see `syncConfigMirror`. */
|
|
319
|
+
export function mirrorConfigFile(source: string | undefined, dir: string): ConfigMirrorOutcome {
|
|
320
|
+
const target = join(dir, CONFIG_FILE);
|
|
321
|
+
try {
|
|
322
|
+
if (source === undefined) {
|
|
323
|
+
if (!existsSync(target)) return "absent";
|
|
324
|
+
rmSync(target, { force: true });
|
|
325
|
+
return "removed";
|
|
326
|
+
}
|
|
327
|
+
const text = readFileSync(source, "utf8");
|
|
328
|
+
let current: string | undefined;
|
|
329
|
+
try {
|
|
330
|
+
current = readFileSync(target, "utf8");
|
|
331
|
+
} catch {
|
|
332
|
+
current = undefined;
|
|
333
|
+
}
|
|
334
|
+
if (current === text) return "unchanged";
|
|
335
|
+
writeAtomic(target, text);
|
|
336
|
+
return "copied";
|
|
337
|
+
} catch (err) {
|
|
338
|
+
return { failed: err instanceof Error ? err.message : String(err) };
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** The three env names the keys file must define. Values are never read into a
|
|
343
|
+
* log line or an argv — docker reads the file itself via `--env-file`. */
|
|
344
|
+
export const KEY_NAMES = [
|
|
345
|
+
"OPENVIKING_API_KEY",
|
|
346
|
+
"OPENVIKING_COMPANION_KEY",
|
|
347
|
+
"OPENVIKING_REVIEWER_KEY",
|
|
348
|
+
] as const;
|
|
349
|
+
|
|
350
|
+
export interface MemoryWiring {
|
|
351
|
+
/** Docker network to attach the container to; "" = memory-less degraded. */
|
|
352
|
+
network: string;
|
|
353
|
+
/** Base URL the container reaches OpenViking on; "" = memory-less. */
|
|
354
|
+
endpoint: string;
|
|
355
|
+
/** Host path of the two-key env file, when it is readable. */
|
|
356
|
+
keysFile?: string;
|
|
357
|
+
/** Human notes worth showing on the status line (never secret values). */
|
|
358
|
+
notes: string[];
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Non-secret memory env + the key file, for a container on `wiring`.
|
|
363
|
+
*
|
|
364
|
+
* The two USER keys ride `--env-file`, so the values never appear in this
|
|
365
|
+
* process's argv (and therefore never in `ps`). docker reads the file
|
|
366
|
+
* itself. Never switch these to `--env NAME=<value>`. The companion and the
|
|
367
|
+
* scan sandbox (src/scanbox.ts) both wire memory with it.
|
|
368
|
+
*/
|
|
369
|
+
export function memoryEnvArgv(wiring: MemoryWiring): string[] {
|
|
370
|
+
const argv: string[] = [];
|
|
371
|
+
if (wiring.endpoint !== "") {
|
|
372
|
+
argv.push(
|
|
373
|
+
// Read straight out of the vendored extension's credential resolver
|
|
374
|
+
// (shared/credentials.mjs): OPENVIKING_URL wins, OPENVIKING_BASE_URL is
|
|
375
|
+
// its documented alias, and the user/peer headers are what select the
|
|
376
|
+
// namespace a request addresses.
|
|
377
|
+
"--env",
|
|
378
|
+
`OPENVIKING_URL=${wiring.endpoint}`,
|
|
379
|
+
"--env",
|
|
380
|
+
`OPENVIKING_BASE_URL=${wiring.endpoint}`,
|
|
381
|
+
"--env",
|
|
382
|
+
`OPENVIKING_USER=${MEMORY.user}`,
|
|
383
|
+
"--env",
|
|
384
|
+
`OPENVIKING_PEER_ID=${MEMORY.peerId}`,
|
|
385
|
+
// Ours: the in-container health watcher (docker/companion/memory-health.mjs).
|
|
386
|
+
"--env",
|
|
387
|
+
`QCR_MEMORY_ENDPOINT=${wiring.endpoint}`,
|
|
388
|
+
"--env",
|
|
389
|
+
`QCR_MEMORY_ALERTS_DIR=${CONTAINER.alerts}`,
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
if (wiring.keysFile) argv.push("--env-file", wiring.keysFile);
|
|
393
|
+
return argv;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export function memoryEndpoint(gateway: string, port: number = MEMORY.port): string {
|
|
397
|
+
return `http://${gateway}:${port}`;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** Host path of the OpenViking key file, decided by the install locator:
|
|
401
|
+
* `QCR_OPENVIKING_KEYS`, else the operator home
|
|
402
|
+
* (`$XDG_CONFIG_HOME/qcr-operator/<install>/`, outside every dir a container
|
|
403
|
+
* mounts). 0600, and handed to the container by `--env-file`, so no value
|
|
404
|
+
* reaches an argv. */
|
|
405
|
+
export function keysFilePath(
|
|
406
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
407
|
+
home: string = homedir(),
|
|
408
|
+
): string {
|
|
409
|
+
return locateKeysFile({ env: { ...env, HOME: home } }).path;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** Which of KEY_NAMES the file defines — names only, values never leave here. */
|
|
413
|
+
export function keyNamesIn(text: string): string[] {
|
|
414
|
+
const found = new Set<string>();
|
|
415
|
+
for (const line of text.split("\n")) {
|
|
416
|
+
const m = /^\s*(?:export\s+)?([A-Z0-9_]+)\s*=/.exec(line);
|
|
417
|
+
if (m && (KEY_NAMES as readonly string[]).includes(m[1]!)) found.add(m[1]!);
|
|
418
|
+
}
|
|
419
|
+
return [...found];
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Lines docker's `--env-file` parser would choke on.
|
|
424
|
+
*
|
|
425
|
+
* This matters more than it looks: `docker run --env-file` rejects a file with
|
|
426
|
+
* `export FOO=…`, `FOO = bar` or a bare word, and a rejected `docker run` is a
|
|
427
|
+
* companion that does not start. Memory must never be able to do that, so a
|
|
428
|
+
* file we cannot vouch for is left out of the argv (degraded, not fatal).
|
|
429
|
+
*/
|
|
430
|
+
export function envFileProblems(text: string): string[] {
|
|
431
|
+
const problems: string[] = [];
|
|
432
|
+
text.split("\n").forEach((line, i) => {
|
|
433
|
+
const trimmed = line.trim();
|
|
434
|
+
if (trimmed === "" || trimmed.startsWith("#")) return;
|
|
435
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*=/.test(line)) problems.push(`line ${i + 1} is not NAME=value`);
|
|
436
|
+
});
|
|
437
|
+
return problems;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** Gateway address out of `docker network inspect -f …` output, or "". */
|
|
441
|
+
export function parseGateway(stdout: string): string {
|
|
442
|
+
const line = stdout.trim().split("\n").pop() ?? "";
|
|
443
|
+
return /^\d{1,3}(\.\d{1,3}){3}$/.test(line.trim()) ? line.trim() : "";
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/** Host side of the companion's inject dir (`/workspace/inject/<session>`):
|
|
447
|
+
* the rollover's day-open and `qcr setup`'s request land here. */
|
|
448
|
+
export function companionInjectDir(cfg: Pick<CompanionConfig, "repo_dir" | "session">): string {
|
|
449
|
+
return `${cfg.repo_dir}/inject/${cfg.session}`;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/** Set on the companion's pi: code that runs there (the setup belt, a
|
|
453
|
+
* `qcr scan` the companion runs) knows it is inside the companion container. */
|
|
454
|
+
export const IN_COMPANION_ENV = "QCR_IN_COMPANION";
|
|
455
|
+
|
|
456
|
+
/** The release image's mode word for the companion container
|
|
457
|
+
* (docker/entrypoint.sh): provision pi, start the tmux server, stay alive. */
|
|
458
|
+
export const COMPANION_MODE = "companion";
|
|
459
|
+
|
|
460
|
+
/** Why the companion image cannot be built from `ctx`, or null: the build
|
|
461
|
+
* compiles the app itself, so all it needs is the release Dockerfile. */
|
|
462
|
+
export function imageBuildProblem(ctx: string, exists: (p: string) => boolean): string | null {
|
|
463
|
+
const dockerfile = join(ctx, "docker", "Dockerfile");
|
|
464
|
+
return exists(dockerfile)
|
|
465
|
+
? null
|
|
466
|
+
: `the companion image is built from the release Dockerfile, and ${dockerfile} is not there — is ${ctx} a qcr checkout?`;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/** `docker build` of the companion image: the release image
|
|
470
|
+
* (docker/Dockerfile, which compiles the app in its own build stage) from the
|
|
471
|
+
* app root `ctx`, as `image`, stamped. Builds an image and nothing else: no
|
|
472
|
+
* container is touched. No uid is baked in: the container is started as the
|
|
473
|
+
* host user (`hostUser`). */
|
|
474
|
+
export function appImageBuildArgv(image: string, ctx: string, stamp: string): string[] {
|
|
475
|
+
return [
|
|
476
|
+
"build",
|
|
477
|
+
"-t",
|
|
478
|
+
image,
|
|
479
|
+
"-f",
|
|
480
|
+
join(ctx, "docker", "Dockerfile"),
|
|
481
|
+
"--build-arg",
|
|
482
|
+
`BUILD_SHA=${stamp}`,
|
|
483
|
+
ctx,
|
|
484
|
+
];
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/** The paths of the release image's build context (docker/Dockerfile): the
|
|
488
|
+
* Dockerfile and its own ignore file, then every `!path` that ignore file
|
|
489
|
+
* admits. Only BuildKit reads a per-Dockerfile ignore file; the legacy
|
|
490
|
+
* builder (a Docker without buildx) reads the root .dockerignore, which is the
|
|
491
|
+
* scan sandbox's narrower allowlist, so the context is assembled from this
|
|
492
|
+
* list instead of relying on either — as bench/aacr/bench.sh does. */
|
|
493
|
+
export function releaseContextPaths(ignoreText: string): string[] {
|
|
494
|
+
const admitted = ignoreText
|
|
495
|
+
.split("\n")
|
|
496
|
+
.map((l) => l.trim())
|
|
497
|
+
.filter((l) => l.startsWith("!"))
|
|
498
|
+
.map((l) => l.slice(1).replace(/^\/+/, ""))
|
|
499
|
+
.filter((l) => l !== "");
|
|
500
|
+
return ["docker/Dockerfile", "docker/Dockerfile.dockerignore", ...admitted];
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/** Copy the release image's context out of `ctx` into a fresh temp dir
|
|
504
|
+
* (node_modules, .git and tsbuildinfo files left out); the caller removes it. */
|
|
505
|
+
export function assembleReleaseContext(ctx: string): string {
|
|
506
|
+
const out = mkdtempSync(join(tmpdir(), "qcr-companion-ctx-"));
|
|
507
|
+
const ignore = readFileSync(join(ctx, "docker", "Dockerfile.dockerignore"), "utf8");
|
|
508
|
+
for (const rel of releaseContextPaths(ignore)) {
|
|
509
|
+
const from = join(ctx, rel);
|
|
510
|
+
if (!existsSync(from)) continue;
|
|
511
|
+
cpSync(from, join(out, rel), {
|
|
512
|
+
recursive: true,
|
|
513
|
+
filter: (src) => !/(^|\/)(node_modules|\.git)(\/|$)|\.tsbuildinfo$/.test(src),
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
return out;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/** `uid:gid` the companion container runs as: the invoking host user, so the
|
|
520
|
+
* host bind mounts it writes (/workspace, /tracker, /feed, /alerts) stay
|
|
521
|
+
* writable and its files stay yours. The release image bakes no user. */
|
|
522
|
+
export function hostUser(): string {
|
|
523
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : 1000;
|
|
524
|
+
const gid = typeof process.getgid === "function" ? process.getgid() : 1000;
|
|
525
|
+
return `${uid}:${gid}`;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
export class CompanionLifecycle {
|
|
529
|
+
/** Resolved by ensureUp() before any `docker run`; the constants are the
|
|
530
|
+
* safe standing answer so runArgv() stays pure and callable in tests. */
|
|
531
|
+
private wiring: MemoryWiring = {
|
|
532
|
+
network: MEMORY.network,
|
|
533
|
+
endpoint: memoryEndpoint(MEMORY.gateway),
|
|
534
|
+
notes: [],
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
constructor(
|
|
538
|
+
readonly cfg: CompanionConfig,
|
|
539
|
+
private readonly docker: DockerRunner,
|
|
540
|
+
private readonly env: NodeJS.ProcessEnv = process.env,
|
|
541
|
+
/** Host dirs are created before a cold run so docker does not invent them
|
|
542
|
+
* root-owned. Injectable so tests touch no filesystem. */
|
|
543
|
+
private readonly ensureDirs: (paths: string[]) => void = (paths) => {
|
|
544
|
+
for (const p of paths) mkdirSync(p, { recursive: true });
|
|
545
|
+
},
|
|
546
|
+
/** Filesystem seam: the keys file and the vendored extension are looked up,
|
|
547
|
+
* never assumed. Injectable so argv assertions need no real files. */
|
|
548
|
+
private readonly fs: CompanionFs = realCompanionFs,
|
|
549
|
+
/** The build stamp of the app under the build context (src/appstamp.ts).
|
|
550
|
+
* Injectable so the build argv is assertable without hashing a tree. */
|
|
551
|
+
private readonly stampOf: (root: string) => string = appStamp,
|
|
552
|
+
/** How the tmux server is reached — see `CompanionMode`. */
|
|
553
|
+
readonly mode: CompanionMode = "docker",
|
|
554
|
+
/** Builds the image's context from the app root (`assembleReleaseContext`);
|
|
555
|
+
* injectable so the build argv is assertable without copying a tree. */
|
|
556
|
+
private readonly assembleContext: (ctx: string) => string = assembleReleaseContext,
|
|
557
|
+
) {}
|
|
558
|
+
|
|
559
|
+
/** How long `ensureUp` waits, in `tmux-socket` mode, for the wrapper-run
|
|
560
|
+
* companion's tmux server. A field so tests need not wait it out. */
|
|
561
|
+
wrapperStartWaitMs = WRAPPER_START_WAIT_MS;
|
|
562
|
+
|
|
563
|
+
/** The shared socket in `tmux-socket` mode, else undefined. */
|
|
564
|
+
get socketPath(): string | undefined {
|
|
565
|
+
return this.mode === "tmux-socket" ? tmuxSocketPath(this.cfg) : undefined;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/** `tmux <argv…>`, on the shared socket when there is one. */
|
|
569
|
+
private tmuxArgv(...args: string[]): string[] {
|
|
570
|
+
const socket = this.socketPath;
|
|
571
|
+
return socket === undefined ? ["tmux", ...args] : ["tmux", "-S", socket, ...args];
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/** Start without memory: no network, no endpoint env (the laptop route, where
|
|
575
|
+
* the memory sidecar is opt-in). Pure; for the argv a host wrapper runs. */
|
|
576
|
+
withoutMemory(): this {
|
|
577
|
+
this.wiring = { network: "", endpoint: "", notes: [] };
|
|
578
|
+
return this;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/** The memory path this instance will wire (or has wired). */
|
|
582
|
+
get memory(): MemoryWiring {
|
|
583
|
+
return this.wiring;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// ---- pure argv ----------------------------------------------------------
|
|
587
|
+
|
|
588
|
+
/** `docker run` for a cold start: every mount the companion needs,
|
|
589
|
+
* the three ssh sources among them (`SSH`, rule 8). Memory still rides the
|
|
590
|
+
* docker network the host-owned forward listens on, never ssh. */
|
|
591
|
+
runArgv(): string[] {
|
|
592
|
+
const argv = [
|
|
593
|
+
"run",
|
|
594
|
+
"--detach",
|
|
595
|
+
"--name",
|
|
596
|
+
this.cfg.container_name,
|
|
597
|
+
"--hostname",
|
|
598
|
+
this.cfg.container_name,
|
|
599
|
+
];
|
|
600
|
+
// Memory-less is a legitimate mode: no network, no endpoint env, container
|
|
601
|
+
// still starts. It must never be able to wedge the companion.
|
|
602
|
+
if (this.wiring.network !== "") argv.push("--network", this.wiring.network);
|
|
603
|
+
// Network-roaming survival: see PUBLIC_DNS.
|
|
604
|
+
for (const ip of PUBLIC_DNS) argv.push("--dns", ip);
|
|
605
|
+
// The host user (the image bakes none), and the checkout as the working
|
|
606
|
+
// directory: pi reads the repo's own context files from there, and the
|
|
607
|
+
// image's WORKDIR is its app root, whose reviewer stub is not the
|
|
608
|
+
// companion's to load.
|
|
609
|
+
argv.push("--user", hostUser(), "--workdir", CONTAINER.workspace);
|
|
610
|
+
// The wrapper-run companion is hardened like every other container
|
|
611
|
+
// the wrapper starts; nothing it does needs a capability.
|
|
612
|
+
if (this.socketPath !== undefined)
|
|
613
|
+
argv.push("--init", "--cap-drop", "ALL", "--security-opt", "no-new-privileges");
|
|
614
|
+
for (const m of this.mountSpec())
|
|
615
|
+
argv.push("--volume", `${m.source}:${m.dest}${m.mode ? `:${m.mode}` : ""}`);
|
|
616
|
+
argv.push(...this.memoryEnvArgv());
|
|
617
|
+
// pi's default model, set by the entrypoint on every start.
|
|
618
|
+
argv.push("--env", `QCR_PI_DEFAULT_MODEL=${this.cfg.model}`);
|
|
619
|
+
if (this.socketPath !== undefined) argv.push("--env", `${TMUX_SOCKET_ENV}=${this.socketPath}`);
|
|
620
|
+
// The agent is reached by its mounted socket; no key is in the container.
|
|
621
|
+
if (this.sshSources().agent !== undefined) argv.push("--env", `SSH_AUTH_SOCK=${SSH.agent}`);
|
|
622
|
+
// The release image, in its companion mode (docker/entrypoint.sh).
|
|
623
|
+
argv.push(this.cfg.image, COMPANION_MODE);
|
|
624
|
+
return argv;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/** Non-secret memory env + the key file, over this instance's wiring
|
|
628
|
+
* (the free `memoryEnvArgv`). */
|
|
629
|
+
memoryEnvArgv(): string[] {
|
|
630
|
+
return memoryEnvArgv(this.wiring);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* The pi argv inside the companion's tmux session.
|
|
635
|
+
*
|
|
636
|
+
* Every extension, the theme and the system prompt are the IMAGE's
|
|
637
|
+
* (`CONTAINER.app`): the companion runs the code it was built with, and
|
|
638
|
+
* the checkout at /workspace is the repo it works on, not where its hands
|
|
639
|
+
* come from.
|
|
640
|
+
*
|
|
641
|
+
* VERIFY-ON-PIN, every flag (same list as sessions.ts buildPiCommand):
|
|
642
|
+
* `--model` is documented; `--session-dir` is documented for RPC mode only
|
|
643
|
+
* and assumed to apply to the interactive TUI; `--system-prompt` and
|
|
644
|
+
* `--name` are not documented at all — the fallback for `--system-prompt` is
|
|
645
|
+
* writing the body to <pi home>/.pi/SYSTEM.md, which is reachable on the
|
|
646
|
+
* pihome volume without an image rebuild.
|
|
647
|
+
*
|
|
648
|
+
* The session dir lives on the pihome volume so the conversation survives
|
|
649
|
+
* `docker stop` (that is what makes quit-then-restart a resume rather than a
|
|
650
|
+
* fresh start), and so daily rollover can archive "the newest .jsonl".
|
|
651
|
+
*/
|
|
652
|
+
piArgv(): string[] {
|
|
653
|
+
const ext = `${CONTAINER.app}/extensions`;
|
|
654
|
+
return [
|
|
655
|
+
`${CONTAINER.app}/node_modules/.bin/pi`,
|
|
656
|
+
// --approve: pre-trust project-local .pi resources for this run. Without
|
|
657
|
+
// it a cwd carrying .pi/ hits pi's interactive "Trust project folder?"
|
|
658
|
+
// modal and the session stalls forever. Same flag sessions.ts passes
|
|
659
|
+
// (the reviewer never blocks on interactive pi input).
|
|
660
|
+
"--approve",
|
|
661
|
+
// The vendored patched OpenViking extension — the companion's memory
|
|
662
|
+
// hands. It rides the image, so it is always there; with no memory
|
|
663
|
+
// wiring it finds no endpoint and stays idle (the companion never wedges).
|
|
664
|
+
"--extension",
|
|
665
|
+
MEMORY.extension,
|
|
666
|
+
// Ambient /feed state is deliberately not an extension: one would turn
|
|
667
|
+
// every meaningful poll into a synthetic user message in the operator's
|
|
668
|
+
// active conversation. The model reads the bounded feed files on demand
|
|
669
|
+
// instead.
|
|
670
|
+
"--extension",
|
|
671
|
+
`${ext}/cli/muted-theme.ts`,
|
|
672
|
+
"--theme",
|
|
673
|
+
`${ext}/muted-theme.json`,
|
|
674
|
+
"--extension",
|
|
675
|
+
// Day-open channel: the daemon's inject bridge, unmodified. It
|
|
676
|
+
// drains its directory on session_start before it starts watching, so
|
|
677
|
+
// the rollover can queue the opening message before pi is spawned and
|
|
678
|
+
// the delivery is race-free. QCR_INJECT_DIR (below) points it at the
|
|
679
|
+
// companion's own dir. VERIFY-ON-PIN: `--extension <path>` again.
|
|
680
|
+
`${ext}/cli/inject-bridge.ts`,
|
|
681
|
+
"--extension",
|
|
682
|
+
// The read-only `tui_*` tools over what the TUI published to
|
|
683
|
+
// /feed (screen.txt + view.json). Reads only — it holds no credential and
|
|
684
|
+
// cannot fetch; see the header of extensions/tui-view.ts for why that is
|
|
685
|
+
// structural and not a promise. QCR_FEED_DIR (below) points it at the
|
|
686
|
+
// same dir the FeedWriter writes. VERIFY-ON-PIN: `--extension <path>`.
|
|
687
|
+
`${ext}/cli/tui-view.ts`,
|
|
688
|
+
"--extension",
|
|
689
|
+
// Our lens tools (`lens`, `lens_wave`) at cap 8 — see LENS and
|
|
690
|
+
// packages/pi-plugin/extensions/lens.ts. First-party and always in the image, so it loads
|
|
691
|
+
// unconditionally like inject-bridge; its lens sessions are SDK
|
|
692
|
+
// sessions of this pi, over pi's own dispatcher. The cap rides
|
|
693
|
+
// QCR_SUBAGENT_MAX_CONCURRENCY in piEnvPrefix (LENS.maxConcurrency).
|
|
694
|
+
LENS.extension,
|
|
695
|
+
"--extension",
|
|
696
|
+
// The companion's bright lines (frozen seeds, no-GitHub,
|
|
697
|
+
// no-discard in the shared checkout) as tool_call vetoes — see the
|
|
698
|
+
// header of extensions/companion-guard.ts. Companion only; the VM has
|
|
699
|
+
// the gate.
|
|
700
|
+
`${ext}/cli/companion-guard.ts`,
|
|
701
|
+
"--extension",
|
|
702
|
+
// The setup & scan tool belt — the deterministic SetupEngine
|
|
703
|
+
// (repo facts, the unit cut) as `setup_*` tools. Reads the reviewed
|
|
704
|
+
// checkout (`:ro`) and writes only the state home; see the header of
|
|
705
|
+
// extensions/setup.ts. Unconditional like every other extension:
|
|
706
|
+
// a missing checkout is a refusal from the tools, never a pi that
|
|
707
|
+
// won't start.
|
|
708
|
+
`${ext}/cli/setup.ts`,
|
|
709
|
+
"--extension",
|
|
710
|
+
// `!{cmd}` inline expansion in typed prompts — vendored
|
|
711
|
+
// verbatim from the pinned pi package's shipped examples (see its
|
|
712
|
+
// header; byte-identical at 0.84.1 and 0.84.4). QoL for the operator
|
|
713
|
+
// typing into the mirrored terminal.
|
|
714
|
+
`${ext}/inline-bash.ts`,
|
|
715
|
+
"--model",
|
|
716
|
+
this.cfg.model,
|
|
717
|
+
"--session-dir",
|
|
718
|
+
companionSessionDir(this.cfg.session),
|
|
719
|
+
"--system-prompt",
|
|
720
|
+
`${CONTAINER.app}/prompts/companion.md`,
|
|
721
|
+
"--name",
|
|
722
|
+
this.cfg.session,
|
|
723
|
+
];
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/** QCR_* routing vars. They ride the pi command line, never the client env:
|
|
727
|
+
* tmux panes inherit the tmux server's environment. */
|
|
728
|
+
piEnvPrefix(): string[] {
|
|
729
|
+
return [
|
|
730
|
+
"env",
|
|
731
|
+
`QCR_ROOT=${CONTAINER.workspace}`,
|
|
732
|
+
`QCR_SESSION=${this.cfg.session}`,
|
|
733
|
+
// inject-bridge resolves its session name from this one.
|
|
734
|
+
`QCR_SESSION_NAME=${this.cfg.session}`,
|
|
735
|
+
`QCR_INJECT_DIR=${CONTAINER.workspace}/inject/${this.cfg.session}`,
|
|
736
|
+
`QCR_FEED_DIR=${CONTAINER.feed}`,
|
|
737
|
+
`QCR_ALERTS_DIR=${CONTAINER.alerts}`,
|
|
738
|
+
`QCR_TRACKER_DIR=${CONTAINER.tracker}`,
|
|
739
|
+
// Where the install's homes and the token file sit in here.
|
|
740
|
+
// The locator (src/install.ts) honours QCR_CONFIG_HOME/QCR_STATE_HOME
|
|
741
|
+
// as whole-home overrides, so code run in the container resolves the
|
|
742
|
+
// mounted homes — profiles written by setup land on the host, not in
|
|
743
|
+
// the container user's ~/.config. QCR_CONFIG always names the mirror:
|
|
744
|
+
// when the host has no config the file is simply absent, which the
|
|
745
|
+
// reader reports — never a silent fall back to /workspace/config.toml.
|
|
746
|
+
`QCR_CONFIG_HOME=${CONTAINER.configHome}`,
|
|
747
|
+
`QCR_CONFIG=${CONTAINER.configFile}`,
|
|
748
|
+
`QCR_STATE_HOME=${CONTAINER.stateHome}`,
|
|
749
|
+
// Where the setup tools read the reviewed checkout.
|
|
750
|
+
`QCR_REVIEWED_REPO=${CONTAINER.reviewedRepo}`,
|
|
751
|
+
`QCR_GITHUB_READ_ENV=${CONTAINER.githubRead}/${TOKEN_FILE}`,
|
|
752
|
+
// Survey and scan fan out one case per unit — see LENS.
|
|
753
|
+
`QCR_SUBAGENT_MAX_CONCURRENCY=${LENS.maxConcurrency}`,
|
|
754
|
+
// The lens tool builds its SDK host over pi's own agent dir and refuses
|
|
755
|
+
// a relative or absent one (packages/pi-plugin/extensions/lens.ts); name the one pi uses.
|
|
756
|
+
`PI_CODING_AGENT_DIR=${CONTAINER.piHome}/agent`,
|
|
757
|
+
// The retired URIs the recall excludes — see MEMORY.retiredFile.
|
|
758
|
+
`QCR_OV_RETIRED_FILE=${CONTAINER.stateHome}/${MEMORY.retiredFile}`,
|
|
759
|
+
// What a `qcr scan` run here reads to default its sandbox to the host.
|
|
760
|
+
`${IN_COMPANION_ENV}=1`,
|
|
761
|
+
];
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* Host side of the install-home mounts, from the install locator over this
|
|
766
|
+
* lifecycle's env (so a test's temp XDG homes are what it mounts):
|
|
767
|
+
* `<config home>/profiles` and `<config home>/portrait`, the live config
|
|
768
|
+
* file when one exists (the source of the mirror — never itself mounted),
|
|
769
|
+
* the mirror dir, the state home, and the token dir. Never the config home
|
|
770
|
+
* as a whole — see `CONTAINER.configHome` for why `secrets/` must stay out.
|
|
771
|
+
*/
|
|
772
|
+
hostHomes(): {
|
|
773
|
+
profiles: string;
|
|
774
|
+
portrait: string;
|
|
775
|
+
configFile?: string;
|
|
776
|
+
configMirror: string;
|
|
777
|
+
state: string;
|
|
778
|
+
github: string;
|
|
779
|
+
} {
|
|
780
|
+
const env = this.installEnv();
|
|
781
|
+
const located = locateConfig({ env });
|
|
782
|
+
return {
|
|
783
|
+
profiles: join(configHome(env), PROFILES_DIR),
|
|
784
|
+
portrait: join(configHome(env), PORTRAIT_DIR),
|
|
785
|
+
configFile: this.fs.exists(located.path) ? located.path : undefined,
|
|
786
|
+
configMirror: companionConfigMirrorDir(env),
|
|
787
|
+
state: stateHome(env),
|
|
788
|
+
github: companionGithubDir(env),
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* Copy the host's live config into the mirror dir the companion mounts
|
|
794
|
+
* (see `CONTAINER.configLive`), or remove the mirror when the host has none
|
|
795
|
+
* — an absent file is the honest answer, a stale one is not. Atomic (tmp +
|
|
796
|
+
* rename inside the mounted dir) and a no-op when nothing changed. Total:
|
|
797
|
+
* a failure is an outcome, never a throw.
|
|
798
|
+
*/
|
|
799
|
+
syncConfigMirror(): ConfigMirrorOutcome {
|
|
800
|
+
const homes = this.hostHomes();
|
|
801
|
+
return mirrorConfigFile(homes.configFile, homes.configMirror);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
private installEnv(): InstallEnv {
|
|
805
|
+
return { ...this.env, HOME: this.env.HOME || homedir() };
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/** `docker exec … tmux new-session -d -s companion env QCR_… pi …`. */
|
|
809
|
+
spawnArgv(): string[] {
|
|
810
|
+
return [
|
|
811
|
+
"exec",
|
|
812
|
+
this.cfg.container_name,
|
|
813
|
+
...this.tmuxArgv("new-session", "-d", "-s", this.cfg.session),
|
|
814
|
+
...this.piEnvPrefix(),
|
|
815
|
+
...this.piArgv(),
|
|
816
|
+
];
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/** Local attach — plain `docker exec -it`, no ssh (this box owns the
|
|
820
|
+
* container). Returned argv is everything after the `docker` binary. */
|
|
821
|
+
attachArgv(): string[] {
|
|
822
|
+
return [
|
|
823
|
+
"exec",
|
|
824
|
+
"-it",
|
|
825
|
+
this.cfg.container_name,
|
|
826
|
+
...this.tmuxArgv("attach", "-t", this.cfg.session),
|
|
827
|
+
];
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/** Host side of `/workspace/inject/<session>` — where the rollover drops the
|
|
831
|
+
* day-open message (the repo is bind-mounted, so no docker exec needed). */
|
|
832
|
+
hostInjectDir(): string {
|
|
833
|
+
return companionInjectDir(this.cfg);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/**
|
|
837
|
+
* Is the reviewed repo checked out on this host?
|
|
838
|
+
*
|
|
839
|
+
* The mount is conditional on this, in `runArgv()` and in `expectedMounts()`,
|
|
840
|
+
* so the two never disagree: present ⇒ the container must carry it (absence
|
|
841
|
+
* is drift, and drift recreates); absent ⇒ nothing is expected and a stale
|
|
842
|
+
* mount is still drift. `docker run` would happily invent an empty host dir
|
|
843
|
+
* for a missing bind source, which is worse than no mount: the companion
|
|
844
|
+
* would read an empty tree and conclude the code says nothing.
|
|
845
|
+
*/
|
|
846
|
+
reviewedRepoPresent(): boolean {
|
|
847
|
+
return this.cfg.reviewed_repo_dir !== "" && this.fs.exists(this.cfg.reviewed_repo_dir);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// ---- state --------------------------------------------------------------
|
|
851
|
+
|
|
852
|
+
async status(): Promise<ContainerStatus> {
|
|
853
|
+
// One inspect answers both questions: is it up, and what was it created
|
|
854
|
+
// with. Line 1 is the running flag; every later line is `dest=source` for
|
|
855
|
+
// one mount. Keeping it to a single call means the drift check
|
|
856
|
+
// costs nothing on the start path.
|
|
857
|
+
const res = await this.docker([
|
|
858
|
+
"inspect",
|
|
859
|
+
"--format",
|
|
860
|
+
// `.Name` for a named volume, `.Source` for a bind. Docker reports a
|
|
861
|
+
// volume's Source as its on-disk _data path, which never equals the
|
|
862
|
+
// volume name the run argv asks for — comparing Source would make the
|
|
863
|
+
// drift check fire on every start and recreate the container each time.
|
|
864
|
+
"{{.State.Running}}\n{{range .Mounts}}{{.Destination}}={{if .Name}}{{.Name}}{{else}}{{.Source}}{{end}}\n{{end}}",
|
|
865
|
+
this.cfg.container_name,
|
|
866
|
+
]);
|
|
867
|
+
if (!res.ok) {
|
|
868
|
+
const none = { exists: false, running: false, mounts: new Map<string, string>() };
|
|
869
|
+
return /no such (object|container)/i.test(res.stderr)
|
|
870
|
+
? none
|
|
871
|
+
: { ...none, unknown: errText(res) };
|
|
872
|
+
}
|
|
873
|
+
const [first = "", ...rest] = res.stdout.split("\n");
|
|
874
|
+
const mounts = new Map<string, string>();
|
|
875
|
+
for (const line of rest) {
|
|
876
|
+
const eq = line.indexOf("=");
|
|
877
|
+
if (eq > 0) mounts.set(line.slice(0, eq).trim(), line.slice(eq + 1).trim());
|
|
878
|
+
}
|
|
879
|
+
return { exists: true, running: first.trim() === "true", mounts };
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/**
|
|
883
|
+
* The DNS servers the container was created with, or undefined when docker
|
|
884
|
+
* could not be asked (no opinion — never drift). Its own inspect call on
|
|
885
|
+
* purpose: on docker 29, referencing `.HostConfig` in the same template as
|
|
886
|
+
* the mounts' `{{if .Name}}` makes execution fall back to the raw JSON map,
|
|
887
|
+
* where a bind mount has no "Name" key and the whole inspect errors. Do not
|
|
888
|
+
* fold this back into status().
|
|
889
|
+
*/
|
|
890
|
+
async createdDns(): Promise<string[] | undefined> {
|
|
891
|
+
const res = await this.docker([
|
|
892
|
+
"inspect",
|
|
893
|
+
"--format",
|
|
894
|
+
"{{range .HostConfig.Dns}}{{.}}\n{{end}}",
|
|
895
|
+
this.cfg.container_name,
|
|
896
|
+
]);
|
|
897
|
+
if (!res.ok) return undefined;
|
|
898
|
+
return res.stdout
|
|
899
|
+
.split("\n")
|
|
900
|
+
.map((s) => s.trim())
|
|
901
|
+
.filter((s) => s !== "");
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/** True when the companion tmux session is already live in the container. */
|
|
905
|
+
async piRunning(): Promise<boolean> {
|
|
906
|
+
const res = await this.docker([
|
|
907
|
+
"exec",
|
|
908
|
+
this.cfg.container_name,
|
|
909
|
+
...this.tmuxArgv("has-session", "-t", this.cfg.session),
|
|
910
|
+
]);
|
|
911
|
+
return res.ok;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/** Is the companion's tmux server answering on its socket? `show-options`
|
|
915
|
+
* never starts a server, so asking cannot birth one in the wrong container. */
|
|
916
|
+
async serverUp(): Promise<boolean> {
|
|
917
|
+
const res = await this.docker([
|
|
918
|
+
"exec",
|
|
919
|
+
this.cfg.container_name,
|
|
920
|
+
...this.tmuxArgv("show-options", "-s", "exit-empty"),
|
|
921
|
+
]);
|
|
922
|
+
return res.ok;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// ---- transitions --------------------------------------------------------
|
|
926
|
+
|
|
927
|
+
/** True when the companion image exists locally. */
|
|
928
|
+
async imageExists(): Promise<boolean> {
|
|
929
|
+
const res = await this.docker(["image", "inspect", this.cfg.image]);
|
|
930
|
+
return res.ok;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
/** Remove the container, running or not. Volumes (pihome: OAuth, the
|
|
934
|
+
* conversation) survive — this replaces the container, never its memory. */
|
|
935
|
+
async removeContainer(): Promise<void> {
|
|
936
|
+
const st = await this.status();
|
|
937
|
+
if (st.unknown) throw new Error(`container status unknown: ${st.unknown}`);
|
|
938
|
+
if (!st.exists) return;
|
|
939
|
+
await this.forceRemove();
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/** `docker rm -f` the container; a refusal throws with docker's reason. */
|
|
943
|
+
private async forceRemove(): Promise<void> {
|
|
944
|
+
const res = await this.docker(["rm", "-f", this.cfg.container_name]);
|
|
945
|
+
if (!res.ok) throw new Error(`docker rm ${this.cfg.container_name} failed: ${errText(res)}`);
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
/** Throws when the image cannot be built (`imageBuildProblem`) — asked
|
|
949
|
+
* before anything is removed for a rebuild. */
|
|
950
|
+
checkBuildable(): void {
|
|
951
|
+
const problem = imageBuildProblem(this.cfg.build_context, (p) => this.fs.exists(p));
|
|
952
|
+
if (problem !== null) throw new Error(problem);
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
/** Build the companion image (idempotent, cached) from the app root, with
|
|
956
|
+
* the release Dockerfile (docker/Dockerfile): the image compiles the app
|
|
957
|
+
* itself, so nothing needs building on the host first, and it is stamped
|
|
958
|
+
* with the checkout's build stamp. The companion's extensions come from this
|
|
959
|
+
* image, so a change to them lands only with a rebuild. */
|
|
960
|
+
async buildImage(onProgress?: (line: string) => void): Promise<void> {
|
|
961
|
+
this.checkBuildable();
|
|
962
|
+
const ctx = this.cfg.build_context;
|
|
963
|
+
onProgress?.(`building ${this.cfg.image} image (first run — takes a few minutes)…`);
|
|
964
|
+
// The stamp is the checkout's; the build reads an assembled copy of the
|
|
965
|
+
// release context, so it works on the legacy builder too.
|
|
966
|
+
const stamp = this.stampOf(ctx);
|
|
967
|
+
const built = this.assembleContext(ctx);
|
|
968
|
+
let res;
|
|
969
|
+
try {
|
|
970
|
+
res = await this.docker(appImageBuildArgv(this.cfg.image, built, stamp), {
|
|
971
|
+
timeoutMs: 1_800_000,
|
|
972
|
+
});
|
|
973
|
+
} finally {
|
|
974
|
+
if (built !== ctx) rmSync(built, { recursive: true, force: true });
|
|
975
|
+
}
|
|
976
|
+
if (!res.ok) throw new Error(`docker build ${this.cfg.image} failed: ${errText(res)}`);
|
|
977
|
+
onProgress?.(`${this.cfg.image} image built`);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// ---- memory wiring --------------------------------------------------------
|
|
981
|
+
|
|
982
|
+
/**
|
|
983
|
+
* Work out how this container will reach memory, without ever failing.
|
|
984
|
+
*
|
|
985
|
+
* The forward (scripts/companion-memory-forward.mjs, run by systemd on the
|
|
986
|
+
* host) owns the network; the lifecycle only joins it. When the network is
|
|
987
|
+
* absent it is created here anyway — idempotent, same fixed subnet — so the
|
|
988
|
+
* container's wiring is stable and memory starts working the moment the
|
|
989
|
+
* forward comes up, rather than needing a container recreate. If even that
|
|
990
|
+
* fails (a subnet clash, docker unhappy) the wiring degrades to memory-less
|
|
991
|
+
* and the companion starts regardless.
|
|
992
|
+
*/
|
|
993
|
+
async resolveMemoryWiring(onProgress?: (line: string) => void): Promise<MemoryWiring> {
|
|
994
|
+
const notes: string[] = [];
|
|
995
|
+
let gateway = await this.networkGateway();
|
|
996
|
+
if (gateway === "") {
|
|
997
|
+
await this.docker(["network", "create", "--subnet", MEMORY.subnet, MEMORY.network]);
|
|
998
|
+
gateway = await this.networkGateway();
|
|
999
|
+
if (gateway !== "") notes.push(`created docker network ${MEMORY.network}`);
|
|
1000
|
+
}
|
|
1001
|
+
if (gateway === "") {
|
|
1002
|
+
notes.push(`docker network ${MEMORY.network} unavailable — companion starts without memory`);
|
|
1003
|
+
this.wiring = { network: "", endpoint: "", notes };
|
|
1004
|
+
for (const n of notes) onProgress?.(n);
|
|
1005
|
+
return this.wiring;
|
|
1006
|
+
}
|
|
1007
|
+
const keysFile = keysFilePath(this.env);
|
|
1008
|
+
const keysText = this.fs.read(keysFile);
|
|
1009
|
+
let usableKeys: string | undefined;
|
|
1010
|
+
if (keysText === undefined) {
|
|
1011
|
+
notes.push(
|
|
1012
|
+
`no OpenViking keys at ${keysFile} — memory reads/writes will be refused (see docs/companion-memory-access.md)`,
|
|
1013
|
+
);
|
|
1014
|
+
// The old-place copy is never read; say so when it is the reason.
|
|
1015
|
+
const stranded = `${legacySecretsDir({ ...this.env, HOME: homedir() })}/${KEYS_FILE}`;
|
|
1016
|
+
if (this.fs.exists(stranded))
|
|
1017
|
+
notes.push(
|
|
1018
|
+
`${stranded} is no longer read — \`qcr install migrate\` moves it to ${keysFile}`,
|
|
1019
|
+
);
|
|
1020
|
+
} else {
|
|
1021
|
+
const names = keyNamesIn(keysText);
|
|
1022
|
+
const missing = KEY_NAMES.filter((n) => !names.includes(n));
|
|
1023
|
+
if (missing.length > 0) notes.push(`${keysFile} is missing ${missing.join(", ")}`);
|
|
1024
|
+
const mode = this.fs.mode(keysFile);
|
|
1025
|
+
if (mode !== undefined && (mode & 0o077) !== 0)
|
|
1026
|
+
notes.push(`${keysFile} is group/world readable — chmod 600 it`);
|
|
1027
|
+
const bad = envFileProblems(keysText);
|
|
1028
|
+
if (bad.length > 0) {
|
|
1029
|
+
// Passing it anyway would make `docker run` refuse and the companion
|
|
1030
|
+
// never start — memory is never allowed to cost that.
|
|
1031
|
+
notes.push(
|
|
1032
|
+
`${keysFile} is not a docker env-file (${bad[0]}) — starting without memory keys`,
|
|
1033
|
+
);
|
|
1034
|
+
} else {
|
|
1035
|
+
usableKeys = keysFile;
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
this.wiring = {
|
|
1039
|
+
network: MEMORY.network,
|
|
1040
|
+
endpoint: memoryEndpoint(gateway),
|
|
1041
|
+
keysFile: usableKeys,
|
|
1042
|
+
notes,
|
|
1043
|
+
};
|
|
1044
|
+
for (const n of notes) onProgress?.(n);
|
|
1045
|
+
return this.wiring;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
private async networkGateway(): Promise<string> {
|
|
1049
|
+
const res = await this.docker([
|
|
1050
|
+
"network",
|
|
1051
|
+
"inspect",
|
|
1052
|
+
"--format",
|
|
1053
|
+
"{{range .IPAM.Config}}{{.Gateway}}{{end}}",
|
|
1054
|
+
MEMORY.network,
|
|
1055
|
+
]);
|
|
1056
|
+
return res.ok ? parseGateway(res.stdout) : "";
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
/** Attach an already-running container to the memory network. Idempotent —
|
|
1060
|
+
* "already exists" is the normal answer and never an error here. */
|
|
1061
|
+
async attachToMemoryNetwork(): Promise<boolean> {
|
|
1062
|
+
if (this.wiring.network === "") return false;
|
|
1063
|
+
const res = await this.docker([
|
|
1064
|
+
"network",
|
|
1065
|
+
"connect",
|
|
1066
|
+
this.wiring.network,
|
|
1067
|
+
this.cfg.container_name,
|
|
1068
|
+
]);
|
|
1069
|
+
return res.ok;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
/**
|
|
1073
|
+
* Bind mounts are frozen at creation, so a container that predates a mount
|
|
1074
|
+
* change keeps the old set forever — `docker start` on it silently resurrects
|
|
1075
|
+
* the old boundary. That is not cosmetic: a stale container can carry a
|
|
1076
|
+
* mount the boundary does not grant (an `~/.ssh` whole, a key file, a dead
|
|
1077
|
+
* agent socket) — the companion boundary means its mounts to be structural.
|
|
1078
|
+
*
|
|
1079
|
+
* So: compare what the container has against what `runArgv()` asks for, and
|
|
1080
|
+
* report the drift. Named volumes are compared by name, binds by host path.
|
|
1081
|
+
* Extra mounts count as drift (an `~/.ssh` whole is that case); an unreadable inspect
|
|
1082
|
+
* reports nothing.
|
|
1083
|
+
*/
|
|
1084
|
+
mountDrift(actual: Map<string, string>): string[] {
|
|
1085
|
+
if (actual.size === 0) return [];
|
|
1086
|
+
const drift: string[] = [];
|
|
1087
|
+
// An agent socket moves with every login and differs between shells: a
|
|
1088
|
+
// container on another agent socket that is still live keeps its ssh, so
|
|
1089
|
+
// a TUI started from another shell never recreates a running companion.
|
|
1090
|
+
// A dead one is drift like any other.
|
|
1091
|
+
const liveAgent = (dest: string, have: string | undefined): boolean =>
|
|
1092
|
+
dest === SSH.agent && have !== undefined && this.fs.exists(have);
|
|
1093
|
+
for (const [dest, source] of this.expectedMounts()) {
|
|
1094
|
+
const have = actual.get(dest);
|
|
1095
|
+
if (have === undefined) drift.push(`missing ${dest}`);
|
|
1096
|
+
else if (have !== source && !liveAgent(dest, have))
|
|
1097
|
+
drift.push(`${dest} points at ${have}, expected ${source}`);
|
|
1098
|
+
}
|
|
1099
|
+
for (const [dest, have] of actual) {
|
|
1100
|
+
if (!this.expectedMounts().has(dest) && !liveAgent(dest, have)) drift.push(`stale ${dest}`);
|
|
1101
|
+
}
|
|
1102
|
+
return drift;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
/**
|
|
1106
|
+
* DNS is frozen at creation exactly like the mounts, so a container created
|
|
1107
|
+
* before PUBLIC_DNS was pinned keeps forwarding lookups to a
|
|
1108
|
+
* network-specific upstream forever (see PUBLIC_DNS).
|
|
1109
|
+
* Same remedy as mount drift: recreate. Consulted only after status()
|
|
1110
|
+
* yielded a mount set (see ensureUp) so an unreadable inspect stays
|
|
1111
|
+
* "no opinion", never a recreate.
|
|
1112
|
+
*/
|
|
1113
|
+
dnsDrift(actual: string[]): string[] {
|
|
1114
|
+
return actual.join(",") === PUBLIC_DNS.join(",")
|
|
1115
|
+
? []
|
|
1116
|
+
: [`dns is [${actual.join(", ")}], expected [${PUBLIC_DNS.join(", ")}]`];
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
/**
|
|
1120
|
+
* Every mount the container is created with, in `runArgv()` order — the
|
|
1121
|
+
* one list `runArgv()` turns into `--volume` flags and `expectedMounts()`
|
|
1122
|
+
* (the drift check) reads, so the two can never disagree.
|
|
1123
|
+
*/
|
|
1124
|
+
mountSpec(): Array<{ dest: string; source: string; mode?: "ro" | "rw" }> {
|
|
1125
|
+
const spec: Array<{ dest: string; source: string; mode?: "ro" | "rw" }> = [
|
|
1126
|
+
{ dest: CONTAINER.piHome, source: this.cfg.pihome_volume },
|
|
1127
|
+
{ dest: CONTAINER.workspace, source: this.cfg.repo_dir, mode: "rw" },
|
|
1128
|
+
];
|
|
1129
|
+
// The tmux socket volume, shared with the containerised TUI. The only
|
|
1130
|
+
// thing in it is the socket; it is no ssh agent and no docker.sock.
|
|
1131
|
+
if (this.socketPath !== undefined)
|
|
1132
|
+
spec.push({ dest: CONTAINER.tmuxDir, source: tmuxVolume(this.cfg), mode: "rw" });
|
|
1133
|
+
// The tracker and its protocol docs are optional: mounted only when the
|
|
1134
|
+
// operator configured them ([companion] tracker_dir / tracker_docs_dir).
|
|
1135
|
+
if (this.cfg.tracker_dir !== "")
|
|
1136
|
+
spec.push({ dest: CONTAINER.tracker, source: this.cfg.tracker_dir, mode: "rw" });
|
|
1137
|
+
if (this.cfg.tracker_docs_dir !== "")
|
|
1138
|
+
spec.push({ dest: CONTAINER.trackerDocs, source: this.cfg.tracker_docs_dir, mode: "ro" });
|
|
1139
|
+
// The reviewed repo, read-only and only when it is really there — see
|
|
1140
|
+
// reviewedRepoPresent().
|
|
1141
|
+
if (this.reviewedRepoPresent())
|
|
1142
|
+
spec.push({ dest: CONTAINER.reviewedRepo, source: this.cfg.reviewed_repo_dir, mode: "ro" });
|
|
1143
|
+
// The evidence store. Created by the TUI before the container starts
|
|
1144
|
+
// (`ensureDirs`), so unlike the reviewed repo it is never absent and the
|
|
1145
|
+
// mount is unconditional — an empty store is a companion that has seen no
|
|
1146
|
+
// rounds yet, which is honest; a missing one would have docker invent a
|
|
1147
|
+
// root-owned dir the copier could not then write.
|
|
1148
|
+
spec.push(
|
|
1149
|
+
{ dest: CONTAINER.evidence, source: this.cfg.evidence_dir, mode: "ro" },
|
|
1150
|
+
{ dest: CONTAINER.feed, source: this.cfg.feed_dir, mode: "rw" },
|
|
1151
|
+
{ dest: CONTAINER.alerts, source: this.cfg.alerts_dir, mode: "rw" },
|
|
1152
|
+
);
|
|
1153
|
+
// The install's homes (profiles/, portrait/, state), the config mirror
|
|
1154
|
+
// and the read-only GitHub token. All directories, created before a cold
|
|
1155
|
+
// run (`ensureDirs`), so unconditional like the evidence store.
|
|
1156
|
+
const homes = this.hostHomes();
|
|
1157
|
+
spec.push(
|
|
1158
|
+
{ dest: CONTAINER.profiles, source: homes.profiles, mode: "rw" },
|
|
1159
|
+
{ dest: CONTAINER.portrait, source: homes.portrait, mode: "rw" },
|
|
1160
|
+
{ dest: CONTAINER.configLive, source: homes.configMirror, mode: "ro" },
|
|
1161
|
+
{ dest: CONTAINER.stateHome, source: homes.state, mode: "rw" },
|
|
1162
|
+
{ dest: CONTAINER.githubRead, source: homes.github, mode: "ro" },
|
|
1163
|
+
);
|
|
1164
|
+
// The locator also searches beside an explicit config file. Keep that
|
|
1165
|
+
// fallback beside the mirrored config without mounting its secret home.
|
|
1166
|
+
const adjacent = homeProfileDirs(this.installEnv())[1];
|
|
1167
|
+
if (adjacent && adjacent !== homes.profiles && this.fs.exists(adjacent))
|
|
1168
|
+
spec.push({ dest: `${CONTAINER.configLive}/profiles`, source: adjacent, mode: "rw" });
|
|
1169
|
+
// The VM access (rule 8): exactly these three, read-only, present ones
|
|
1170
|
+
// only. An agent socket moves with every login, so a new path is mount
|
|
1171
|
+
// drift and the container is recreated onto it, unless the previous
|
|
1172
|
+
// socket still exists (mountDrift keeps the live agent's mount).
|
|
1173
|
+
const ssh = this.sshSources();
|
|
1174
|
+
if (ssh.agent !== undefined) spec.push({ dest: SSH.agent, source: ssh.agent, mode: "ro" });
|
|
1175
|
+
if (ssh.config !== undefined) spec.push({ dest: SSH.config, source: ssh.config, mode: "ro" });
|
|
1176
|
+
if (ssh.knownHosts !== undefined)
|
|
1177
|
+
spec.push({ dest: SSH.knownHosts, source: ssh.knownHosts, mode: "ro" });
|
|
1178
|
+
return spec;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
/**
|
|
1182
|
+
* The host's ssh sources the companion is given (`SSH`): the agent socket
|
|
1183
|
+
* `SSH_AUTH_SOCK` names, and `config` and `known_hosts` under the
|
|
1184
|
+
* configured `ssh_dir` — each only when it exists. In `tmux-socket` mode
|
|
1185
|
+
* this process cannot see the host, so the wrapper names them
|
|
1186
|
+
* (`HOST_SSH_ENV`).
|
|
1187
|
+
*/
|
|
1188
|
+
sshSources(): SshSources {
|
|
1189
|
+
const out: SshSources = {};
|
|
1190
|
+
const set = (key: keyof SshSources, path: string | undefined): void => {
|
|
1191
|
+
if (path !== undefined) out[key] = path;
|
|
1192
|
+
};
|
|
1193
|
+
if (this.socketPath !== undefined) {
|
|
1194
|
+
set("agent", hostPath(this.env[HOST_SSH_ENV.agent]));
|
|
1195
|
+
set("config", hostPath(this.env[HOST_SSH_ENV.config]));
|
|
1196
|
+
set("knownHosts", hostPath(this.env[HOST_SSH_ENV.knownHosts]));
|
|
1197
|
+
return out;
|
|
1198
|
+
}
|
|
1199
|
+
const present = (path: string | undefined): string | undefined =>
|
|
1200
|
+
path !== undefined && this.fs.exists(path) ? path : undefined;
|
|
1201
|
+
const dir = hostPath(this.cfg.ssh_dir);
|
|
1202
|
+
set("agent", present(hostPath(this.env["SSH_AUTH_SOCK"])));
|
|
1203
|
+
set("config", present(dir === undefined ? undefined : join(dir, "config")));
|
|
1204
|
+
set("knownHosts", present(dir === undefined ? undefined : join(dir, "known_hosts")));
|
|
1205
|
+
return out;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
/** `dest -> source` for every mount `runArgv()` declares. */
|
|
1209
|
+
expectedMounts(): Map<string, string> {
|
|
1210
|
+
return new Map(this.mountSpec().map((m) => [m.dest, m.source]));
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
/** Idempotent: running → adopt; stopped → start; absent → run. */
|
|
1214
|
+
async ensureUp(onProgress?: (line: string) => void): Promise<EnsureOutcome> {
|
|
1215
|
+
if (this.socketPath !== undefined) return this.adoptWrapperRun(onProgress);
|
|
1216
|
+
await this.resolveMemoryWiring(onProgress);
|
|
1217
|
+
// Say which of the two worlds this start is in, either way. A missing
|
|
1218
|
+
// checkout is a normal, non-fatal state — but a companion silently reading
|
|
1219
|
+
// no reviewed source at all must never be left implicit.
|
|
1220
|
+
onProgress?.(
|
|
1221
|
+
this.reviewedRepoPresent()
|
|
1222
|
+
? `reviewed repo ${this.cfg.reviewed_repo_dir} → ${CONTAINER.reviewedRepo} (read-only)`
|
|
1223
|
+
: `no reviewed repo at ${this.cfg.reviewed_repo_dir} — companion starts without it ([companion] reviewed_repo_dir)`,
|
|
1224
|
+
);
|
|
1225
|
+
let st = await this.status();
|
|
1226
|
+
let recreating = false;
|
|
1227
|
+
// Mount drift is repaired by recreation and nothing else. Do it before the
|
|
1228
|
+
// adopt/start branches, so a stale container never survives a TUI start.
|
|
1229
|
+
// The conversation is on the pihome volume and survives the rm.
|
|
1230
|
+
if (st.exists) {
|
|
1231
|
+
// DNS drift is only worth asking about when the mounts came back clean
|
|
1232
|
+
// (drifted mounts already force the recreate) and status() actually
|
|
1233
|
+
// yielded a mount set: a real companion container always reports its
|
|
1234
|
+
// mounts, so an empty set means the answer was unreadable — no opinion,
|
|
1235
|
+
// never a recreate. An unreadable createdDns() is no opinion too.
|
|
1236
|
+
const mountIssues = this.mountDrift(st.mounts);
|
|
1237
|
+
const dns =
|
|
1238
|
+
mountIssues.length === 0 && st.mounts.size > 0 ? await this.createdDns() : undefined;
|
|
1239
|
+
const drift = [...mountIssues, ...(dns === undefined ? [] : this.dnsDrift(dns))];
|
|
1240
|
+
if (drift.length > 0) {
|
|
1241
|
+
onProgress?.(`companion mounts drifted (${drift.join("; ")}) — recreating`);
|
|
1242
|
+
await this.forceRemove();
|
|
1243
|
+
st = { exists: false, running: false, mounts: new Map() };
|
|
1244
|
+
recreating = true;
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
if (st.running) {
|
|
1248
|
+
// A container created before the memory forward existed is still on the
|
|
1249
|
+
// old network; connecting is live and needs no recreate.
|
|
1250
|
+
await this.attachToMemoryNetwork();
|
|
1251
|
+
return "adopted";
|
|
1252
|
+
}
|
|
1253
|
+
if (st.exists) {
|
|
1254
|
+
const started = await this.docker(["start", this.cfg.container_name]);
|
|
1255
|
+
if (started.ok) {
|
|
1256
|
+
// Same repair as adopting a running container: one created before
|
|
1257
|
+
// the memory network existed joins it now.
|
|
1258
|
+
await this.attachToMemoryNetwork();
|
|
1259
|
+
return "started";
|
|
1260
|
+
}
|
|
1261
|
+
// A stopped container can be permanently unstartable: bind-mount
|
|
1262
|
+
// sources are recorded at creation, and a source that no longer exists
|
|
1263
|
+
// makes `docker start` fail forever. Remove the husk and recreate with
|
|
1264
|
+
// the current environment; named volumes (pihome auth, conversation)
|
|
1265
|
+
// survive the rm.
|
|
1266
|
+
onProgress?.(`companion container unstartable (${firstLine(errText(started))}) — recreating`);
|
|
1267
|
+
await this.forceRemove();
|
|
1268
|
+
}
|
|
1269
|
+
// Self-healing first run: build the image rather than failing on its absence.
|
|
1270
|
+
if (!(await this.imageExists())) await this.buildImage(onProgress);
|
|
1271
|
+
const homes = this.hostHomes();
|
|
1272
|
+
this.ensureDirs([
|
|
1273
|
+
this.cfg.feed_dir,
|
|
1274
|
+
this.cfg.alerts_dir,
|
|
1275
|
+
this.cfg.evidence_dir,
|
|
1276
|
+
homes.profiles,
|
|
1277
|
+
homes.portrait,
|
|
1278
|
+
homes.configMirror,
|
|
1279
|
+
homes.state,
|
|
1280
|
+
homes.github,
|
|
1281
|
+
]);
|
|
1282
|
+
const created = await this.docker(this.runArgv(), { timeoutMs: 120_000 });
|
|
1283
|
+
if (!created.ok) throw new Error(`docker run ${this.cfg.image} failed: ${errText(created)}`);
|
|
1284
|
+
return st.exists || recreating ? "recreated" : "created";
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
/**
|
|
1288
|
+
* `tmux-socket` mode: the host wrapper started (or adopted) the companion
|
|
1289
|
+
* container before this process existed, and this one has no docker, so the
|
|
1290
|
+
* start is only a wait for the tmux server to answer on the shared socket.
|
|
1291
|
+
* A server that never answers is the wrapper's failure to report, not ours
|
|
1292
|
+
* to repair.
|
|
1293
|
+
*/
|
|
1294
|
+
private async adoptWrapperRun(onProgress?: (line: string) => void): Promise<EnsureOutcome> {
|
|
1295
|
+
onProgress?.(`companion container run by the qcr wrapper — tmux socket ${this.socketPath}`);
|
|
1296
|
+
if (await this.waitRunning(this.wrapperStartWaitMs, 250)) return "adopted";
|
|
1297
|
+
throw new Error(
|
|
1298
|
+
`the companion's tmux server is not answering on ${this.socketPath} — the qcr wrapper starts the companion container before the TUI (is ${this.cfg.container_name} running?)`,
|
|
1299
|
+
);
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
/**
|
|
1303
|
+
* Wait until the container is actually running.
|
|
1304
|
+
*
|
|
1305
|
+
* `docker run --detach` returns once the container is created, and a
|
|
1306
|
+
* `docker exec` an instant later can still lose the race with "container is
|
|
1307
|
+
* not running", typically right after a recreate. Bounded and total: if
|
|
1308
|
+
* it never comes up, the caller's own error is the better message.
|
|
1309
|
+
*/
|
|
1310
|
+
async waitRunning(timeoutMs = 5_000, stepMs = 100): Promise<boolean> {
|
|
1311
|
+
const deadline = Date.now() + timeoutMs;
|
|
1312
|
+
for (;;) {
|
|
1313
|
+
if (this.socketPath !== undefined) {
|
|
1314
|
+
// No docker to ask: the server answering is "running".
|
|
1315
|
+
if (await this.serverUp()) return true;
|
|
1316
|
+
if (Date.now() >= deadline) return false;
|
|
1317
|
+
await new Promise((r) => setTimeout(r, stepMs));
|
|
1318
|
+
continue;
|
|
1319
|
+
}
|
|
1320
|
+
const st = await this.status();
|
|
1321
|
+
if (st.running) return true;
|
|
1322
|
+
// Gone rather than starting: waiting cannot help, and the caller's own
|
|
1323
|
+
// failure is a better message than a five-second stall.
|
|
1324
|
+
if (!st.exists) return false;
|
|
1325
|
+
if (Date.now() >= deadline) return false;
|
|
1326
|
+
await new Promise((r) => setTimeout(r, stepMs));
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
/** Idempotent: a live companion session is left strictly alone (a second
|
|
1331
|
+
* TUI attaches to it). Returns true when this call birthed the session. */
|
|
1332
|
+
async spawnPi(): Promise<boolean> {
|
|
1333
|
+
if (await this.piRunning()) return false;
|
|
1334
|
+
let res = await this.docker(this.spawnArgv());
|
|
1335
|
+
// `docker run --detach` returns at create, not at running, so an exec an
|
|
1336
|
+
// instant after a recreate can lose the race with "container is not
|
|
1337
|
+
// running", which would report the companion unavailable for the whole
|
|
1338
|
+
// session. Wait for it once and try again;
|
|
1339
|
+
// anything else is a real failure and is raised as one.
|
|
1340
|
+
if (!res.ok && /is not running|tmux server is not up/i.test(errText(res))) {
|
|
1341
|
+
await this.waitRunning();
|
|
1342
|
+
res = await this.docker(this.spawnArgv());
|
|
1343
|
+
}
|
|
1344
|
+
// Another TUI may have created the session between the check above and
|
|
1345
|
+
// this spawn (tmux refuses a duplicate): that session is the one to adopt.
|
|
1346
|
+
if (!res.ok && (await this.piRunning())) return false;
|
|
1347
|
+
if (!res.ok) throw new Error(`spawning the companion pi session failed: ${errText(res)}`);
|
|
1348
|
+
return true;
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
/** Graceful `docker stop` — SIGTERM to the entrypoint; volumes persist.
|
|
1352
|
+
* A container docker could not be asked about is not a stopped one: that
|
|
1353
|
+
* throws rather than return as if the stop had happened. */
|
|
1354
|
+
async gracefulStop(): Promise<void> {
|
|
1355
|
+
// The wrapper that ran the container stops it, after the TUI container
|
|
1356
|
+
// has exited; this process cannot, and must not pretend to.
|
|
1357
|
+
if (this.socketPath !== undefined) return;
|
|
1358
|
+
const st = await this.status();
|
|
1359
|
+
if (st.unknown !== undefined)
|
|
1360
|
+
throw new Error(
|
|
1361
|
+
`cannot tell whether ${this.cfg.container_name} is running (docker inspect failed: ${st.unknown}) — nothing was stopped`,
|
|
1362
|
+
);
|
|
1363
|
+
if (!st.running) return;
|
|
1364
|
+
const res = await this.docker(["stop", this.cfg.container_name], { timeoutMs: 60_000 });
|
|
1365
|
+
if (!res.ok) throw new Error(`docker stop ${this.cfg.container_name} failed: ${errText(res)}`);
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
/** One-line cause from a failed docker call: the last line of stderr, else
|
|
1370
|
+
* of stdout, else the exit status. */
|
|
1371
|
+
export function dockerFailureLine(r: DockerResult): string {
|
|
1372
|
+
return (r.stderr.trim() || r.stdout.trim() || `exit ${r.code}`).split("\n").slice(-1)[0]!;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
const errText = dockerFailureLine;
|
|
1376
|
+
|
|
1377
|
+
/** The companion's pi session dir inside the container — where `piArgv` points
|
|
1378
|
+
* pi and where the day rollover looks for the conversation. */
|
|
1379
|
+
export function companionSessionDir(session: string): string {
|
|
1380
|
+
return `${CONTAINER.piHome}/agent/sessions/${session}`;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
function firstLine(s: string): string {
|
|
1384
|
+
return s.split("\n", 1)[0] ?? "";
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
export const realDockerRunner: DockerRunner = (argv, opts) => runCommand("docker", argv, opts);
|
|
1388
|
+
|
|
1389
|
+
/** Run `bin argv…` and collect its output. A spawn failure (no such binary,
|
|
1390
|
+
* no permission) or the timeout is said in `stderr`, so the caller's
|
|
1391
|
+
* one-line cause names it instead of "exit null". */
|
|
1392
|
+
export function runCommand(
|
|
1393
|
+
bin: string,
|
|
1394
|
+
argv: string[],
|
|
1395
|
+
opts?: { timeoutMs?: number },
|
|
1396
|
+
): Promise<DockerResult> {
|
|
1397
|
+
return new Promise((resolvePromise) => {
|
|
1398
|
+
const child = spawn(bin, argv, { stdio: ["ignore", "pipe", "pipe"] });
|
|
1399
|
+
let stdout = "";
|
|
1400
|
+
let stderr = "";
|
|
1401
|
+
let settled = false;
|
|
1402
|
+
const settle = (ok: boolean, code: number | null): void => {
|
|
1403
|
+
if (settled) return;
|
|
1404
|
+
settled = true;
|
|
1405
|
+
clearTimeout(timer);
|
|
1406
|
+
resolvePromise({ ok, stdout, stderr, code });
|
|
1407
|
+
};
|
|
1408
|
+
const timeoutMs = opts?.timeoutMs ?? 20_000;
|
|
1409
|
+
const timer = setTimeout(() => {
|
|
1410
|
+
child.kill("SIGKILL");
|
|
1411
|
+
stderr += `${stderr === "" || stderr.endsWith("\n") ? "" : "\n"}${bin} timed out after ${timeoutMs} ms`;
|
|
1412
|
+
settle(false, null);
|
|
1413
|
+
}, timeoutMs);
|
|
1414
|
+
// Decoded by the stream, never per chunk: a multi-byte character split
|
|
1415
|
+
// across two reads (a `─` in a large capture, any non-ASCII in a long
|
|
1416
|
+
// copied buffer) otherwise becomes two U+FFFD.
|
|
1417
|
+
child.stdout.setEncoding("utf8");
|
|
1418
|
+
child.stderr.setEncoding("utf8");
|
|
1419
|
+
child.stdout.on("data", (d: string) => (stdout += d));
|
|
1420
|
+
child.stderr.on("data", (d: string) => (stderr += d));
|
|
1421
|
+
child.on("error", (err) => {
|
|
1422
|
+
stderr += `${stderr === "" || stderr.endsWith("\n") ? "" : "\n"}${bin}: ${err.message}`;
|
|
1423
|
+
settle(false, null);
|
|
1424
|
+
});
|
|
1425
|
+
child.on("close", (code) => settle(code === 0, code));
|
|
1426
|
+
});
|
|
1427
|
+
}
|