toolroll 0.9.41 → 0.9.42
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/CHANGELOG.md +18 -0
- package/dist/cli.js +3 -1
- package/dist/coding-types.d.ts +2 -24
- package/dist/coding-workspace.js +12 -11
- package/dist/contracts/cli.d.ts +736 -0
- package/dist/contracts/cli.js +223 -0
- package/dist/contracts/coding-activity.d.ts +64 -0
- package/dist/contracts/coding-activity.js +58 -0
- package/dist/contracts/console-api.d.ts +483 -0
- package/dist/contracts/console-api.js +273 -0
- package/dist/contracts/diff-stat.d.ts +58 -0
- package/dist/contracts/diff-stat.js +59 -0
- package/dist/contracts/integration-metadata.d.ts +99 -0
- package/dist/contracts/integration-metadata.js +101 -0
- package/dist/contracts/model-catalog.d.ts +21 -0
- package/dist/contracts/model-catalog.js +14 -0
- package/dist/contracts/native-census.d.ts +42 -0
- package/dist/contracts/native-census.js +36 -0
- package/dist/contracts/observation-request.d.ts +37 -0
- package/dist/contracts/observation-request.js +25 -0
- package/dist/contracts/provider.d.ts +30 -0
- package/dist/contracts/provider.js +15 -0
- package/dist/contracts/recipes.d.ts +166 -0
- package/dist/contracts/recipes.js +52 -0
- package/dist/contracts/retention.d.ts +42 -0
- package/dist/contracts/retention.js +24 -0
- package/dist/contracts/storage.d.ts +23 -0
- package/dist/contracts/storage.js +10 -0
- package/dist/contracts/store-json.d.ts +315 -0
- package/dist/contracts/store-json.js +196 -0
- package/dist/contracts/trigger-payloads.d.ts +369 -0
- package/dist/contracts/trigger-payloads.js +144 -0
- package/dist/contracts/update-journal.d.ts +254 -0
- package/dist/contracts/update-journal.js +155 -0
- package/dist/contracts/verification-view.d.ts +93 -0
- package/dist/contracts/verification-view.js +43 -0
- package/dist/control-setup.d.ts +1 -1
- package/dist/desktop-update.d.ts +7 -40
- package/dist/desktop-update.js +36 -8
- package/dist/evidence.d.ts +3 -19
- package/dist/flow-triggers.js +61 -60
- package/dist/mcp-connect.js +37 -22
- package/dist/model-catalog.js +8 -6
- package/dist/observations.d.ts +2 -6
- package/dist/observations.js +13 -9
- package/dist/operate.js +2 -1
- package/dist/process-recovery-native.d.ts +2 -13
- package/dist/process-recovery-native.js +8 -5
- package/dist/project-tools.d.ts +6 -24
- package/dist/project-tools.js +22 -18
- package/dist/provider.d.ts +3 -8
- package/dist/provider.js +15 -11
- package/dist/recipes.d.ts +3 -28
- package/dist/recipes.js +79 -52
- package/dist/retention.d.ts +4 -5
- package/dist/retention.js +5 -4
- package/dist/serve.js +142 -28
- package/dist/storage.d.ts +2 -1
- package/dist/storage.js +12 -9
- package/dist/store.d.ts +3 -15
- package/dist/store.js +80 -158
- package/dist/surface.d.ts +6 -54
- package/dist/surface.js +6 -1
- package/dist/toolroll-update.d.ts +8 -99
- package/dist/toolroll-update.js +28 -18
- package/dist/verification-evidence.js +27 -12
- package/package.json +1 -1
package/dist/surface.d.ts
CHANGED
|
@@ -1,57 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* shape, not authority — legacy verbs do not execute from it,
|
|
5
|
-
* and the runtime envelope remains the truth. Its honest limits ride the
|
|
6
|
-
* envelope itself as SURFACE_NOTES, because a source comment is invisible
|
|
7
|
-
* to consumers (arc-5 review, finding 1).
|
|
8
|
-
*
|
|
9
|
-
* What the tests DO hold to the code: every root here is a routed verb
|
|
10
|
-
* and every routed verb appears here; every declared flag lives in the
|
|
11
|
-
* parser's global vocabulary with the declared arity; the subcommand
|
|
12
|
-
* inventories for task/publish/config/approver/routine/contest match the
|
|
13
|
-
* exported action lists their dispatchers consult; keyed mutations
|
|
14
|
-
* declare --key; notableReasons come from DOCUMENTED_REASONS. What they
|
|
15
|
-
* CANNOT prove — that a given flag is read by a given handler — is why
|
|
16
|
-
* SURFACE_NOTES.flags says "intended".
|
|
17
|
-
*/
|
|
18
|
-
import { type SessionDescriptor } from './session-contract.js';
|
|
19
|
-
export type CommandFlag = {
|
|
20
|
-
readonly name: string;
|
|
21
|
-
readonly takesValue: boolean;
|
|
22
|
-
readonly meaning: string;
|
|
23
|
-
};
|
|
24
|
-
export type CommandRow = {
|
|
25
|
-
/** What you type after `toolroll` (subcommands included). */
|
|
26
|
-
readonly invocation: string;
|
|
27
|
-
/** The `command` field the envelope answers with, where it differs
|
|
28
|
-
* from the invocation (the no-verb report answers as "scan"). */
|
|
29
|
-
readonly envelopeCommand?: string;
|
|
30
|
-
readonly synopsis: string;
|
|
31
|
-
/** Who this act belongs to. "operator" rows are ceremonies or
|
|
32
|
-
* infrastructure: an agent must not invoke them even when credentials
|
|
33
|
-
* are within reach — the credential IS the person. The operator() helper
|
|
34
|
-
* omits flag detail on purpose; an explicit row may document flags when
|
|
35
|
-
* its command contract requires them. A schema is never permission. */
|
|
36
|
-
readonly audience: "agent" | "operator";
|
|
37
|
-
readonly agentMayInvoke: boolean;
|
|
38
|
-
/** Truthful retry semantics, not a boolean:
|
|
39
|
-
* keyed — takes --key; same key returns the first answer.
|
|
40
|
-
* identity-idempotent — repeating it converges (same lease, same
|
|
41
|
-
* path, same managed file); no key needed.
|
|
42
|
-
* unkeyed — a mutation without replay protection: do not blind-retry.
|
|
43
|
-
* none — a read. */
|
|
44
|
-
readonly mutation: "keyed" | "identity-idempotent" | "unkeyed" | "none";
|
|
45
|
-
readonly positionals?: readonly {
|
|
46
|
-
name: string;
|
|
47
|
-
required: boolean;
|
|
48
|
-
meaning: string;
|
|
49
|
-
}[];
|
|
50
|
-
readonly flags?: readonly CommandFlag[];
|
|
51
|
-
readonly notableReasons?: readonly string[];
|
|
52
|
-
/** Session schemas are executable input contracts, not a grant of authority. */
|
|
53
|
-
readonly inputSchema?: SessionDescriptor['inputSchema'];
|
|
54
|
-
};
|
|
1
|
+
import { type CommandFlag, type CommandRow } from './contracts/cli.js';
|
|
2
|
+
/** A guide row and its flags are derived from their schemas in src/contracts/cli.ts. */
|
|
3
|
+
export type { CommandFlag, CommandRow };
|
|
55
4
|
/** The guide's limits, stated machine-readably in every dump. */
|
|
56
5
|
export declare const SURFACE_NOTES: {
|
|
57
6
|
readonly authority: "documentation is not permission; session descriptors also drive parser and request validation; legacy entries remain advisory";
|
|
@@ -60,4 +9,7 @@ export declare const SURFACE_NOTES: {
|
|
|
60
9
|
readonly database: "local database commands wait up to 15 seconds for another writer; an exhausted wait returns database-busy and names the selected file";
|
|
61
10
|
};
|
|
62
11
|
export declare const SURFACE_SCHEMA_VERSION = 1;
|
|
12
|
+
/** Every declared command, in guide order, paired with the schema of its `--json` answer (src/contracts/cli.ts). */
|
|
13
|
+
export declare const COMMAND_ENTRIES: readonly import("./contracts/cli.js").CommandEntry[];
|
|
14
|
+
/** The guide `contract --commands` dumps: each declared command's row, projected from COMMAND_ENTRIES. */
|
|
63
15
|
export declare const COMMAND_GUIDE: readonly CommandRow[];
|
package/dist/surface.js
CHANGED
|
@@ -21,6 +21,7 @@ import { FLOWS_DESCRIPTORS } from "./flows-cli.js";
|
|
|
21
21
|
*/
|
|
22
22
|
import { SESSION_DESCRIPTORS } from './session-contract.js';
|
|
23
23
|
import { sessionCliFlags } from './session-cli.js';
|
|
24
|
+
import { commandEntries } from './contracts/cli.js';
|
|
24
25
|
/** The guide's limits, stated machine-readably in every dump. */
|
|
25
26
|
export const SURFACE_NOTES = {
|
|
26
27
|
authority: "documentation is not permission; session descriptors also drive parser and request validation; legacy entries remain advisory",
|
|
@@ -47,7 +48,7 @@ const operatorRead = (invocation, synopsis) => ({
|
|
|
47
48
|
agentMayInvoke: false,
|
|
48
49
|
mutation: "none",
|
|
49
50
|
});
|
|
50
|
-
|
|
51
|
+
const GUIDE_ROWS = [
|
|
51
52
|
// ---- reports (reads) ----
|
|
52
53
|
{
|
|
53
54
|
invocation: "",
|
|
@@ -412,3 +413,7 @@ export const COMMAND_GUIDE = [
|
|
|
412
413
|
operator("onboard", "add this repository, report signed-in agents, install or --remove the operator skill (--yes), offer the starter flows (--starter <ids>), and print the handoff"),
|
|
413
414
|
operator("daemon", "install the loop under the OS service manager"),
|
|
414
415
|
];
|
|
416
|
+
/** Every declared command, in guide order, paired with the schema of its `--json` answer (src/contracts/cli.ts). */
|
|
417
|
+
export const COMMAND_ENTRIES = commandEntries(GUIDE_ROWS);
|
|
418
|
+
/** The guide `contract --commands` dumps: each declared command's row, projected from COMMAND_ENTRIES. */
|
|
419
|
+
export const COMMAND_GUIDE = COMMAND_ENTRIES.map(entry => entry.guide);
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import type { DatabaseSync } from "node:sqlite";
|
|
2
|
-
import { type
|
|
3
|
-
import { type ReleasedCodingOwner } from "./coding-update.js";
|
|
2
|
+
import { type UpdateWaiting } from "./desktop-update.js";
|
|
4
3
|
import { type SupervisorRunner } from "./daemon.js";
|
|
4
|
+
import { RUNTIME_PHASES, type RuntimeUpdateJournalRecord } from "./contracts/update-journal.js";
|
|
5
5
|
export declare const PROVENANCE_REPOSITORY = "https://github.com/ap9000/toolroll";
|
|
6
6
|
export declare const PROVENANCE_WORKFLOW = ".github/workflows/publish.yml";
|
|
7
7
|
export declare const PROVENANCE_ISSUER = "https://token.actions.githubusercontent.com";
|
|
8
8
|
/** The steps a person sees, in order. */
|
|
9
9
|
export declare const UPDATE_STEPS: readonly ["verifying", "draining", "backing-up", "rehearsing", "switching", "restarting", "health"];
|
|
10
10
|
export type UpdateStep = typeof UPDATE_STEPS[number];
|
|
11
|
-
export type RuntimePhase =
|
|
11
|
+
export type RuntimePhase = typeof RUNTIME_PHASES[number];
|
|
12
12
|
export declare const STEP_WORDS: Record<UpdateStep, string>;
|
|
13
13
|
export type When = "now" | "when-idle" | "at";
|
|
14
14
|
export type RuntimeRef = {
|
|
@@ -19,100 +19,9 @@ export type RuntimeRef = {
|
|
|
19
19
|
export declare const KEEP_RUNTIMES = 2;
|
|
20
20
|
/** The one-off launchd job the console starts the updater as. */
|
|
21
21
|
export declare const UPDATE_JOB_LABEL = "com.toolroll.update";
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
kind: "update" | "rollback";
|
|
26
|
-
stateDir: string;
|
|
27
|
-
databaseFile: string;
|
|
28
|
-
stageDir: string;
|
|
29
|
-
from: RuntimeRef;
|
|
30
|
-
to: RuntimeRef;
|
|
31
|
-
when: When;
|
|
32
|
-
at: string | null;
|
|
33
|
-
actor: string;
|
|
34
|
-
phase: RuntimePhase;
|
|
35
|
-
detail: string;
|
|
36
|
-
error?: string;
|
|
37
|
-
steps: {
|
|
38
|
-
phase: RuntimePhase;
|
|
39
|
-
at: string;
|
|
40
|
-
}[];
|
|
41
|
-
startedAt: string;
|
|
42
|
-
updatedAt: string;
|
|
43
|
-
finishedAt?: string;
|
|
44
|
-
package?: {
|
|
45
|
-
sha512: string;
|
|
46
|
-
repository: string;
|
|
47
|
-
workflow: string;
|
|
48
|
-
};
|
|
49
|
-
notes?: string[];
|
|
50
|
-
/** This run's own verified copies of the live database and coding catalog: what a failure restores. */
|
|
51
|
-
backupPath?: string;
|
|
52
|
-
backupHash?: string;
|
|
53
|
-
codingBackupPath?: string;
|
|
54
|
-
codingBackupHash?: string;
|
|
55
|
-
/** A rollback installs the update's earlier backups (checked against their recorded hashes). */
|
|
56
|
-
restoreFrom?: {
|
|
57
|
-
path: string;
|
|
58
|
-
hash: string;
|
|
59
|
-
updateId: string;
|
|
60
|
-
codingPath?: string;
|
|
61
|
-
codingHash?: string;
|
|
62
|
-
};
|
|
63
|
-
rehearsal?: {
|
|
64
|
-
tables: number;
|
|
65
|
-
rows: number;
|
|
66
|
-
};
|
|
67
|
-
/** The background service, recorded before it is stopped: its definition and the processes that must be gone. */
|
|
68
|
-
service?: {
|
|
69
|
-
unit: string;
|
|
70
|
-
pids: number[];
|
|
71
|
-
};
|
|
72
|
-
/** Each repo's watch daemon that runs this version, recorded the same way: switched with the service, and stopped and
|
|
73
|
-
* restarted with it only when launchd had it loaded before the update (`loaded` absent: 0.8.1 stopped every one). */
|
|
74
|
-
watches?: {
|
|
75
|
-
unit: string;
|
|
76
|
-
pids: number[];
|
|
77
|
-
loaded?: boolean;
|
|
78
|
-
}[];
|
|
79
|
-
/** Recorded before each change so a resumed or failed run knows what to put back. */
|
|
80
|
-
switched?: {
|
|
81
|
-
links: {
|
|
82
|
-
path: string;
|
|
83
|
-
previous: string;
|
|
84
|
-
}[];
|
|
85
|
-
unit: {
|
|
86
|
-
path: string;
|
|
87
|
-
saved: string;
|
|
88
|
-
} | null;
|
|
89
|
-
watches?: {
|
|
90
|
-
path: string;
|
|
91
|
-
saved: string;
|
|
92
|
-
}[];
|
|
93
|
-
databaseRestored?: boolean;
|
|
94
|
-
};
|
|
95
|
-
/** What the live database held when a failed run restored its backup: nothing written is lost. The newest of `kept`. */
|
|
96
|
-
keptAside?: string;
|
|
97
|
-
/** The live database could not be read, so it was moved aside whole rather than copied. */
|
|
98
|
-
keptAsideUnreadable?: boolean;
|
|
99
|
-
/** Every copy kept aside, oldest first: a retried restore adds one and never drops the pointer to an earlier one. */
|
|
100
|
-
kept?: {
|
|
101
|
-
path: string;
|
|
102
|
-
unreadable: boolean;
|
|
103
|
-
}[];
|
|
104
|
-
/** The restore put the backup back: a retried restore never puts it back again (what was written since belongs to
|
|
105
|
-
* the restored version and would be lost), and it keeps a fresh copy aside before every attempt until then. */
|
|
106
|
-
restoredDatabase?: boolean;
|
|
107
|
-
/** A stale coding owner record this run released after proving its processes gone. */
|
|
108
|
-
codingOwnerReleased?: ReleasedCodingOwner;
|
|
109
|
-
seen?: boolean;
|
|
110
|
-
/** The finished run the update is waiting on (or stopped waiting on): what is in the way and the command that
|
|
111
|
-
* clears it. Absent while it waits on ordinary running work. */
|
|
112
|
-
waiting?: LingeringRun & {
|
|
113
|
-
since: string;
|
|
114
|
-
};
|
|
115
|
-
};
|
|
22
|
+
/** The update's journal (`toolroll-update.json`, `.last.json` and the stage's `update.json`):
|
|
23
|
+
* src/contracts/update-journal.ts. */
|
|
24
|
+
export type RuntimeUpdateJournal = RuntimeUpdateJournalRecord;
|
|
116
25
|
export type PackageRelease = {
|
|
117
26
|
version: string;
|
|
118
27
|
tarball: string;
|
|
@@ -164,11 +73,11 @@ export type UpdateSystem = {
|
|
|
164
73
|
};
|
|
165
74
|
export type UpdateOutcome = {
|
|
166
75
|
ok: boolean;
|
|
167
|
-
phase:
|
|
76
|
+
phase: RuntimeUpdateJournal["phase"];
|
|
168
77
|
message: string;
|
|
169
78
|
journal: RuntimeUpdateJournal | null;
|
|
170
79
|
};
|
|
171
|
-
export declare const runtimeUpdateTerminal: (phase:
|
|
80
|
+
export declare const runtimeUpdateTerminal: (phase: string) => boolean;
|
|
172
81
|
/** How long a launched or resuming updater is expected to take to start and hold the lock. */
|
|
173
82
|
export declare const UPDATER_START_MS: number;
|
|
174
83
|
export declare function readRuntimeUpdate(stateDir: string, file?: string): RuntimeUpdateJournal | null;
|
package/dist/toolroll-update.js
CHANGED
|
@@ -37,6 +37,7 @@ import { installLaunchdService, launchdPlist, stopLaunchdService, writeFileDurab
|
|
|
37
37
|
import { NAME } from "./names.js";
|
|
38
38
|
import { isNewer, REGISTRY } from "./releases.js";
|
|
39
39
|
import { markNeverIndex } from "./never-index.js";
|
|
40
|
+
import { readRuntimeUpdateJournal, RUNTIME_PHASES, RUNTIME_UPDATE_STEPS, stagedStartedAt, updaterStartingOf } from "./contracts/update-journal.js";
|
|
40
41
|
/** Loaded on first use (as backup.ts and store.ts do), so modules that only import this one (the console,
|
|
41
42
|
* and tests that load it in a browser-like environment) never need `node:sqlite` itself. */
|
|
42
43
|
function sqlite() {
|
|
@@ -48,7 +49,7 @@ export const PROVENANCE_ISSUER = "https://token.actions.githubusercontent.com";
|
|
|
48
49
|
/** How long a watch's `toolroll up` may take to exit once launchd stops it (controller-supervisor's shutdown). */
|
|
49
50
|
const UP_EXIT_MS = 45_000;
|
|
50
51
|
/** The steps a person sees, in order. */
|
|
51
|
-
export const UPDATE_STEPS =
|
|
52
|
+
export const UPDATE_STEPS = RUNTIME_UPDATE_STEPS;
|
|
52
53
|
export const STEP_WORDS = {
|
|
53
54
|
verifying: "Verify the package", draining: "Let running work finish", "backing-up": "Stop and back up",
|
|
54
55
|
rehearsing: "Rehearse the migration", switching: "Switch to the new version", restarting: "Restart", health: "Health check",
|
|
@@ -58,7 +59,7 @@ export const KEEP_RUNTIMES = 2;
|
|
|
58
59
|
/** The one-off launchd job the console starts the updater as. */
|
|
59
60
|
export const UPDATE_JOB_LABEL = "com.toolroll.update";
|
|
60
61
|
const TERMINAL = ["complete", "restored", "refused", "cancelled"];
|
|
61
|
-
export const runtimeUpdateTerminal = (phase) => TERMINAL.
|
|
62
|
+
export const runtimeUpdateTerminal = (phase) => TERMINAL.some(known => known === phase);
|
|
62
63
|
const journalFile = (stateDir) => join(stateDir, "toolroll-update.json");
|
|
63
64
|
/** The last completed update, kept apart from the journal: what `--rollback` returns from. */
|
|
64
65
|
const lastUpdateFile = (stateDir) => join(stateDir, "toolroll-update.last.json");
|
|
@@ -73,8 +74,10 @@ function markStarting(j, state, now) {
|
|
|
73
74
|
}
|
|
74
75
|
function updaterStarting(j, now) {
|
|
75
76
|
try {
|
|
76
|
-
const mark = JSON.parse(readFileSync(startingFile(j), "utf8"));
|
|
77
|
-
|
|
77
|
+
const mark = updaterStartingOf(JSON.parse(readFileSync(startingFile(j), "utf8")));
|
|
78
|
+
if (mark === null)
|
|
79
|
+
return false;
|
|
80
|
+
const age = now.getTime() - mark.at;
|
|
78
81
|
return mark.id === j.id && age >= 0 && age < UPDATER_START_MS;
|
|
79
82
|
}
|
|
80
83
|
catch {
|
|
@@ -93,14 +96,17 @@ export function readRuntimeUpdate(stateDir, file = journalFile(stateDir)) {
|
|
|
93
96
|
const stat = lstatSync(file);
|
|
94
97
|
if (!stat.isFile() || stat.isSymbolicLink())
|
|
95
98
|
throw Error("The saved update record is not a regular file. Nothing was changed.");
|
|
96
|
-
const
|
|
99
|
+
const read = readRuntimeUpdateJournal(JSON.parse(readFileSync(file, "utf8")));
|
|
100
|
+
if (!read.ok)
|
|
101
|
+
throw Error(`The saved update record is invalid. Preserve it and its backups; nothing was changed. (${read.issues.map(issue => issue.line).join("; ")})`);
|
|
102
|
+
const j = read.value;
|
|
97
103
|
const real = (path) => { try {
|
|
98
104
|
return realpathSync(path);
|
|
99
105
|
}
|
|
100
106
|
catch {
|
|
101
107
|
return resolve(path);
|
|
102
108
|
} };
|
|
103
|
-
if (
|
|
109
|
+
if (!/^[a-f0-9-]{36}$/.test(j.id) || typeof j.stateDir !== "string" || real(j.stateDir) !== real(stateDir) || !isAbsolute(j.stageDir ?? "") || real(dirname(j.stageDir)) !== real(join(stateDir, "staged-upgrades")))
|
|
104
110
|
throw Error("The saved update record is invalid. Preserve it and its backups; nothing was changed.");
|
|
105
111
|
return j;
|
|
106
112
|
}
|
|
@@ -121,13 +127,16 @@ function keepCompletedUpdate(stateDir) {
|
|
|
121
127
|
return;
|
|
122
128
|
durableJson(lastUpdateFile(stateDir), saved);
|
|
123
129
|
}
|
|
130
|
+
/** A null phase saves progress without changing the saved phase, including one a newer release wrote. */
|
|
124
131
|
function save(j, phase, detail, now) {
|
|
125
|
-
if (
|
|
126
|
-
j.
|
|
127
|
-
|
|
132
|
+
if (phase !== null) {
|
|
133
|
+
if (j.phase !== phase)
|
|
134
|
+
j.steps.push({ phase, at: now.toISOString() });
|
|
135
|
+
j.phase = phase;
|
|
136
|
+
}
|
|
128
137
|
j.detail = detail;
|
|
129
138
|
j.updatedAt = now.toISOString();
|
|
130
|
-
if (runtimeUpdateTerminal(phase))
|
|
139
|
+
if (runtimeUpdateTerminal(j.phase))
|
|
131
140
|
j.finishedAt = now.toISOString();
|
|
132
141
|
durableJson(join(j.stageDir, "update.json"), j);
|
|
133
142
|
durableJson(journalFile(j.stateDir), j);
|
|
@@ -518,7 +527,7 @@ async function stopServices(j, system, main, watches) {
|
|
|
518
527
|
}
|
|
519
528
|
if (watching.length > 0)
|
|
520
529
|
j.watches = watching;
|
|
521
|
-
save(j,
|
|
530
|
+
save(j, null, j.detail, system.now());
|
|
522
531
|
for (const unit of [main, ...loadedWatches(j, watches)])
|
|
523
532
|
if (unit)
|
|
524
533
|
await system.stopService(unit);
|
|
@@ -781,7 +790,7 @@ export function requestRuntimeUpdateCancel(stateDir, now = new Date(), seams = {
|
|
|
781
790
|
if (!j || runtimeUpdateTerminal(j.phase))
|
|
782
791
|
return "No update is in progress.";
|
|
783
792
|
const tooLate = (at) => `The update to ${at.to.version} is past the point it can be cancelled (${at.phase}); it will finish or restore on its own.`;
|
|
784
|
-
if (!CANCELLABLE.
|
|
793
|
+
if (!CANCELLABLE.some(phase => phase === j.phase))
|
|
785
794
|
return tooLate(j);
|
|
786
795
|
// No updater is running to see the request: cancel here, so the admission pause does not outlive it.
|
|
787
796
|
const lock = sqliteLock(join(j.stageDir, "worker.sqlite"));
|
|
@@ -797,7 +806,7 @@ export function requestRuntimeUpdateCancel(stateDir, now = new Date(), seams = {
|
|
|
797
806
|
return "No update is in progress.";
|
|
798
807
|
if (held.id !== j.id)
|
|
799
808
|
return "A different update was saved while cancelling. Nothing was cancelled.";
|
|
800
|
-
if (!CANCELLABLE.
|
|
809
|
+
if (!CANCELLABLE.some(phase => phase === held.phase))
|
|
801
810
|
return tooLate(held);
|
|
802
811
|
durableJson(cancelFile(held), { id: held.id, action: "cancel" });
|
|
803
812
|
try {
|
|
@@ -840,7 +849,7 @@ async function driveRuntimeUpdate(j, system) {
|
|
|
840
849
|
}
|
|
841
850
|
// The lock now says an updater is here; the starting mark has done its job.
|
|
842
851
|
rmSync(startingFile(j), { force: true });
|
|
843
|
-
const at = (phase) => UPDATE_STEPS.
|
|
852
|
+
const at = (phase) => UPDATE_STEPS.findIndex(step => step === phase);
|
|
844
853
|
const step = (phase, detail) => { save(j, phase, detail, system.now()); system.checkpoint?.(phase); };
|
|
845
854
|
const noun = j.kind === "update" ? "update" : "rollback";
|
|
846
855
|
const verb = j.kind === "update" ? `${j.from.version} → ${j.to.version}` : `${j.from.version} → ${j.to.version} (back)`;
|
|
@@ -1137,12 +1146,13 @@ async function drain(j, system) {
|
|
|
1137
1146
|
* the rollback-* records are not this updater's to remove. */
|
|
1138
1147
|
export function pruneRuntimes(stateDir, keep) {
|
|
1139
1148
|
const root = join(stateDir, "staged-upgrades");
|
|
1149
|
+
// A partial or older stage file still says when it started; one that is not JSON (or is null) falls back to the mtime.
|
|
1140
1150
|
const started = (dir) => { try {
|
|
1141
|
-
|
|
1151
|
+
const at = stagedStartedAt(JSON.parse(readFileSync(join(dir, "update.json"), "utf8")));
|
|
1152
|
+
if (at !== null)
|
|
1153
|
+
return at;
|
|
1142
1154
|
}
|
|
1143
|
-
catch {
|
|
1144
|
-
return statSync(dir).mtime.toISOString();
|
|
1145
|
-
} };
|
|
1155
|
+
catch { /* below */ } return statSync(dir).mtime.toISOString(); };
|
|
1146
1156
|
const releases = readdirSync(root, { withFileTypes: true }).filter(entry => entry.isDirectory() && entry.name.startsWith("release-"))
|
|
1147
1157
|
.map(entry => join(root, entry.name)).sort((a, b) => started(b).localeCompare(started(a)));
|
|
1148
1158
|
const kept = new Set(releases.filter(dir => keep.some(dist => dist !== "" && dist.startsWith(dir + sep))));
|
|
@@ -8,6 +8,8 @@ import { adjudicate } from "./proof.js";
|
|
|
8
8
|
import { parseReviewContext, reviewContextCustodyProblem } from "./review-context.js";
|
|
9
9
|
import { readVerifiedArtifact, storeEvidence, scanForSecrets, redactSecretLines } from "./evidence.js";
|
|
10
10
|
import { readVerificationReceipt } from "./contracts/verification-receipt.js";
|
|
11
|
+
import { LEGACY_GATE_SOURCE, readVerificationView } from "./contracts/verification-view.js";
|
|
12
|
+
import { readCandidateEndpoints, savedInventoryPaths } from "./contracts/diff-stat.js";
|
|
11
13
|
import { liveQuickCommand, quickVerifyKey, runCheckLevel } from "./check-levels.js";
|
|
12
14
|
export const VERIFICATION_RECEIPT_CAPTURE = "machine verification receipt v1";
|
|
13
15
|
export const REVIEW_GATE_NAME = "REVIEW-VERIFICATION.json";
|
|
@@ -97,7 +99,10 @@ export function verificationEvidence(store, root, runId) {
|
|
|
97
99
|
const original = verificationEvidence(store, root, reused.run);
|
|
98
100
|
if (!original.ok || !original.bytes || original.digest !== reused.digest)
|
|
99
101
|
return fail("The original passing gate no longer verifies.");
|
|
100
|
-
const
|
|
102
|
+
const previousRead = readVerificationView(original.bytes);
|
|
103
|
+
if (!previousRead.ok)
|
|
104
|
+
return fail("The original passing gate no longer verifies.");
|
|
105
|
+
const previous = previousRead.value;
|
|
101
106
|
const originalLog = store.getArtifact(previous.log.artifactId);
|
|
102
107
|
if (previous.head !== source.headRevision || !("exitCode" in previous.result) || previous.result.exitCode !== 0 || JSON.stringify(previous.command) !== JSON.stringify(command) ||
|
|
103
108
|
JSON.stringify(previous.result) !== JSON.stringify(receipt.result) || originalLog?.sha256 !== log.sha256 ||
|
|
@@ -123,13 +128,17 @@ export function verificationEvidence(store, root, runId) {
|
|
|
123
128
|
const readStat = readVerifiedArtifact(root, stat[0]);
|
|
124
129
|
if (!readStat.ok)
|
|
125
130
|
return fail("Legacy candidate inventory no longer verifies.");
|
|
126
|
-
let
|
|
131
|
+
let inventory;
|
|
127
132
|
try {
|
|
128
|
-
|
|
133
|
+
inventory = JSON.parse(readStat.content.toString("utf8"));
|
|
129
134
|
}
|
|
130
135
|
catch {
|
|
131
136
|
return fail("Legacy candidate inventory is unreadable.");
|
|
132
137
|
}
|
|
138
|
+
const endpointsRead = readCandidateEndpoints(inventory);
|
|
139
|
+
if (!endpointsRead.ok)
|
|
140
|
+
return fail("Legacy verification candidate endpoints do not match.");
|
|
141
|
+
const endpoints = endpointsRead.value;
|
|
133
142
|
if (!source.headRevision || endpoints.head !== source.headRevision || endpoints.base !== source.baseRevision || endpoints.filesTruncated !== false)
|
|
134
143
|
return fail("Legacy verification candidate endpoints do not match.");
|
|
135
144
|
const text = read.content.toString("utf8");
|
|
@@ -139,7 +148,8 @@ export function verificationEvidence(store, root, runId) {
|
|
|
139
148
|
return fail("The original log lacks an unambiguous machine gate receipt; historical output was not reconstructed.");
|
|
140
149
|
if (verified && exitCode !== 0)
|
|
141
150
|
return fail("The passing machine verdict disagrees with the original failed check.");
|
|
142
|
-
|
|
151
|
+
const view = { version: 1, source: LEGACY_GATE_SOURCE, run: runId, head: source.headRevision, base: source.baseRevision, scopeDigest: source.scopeDigest, command, result: { configured: true, ran: true, exitCode }, log: binding(log), candidate: binding(stat[0]) };
|
|
152
|
+
receipt = view;
|
|
143
153
|
}
|
|
144
154
|
// The view preserves shortened/redacted status and does not claim the omitted
|
|
145
155
|
// output exists. Its fingerprint is re-proved before every turn and ingestion.
|
|
@@ -173,8 +183,10 @@ export function failedVerificationEvidence(store, root, runId) {
|
|
|
173
183
|
return { kind: "unavailable", problem: verified.problem };
|
|
174
184
|
if (verified.bytes === null)
|
|
175
185
|
return { kind: "none" };
|
|
176
|
-
const
|
|
177
|
-
|
|
186
|
+
const view = readVerificationView(verified.bytes);
|
|
187
|
+
if (!view.ok)
|
|
188
|
+
return { kind: "unavailable", problem: "The verification receipt cannot be read." };
|
|
189
|
+
const receipt = view.value, result = receipt.result;
|
|
178
190
|
if (!result.configured || (result.ran ? result.exitCode === 0 :
|
|
179
191
|
result.failure !== "timed-out" && result.failure !== "retry-timed-out"))
|
|
180
192
|
return { kind: "none" };
|
|
@@ -223,15 +235,15 @@ export function assessmentFromSavedEvidence(store, root, runId) {
|
|
|
223
235
|
if (!gate.ok || gate.bytes === null)
|
|
224
236
|
return null;
|
|
225
237
|
try {
|
|
226
|
-
const
|
|
238
|
+
const view = readVerificationView(gate.bytes);
|
|
227
239
|
const base = source.branch ? store.firstBuilderBase(source.taskRef, source.branch) ?? source.baseRevision : source.baseRevision;
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
new Set(inventory.files.map((one) => one.path)).size !== inventory.files.length)
|
|
240
|
+
const paths = savedInventoryPaths(stat, source.headRevision, base);
|
|
241
|
+
if (!view.ok || paths === null)
|
|
231
242
|
return null;
|
|
243
|
+
const receipt = view.value;
|
|
232
244
|
return adjudicate({ directAssessment: true, proofArtifactPresent: false, proofParse: null,
|
|
233
245
|
handoffPresent: true, terminalDiffPresent: true, terminalDiffCaptureStatus: "ok",
|
|
234
|
-
diffStat: { captured: true, truncated: false, paths
|
|
246
|
+
diffStat: { captured: true, truncated: false, paths },
|
|
235
247
|
verifyCommand: receipt.result, verificationCommand: receipt.command.command, screenshots: [],
|
|
236
248
|
approvedCriteria: scope.acceptance, reviewContext: parsedContext.inventory.coverage });
|
|
237
249
|
}
|
|
@@ -255,7 +267,10 @@ export function reuseObservationVerification(store, root, runId, now) {
|
|
|
255
267
|
const original = verificationEvidence(store, root, brief.sourceRun);
|
|
256
268
|
if (!original.ok || !original.bytes || original.digest !== brief.gateDigest)
|
|
257
269
|
throw Error("The original passing gate is unavailable or changed.");
|
|
258
|
-
const
|
|
270
|
+
const view = readVerificationView(original.bytes);
|
|
271
|
+
if (!view.ok)
|
|
272
|
+
throw Error("The original passing gate is unavailable or changed.");
|
|
273
|
+
const receipt = view.value, log = store.getArtifact(receipt.log.artifactId);
|
|
259
274
|
if (!log || receipt.head !== brief.head || !("ran" in receipt.result) || receipt.result.ran !== true || receipt.result.exitCode !== 0)
|
|
260
275
|
throw Error("The source gate did not pass this candidate.");
|
|
261
276
|
const read = readVerifiedArtifact(root, log);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toolroll",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.42",
|
|
4
4
|
"description": "A control plane for unattended coding agents — queue tasks, walk away, come back to pull requests. Agents build in leased worktrees, never touch your default branch, and interrupt you only for decisions that need a human.",
|
|
5
5
|
"directories": {
|
|
6
6
|
"doc": "docs"
|