@quickchatai/quickreview 1.2.0 → 1.3.1
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/README.md +114 -50
- package/bin/qcr.js +1 -1
- package/config.example.toml +20 -6
- package/deploy/ansible/README.md +20 -12
- package/deploy/ansible/inventory.example.ini +1 -1
- package/deploy/collect-secrets.sh +1 -1
- package/deploy/qcr-update.sh +1 -1
- package/dist/extensions/cli/home.js +14 -0
- package/dist/extensions/cli/home.js.map +1 -0
- package/dist/extensions/cli/selfimprove.js +18 -0
- package/dist/extensions/cli/selfimprove.js.map +1 -0
- package/dist/extensions/home.js +350 -0
- package/dist/extensions/home.js.map +1 -0
- package/dist/extensions/login-route.js +1 -1
- package/dist/extensions/login-route.js.map +1 -1
- package/dist/extensions/onboarding.js +8 -7
- package/dist/extensions/onboarding.js.map +1 -1
- package/dist/extensions/selfimprove.js +49 -0
- package/dist/extensions/selfimprove.js.map +1 -0
- package/dist/extensions/setup-scan.js +23 -592
- package/dist/extensions/setup-scan.js.map +1 -1
- package/dist/extensions/setup.js +2 -47
- package/dist/extensions/setup.js.map +1 -1
- package/dist/src/archive/restore.js +18 -15
- package/dist/src/archive/restore.js.map +1 -1
- package/dist/src/botworker.js +13 -2
- package/dist/src/botworker.js.map +1 -1
- package/dist/src/channelfs.js +3 -26
- package/dist/src/channelfs.js.map +1 -1
- package/dist/src/cli.js +12 -50
- package/dist/src/cli.js.map +1 -1
- package/dist/src/config.js +16 -0
- package/dist/src/config.js.map +1 -1
- package/dist/src/daemon.js +26 -0
- package/dist/src/daemon.js.map +1 -1
- package/dist/src/debuground.js +1 -0
- package/dist/src/debuground.js.map +1 -1
- package/dist/src/debugroundpi.js +1 -0
- package/dist/src/debugroundpi.js.map +1 -1
- package/dist/src/engine/caserunner-sdk.js +15 -251
- package/dist/src/engine/caserunner-sdk.js.map +1 -1
- package/dist/src/home/adapters.js +349 -0
- package/dist/src/home/adapters.js.map +1 -0
- package/dist/src/home/advanced.js +318 -0
- package/dist/src/home/advanced.js.map +1 -0
- package/dist/src/home/advancedtools.js +57 -0
- package/dist/src/home/advancedtools.js.map +1 -0
- package/dist/src/home/autoreview.js +325 -0
- package/dist/src/home/autoreview.js.map +1 -0
- package/dist/src/home/daemonstatus.js +32 -0
- package/dist/src/home/daemonstatus.js.map +1 -0
- package/dist/src/home/devicelogin.js +61 -0
- package/dist/src/home/devicelogin.js.map +1 -0
- package/dist/src/home/launch.js +202 -0
- package/dist/src/home/launch.js.map +1 -0
- package/dist/src/home/loginowner.js +205 -0
- package/dist/src/home/loginowner.js.map +1 -0
- package/dist/src/home/memory.js +122 -0
- package/dist/src/home/memory.js.map +1 -0
- package/dist/src/home/model.js +811 -0
- package/dist/src/home/model.js.map +1 -0
- package/dist/src/home/panel.js +520 -0
- package/dist/src/home/panel.js.map +1 -0
- package/dist/src/home/ports.js +19 -0
- package/dist/src/home/ports.js.map +1 -0
- package/dist/src/home/review.js +163 -0
- package/dist/src/home/review.js.map +1 -0
- package/dist/src/home/reviews.js +478 -0
- package/dist/src/home/reviews.js.map +1 -0
- package/dist/src/home/setup.js +507 -0
- package/dist/src/home/setup.js.map +1 -0
- package/dist/src/home/setupadapters.js +214 -0
- package/dist/src/home/setupadapters.js.map +1 -0
- package/dist/src/home/setupcard.js +199 -0
- package/dist/src/home/setupcard.js.map +1 -0
- package/dist/src/home/setuptools.js +45 -0
- package/dist/src/home/setuptools.js.map +1 -0
- package/dist/src/home/spend.js +160 -0
- package/dist/src/home/spend.js.map +1 -0
- package/dist/src/home/switches.js +209 -0
- package/dist/src/home/switches.js.map +1 -0
- package/dist/src/home/switchon.js +439 -0
- package/dist/src/home/switchon.js.map +1 -0
- package/dist/src/home/toolnames.js +41 -0
- package/dist/src/home/toolnames.js.map +1 -0
- package/dist/src/home/tune.js +341 -0
- package/dist/src/home/tune.js.map +1 -0
- package/dist/src/home/words.js +7 -0
- package/dist/src/home/words.js.map +1 -0
- package/dist/src/homes.js +14 -0
- package/dist/src/homes.js.map +1 -1
- package/dist/src/install.js +1 -1
- package/dist/src/install.js.map +1 -1
- package/dist/src/ipc/workerspec.js +8 -0
- package/dist/src/ipc/workerspec.js.map +1 -1
- package/dist/src/live/publish.js +1 -1
- package/dist/src/live/publish.js.map +1 -1
- package/dist/src/livecli.js +1 -1
- package/dist/src/livecli.js.map +1 -1
- package/dist/src/loginrefresh/handover.js +121 -0
- package/dist/src/loginrefresh/handover.js.map +1 -0
- package/dist/src/loginrefresh/lock.js +54 -3
- package/dist/src/loginrefresh/lock.js.map +1 -1
- package/dist/src/loginrefresh/refresher.js +33 -5
- package/dist/src/loginrefresh/refresher.js.map +1 -1
- package/dist/src/loginrefresh/supervisor.js +18 -1
- package/dist/src/loginrefresh/supervisor.js.map +1 -1
- package/dist/src/npmverbs.js +42 -12
- package/dist/src/npmverbs.js.map +1 -1
- package/dist/src/onboard/choices.js +6 -3
- package/dist/src/onboard/choices.js.map +1 -1
- package/dist/src/onboard/engine.js +33 -292
- package/dist/src/onboard/engine.js.map +1 -1
- package/dist/src/onboard/steps/bringup.js +3 -2
- package/dist/src/onboard/steps/bringup.js.map +1 -1
- package/dist/src/onboard/steps/configfill.js +18 -7
- package/dist/src/onboard/steps/configfill.js.map +1 -1
- package/dist/src/onboard/steps/loginshare.js +213 -0
- package/dist/src/onboard/steps/loginshare.js.map +1 -0
- package/dist/src/onboard/steps/memory.js +14 -2
- package/dist/src/onboard/steps/memory.js.map +1 -1
- package/dist/src/onboard/steps/repoconfig.js +72 -0
- package/dist/src/onboard/steps/repoconfig.js.map +1 -0
- package/dist/src/onboard/toolnames.js +2 -2
- package/dist/src/onboard/toolnames.js.map +1 -1
- package/dist/src/piagentdir.js +13 -0
- package/dist/src/piagentdir.js.map +1 -0
- package/dist/src/pilogin.js +31 -16
- package/dist/src/pilogin.js.map +1 -1
- package/dist/src/qcrbin.js +16 -11
- package/dist/src/qcrbin.js.map +1 -1
- package/dist/src/reviewargs.js +122 -0
- package/dist/src/reviewargs.js.map +1 -0
- package/dist/src/reviewcli.js +7 -161
- package/dist/src/reviewcli.js.map +1 -1
- package/dist/src/reviewdetach.js +76 -11
- package/dist/src/reviewdetach.js.map +1 -1
- package/dist/src/scancli.js +39 -691
- package/dist/src/scancli.js.map +1 -1
- package/dist/src/selfimprove.js +66 -0
- package/dist/src/selfimprove.js.map +1 -0
- package/dist/src/setup/adapters.js +5 -223
- package/dist/src/setup/adapters.js.map +1 -1
- package/dist/src/setup/caserunner.js +15 -249
- package/dist/src/setup/caserunner.js.map +1 -1
- package/dist/src/setup/engine.js +32 -341
- package/dist/src/setup/engine.js.map +1 -1
- package/dist/src/setup/facts.js +3 -592
- package/dist/src/setup/facts.js.map +1 -1
- package/dist/src/setup/findings.js +3 -947
- package/dist/src/setup/findings.js.map +1 -1
- package/dist/src/setup/presentation.js +3 -8
- package/dist/src/setup/presentation.js.map +1 -1
- package/dist/src/setup/scan.js +3 -724
- package/dist/src/setup/scan.js.map +1 -1
- package/dist/src/setup/scanbuild.js +9 -92
- package/dist/src/setup/scanbuild.js.map +1 -1
- package/dist/src/setup/scanlease.js +3 -250
- package/dist/src/setup/scanlease.js.map +1 -1
- package/dist/src/setup/scanreport.js +3 -288
- package/dist/src/setup/scanreport.js.map +1 -1
- package/dist/src/setup/scanrun.js +3 -1732
- package/dist/src/setup/scanrun.js.map +1 -1
- package/dist/src/setup/scantree.js +3 -256
- package/dist/src/setup/scantree.js.map +1 -1
- package/dist/src/setup/stage2request.js +3 -258
- package/dist/src/setup/stage2request.js.map +1 -1
- package/dist/src/setup/unitcut.js +3 -436
- package/dist/src/setup/unitcut.js.map +1 -1
- package/dist/src/state.js +2 -0
- package/dist/src/state.js.map +1 -1
- package/dist/src/takeover.js +1 -0
- package/dist/src/takeover.js.map +1 -1
- package/dist/src/takeoverpi.js +2 -1
- package/dist/src/takeoverpi.js.map +1 -1
- package/dist/src/tui/board.js +1 -1
- package/dist/src/tui/board.js.map +1 -1
- package/dist/src/tui/companion.js +2 -1
- package/dist/src/tui/companion.js.map +1 -1
- package/dist/src/tui/deploytab.js +2 -2
- package/dist/src/tui/deploytab.js.map +1 -1
- package/dist/src/tui/index.js +9 -6
- package/dist/src/tui/index.js.map +1 -1
- package/dist/src/tui/memory.js +3 -16
- package/dist/src/tui/memory.js.map +1 -1
- package/dist/src/tui/memoryurl.js +22 -0
- package/dist/src/tui/memoryurl.js.map +1 -0
- package/dist/src/tui/remote.js +1 -1
- package/dist/src/tui/remote.js.map +1 -1
- package/dist/src/tui/route.js +2 -2
- package/dist/src/tui/route.js.map +1 -1
- package/dist/src/tui/setupstate.js +8 -5
- package/dist/src/tui/setupstate.js.map +1 -1
- package/dist/src/tui/sshtriage.js +1 -1
- package/dist/src/tui/sshtriage.js.map +1 -1
- package/dist/src/wire.js +6 -2
- package/dist/src/wire.js.map +1 -1
- package/dist/src/workerworld.js +4 -0
- package/dist/src/workerworld.js.map +1 -1
- package/docker/README.md +1 -1
- package/docker/entrypoint.sh +40 -6
- package/docker-compose.yml +16 -1
- package/extensions/cli/home.ts +14 -0
- package/extensions/cli/selfimprove.ts +18 -0
- package/extensions/home.ts +484 -0
- package/extensions/login-route.ts +1 -1
- package/extensions/onboarding.ts +8 -7
- package/extensions/selfimprove.ts +65 -0
- package/extensions/setup-scan.ts +79 -766
- package/extensions/setup.ts +14 -70
- package/package.json +6 -4
- package/prompts/onboarding.md +8 -7
- package/schemas/review-result.v1.json +60 -0
- package/scripts/deploy.sh +8 -6
- package/scripts/qcr-host.sh +28 -5
- package/scripts/vm-ghcr-login.sh +7 -4
- package/src/archive/restore.ts +18 -21
- package/src/botworker.ts +13 -2
- package/src/channelfs.ts +3 -26
- package/src/cli.ts +11 -54
- package/src/config.ts +38 -0
- package/src/daemon.ts +26 -0
- package/src/debuground.ts +3 -0
- package/src/debugroundpi.ts +1 -0
- package/src/engine/caserunner-sdk.ts +29 -314
- package/src/home/adapters.ts +418 -0
- package/src/home/advanced.ts +421 -0
- package/src/home/advancedtools.ts +96 -0
- package/src/home/autoreview.ts +385 -0
- package/src/home/daemonstatus.ts +55 -0
- package/src/home/devicelogin.ts +107 -0
- package/src/home/launch.ts +257 -0
- package/src/home/loginowner.ts +248 -0
- package/src/home/memory.ts +176 -0
- package/src/home/model.ts +965 -0
- package/src/home/panel.ts +666 -0
- package/src/home/ports.ts +338 -0
- package/src/home/review.ts +206 -0
- package/src/home/reviews.ts +565 -0
- package/src/home/setup.ts +689 -0
- package/src/home/setupadapters.ts +217 -0
- package/src/home/setupcard.ts +288 -0
- package/src/home/setuptools.ts +57 -0
- package/src/home/spend.ts +194 -0
- package/src/home/switches.ts +293 -0
- package/src/home/switchon.ts +554 -0
- package/src/home/toolnames.ts +46 -0
- package/src/home/tune.ts +405 -0
- package/src/home/words.ts +8 -0
- package/src/homes.ts +17 -0
- package/src/install.ts +1 -1
- package/src/ipc/workerspec.ts +15 -0
- package/src/live/publish.ts +4 -1
- package/src/livecli.ts +6 -1
- package/src/loginrefresh/handover.ts +138 -0
- package/src/loginrefresh/lock.ts +72 -3
- package/src/loginrefresh/refresher.ts +34 -5
- package/src/loginrefresh/supervisor.ts +25 -2
- package/src/npmverbs.ts +51 -11
- package/src/onboard/choices.ts +6 -3
- package/src/onboard/engine.ts +64 -434
- package/src/onboard/steps/bringup.ts +3 -2
- package/src/onboard/steps/configfill.ts +23 -7
- package/src/onboard/steps/loginshare.ts +258 -0
- package/src/onboard/steps/memory.ts +18 -2
- package/src/onboard/steps/repoconfig.ts +85 -0
- package/src/onboard/toolnames.ts +2 -2
- package/src/piagentdir.ts +12 -0
- package/src/pilogin.ts +32 -16
- package/src/qcrbin.ts +16 -11
- package/src/reviewargs.ts +119 -0
- package/src/reviewcli.ts +6 -167
- package/src/reviewdetach.ts +107 -13
- package/src/scancli.ts +75 -826
- package/src/selfimprove.ts +90 -0
- package/src/setup/adapters.ts +13 -221
- package/src/setup/caserunner.ts +34 -273
- package/src/setup/engine.ts +62 -496
- package/src/setup/facts.ts +3 -627
- package/src/setup/findings.ts +3 -1264
- package/src/setup/presentation.ts +3 -18
- package/src/setup/scan.ts +3 -1033
- package/src/setup/scanbuild.ts +20 -114
- package/src/setup/scanlease.ts +3 -297
- package/src/setup/scanreport.ts +3 -373
- package/src/setup/scanrun.ts +3 -2268
- package/src/setup/scantree.ts +3 -259
- package/src/setup/stage2request.ts +3 -347
- package/src/setup/unitcut.ts +3 -523
- package/src/state.ts +25 -0
- package/src/takeover.ts +5 -0
- package/src/takeoverpi.ts +3 -2
- package/src/tui/board.ts +1 -1
- package/src/tui/companion.ts +2 -1
- package/src/tui/deploytab.ts +2 -2
- package/src/tui/index.ts +9 -6
- package/src/tui/memory.ts +3 -15
- package/src/tui/memoryurl.ts +22 -0
- package/src/tui/remote.ts +1 -1
- package/src/tui/route.ts +2 -2
- package/src/tui/setupstate.ts +20 -16
- package/src/tui/sshtriage.ts +1 -1
- package/src/wire.ts +6 -2
- package/src/workerworld.ts +9 -1
- package/dist/src/live/projection.js +0 -450
- package/dist/src/live/projection.js.map +0 -1
- package/dist/src/live/read.js +0 -60
- package/dist/src/live/read.js.map +0 -1
- package/dist/src/onboard/launch.js +0 -75
- package/dist/src/onboard/launch.js.map +0 -1
- package/dist/src/setup/ledger.js +0 -207
- package/dist/src/setup/ledger.js.map +0 -1
- package/dist/src/setup/stage2.js +0 -617
- package/dist/src/setup/stage2.js.map +0 -1
- package/dist/src/setup/textsniff.js +0 -89
- package/dist/src/setup/textsniff.js.map +0 -1
- package/src/live/projection.ts +0 -491
- package/src/live/read.ts +0 -86
- package/src/onboard/launch.ts +0 -103
- package/src/setup/ledger.ts +0 -324
- package/src/setup/stage2.ts +0 -926
- package/src/setup/textsniff.ts +0 -95
package/src/setup/unitcut.ts
CHANGED
|
@@ -1,523 +1,3 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
// by. The COMPANION proposes it, the OPERATOR adjusts it, and once saved it is
|
|
5
|
-
// reused by every later survey and scan; the companion re-cuts only on request.
|
|
6
|
-
//
|
|
7
|
-
// A unit OWNS path prefixes (`roots`). A file belongs to the unit whose root is
|
|
8
|
-
// the LONGEST prefix of it, so units nest naturally (`backend` and
|
|
9
|
-
// `backend/payments` are two units, and `backend/payments/x.py` is the
|
|
10
|
-
// second's); an `ignored` prefix is the same kind of claim held by nobody, so
|
|
11
|
-
// `backend/vendor` can be carved out of `backend` and a unit inside an ignored
|
|
12
|
-
// tree takes its files back. The root `""` claims everything, which is how a
|
|
13
|
-
// catch-all "rest" unit is spelt. A SAVED cut is a partition: every file is in
|
|
14
|
-
// exactly one unit or deliberately ignored — a file nobody claims refuses the
|
|
15
|
-
// save, because a scan that silently skips it would read as "covered".
|
|
16
|
-
//
|
|
17
|
-
// Pure: the cut is a value, validation and the adjust ops are functions over
|
|
18
|
-
// it and a file listing. The engine persists it (src/setup/engine.ts).
|
|
19
|
-
import { SCAN_DEFAULTS } from "../config.js";
|
|
20
|
-
import { displayRoot, UNIT_KINDS, UNIT_NAME_MAX, UNIT_NAME_RE, type UnitKind } from "./facts.js";
|
|
21
|
-
|
|
22
|
-
export interface Unit {
|
|
23
|
-
name: string;
|
|
24
|
-
roots: string[];
|
|
25
|
-
kind: UnitKind;
|
|
26
|
-
/** Why this unit exists — the marker that proposed it, or the operator's
|
|
27
|
-
* reason. Shown beside the unit; never parsed. */
|
|
28
|
-
note: string;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface UnitCut {
|
|
32
|
-
units: Unit[];
|
|
33
|
-
/** Prefixes nobody reviews (vendored, generated, fixtures the operator
|
|
34
|
-
* chose to leave out). */
|
|
35
|
-
ignored: string[];
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// ---- normalisation -----------------------------------------------------------
|
|
39
|
-
|
|
40
|
-
/** A root as stored: repo-relative, no leading `./` or `/`, no trailing `/`.
|
|
41
|
-
* Null for anything that could leave the repo (`..`, an absolute path after
|
|
42
|
-
* stripping is fine — it is relative by construction — but `..` is not). */
|
|
43
|
-
export function normalizeRoot(raw: string): string | null {
|
|
44
|
-
let r = raw.trim().replace(/\\/g, "/");
|
|
45
|
-
while (r.startsWith("./")) r = r.slice(2);
|
|
46
|
-
r = r
|
|
47
|
-
.replace(/^\/+/, "")
|
|
48
|
-
.replace(/\/+$/, "")
|
|
49
|
-
.replace(/\/{2,}/g, "/");
|
|
50
|
-
if (r === ".") r = "";
|
|
51
|
-
if (r.split("/").some((s) => s === ".." || s === ".")) return null;
|
|
52
|
-
return r;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function covers(root: string, path: string): boolean {
|
|
56
|
-
return root === "" || path === root || path.startsWith(`${root}/`);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// ---- assignment --------------------------------------------------------------
|
|
60
|
-
|
|
61
|
-
export interface Assignment {
|
|
62
|
-
/** unit name → its files, in listing order. Every unit has an entry. */
|
|
63
|
-
byUnit: Map<string, string[]>;
|
|
64
|
-
ignored: string[];
|
|
65
|
-
unassigned: string[];
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** Longest-prefix assignment of every listed file. */
|
|
69
|
-
export function assign(cut: UnitCut, files: readonly string[]): Assignment {
|
|
70
|
-
const claims: Array<{ root: string; unit: string | null }> = [
|
|
71
|
-
...cut.units.flatMap((u) => u.roots.map((root) => ({ root, unit: u.name }))),
|
|
72
|
-
...cut.ignored.map((root) => ({ root, unit: null })),
|
|
73
|
-
].sort((a, b) => b.root.length - a.root.length);
|
|
74
|
-
const byUnit = new Map<string, string[]>(cut.units.map((u) => [u.name, []]));
|
|
75
|
-
const ignored: string[] = [];
|
|
76
|
-
const unassigned: string[] = [];
|
|
77
|
-
for (const f of files) {
|
|
78
|
-
const hit = claims.find((c) => covers(c.root, f));
|
|
79
|
-
if (!hit) unassigned.push(f);
|
|
80
|
-
else if (hit.unit === null) ignored.push(f);
|
|
81
|
-
else byUnit.get(hit.unit)!.push(f);
|
|
82
|
-
}
|
|
83
|
-
return { byUnit, ignored, unassigned };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// ---- validation --------------------------------------------------------------
|
|
87
|
-
|
|
88
|
-
export interface CutProblems {
|
|
89
|
-
/** Refusals: the cut cannot be stored at all. */
|
|
90
|
-
errors: string[];
|
|
91
|
-
/** Fine for a draft, refused by save (an empty unit, unclaimed files). */
|
|
92
|
-
blocking_save: string[];
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const SAMPLE = 8;
|
|
96
|
-
|
|
97
|
-
/** Refused both when a proposal is read and when a cut is checked. */
|
|
98
|
-
const ROOT_IGNORED = "the repo root cannot be ignored — that would leave nothing to review";
|
|
99
|
-
|
|
100
|
-
function sample(paths: readonly string[]): string {
|
|
101
|
-
const head = paths.slice(0, SAMPLE).join(", ");
|
|
102
|
-
return paths.length > SAMPLE ? `${head}, … (+${paths.length - SAMPLE} more)` : head;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Structural refusals (bad names, duplicate names or roots, a root that could
|
|
107
|
-
* leave the repo or matches no file, an ignored repo root) and the two
|
|
108
|
-
* save-only conditions (a unit that ends up with no files, files no unit
|
|
109
|
-
* claims). The cut is expected normalised (see `normalizeCut`).
|
|
110
|
-
*/
|
|
111
|
-
export function checkCut(cut: UnitCut, files: readonly string[]): CutProblems {
|
|
112
|
-
const errors: string[] = [];
|
|
113
|
-
const blocking: string[] = [];
|
|
114
|
-
if (cut.units.length === 0) errors.push("a unit cut needs at least one unit");
|
|
115
|
-
const names = new Set<string>();
|
|
116
|
-
const roots = new Map<string, string>();
|
|
117
|
-
for (const u of cut.units) {
|
|
118
|
-
if (!UNIT_NAME_RE.test(u.name) || u.name.includes("..")) {
|
|
119
|
-
errors.push(
|
|
120
|
-
`unit name ${JSON.stringify(u.name)} is not a plain name — letters, digits, '.', '_' and '-', at most ${UNIT_NAME_MAX}`,
|
|
121
|
-
);
|
|
122
|
-
}
|
|
123
|
-
if (names.has(u.name)) errors.push(`unit name "${u.name}" is used twice`);
|
|
124
|
-
names.add(u.name);
|
|
125
|
-
if (u.roots.length === 0) errors.push(`unit "${u.name}" owns no root`);
|
|
126
|
-
for (const r of u.roots) {
|
|
127
|
-
const owner = roots.get(r);
|
|
128
|
-
if (owner !== undefined)
|
|
129
|
-
errors.push(`root "${displayRoot(r)}" is claimed by both "${owner}" and "${u.name}"`);
|
|
130
|
-
roots.set(r, u.name);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
for (const r of cut.ignored) {
|
|
134
|
-
if (r === "") errors.push(ROOT_IGNORED);
|
|
135
|
-
const owner = roots.get(r);
|
|
136
|
-
if (owner !== undefined) errors.push(`root "${r}" is both ignored and owned by "${owner}"`);
|
|
137
|
-
roots.set(r, "(ignored)");
|
|
138
|
-
}
|
|
139
|
-
for (const [r, owner] of roots) {
|
|
140
|
-
if (r !== "" && !files.some((f) => covers(r, f))) {
|
|
141
|
-
errors.push(`root "${r}" (${owner}) matches no file in the repo`);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
if (errors.length === 0) {
|
|
145
|
-
const a = assign(cut, files);
|
|
146
|
-
for (const [name, owned] of a.byUnit) {
|
|
147
|
-
if (owned.length === 0) {
|
|
148
|
-
blocking.push(
|
|
149
|
-
`unit "${name}" ends up with no files — every file under its roots belongs to a deeper unit or is ignored`,
|
|
150
|
-
);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
if (a.unassigned.length > 0) {
|
|
154
|
-
blocking.push(
|
|
155
|
-
`${a.unassigned.length} file(s) belong to no unit: ${sample(a.unassigned)} — add them to a unit, add a catch-all unit with root "", or ignore them`,
|
|
156
|
-
);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
return { errors, blocking_save: blocking };
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/** Raw input (a tool call's JSON) → a normalised cut, or the reasons it is not
|
|
163
|
-
* one. Normalisation never guesses: an unspellable root is an error, not a
|
|
164
|
-
* silently-dropped entry. */
|
|
165
|
-
export function normalizeCut(
|
|
166
|
-
raw: unknown,
|
|
167
|
-
): { ok: true; cut: UnitCut } | { ok: false; errors: string[] } {
|
|
168
|
-
const errors: string[] = [];
|
|
169
|
-
const obj = (raw ?? {}) as { units?: unknown; ignored?: unknown };
|
|
170
|
-
if (!Array.isArray(obj.units))
|
|
171
|
-
return { ok: false, errors: ["`units` must be an array of {name, roots, kind?, note?}"] };
|
|
172
|
-
const units: Unit[] = [];
|
|
173
|
-
for (const [i, u] of obj.units.entries()) {
|
|
174
|
-
const unit = (u ?? {}) as { name?: unknown; roots?: unknown; kind?: unknown; note?: unknown };
|
|
175
|
-
if (typeof unit.name !== "string") {
|
|
176
|
-
errors.push(`units[${i}]: name must be a string`);
|
|
177
|
-
continue;
|
|
178
|
-
}
|
|
179
|
-
const rawRoots = typeof unit.roots === "string" ? [unit.roots] : unit.roots;
|
|
180
|
-
if (!Array.isArray(rawRoots) || rawRoots.some((r) => typeof r !== "string")) {
|
|
181
|
-
errors.push(`unit "${unit.name}": roots must be an array of repo-relative paths`);
|
|
182
|
-
continue;
|
|
183
|
-
}
|
|
184
|
-
const roots = normalizeRoots(rawRoots as string[], `unit "${unit.name}"`, errors);
|
|
185
|
-
units.push({
|
|
186
|
-
name: unit.name.trim(),
|
|
187
|
-
roots,
|
|
188
|
-
kind: unitKind(unit.kind),
|
|
189
|
-
note: typeof unit.note === "string" ? unit.note.trim() : "",
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
let ignored: string[] = [];
|
|
193
|
-
if (obj.ignored !== undefined) {
|
|
194
|
-
if (!Array.isArray(obj.ignored) || obj.ignored.some((r) => typeof r !== "string")) {
|
|
195
|
-
errors.push("`ignored` must be an array of repo-relative paths");
|
|
196
|
-
} else {
|
|
197
|
-
ignored = normalizeRoots(obj.ignored as string[], "ignored", errors);
|
|
198
|
-
if (ignored.includes("")) errors.push(ROOT_IGNORED);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
return errors.length > 0 ? { ok: false, errors } : { ok: true, cut: { units, ignored } };
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/** A supplied kind, or `custom` for anything that is not one of `UNIT_KINDS`. */
|
|
205
|
-
function unitKind(raw: unknown): UnitKind {
|
|
206
|
-
return typeof raw === "string" && new Set<string>(UNIT_KINDS).has(raw)
|
|
207
|
-
? (raw as UnitKind)
|
|
208
|
-
: "custom";
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function normalizeRoots(raw: readonly string[], owner: string, errors: string[]): string[] {
|
|
212
|
-
const out: string[] = [];
|
|
213
|
-
for (const r of raw) {
|
|
214
|
-
const n = normalizeRoot(r);
|
|
215
|
-
if (n === null) errors.push(`${owner}: root ${JSON.stringify(r)} leaves the repo`);
|
|
216
|
-
else if (!out.includes(n)) out.push(n);
|
|
217
|
-
}
|
|
218
|
-
return out;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// ---- adjust ops --------------------------------------------------------------
|
|
222
|
-
|
|
223
|
-
/** The operator's corrections, as the companion relays them. Applied to a
|
|
224
|
-
* draft all-or-nothing: one refused op leaves the draft untouched. */
|
|
225
|
-
export type AdjustOp =
|
|
226
|
-
| { op: "rename"; unit: string; to: string }
|
|
227
|
-
| { op: "add"; name: string; roots: string[]; kind?: string; note?: string }
|
|
228
|
-
| { op: "remove"; unit: string }
|
|
229
|
-
| { op: "merge"; units: string[]; into: string }
|
|
230
|
-
| { op: "move"; root: string; to: string }
|
|
231
|
-
| { op: "set_roots"; unit: string; roots: string[] }
|
|
232
|
-
| { op: "ignore"; roots: string[] }
|
|
233
|
-
| { op: "unignore"; roots: string[] }
|
|
234
|
-
| { op: "note"; unit: string; note: string };
|
|
235
|
-
|
|
236
|
-
export const ADJUST_OPS = [
|
|
237
|
-
"rename",
|
|
238
|
-
"add",
|
|
239
|
-
"remove",
|
|
240
|
-
"merge",
|
|
241
|
-
"move",
|
|
242
|
-
"set_roots",
|
|
243
|
-
"ignore",
|
|
244
|
-
"unignore",
|
|
245
|
-
"note",
|
|
246
|
-
] as const;
|
|
247
|
-
|
|
248
|
-
function clone(cut: UnitCut): UnitCut {
|
|
249
|
-
return {
|
|
250
|
-
units: cut.units.map((u) => ({ ...u, roots: [...u.roots] })),
|
|
251
|
-
ignored: [...cut.ignored],
|
|
252
|
-
};
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function strings(v: unknown): string[] | null {
|
|
256
|
-
if (typeof v === "string") return [v];
|
|
257
|
-
return Array.isArray(v) && v.every((x) => typeof x === "string") ? v : null;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/**
|
|
261
|
-
* Apply `ops` in order to a copy of `cut`. Returns the new cut, or every
|
|
262
|
-
* reason it could not (unknown unit, unknown op, a root that leaves the repo).
|
|
263
|
-
* The structural check of the RESULT is the caller's (`checkCut`) — an op
|
|
264
|
-
* sequence may pass through an invalid intermediate state (swap two names).
|
|
265
|
-
*/
|
|
266
|
-
export function applyOps(
|
|
267
|
-
cut: UnitCut,
|
|
268
|
-
ops: readonly unknown[],
|
|
269
|
-
): { ok: true; cut: UnitCut } | { ok: false; errors: string[] } {
|
|
270
|
-
const next = clone(cut);
|
|
271
|
-
const errors: string[] = [];
|
|
272
|
-
const unit = (name: unknown, i: number): Unit | null => {
|
|
273
|
-
const u = typeof name === "string" ? next.units.find((x) => x.name === name) : undefined;
|
|
274
|
-
if (!u)
|
|
275
|
-
errors.push(
|
|
276
|
-
`ops[${i}]: no unit named ${JSON.stringify(name)} — units: ${next.units.map((x) => x.name).join(", ")}`,
|
|
277
|
-
);
|
|
278
|
-
return u ?? null;
|
|
279
|
-
};
|
|
280
|
-
const rootsOf = (v: unknown, i: number): string[] | null => {
|
|
281
|
-
const raw = strings(v);
|
|
282
|
-
if (raw === null) {
|
|
283
|
-
errors.push(`ops[${i}]: roots must be a path or an array of paths`);
|
|
284
|
-
return null;
|
|
285
|
-
}
|
|
286
|
-
return normalizeRoots(raw, `ops[${i}]`, errors);
|
|
287
|
-
};
|
|
288
|
-
const release = (roots: readonly string[]): void => {
|
|
289
|
-
for (const u of next.units) u.roots = u.roots.filter((r) => !roots.includes(r));
|
|
290
|
-
next.ignored = next.ignored.filter((r) => !roots.includes(r));
|
|
291
|
-
};
|
|
292
|
-
for (const [i, raw] of ops.entries()) {
|
|
293
|
-
const o = (raw ?? {}) as Record<string, unknown>;
|
|
294
|
-
switch (o.op) {
|
|
295
|
-
case "rename": {
|
|
296
|
-
const u = unit(o.unit, i);
|
|
297
|
-
if (u && typeof o.to === "string") u.name = o.to.trim();
|
|
298
|
-
else if (u) errors.push(`ops[${i}]: rename needs \`to\``);
|
|
299
|
-
break;
|
|
300
|
-
}
|
|
301
|
-
case "add": {
|
|
302
|
-
const roots = rootsOf(o.roots, i);
|
|
303
|
-
if (typeof o.name !== "string") errors.push(`ops[${i}]: add needs \`name\``);
|
|
304
|
-
else if (roots) {
|
|
305
|
-
release(roots);
|
|
306
|
-
next.units.push({
|
|
307
|
-
name: o.name.trim(),
|
|
308
|
-
roots,
|
|
309
|
-
kind: unitKind(o.kind),
|
|
310
|
-
note: typeof o.note === "string" ? o.note.trim() : "added by the operator",
|
|
311
|
-
});
|
|
312
|
-
}
|
|
313
|
-
break;
|
|
314
|
-
}
|
|
315
|
-
case "remove": {
|
|
316
|
-
const u = unit(o.unit, i);
|
|
317
|
-
if (u) next.units = next.units.filter((x) => x !== u);
|
|
318
|
-
break;
|
|
319
|
-
}
|
|
320
|
-
case "merge": {
|
|
321
|
-
const names = strings(o.units);
|
|
322
|
-
if (!names || names.length < 2 || typeof o.into !== "string") {
|
|
323
|
-
errors.push(`ops[${i}]: merge needs \`units\` (two or more) and \`into\``);
|
|
324
|
-
break;
|
|
325
|
-
}
|
|
326
|
-
const members = names.map((n) => unit(n, i));
|
|
327
|
-
if (members.some((m) => m === null)) break;
|
|
328
|
-
const into = o.into.trim();
|
|
329
|
-
const keep = members.find((m) => m!.name === into) ?? members[0]!;
|
|
330
|
-
keep.roots = [...new Set(members.flatMap((m) => m!.roots))];
|
|
331
|
-
keep.name = into;
|
|
332
|
-
keep.note = keep.note
|
|
333
|
-
? `${keep.note}; merged from ${names.join(", ")}`
|
|
334
|
-
: `merged from ${names.join(", ")}`;
|
|
335
|
-
next.units = next.units.filter((x) => x === keep || !members.includes(x));
|
|
336
|
-
break;
|
|
337
|
-
}
|
|
338
|
-
case "move": {
|
|
339
|
-
const roots = rootsOf(o.root, i);
|
|
340
|
-
const u = unit(o.to, i);
|
|
341
|
-
if (roots && u) {
|
|
342
|
-
release(roots);
|
|
343
|
-
u.roots.push(...roots);
|
|
344
|
-
}
|
|
345
|
-
break;
|
|
346
|
-
}
|
|
347
|
-
case "set_roots": {
|
|
348
|
-
const u = unit(o.unit, i);
|
|
349
|
-
const roots = rootsOf(o.roots, i);
|
|
350
|
-
if (u && roots) {
|
|
351
|
-
release(roots);
|
|
352
|
-
u.roots = roots;
|
|
353
|
-
}
|
|
354
|
-
break;
|
|
355
|
-
}
|
|
356
|
-
case "ignore": {
|
|
357
|
-
const roots = rootsOf(o.roots, i);
|
|
358
|
-
if (roots) {
|
|
359
|
-
release(roots);
|
|
360
|
-
next.ignored.push(...roots);
|
|
361
|
-
}
|
|
362
|
-
break;
|
|
363
|
-
}
|
|
364
|
-
case "unignore": {
|
|
365
|
-
const roots = rootsOf(o.roots, i);
|
|
366
|
-
if (roots) {
|
|
367
|
-
const missing = roots.filter((r) => !next.ignored.includes(r));
|
|
368
|
-
if (missing.length > 0) errors.push(`ops[${i}]: not ignored: ${missing.join(", ")}`);
|
|
369
|
-
next.ignored = next.ignored.filter((r) => !roots.includes(r));
|
|
370
|
-
}
|
|
371
|
-
break;
|
|
372
|
-
}
|
|
373
|
-
case "note": {
|
|
374
|
-
const u = unit(o.unit, i);
|
|
375
|
-
if (u && typeof o.note === "string") u.note = o.note.trim();
|
|
376
|
-
else if (u) errors.push(`ops[${i}]: note needs \`note\``);
|
|
377
|
-
break;
|
|
378
|
-
}
|
|
379
|
-
default:
|
|
380
|
-
errors.push(
|
|
381
|
-
`ops[${i}]: unknown op ${JSON.stringify(o.op)} — one of ${ADJUST_OPS.join(", ")}`,
|
|
382
|
-
);
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
return errors.length > 0 ? { ok: false, errors } : { ok: true, cut: next };
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
// ---- cases: an oversized unit split to fit, its name kept --------------------
|
|
389
|
-
|
|
390
|
-
/** How much one case may read. The default is one PR round's sweep capacity
|
|
391
|
-
* (`sweep_max_groups` × a sweep group, 6 × 5 files / 500 lines): a case is one
|
|
392
|
-
* main pi fanning out leaf lenses, so it holds what a round holds. The scan's
|
|
393
|
-
* `[scan]` config is where a repo tunes it. */
|
|
394
|
-
export interface CaseBudget {
|
|
395
|
-
maxFiles: number;
|
|
396
|
-
maxLines: number;
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
export const DEFAULT_CASE_BUDGET: CaseBudget = {
|
|
400
|
-
maxFiles: SCAN_DEFAULTS.case_max_files,
|
|
401
|
-
maxLines: SCAN_DEFAULTS.case_max_lines,
|
|
402
|
-
};
|
|
403
|
-
|
|
404
|
-
export interface CasePlan {
|
|
405
|
-
/** The UNIT's name — never the part's. Reports group by this. */
|
|
406
|
-
unit: string;
|
|
407
|
-
/** `<unit>` when the unit fits, `<unit>#<i>` (1-based) when it was split. */
|
|
408
|
-
id: string;
|
|
409
|
-
part: number;
|
|
410
|
-
parts: number;
|
|
411
|
-
files: string[];
|
|
412
|
-
lines: number;
|
|
413
|
-
/** One file alone over the budget: it is its own case here — this split
|
|
414
|
-
* never cuts a file. The SCAN then reads such a file in line ranges, one
|
|
415
|
-
* case per range under the unit's name (scan.ts cutRanges). */
|
|
416
|
-
over_budget: boolean;
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
interface Piece {
|
|
420
|
-
files: string[];
|
|
421
|
-
lines: number;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
/**
|
|
425
|
-
* Split one unit's files into cases that fit `budget`: subdirectory first
|
|
426
|
-
* (a directory that fits stays whole), then file chunks in path order; small
|
|
427
|
-
* neighbouring pieces are packed back together so a split never produces a
|
|
428
|
-
* case per tiny directory. Every case keeps the unit's name. Deterministic:
|
|
429
|
-
* the same files, sizes and budget always give the same cases. No files, no
|
|
430
|
-
* case.
|
|
431
|
-
*/
|
|
432
|
-
export function splitUnit(
|
|
433
|
-
unit: string,
|
|
434
|
-
files: readonly string[],
|
|
435
|
-
linesOf: (path: string) => number,
|
|
436
|
-
budget: CaseBudget = DEFAULT_CASE_BUDGET,
|
|
437
|
-
): CasePlan[] {
|
|
438
|
-
if (files.length === 0) return [];
|
|
439
|
-
const sorted = [...files].sort();
|
|
440
|
-
const size = new Map(sorted.map((f) => [f, Math.max(0, linesOf(f))]));
|
|
441
|
-
const fits = (n: number, lines: number): boolean =>
|
|
442
|
-
n <= budget.maxFiles && lines <= budget.maxLines;
|
|
443
|
-
const total = (fs: readonly string[]): number => fs.reduce((n, f) => n + size.get(f)!, 0);
|
|
444
|
-
|
|
445
|
-
const pieces = (fs: string[], depth: number): Piece[] => {
|
|
446
|
-
const lines = total(fs);
|
|
447
|
-
if (fits(fs.length, lines)) return [{ files: fs, lines }];
|
|
448
|
-
// By the next path segment below the common prefix of this depth.
|
|
449
|
-
const groups = new Map<string, string[]>();
|
|
450
|
-
const loose: string[] = [];
|
|
451
|
-
for (const f of fs) {
|
|
452
|
-
const segs = f.split("/");
|
|
453
|
-
if (segs.length - 1 > depth) {
|
|
454
|
-
const key = segs.slice(0, depth + 1).join("/");
|
|
455
|
-
if (!groups.has(key)) groups.set(key, []);
|
|
456
|
-
groups.get(key)!.push(f);
|
|
457
|
-
} else {
|
|
458
|
-
loose.push(f);
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
// One directory holding everything: descend without spending a split.
|
|
462
|
-
if (loose.length === 0 && groups.size === 1) return pieces(fs, depth + 1);
|
|
463
|
-
const out: Piece[] = [];
|
|
464
|
-
for (const key of [...groups.keys()].sort()) out.push(...pieces(groups.get(key)!, depth + 1));
|
|
465
|
-
out.push(...chunk(loose));
|
|
466
|
-
return out;
|
|
467
|
-
};
|
|
468
|
-
|
|
469
|
-
const chunk = (fs: readonly string[]): Piece[] => {
|
|
470
|
-
const out: Piece[] = [];
|
|
471
|
-
let cur: Piece = { files: [], lines: 0 };
|
|
472
|
-
for (const f of fs) {
|
|
473
|
-
const n = size.get(f)!;
|
|
474
|
-
if (cur.files.length > 0 && !fits(cur.files.length + 1, cur.lines + n)) {
|
|
475
|
-
out.push(cur);
|
|
476
|
-
cur = { files: [], lines: 0 };
|
|
477
|
-
}
|
|
478
|
-
cur.files.push(f);
|
|
479
|
-
cur.lines += n;
|
|
480
|
-
}
|
|
481
|
-
if (cur.files.length > 0) out.push(cur);
|
|
482
|
-
return out;
|
|
483
|
-
};
|
|
484
|
-
|
|
485
|
-
// Pack neighbours: a directory that fit on its own may share a case with
|
|
486
|
-
// the next one when both still fit together.
|
|
487
|
-
const packed: Piece[] = [];
|
|
488
|
-
for (const p of pieces(sorted, commonDepth(sorted))) {
|
|
489
|
-
const last = packed[packed.length - 1];
|
|
490
|
-
if (last && fits(last.files.length + p.files.length, last.lines + p.lines)) {
|
|
491
|
-
last.files.push(...p.files);
|
|
492
|
-
last.lines += p.lines;
|
|
493
|
-
} else {
|
|
494
|
-
packed.push({ files: [...p.files], lines: p.lines });
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
const parts = packed.length;
|
|
498
|
-
return packed.map((p, i) => ({
|
|
499
|
-
unit,
|
|
500
|
-
id: parts === 1 ? unit : `${unit}#${i + 1}`,
|
|
501
|
-
part: i + 1,
|
|
502
|
-
parts,
|
|
503
|
-
files: p.files,
|
|
504
|
-
lines: p.lines,
|
|
505
|
-
over_budget: !fits(p.files.length, p.lines),
|
|
506
|
-
}));
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
/** Depth of the directory every file shares, so a unit rooted at
|
|
510
|
-
* `backend/payments` splits by what is BELOW it rather than re-grouping on
|
|
511
|
-
* `backend`. */
|
|
512
|
-
function commonDepth(files: readonly string[]): number {
|
|
513
|
-
if (files.length === 0) return 0;
|
|
514
|
-
const first = files[0]!.split("/").slice(0, -1);
|
|
515
|
-
let depth = first.length;
|
|
516
|
-
for (const f of files) {
|
|
517
|
-
const segs = f.split("/").slice(0, -1);
|
|
518
|
-
let d = 0;
|
|
519
|
-
while (d < depth && d < segs.length && segs[d] === first[d]) d++;
|
|
520
|
-
depth = d;
|
|
521
|
-
}
|
|
522
|
-
return depth;
|
|
523
|
-
}
|
|
1
|
+
// Moved to the plugin (packages/pi-plugin/src/scan/unitcut.ts), the scan engine's
|
|
2
|
+
// home: this name stays for the app's importers.
|
|
3
|
+
export * from "@quickchatai/pi-quickreview/src/scan/unitcut.js";
|
package/src/state.ts
CHANGED
|
@@ -7781,9 +7781,27 @@ export interface OpenPrsSnapshot {
|
|
|
7781
7781
|
prs: OpenPrSummary[];
|
|
7782
7782
|
}
|
|
7783
7783
|
|
|
7784
|
+
/** The authoritative reconcile poll's last pass (status.json `poll`): what
|
|
7785
|
+
* "auto-review is working" is judged on, beside the heartbeat (the home's
|
|
7786
|
+
* label, src/home/autoreview.ts). */
|
|
7787
|
+
export interface PollStatus {
|
|
7788
|
+
/** When the last pass ran, ISO; null before the boot's first pass has ended. */
|
|
7789
|
+
last_at: string | null;
|
|
7790
|
+
/** That pass ran with no step error; null before the first pass. */
|
|
7791
|
+
ok: boolean | null;
|
|
7792
|
+
/** The first error, while not ok. */
|
|
7793
|
+
error?: string;
|
|
7794
|
+
/** The daemon's `[daemon] poll_interval_s`: freshness is judged against
|
|
7795
|
+
* the daemon's own cadence, never a reader's guess. */
|
|
7796
|
+
interval_s: number;
|
|
7797
|
+
}
|
|
7798
|
+
|
|
7784
7799
|
export interface StatusSnapshot {
|
|
7785
7800
|
written_at: string;
|
|
7786
7801
|
dry_run: boolean;
|
|
7802
|
+
/** The reconcile poll's last pass. Absent in snapshots from an older
|
|
7803
|
+
* daemon — readers say "unknown", never "working". */
|
|
7804
|
+
poll?: PollStatus;
|
|
7787
7805
|
/** Where the daemon runs (src/runhost.ts). Absent: not reported (an older
|
|
7788
7806
|
* daemon, or one assembled without its host facts). */
|
|
7789
7807
|
run_host?: import("./runhost.js").RunHostStatus;
|
|
@@ -7874,6 +7892,11 @@ export interface StatusSnapshot {
|
|
|
7874
7892
|
max_concurrent_rounds?: number;
|
|
7875
7893
|
total_spend_usd: number;
|
|
7876
7894
|
today_spend_usd?: number;
|
|
7895
|
+
/** The month's spend so far: every round started in the snapshot's UTC
|
|
7896
|
+
* month (the same rule as `today_spend_usd`, expired rounds included).
|
|
7897
|
+
* Absent in snapshots from an older daemon: the home's spend line then
|
|
7898
|
+
* reads unknown, never a zero. */
|
|
7899
|
+
month_spend_usd?: number;
|
|
7877
7900
|
digest_headline?: string;
|
|
7878
7901
|
/** Present while pi sessions are failing API calls with token-expired
|
|
7879
7902
|
* errors (packages/pi-plugin/src/authwatch.ts, fresh < 30 min). The TUI raises an alert. */
|
|
@@ -8224,6 +8247,8 @@ export function buildStatusSnapshot(
|
|
|
8224
8247
|
...(maxConcurrentRounds !== undefined ? { max_concurrent_rounds: maxConcurrentRounds } : {}),
|
|
8225
8248
|
total_spend_usd: store.totalSpend(),
|
|
8226
8249
|
today_spend_usd: roundUsage(store.roundsOn(today)).spend_usd,
|
|
8250
|
+
// roundsOn matches by prefix: the month's `YYYY-MM` is every day of it
|
|
8251
|
+
month_spend_usd: roundUsage(store.roundsOn(today.slice(0, 7))).spend_usd,
|
|
8227
8252
|
...(digestHeadline ? { digest_headline: digestHeadline } : {}),
|
|
8228
8253
|
...(piAuth
|
|
8229
8254
|
? {
|
package/src/takeover.ts
CHANGED
|
@@ -82,6 +82,9 @@ export interface TakeoverPlan {
|
|
|
82
82
|
cwd: string;
|
|
83
83
|
/** The login's owner dir (`pihome/agent`): read-only, borrowed from. */
|
|
84
84
|
agent_dir: string;
|
|
85
|
+
/** The login's dir when it is not `agent_dir` (the single box's shared
|
|
86
|
+
* login): the login is borrowed from here, the rest from `agent_dir`. */
|
|
87
|
+
login_dir?: string;
|
|
85
88
|
/** The PR's JSONL, which the pi resumes and appends to. */
|
|
86
89
|
session_file: string;
|
|
87
90
|
model: string;
|
|
@@ -597,6 +600,7 @@ function rmQuiet(file: string): void {
|
|
|
597
600
|
export function takeoverPlanFor(o: {
|
|
598
601
|
root: string;
|
|
599
602
|
agentDir: string;
|
|
603
|
+
loginDir?: string;
|
|
600
604
|
sessionsRoot: string;
|
|
601
605
|
model: string;
|
|
602
606
|
pr: number;
|
|
@@ -612,6 +616,7 @@ export function takeoverPlanFor(o: {
|
|
|
612
616
|
root: o.root,
|
|
613
617
|
cwd: o.root,
|
|
614
618
|
agent_dir: o.agentDir,
|
|
619
|
+
...(o.loginDir !== undefined && o.loginDir !== o.agentDir ? { login_dir: o.loginDir } : {}),
|
|
615
620
|
session_file: file,
|
|
616
621
|
model: o.model,
|
|
617
622
|
...(o.systemPromptFile ? { system_prompt_file: o.systemPromptFile } : {}),
|
package/src/takeoverpi.ts
CHANGED
|
@@ -91,6 +91,7 @@ export function readTakeoverSpec(file: string): TakeoverSpec {
|
|
|
91
91
|
raw.session !== `takeover-pr-${raw.pr}` ||
|
|
92
92
|
typeof raw.session_file !== "string" ||
|
|
93
93
|
typeof raw.agent_dir !== "string" ||
|
|
94
|
+
(raw.login_dir !== undefined && typeof raw.login_dir !== "string") ||
|
|
94
95
|
typeof raw.borrow_dir !== "string" ||
|
|
95
96
|
typeof raw.model !== "string" ||
|
|
96
97
|
typeof raw.cwd !== "string" ||
|
|
@@ -119,11 +120,11 @@ export function readLogin(agentDir: string): string | null {
|
|
|
119
120
|
* the owner's dir.
|
|
120
121
|
*/
|
|
121
122
|
export function prepareBorrowedAgent(
|
|
122
|
-
spec: Pick<TakeoverSpec, "agent_dir" | "borrow_dir" | "idle_ms">,
|
|
123
|
+
spec: Pick<TakeoverSpec, "agent_dir" | "login_dir" | "borrow_dir" | "idle_ms">,
|
|
123
124
|
now?: () => number,
|
|
124
125
|
): { expiresAt: number | null } {
|
|
125
126
|
const { expiresAt } = copyPiAuth({
|
|
126
|
-
srcDir: spec.agent_dir,
|
|
127
|
+
srcDir: spec.login_dir ?? spec.agent_dir,
|
|
127
128
|
dstDir: spec.borrow_dir,
|
|
128
129
|
needMs: spec.idle_ms + PI_REFRESH_MARGIN_MS,
|
|
129
130
|
words: ENGINE_AUTH_WORDS,
|
package/src/tui/board.ts
CHANGED
|
@@ -522,7 +522,7 @@ function barSegments(view: BoardView, alerts: boolean): string[] {
|
|
|
522
522
|
}
|
|
523
523
|
// Short on the bar, where every segment competes for the width; the list
|
|
524
524
|
// carries the full sentence with the next step.
|
|
525
|
-
const setupSeg = view.setup ? `${WARN}▲ setup not finished
|
|
525
|
+
const setupSeg = view.setup ? `${WARN}▲ setup not finished${DRESET}` : "";
|
|
526
526
|
const s = view.snapshot;
|
|
527
527
|
const rounds = s ? `rounds ${s.running_rounds}/${s.max_concurrent_rounds ?? "?"}` : "";
|
|
528
528
|
const spend = s?.today_spend_usd !== undefined ? `today $${s.today_spend_usd.toFixed(2)}` : "";
|
package/src/tui/companion.ts
CHANGED
|
@@ -45,6 +45,7 @@ import { COMPANION_IMAGE, type CompanionConfig } from "../config.js";
|
|
|
45
45
|
import { writeAtomic } from "../channelfs.js";
|
|
46
46
|
import { appStamp } from "../appstamp.js";
|
|
47
47
|
import { TOKEN_FILE } from "./ghread.js";
|
|
48
|
+
import { MEMORY_PORT } from "./memoryurl.js";
|
|
48
49
|
|
|
49
50
|
export interface DockerResult {
|
|
50
51
|
ok: boolean;
|
|
@@ -274,7 +275,7 @@ export const MEMORY = {
|
|
|
274
275
|
subnet: "198.51.100.0/24",
|
|
275
276
|
gateway: "198.51.100.1",
|
|
276
277
|
/** Same port the server listens on in the VM container ([openviking] port). */
|
|
277
|
-
port:
|
|
278
|
+
port: MEMORY_PORT,
|
|
278
279
|
/** The OpenViking user whose memory the companion owns — becomes the
|
|
279
280
|
* `X-OpenViking-User` header on every request it makes. */
|
|
280
281
|
user: "companion",
|
package/src/tui/deploytab.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// `[5] deploy` — "what would a deploy ship?" plus the deploy button.
|
|
2
|
-
// Every deploy is a RELEASE: the tag
|
|
3
|
-
//
|
|
2
|
+
// Every deploy is a RELEASE: the tag and the two-arch build with its smoke
|
|
3
|
+
// already happened in CI, so the tab only chooses a version.
|
|
4
4
|
// It shows the version and revision the VM runs (`qcr-update status --json`,
|
|
5
5
|
// read over ssh) against the chosen version, the commits between the two
|
|
6
6
|
// revisions (from the local checkout), and deploy.sh's own
|