toolroll 0.9.8 → 0.9.9
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 +22 -0
- package/dist/assignment-brief.d.ts +2 -0
- package/dist/assignment-brief.js +2 -1
- package/dist/assignment-presentation.js +3 -1
- package/dist/assignment-status.d.ts +1 -1
- package/dist/assignment-ui.js +5 -0
- package/dist/assignment.d.ts +9 -0
- package/dist/assignment.js +39 -7
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +18 -18
- package/dist/browser-crew.js +4 -0
- package/dist/browser-workspace.d.ts +40 -0
- package/dist/builder.d.ts +2 -0
- package/dist/builder.js +114 -8
- package/dist/chat-controls.d.ts +1 -1
- package/dist/chat-controls.js +1 -1
- package/dist/check-progress.js +4 -3
- package/dist/checkout-cleanup.d.ts +40 -2
- package/dist/checkout-cleanup.js +129 -9
- package/dist/desktop-update.d.ts +52 -0
- package/dist/desktop-update.js +147 -5
- package/dist/evidence.d.ts +6 -0
- package/dist/evidence.js +8 -0
- package/dist/first-run.d.ts +1 -1
- package/dist/first-run.js +7 -5
- package/dist/lead-context.d.ts +1 -1
- package/dist/lead-context.js +4 -2
- package/dist/lead-status.d.ts +10 -1
- package/dist/lead-status.js +21 -24
- package/dist/needs-you.d.ts +55 -0
- package/dist/needs-you.js +83 -0
- package/dist/operate.d.ts +2 -0
- package/dist/operate.js +112 -14
- package/dist/plane-review.js +4 -0
- package/dist/process-recovery-eligibility.d.ts +1 -0
- package/dist/project-learning.d.ts +3 -3
- package/dist/project-learning.js +28 -17
- package/dist/proof.d.ts +3 -1
- package/dist/proof.js +4 -2
- package/dist/result-completion.d.ts +4 -0
- package/dist/result-completion.js +11 -0
- package/dist/review-context.d.ts +3 -0
- package/dist/review-context.js +45 -5
- package/dist/run-closeout.d.ts +89 -0
- package/dist/run-closeout.js +302 -0
- package/dist/serve.d.ts +15 -1
- package/dist/serve.js +129 -14
- package/dist/setup-guide.d.ts +2 -2
- package/dist/shared-deps.d.ts +94 -0
- package/dist/shared-deps.js +365 -0
- package/dist/storage.js +2 -0
- package/dist/store.d.ts +36 -0
- package/dist/store.js +125 -29
- package/dist/task-status.d.ts +18 -4
- package/dist/task-status.js +35 -18
- package/dist/telegram-progress.js +2 -1
- package/dist/telegram-status.js +3 -3
- package/dist/test-temp.d.ts +19 -0
- package/dist/test-temp.js +98 -0
- package/dist/toolroll-update-ui.js +7 -2
- package/dist/toolroll-update.d.ts +21 -1
- package/dist/toolroll-update.js +135 -5
- package/dist/work-index.d.ts +11 -0
- package/dist/work-index.js +103 -16
- package/dist/work-summary.d.ts +3 -1
- package/dist/workspace-ui.js +1 -1
- package/dist/worktree.d.ts +16 -0
- package/dist/worktree.js +47 -0
- package/package.json +1 -1
package/dist/desktop-update.d.ts
CHANGED
|
@@ -27,6 +27,10 @@ export type UpdateJournal = {
|
|
|
27
27
|
serviceInterrupted?: boolean;
|
|
28
28
|
replacementOccurred?: boolean;
|
|
29
29
|
retryableRecovery?: boolean;
|
|
30
|
+
/** The finished run the update is waiting on (or stopped waiting on). Absent while it waits on ordinary work. */
|
|
31
|
+
waiting?: LingeringRun & {
|
|
32
|
+
since: string;
|
|
33
|
+
};
|
|
30
34
|
};
|
|
31
35
|
type ServiceState = {
|
|
32
36
|
state: string;
|
|
@@ -39,6 +43,8 @@ export type UpdateHooks = {
|
|
|
39
43
|
healthy?: (bundle: DesktopBundle, journal: UpdateJournal, since: string) => Promise<boolean>;
|
|
40
44
|
swap?: (journal: UpdateJournal) => Promise<void>;
|
|
41
45
|
sleep?: (ms: number) => Promise<void>;
|
|
46
|
+
/** The clock the drain's two-minute limit reads (tests). */
|
|
47
|
+
now?: () => Date;
|
|
42
48
|
healthTimeoutMs?: number;
|
|
43
49
|
/** Fault injection for state-machine tests, never selectable by a CLI flag. */
|
|
44
50
|
checkpoint?: (phase: Phase) => void;
|
|
@@ -48,7 +54,53 @@ export declare const updateTerminal: (phase: string) => boolean;
|
|
|
48
54
|
/** Durable receipts are private and independent of the app being replaced. */
|
|
49
55
|
export declare function durableJson(file: string, value: unknown): void;
|
|
50
56
|
export declare function readUpdateJournal(stateDir: string, retainedReceipt?: string): UpdateJournal | null;
|
|
57
|
+
/** A finished run whose end Toolroll cannot show yet: in plain words, with the one command that clears it when a
|
|
58
|
+
* person can. */
|
|
59
|
+
export type LingeringRun = {
|
|
60
|
+
run: number;
|
|
61
|
+
on: string;
|
|
62
|
+
action: string | null;
|
|
63
|
+
};
|
|
64
|
+
/** How long an update waits on a finished run it cannot show has ended before it stops waiting and says why. */
|
|
65
|
+
export declare const LINGERING_LIMIT_MS = 120000;
|
|
66
|
+
export declare function lingeringRun(db: DatabaseSync): LingeringRun | null;
|
|
51
67
|
export declare function lingeringWork(db: DatabaseSync): string | null;
|
|
68
|
+
export declare const lingeringWords: (l: LingeringRun) => string;
|
|
69
|
+
/** What an update says when it stops waiting on `l`: what is in the way, the command, and that new work resumed. */
|
|
70
|
+
export declare const stoppedWaitingWords: (l: LingeringRun) => string;
|
|
71
|
+
/** The next step once an update has stopped on `l`. */
|
|
72
|
+
export declare const clearWords: (l: LingeringRun) => string;
|
|
73
|
+
/** An update that is waiting or stopped, what on, and the one action that clears it: for `toolroll status` and the
|
|
74
|
+
* console. `app`: the desktop app's update rather than `toolroll update`. */
|
|
75
|
+
export type UpdateWaiting = {
|
|
76
|
+
app: boolean;
|
|
77
|
+
version: string;
|
|
78
|
+
stopped: boolean;
|
|
79
|
+
run: number | null;
|
|
80
|
+
on: string;
|
|
81
|
+
action: string | null;
|
|
82
|
+
words: string;
|
|
83
|
+
short: string;
|
|
84
|
+
};
|
|
85
|
+
export declare function updateWaitingWords(o: {
|
|
86
|
+
app: boolean;
|
|
87
|
+
version: string;
|
|
88
|
+
stopped: boolean;
|
|
89
|
+
lingering: LingeringRun | null;
|
|
90
|
+
}): UpdateWaiting;
|
|
91
|
+
/** An app updater that ended while letting work finish, with no automatic recovery coming to resume it, leaves new
|
|
92
|
+
* work paused with nobody left to lift the pause. Nothing was stopped or swapped yet, so the pause is lifted here and
|
|
93
|
+
* the update says it stopped. Returns what it said, or null. */
|
|
94
|
+
export declare function releaseStalledDesktopUpdate(stateDir: string): string | null;
|
|
95
|
+
/** The desktop app's update, when it is waiting on work in `databaseFile`'s installation, or stopped on a run still in
|
|
96
|
+
* the way. An app updater that ended mid-drain is released first, as `toolroll update`'s is. */
|
|
97
|
+
export declare function desktopUpdateWaitingOf(databaseFile: string, inTheWay: (run: number) => boolean, stateDirs?: readonly string[]): UpdateWaiting | null;
|
|
98
|
+
/** The same settling reconcile the background worker runs: a finished run's leftover record of a process that never
|
|
99
|
+
* started settles once the run's process groups are proven gone. A record nothing can prove either way stays. */
|
|
100
|
+
export declare function settleLeftoverRecords(db: DatabaseSync, now: Date): number;
|
|
101
|
+
/** Store.stopQuiescenceProblem's reasons, said for a person: no internal terms, and `toolroll run settle` only where it
|
|
102
|
+
* can clear the record (it refuses while anything of the run is alive). */
|
|
103
|
+
export declare function plainLingering(fallbackRun: number, problem: string): LingeringRun;
|
|
52
104
|
export declare function previewDesktopUpdate(stateDir: string, installed: string, candidate: string, label: string, hooks?: UpdateHooks): Promise<{
|
|
53
105
|
old: DesktopBundle;
|
|
54
106
|
next: DesktopBundle;
|
package/dist/desktop-update.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { closeSync, chmodSync, copyFileSync, cpSync, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
3
4
|
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
4
5
|
import { createRequire } from "node:module";
|
|
5
6
|
import { bundleHash, readDesktopBundle, verifyDesktopUpdateBundles } from "./desktop-bundle.js";
|
|
@@ -139,15 +140,135 @@ async function otherControllers(app, stateDir) {
|
|
|
139
140
|
const command = `${join(resources(app), "runtime", "node")} ${join(resources(app), "dist", "desktop-host.js")} serve --state `;
|
|
140
141
|
return result.stdout.split("\n").some(line => line.trim().startsWith(command) && line.trim() !== command + resolve(stateDir));
|
|
141
142
|
}
|
|
142
|
-
|
|
143
|
+
/** How long an update waits on a finished run it cannot show has ended before it stops waiting and says why. */
|
|
144
|
+
export const LINGERING_LIMIT_MS = 120_000;
|
|
145
|
+
export function lingeringRun(db) {
|
|
143
146
|
const store = new Store(db);
|
|
144
147
|
for (const row of db.prepare("SELECT DISTINCT run FROM run_process WHERE exited_at IS NULL").all()) {
|
|
145
148
|
const problem = store.stopQuiescenceProblem(Number(row.run));
|
|
146
149
|
if (problem)
|
|
147
|
-
return problem;
|
|
150
|
+
return plainLingering(Number(row.run), problem);
|
|
148
151
|
}
|
|
149
152
|
return null;
|
|
150
153
|
}
|
|
154
|
+
export function lingeringWork(db) {
|
|
155
|
+
const lingering = lingeringRun(db);
|
|
156
|
+
return lingering && lingeringWords(lingering);
|
|
157
|
+
}
|
|
158
|
+
export const lingeringWords = (l) => l.action ? `${l.on}. If nothing of it is running, run: ${l.action}` : `${l.on}.`;
|
|
159
|
+
/** What an update says when it stops waiting on `l`: what is in the way, the command, and that new work resumed. */
|
|
160
|
+
export const stoppedWaitingWords = (l) => `Stopped waiting after 2 minutes. ${l.on}. ${clearWords(l)} New work resumed; nothing was changed.`;
|
|
161
|
+
/** The next step once an update has stopped on `l`. */
|
|
162
|
+
export const clearWords = (l) => l.action ? `If nothing of it is running, run ${l.action}, then update again.` : "Update again once it has stopped.";
|
|
163
|
+
export function updateWaitingWords(o) {
|
|
164
|
+
const { app, version, stopped, lingering: l } = o;
|
|
165
|
+
const update = `${app ? "App update" : "Update"} to ${version}`;
|
|
166
|
+
if (!l) {
|
|
167
|
+
const on = "running work to finish";
|
|
168
|
+
const words = `${update} is waiting for ${on}. New work is paused.`;
|
|
169
|
+
return { app, version, stopped: false, run: null, on, action: null, words, short: words };
|
|
170
|
+
}
|
|
171
|
+
const next = l.action ? ` If nothing of it is running, run ${l.action}${stopped ? ", then update again" : ""}.` : stopped ? " Update again once it has stopped." : "";
|
|
172
|
+
return { app, version, stopped, run: l.run, on: l.on, action: l.action,
|
|
173
|
+
words: `${update} ${stopped ? "stopped waiting" : "is waiting"}: ${l.on}.${next}`,
|
|
174
|
+
short: `${update} ${stopped ? `stopped: run #${l.run} is in the way` : `is waiting on run #${l.run}`}.${next}` };
|
|
175
|
+
}
|
|
176
|
+
/** An app updater that ended while letting work finish, with no automatic recovery coming to resume it, leaves new
|
|
177
|
+
* work paused with nobody left to lift the pause. Nothing was stopped or swapped yet, so the pause is lifted here and
|
|
178
|
+
* the update says it stopped. Returns what it said, or null. */
|
|
179
|
+
export function releaseStalledDesktopUpdate(stateDir) {
|
|
180
|
+
let j;
|
|
181
|
+
try {
|
|
182
|
+
j = readUpdateJournal(stateDir);
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
if (!j || j.phase !== "draining" || j.serviceInterrupted || j.replacementOccurred)
|
|
188
|
+
return null;
|
|
189
|
+
const status = desktopUpdateStatus(stateDir);
|
|
190
|
+
if (status.running || status.automaticRecovery)
|
|
191
|
+
return null;
|
|
192
|
+
const lock = workerLock(j);
|
|
193
|
+
if (!lock)
|
|
194
|
+
return null;
|
|
195
|
+
try {
|
|
196
|
+
const held = readUpdateJournal(stateDir);
|
|
197
|
+
if (!held || held.id !== j.id || held.phase !== "draining" || held.serviceInterrupted || held.replacementOccurred || bundleHash(held.old.path) !== held.old.hash)
|
|
198
|
+
return null;
|
|
199
|
+
const db = connect(held.databaseFile);
|
|
200
|
+
try {
|
|
201
|
+
removeUpdateGate(db, held.id);
|
|
202
|
+
}
|
|
203
|
+
finally {
|
|
204
|
+
db.close();
|
|
205
|
+
}
|
|
206
|
+
const words = `The app update to ${held.next.version} stopped: its updater ended before running work finished. Nothing was changed; new work resumed. Update again from the app.`;
|
|
207
|
+
held.error = "The updater ended while letting work finish.";
|
|
208
|
+
save(held, "cancelled", words);
|
|
209
|
+
return words;
|
|
210
|
+
}
|
|
211
|
+
finally {
|
|
212
|
+
lock.close();
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/** The desktop app's update, when it is waiting on work in `databaseFile`'s installation, or stopped on a run still in
|
|
216
|
+
* the way. An app updater that ended mid-drain is released first, as `toolroll update`'s is. */
|
|
217
|
+
export function desktopUpdateWaitingOf(databaseFile, inTheWay, stateDirs = [dirname(databaseFile), join(homedir(), "Library", "Application Support", "Standing Orders")]) {
|
|
218
|
+
for (const stateDir of new Set(stateDirs.map(dir => resolve(dir)))) {
|
|
219
|
+
let j;
|
|
220
|
+
try {
|
|
221
|
+
j = readUpdateJournal(stateDir);
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (!j || resolve(j.databaseFile) !== resolve(databaseFile))
|
|
227
|
+
continue;
|
|
228
|
+
try {
|
|
229
|
+
if (releaseStalledDesktopUpdate(stateDir))
|
|
230
|
+
j = readUpdateJournal(stateDir);
|
|
231
|
+
}
|
|
232
|
+
catch { /* shown as last saved */ }
|
|
233
|
+
const version = j.next.version;
|
|
234
|
+
if (j.phase === "draining")
|
|
235
|
+
return updateWaitingWords({ app: true, version, stopped: false, lingering: j.waiting ?? null });
|
|
236
|
+
if (j.phase === "cancelled" && j.waiting && inTheWay(j.waiting.run))
|
|
237
|
+
return updateWaitingWords({ app: true, version, stopped: true, lingering: j.waiting });
|
|
238
|
+
}
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
/** The same settling reconcile the background worker runs: a finished run's leftover record of a process that never
|
|
242
|
+
* started settles once the run's process groups are proven gone. A record nothing can prove either way stays. */
|
|
243
|
+
export function settleLeftoverRecords(db, now) {
|
|
244
|
+
return new Store(db).settleUnspawnedWitnesses(now);
|
|
245
|
+
}
|
|
246
|
+
/** Store.stopQuiescenceProblem's reasons, said for a person: no internal terms, and `toolroll run settle` only where it
|
|
247
|
+
* can clear the record (it refuses while anything of the run is alive). */
|
|
248
|
+
export function plainLingering(fallbackRun, problem) {
|
|
249
|
+
const run = Number(/run #(\d+)/.exec(problem)?.[1] ?? fallbackRun);
|
|
250
|
+
const settle = `toolroll run settle ${run} --why "it is not running"`;
|
|
251
|
+
const pid = /process (\d+)/.exec(problem)?.[1];
|
|
252
|
+
if (/incomplete spawn witness|before its process witness was recorded/.test(problem))
|
|
253
|
+
return { run, on: `Run #${run} finished, but Toolroll has no process ID for one of its processes, so it can't confirm that process ended`, action: settle };
|
|
254
|
+
if (/belongs to another host|another host/.test(problem))
|
|
255
|
+
return { run, on: `Run #${run} finished on another computer, and Toolroll can only confirm its processes ended there`, action: `${settle} (on that computer)` };
|
|
256
|
+
if (/incomplete native containment custody|cannot be proven empty/.test(problem))
|
|
257
|
+
return { run, on: `Run #${run} finished, but Toolroll can't confirm its sandbox is empty`, action: settle };
|
|
258
|
+
if (/still has members/.test(problem))
|
|
259
|
+
return { run, on: `Run #${run} finished, but processes are still running in its sandbox`, action: null };
|
|
260
|
+
if (/workspace is still held/.test(problem))
|
|
261
|
+
return { run, on: `Run #${run} finished, but process ${pid ?? "?"} is still using its folder`, action: null };
|
|
262
|
+
if (/workspace occupancy/.test(problem))
|
|
263
|
+
return { run, on: `Run #${run} finished, but Toolroll can't check whether its folder is still in use`, action: null };
|
|
264
|
+
if (/may still be running|owned subprocess/.test(problem))
|
|
265
|
+
return { run, on: `Run #${run} finished, but ${pid ? `its process ${pid}` : "one of its processes"} is still running`, action: null };
|
|
266
|
+
if (/held supervisor/.test(problem))
|
|
267
|
+
return { run, on: `Run #${run} is still shutting down`, action: null };
|
|
268
|
+
if (/still open/.test(problem))
|
|
269
|
+
return { run, on: `Run #${run} is still running`, action: null };
|
|
270
|
+
return { run, on: `Run #${run} hasn't finished shutting down`, action: null };
|
|
271
|
+
}
|
|
151
272
|
export async function previewDesktopUpdate(stateDir, installed, candidate, label, hooks = {}) {
|
|
152
273
|
const config = readDesktopConfig(stateDir), old = readDesktopBundle(resolve(installed)), next = readDesktopBundle(resolve(candidate));
|
|
153
274
|
if (!/^[a-zA-Z0-9.-]+$/.test(label))
|
|
@@ -634,6 +755,7 @@ export async function runDesktopUpdate(stateDir, hooks = {}) {
|
|
|
634
755
|
return;
|
|
635
756
|
}
|
|
636
757
|
checkpoint("draining", "Waiting for current work to finish. New work is paused; no task will be killed for this update.");
|
|
758
|
+
const now = hooks.now ?? (() => new Date());
|
|
637
759
|
for (;;) {
|
|
638
760
|
if (restoreRequested(j) || updateStopRequested(j)) {
|
|
639
761
|
await cancel();
|
|
@@ -642,16 +764,36 @@ export async function runDesktopUpdate(stateDir, hooks = {}) {
|
|
|
642
764
|
assertConfig(j);
|
|
643
765
|
assertCodingCatalogPresent(j);
|
|
644
766
|
const checking = connect(j.databaseFile);
|
|
767
|
+
let stuck = null;
|
|
645
768
|
try {
|
|
646
769
|
const active = activeUpdateWork(checking);
|
|
647
|
-
const
|
|
648
|
-
|
|
770
|
+
const idle = Object.values(active).every(n => n === 0);
|
|
771
|
+
// A finished run's leftover record settles here first, as the background worker would settle it.
|
|
772
|
+
if (idle)
|
|
773
|
+
settleLeftoverRecords(checking, now());
|
|
774
|
+
const lingering = idle ? lingeringRun(checking) : null;
|
|
775
|
+
if (!lingering && idle && freezeUpdateGate(checking, j.id)) {
|
|
776
|
+
delete j.waiting;
|
|
649
777
|
break;
|
|
650
|
-
|
|
778
|
+
}
|
|
779
|
+
// Saved, so a resumed updater keeps counting from when it began, and status can say what it waits on.
|
|
780
|
+
if (!lingering)
|
|
781
|
+
delete j.waiting;
|
|
782
|
+
else if (j.waiting?.run !== lingering.run || j.waiting.on !== lingering.on)
|
|
783
|
+
j.waiting = { ...lingering, since: now().toISOString() };
|
|
784
|
+
if (j.waiting && now().getTime() - Date.parse(j.waiting.since) >= LINGERING_LIMIT_MS)
|
|
785
|
+
stuck = lingering;
|
|
786
|
+
else
|
|
787
|
+
save(j, "draining", lingering ? `New work is paused. Waiting for run #${lingering.run}: ${lingeringWords(lingering)} No process is being killed.` : `Waiting for current work: ${active.runs} runs, ${active.claims} leases, ${active.conversations} chat requests, ${active.sessions} sessions, ${active.stopping} shutdowns, ${active.coding} coding sessions, ${active.codingDeliveries} unconfirmed messages. Nothing is being cancelled.`);
|
|
651
788
|
}
|
|
652
789
|
finally {
|
|
653
790
|
checking.close();
|
|
654
791
|
}
|
|
792
|
+
// Waiting longer proves nothing more: new work resumes, and the one thing in the way is named.
|
|
793
|
+
if (stuck) {
|
|
794
|
+
await release("cancelled", stoppedWaitingWords(stuck));
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
655
797
|
assertCodingCatalogPresent(j);
|
|
656
798
|
await sleep(1000);
|
|
657
799
|
}
|
package/dist/evidence.d.ts
CHANGED
|
@@ -50,6 +50,12 @@ export declare const RETENTION_NOTE = "REMOVED-BY-RETENTION";
|
|
|
50
50
|
/** Bounds on a claimed screenshot file's own bytes — independent of the
|
|
51
51
|
* proof manifest's byte cap, since these are binary images, not JSON. */
|
|
52
52
|
export declare const SCREENSHOT_BYTE_CAP: number;
|
|
53
|
+
/** Check evidence lives in the run's evidence, never on the branch: images a
|
|
54
|
+
* build adds under this folder are left out of its commit. */
|
|
55
|
+
export declare const WORKTREE_EVIDENCE_DIR = "evidence/";
|
|
56
|
+
export declare function isImagePath(path: string): boolean;
|
|
57
|
+
/** Images and other files that are never read as text, judged by name alone. */
|
|
58
|
+
export declare function isBinaryAssetPath(path: string): boolean;
|
|
53
59
|
/** Bytes each kind may store. Originals can be any size; the record says what was cut. */
|
|
54
60
|
/** Bound one captured stream to `cap` bytes keeping its beginning and, mostly,
|
|
55
61
|
* its end: a test runner's summary and failure list come last, and that is
|
package/dist/evidence.js
CHANGED
|
@@ -55,6 +55,14 @@ export const RETENTION_NOTE = "REMOVED-BY-RETENTION";
|
|
|
55
55
|
/** Bounds on a claimed screenshot file's own bytes — independent of the
|
|
56
56
|
* proof manifest's byte cap, since these are binary images, not JSON. */
|
|
57
57
|
export const SCREENSHOT_BYTE_CAP = 5 * 1024 * 1024;
|
|
58
|
+
/** Check evidence lives in the run's evidence, never on the branch: images a
|
|
59
|
+
* build adds under this folder are left out of its commit. */
|
|
60
|
+
export const WORKTREE_EVIDENCE_DIR = "evidence/";
|
|
61
|
+
const IMAGE_PATH = /\.(?:png|jpe?g|gif|webp|bmp|ico|tiff?|avif|heic)$/i;
|
|
62
|
+
const BINARY_PATH = /\.(?:pdf|zip|gz|tgz|tar|7z|woff2?|ttf|otf|eot|mp4|mov|webm|mp3|wav|sqlite3?|db|wasm|bin|jar|exe|dll|so|dylib)$/i;
|
|
63
|
+
export function isImagePath(path) { return IMAGE_PATH.test(path); }
|
|
64
|
+
/** Images and other files that are never read as text, judged by name alone. */
|
|
65
|
+
export function isBinaryAssetPath(path) { return IMAGE_PATH.test(path) || BINARY_PATH.test(path); }
|
|
58
66
|
/** Bytes each kind may store. Originals can be any size; the record says what was cut. */
|
|
59
67
|
/** Bound one captured stream to `cap` bytes keeping its beginning and, mostly,
|
|
60
68
|
* its end: a test runner's summary and failure list come last, and that is
|
package/dist/first-run.d.ts
CHANGED
|
@@ -88,7 +88,7 @@ export type JourneyStep = {
|
|
|
88
88
|
* the build finished with a result.
|
|
89
89
|
*/
|
|
90
90
|
export declare function firstTaskJourney(read: {
|
|
91
|
-
stage: "queued" | "planning" | "needs-you" | "building" | "checking" | "reviewing" | "finished" | "complete" | "failed" | "stopped";
|
|
91
|
+
stage: "queued" | "planning" | "needs-you" | "waiting" | "building" | "checking" | "reviewing" | "finished" | "complete" | "failed" | "stopped";
|
|
92
92
|
need?: string | undefined;
|
|
93
93
|
}, approved: boolean, built?: boolean): JourneyStep[];
|
|
94
94
|
export {};
|
package/dist/first-run.js
CHANGED
|
@@ -131,11 +131,13 @@ export function firstTaskJourney(read, approved, built = false) {
|
|
|
131
131
|
const at = read.stage === "finished" || read.stage === "complete" ? { index: 5, stuck: false }
|
|
132
132
|
: read.stage === "checking" || read.stage === "reviewing" ? { index: 3, stuck: false }
|
|
133
133
|
: read.stage === "building" ? { index: 2, stuck: false }
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
:
|
|
134
|
+
// Waiting on nothing a person does: the build finished and is settling.
|
|
135
|
+
: read.stage === "waiting" ? { index: built ? 3 : 2, stuck: false }
|
|
136
|
+
: read.stage === "needs-you" && read.need === "approval" ? { index: 1, stuck: false }
|
|
137
|
+
: read.stage === "planning" ? { index: 0, stuck: false }
|
|
138
|
+
: read.stage === "queued" ? { index: approved ? 2 : 0, stuck: false }
|
|
139
|
+
// A build that finished but isn't Ready (its checks failed or didn't run, or it needs a decision) stopped at Checks.
|
|
140
|
+
: { index: built ? 3 : approved ? 2 : 0, stuck: true };
|
|
139
141
|
return JOURNEY_STEPS.map(([key, label], index) => ({ key, label,
|
|
140
142
|
state: index < at.index ? "done" : index === at.index ? (at.stuck ? "stuck" : "current") : "todo" }));
|
|
141
143
|
}
|
package/dist/lead-context.d.ts
CHANGED
|
@@ -5,4 +5,4 @@ import { type AssignmentCatchUp } from './assignment-brief.js';
|
|
|
5
5
|
export declare function leadContext(store: Store, repos: readonly string[], now: Date, evidenceRoot?: string): string;
|
|
6
6
|
/** The same saved brief is useful before chat spending is authorized. */
|
|
7
7
|
export declare function leadBriefHtml(brief: AssignmentCatchUp): string;
|
|
8
|
-
export declare const LEAD_CONTEXT_CSS = ".lead-brief{margin:1rem 0;min-width:0}.lead-brief h2{font-size:1.1rem;margin:0 0 .8rem}.repository-context input[name=q]{display:block;box-sizing:border-box;min-height:44px;width:100%;margin:.4rem 0 .75rem;padding:.6rem .75rem;border:1px solid var(--border);border-radius:8px;background:var(--surface);color:inherit;font:inherit}.repository-context form{margin:.5rem 0 1rem}.repository-context summary{min-height:44px;padding:.75rem 0;overflow-wrap:anywhere}.lead-brief h3{font-size:.85rem;margin:1rem 0 .4rem;color:var(--muted-foreground)}.lead-brief ul{list-style:none;margin:0;padding:0}.lead-brief li{display:grid;grid-template-columns:minmax(0,1fr) auto;column-gap:.75rem;align-items:center;padding:.35rem 0;border-bottom:1px solid var(--border);min-width:0}.lead-brief li a{display:flex;min-height:44px;align-items:center;font-weight:500;overflow-wrap:anywhere;text-decoration:none}.lead-brief li a:hover{text-decoration:underline}.lead-brief-state{font-size:.75rem;font-weight:600;padding:.15rem .5rem;border-radius:.375rem;background:var(--so-neutral-soft);color:var(--so-neutral-ink);white-space:nowrap}.lead-brief-state--needs-decision,.lead-brief-state--ready-to-check{background:var(--so-attention-soft);color:var(--so-attention)}.lead-brief-state--working,.lead-brief-state--checking{background:var(--so-info-soft);color:var(--so-info)}.lead-brief-state--complete{background:var(--so-success-soft);color:var(--so-success)}.lead-brief-detail{grid-column:1/-1;font-size:.85rem;color:var(--muted-foreground);overflow-wrap:anywhere;padding-bottom:.35rem}@media(max-width:760px){.lead-brief{margin:.5rem 0}.lead-brief h2{margin:0 0 .25rem}.lead-brief h3{margin:.5rem 0 0}.lead-brief li{padding:0 0 .375rem}.lead-brief-detail{margin-top:-.375rem;padding-bottom:0;line-height:1.35;pointer-events:none}}";
|
|
8
|
+
export declare const LEAD_CONTEXT_CSS = ".lead-brief{margin:1rem 0;min-width:0}.lead-brief h2{font-size:1.1rem;margin:0 0 .8rem}.repository-context input[name=q]{display:block;box-sizing:border-box;min-height:44px;width:100%;margin:.4rem 0 .75rem;padding:.6rem .75rem;border:1px solid var(--border);border-radius:8px;background:var(--surface);color:inherit;font:inherit}.repository-context form{margin:.5rem 0 1rem}.repository-context summary{min-height:44px;padding:.75rem 0;overflow-wrap:anywhere}.lead-brief h3{font-size:.85rem;margin:1rem 0 .4rem;color:var(--muted-foreground)}.lead-brief ul{list-style:none;margin:0;padding:0}.lead-brief li{display:grid;grid-template-columns:minmax(0,1fr) auto;column-gap:.75rem;align-items:center;padding:.35rem 0;border-bottom:1px solid var(--border);min-width:0}.lead-brief li a{display:flex;min-height:44px;align-items:center;font-weight:500;overflow-wrap:anywhere;text-decoration:none}.lead-brief li a:hover{text-decoration:underline}.lead-brief-state{font-size:.75rem;font-weight:600;padding:.15rem .5rem;border-radius:.375rem;background:var(--so-neutral-soft);color:var(--so-neutral-ink);white-space:nowrap}.lead-brief-state--needs-decision,.lead-brief-state--ready-to-check{background:var(--so-attention-soft);color:var(--so-attention)}.lead-brief-state--working,.lead-brief-state--checking{background:var(--so-info-soft);color:var(--so-info)}.lead-brief-state--complete{background:var(--so-success-soft);color:var(--so-success)}.lead-brief li a.lead-brief-act{grid-column:1/-1;justify-self:start;display:inline-flex;min-height:40px;margin:.15rem 0 .4rem;font-weight:600}@media(max-width:760px){.lead-brief li a.lead-brief-act{justify-self:stretch;justify-content:center;min-height:44px}}.lead-brief-detail{grid-column:1/-1;font-size:.85rem;color:var(--muted-foreground);overflow-wrap:anywhere;padding-bottom:.35rem}@media(max-width:760px){.lead-brief{margin:.5rem 0}.lead-brief h2{margin:0 0 .25rem}.lead-brief h3{margin:.5rem 0 0}.lead-brief li{padding:0 0 .375rem}.lead-brief-detail{margin-top:-.375rem;padding-bottom:0;line-height:1.35;pointer-events:none}}";
|
package/dist/lead-context.js
CHANGED
|
@@ -53,8 +53,10 @@ export function leadBriefHtml(brief) {
|
|
|
53
53
|
};
|
|
54
54
|
return [`<section><h3>${group.title}</h3><ul>${entries.map(one => `<li><a href="/chat?task=${encodeURIComponent(one.rootId)}">${escape(one.title)}</a>` +
|
|
55
55
|
stateChip(one.state, one.detail) +
|
|
56
|
-
(quiet ? '' : `<span class="lead-brief-detail">${escape(publicChatText(one.detail || one.outcome || '', 160))}</span>`) +
|
|
56
|
+
(quiet ? '' : `<span class="lead-brief-detail">${escape(publicChatText(one.detail || one.outcome || '', 160))}</span>`) +
|
|
57
|
+
// Needs you: the one action that resolves it, under its sentence.
|
|
58
|
+
(one.state === 'needs-decision' && one.nextAction !== null && one.nextHref ? `<a class="lead-brief-act button-link" href="${escape(one.nextHref)}" data-catch-up-action>${escape(one.nextAction.label)}</a>` : '') + `</li>`).join('')}</ul></section>`];
|
|
57
59
|
});
|
|
58
60
|
return `<section class="lead-brief" aria-label="Project catch-up"><h2>Catch up</h2>${sections.length ? sections.join('') : '<p class="meta">Nothing needs you right now.</p>'}${brief.assignments.length > 3 || brief.omissions.candidateScanLimited || brief.omissions.assignments > 0 ? '<a href="/work">See all tasks</a>' : ''}</section>`;
|
|
59
61
|
}
|
|
60
|
-
export const LEAD_CONTEXT_CSS = '.lead-brief{margin:1rem 0;min-width:0}.lead-brief h2{font-size:1.1rem;margin:0 0 .8rem}.repository-context input[name=q]{display:block;box-sizing:border-box;min-height:44px;width:100%;margin:.4rem 0 .75rem;padding:.6rem .75rem;border:1px solid var(--border);border-radius:8px;background:var(--surface);color:inherit;font:inherit}.repository-context form{margin:.5rem 0 1rem}.repository-context summary{min-height:44px;padding:.75rem 0;overflow-wrap:anywhere}.lead-brief h3{font-size:.85rem;margin:1rem 0 .4rem;color:var(--muted-foreground)}.lead-brief ul{list-style:none;margin:0;padding:0}.lead-brief li{display:grid;grid-template-columns:minmax(0,1fr) auto;column-gap:.75rem;align-items:center;padding:.35rem 0;border-bottom:1px solid var(--border);min-width:0}.lead-brief li a{display:flex;min-height:44px;align-items:center;font-weight:500;overflow-wrap:anywhere;text-decoration:none}.lead-brief li a:hover{text-decoration:underline}.lead-brief-state{font-size:.75rem;font-weight:600;padding:.15rem .5rem;border-radius:.375rem;background:var(--so-neutral-soft);color:var(--so-neutral-ink);white-space:nowrap}.lead-brief-state--needs-decision,.lead-brief-state--ready-to-check{background:var(--so-attention-soft);color:var(--so-attention)}.lead-brief-state--working,.lead-brief-state--checking{background:var(--so-info-soft);color:var(--so-info)}.lead-brief-state--complete{background:var(--so-success-soft);color:var(--so-success)}.lead-brief-detail{grid-column:1/-1;font-size:.85rem;color:var(--muted-foreground);overflow-wrap:anywhere;padding-bottom:.35rem}@media(max-width:760px){.lead-brief{margin:.5rem 0}.lead-brief h2{margin:0 0 .25rem}.lead-brief h3{margin:.5rem 0 0}.lead-brief li{padding:0 0 .375rem}.lead-brief-detail{margin-top:-.375rem;padding-bottom:0;line-height:1.35;pointer-events:none}}';
|
|
62
|
+
export const LEAD_CONTEXT_CSS = '.lead-brief{margin:1rem 0;min-width:0}.lead-brief h2{font-size:1.1rem;margin:0 0 .8rem}.repository-context input[name=q]{display:block;box-sizing:border-box;min-height:44px;width:100%;margin:.4rem 0 .75rem;padding:.6rem .75rem;border:1px solid var(--border);border-radius:8px;background:var(--surface);color:inherit;font:inherit}.repository-context form{margin:.5rem 0 1rem}.repository-context summary{min-height:44px;padding:.75rem 0;overflow-wrap:anywhere}.lead-brief h3{font-size:.85rem;margin:1rem 0 .4rem;color:var(--muted-foreground)}.lead-brief ul{list-style:none;margin:0;padding:0}.lead-brief li{display:grid;grid-template-columns:minmax(0,1fr) auto;column-gap:.75rem;align-items:center;padding:.35rem 0;border-bottom:1px solid var(--border);min-width:0}.lead-brief li a{display:flex;min-height:44px;align-items:center;font-weight:500;overflow-wrap:anywhere;text-decoration:none}.lead-brief li a:hover{text-decoration:underline}.lead-brief-state{font-size:.75rem;font-weight:600;padding:.15rem .5rem;border-radius:.375rem;background:var(--so-neutral-soft);color:var(--so-neutral-ink);white-space:nowrap}.lead-brief-state--needs-decision,.lead-brief-state--ready-to-check{background:var(--so-attention-soft);color:var(--so-attention)}.lead-brief-state--working,.lead-brief-state--checking{background:var(--so-info-soft);color:var(--so-info)}.lead-brief-state--complete{background:var(--so-success-soft);color:var(--so-success)}.lead-brief li a.lead-brief-act{grid-column:1/-1;justify-self:start;display:inline-flex;min-height:40px;margin:.15rem 0 .4rem;font-weight:600}@media(max-width:760px){.lead-brief li a.lead-brief-act{justify-self:stretch;justify-content:center;min-height:44px}}.lead-brief-detail{grid-column:1/-1;font-size:.85rem;color:var(--muted-foreground);overflow-wrap:anywhere;padding-bottom:.35rem}@media(max-width:760px){.lead-brief{margin:.5rem 0}.lead-brief h2{margin:0 0 .25rem}.lead-brief h3{margin:.5rem 0 0}.lead-brief li{padding:0 0 .375rem}.lead-brief-detail{margin-top:-.375rem;padding-bottom:0;line-height:1.35;pointer-events:none}}';
|
package/dist/lead-status.d.ts
CHANGED
|
@@ -47,7 +47,7 @@ export type InstallationStatus = {
|
|
|
47
47
|
count: number;
|
|
48
48
|
results: {
|
|
49
49
|
task: string;
|
|
50
|
-
run: number;
|
|
50
|
+
run: number | null;
|
|
51
51
|
check: CheckSummary;
|
|
52
52
|
}[];
|
|
53
53
|
};
|
|
@@ -84,6 +84,15 @@ export type InstallationStatus = {
|
|
|
84
84
|
* has been observed yet. */
|
|
85
85
|
export declare function taskWaitSnapshot(store: Store, taskId: string, now: Date, watchedRun?: number): TaskWaitSnapshot | null;
|
|
86
86
|
/** Whole-installation status, deliberately aggregated and bounded. */
|
|
87
|
+
/** The one-off repair on start. An older build left finished runs' process
|
|
88
|
+
* exits unrecorded and counted completed families as waiting; nothing about
|
|
89
|
+
* a status is stored, so recording every exit that can now be proven and
|
|
90
|
+
* re-deriving each open task's headline from the shared projection clears
|
|
91
|
+
* them. What can't be proven stays as it was. */
|
|
92
|
+
export declare function repairStaleStatuses(store: Store, now: Date): {
|
|
93
|
+
exitsRecorded: number;
|
|
94
|
+
readyForReview: number;
|
|
95
|
+
};
|
|
87
96
|
export declare function installationStatus(store: Store, now: Date): InstallationStatus;
|
|
88
97
|
export declare function renderInstallationStatus(status: InstallationStatus): string[];
|
|
89
98
|
export declare function renderTaskWait(snapshot: TaskWaitSnapshot, outcome?: string): string;
|
package/dist/lead-status.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/** Cheap lead-facing status. Every fact comes from indexed SQLite records;
|
|
2
2
|
* this module never opens a repository, retained evidence, or a live log. */
|
|
3
|
-
import {
|
|
3
|
+
import { familyCompleted } from "./result-completion.js";
|
|
4
4
|
import { windowLabel } from "./provider-limits.js";
|
|
5
5
|
import { openAuthPauses, signInCommand, signInReason } from "./provider-auth.js";
|
|
6
6
|
import { BUILT_IN, checkSuitesOf } from "./store.js";
|
|
7
|
-
import { workIndexPage } from "./work-index.js";
|
|
7
|
+
import { workIndexPage, workReadyForReview } from "./work-index.js";
|
|
8
8
|
const maybeNumber = (value) => value === null || value === undefined || !Number.isInteger(Number(value)) ? null : Number(value);
|
|
9
9
|
const CHECK_COLUMNS = "rc.status AS check_status, rc.exit_code AS check_exit_code, rc.suites_json AS check_suites_json";
|
|
10
10
|
function checkOf(row) {
|
|
@@ -90,9 +90,9 @@ export function taskWaitSnapshot(store, taskId, now, watchedRun) {
|
|
|
90
90
|
if (state !== "done")
|
|
91
91
|
return answer("Ready", "Review result", true, 0, "ready");
|
|
92
92
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if (
|
|
93
|
+
// A completed family is Complete from any of its tasks.
|
|
94
|
+
const completed = state === "done" && familyCompleted(store, taskId);
|
|
95
|
+
if (completed)
|
|
96
96
|
return answer("Complete", "No action needed", true, 0, "complete");
|
|
97
97
|
if (state === "done")
|
|
98
98
|
return answer("Ready", "Review result", true, 0, "ready");
|
|
@@ -113,7 +113,7 @@ export function taskWaitSnapshot(store, taskId, now, watchedRun) {
|
|
|
113
113
|
return answer("Needs a person", next, true, 1, "needs-person");
|
|
114
114
|
}
|
|
115
115
|
if (task["plan"] === "drafted")
|
|
116
|
-
return answer("Needs a person", "
|
|
116
|
+
return answer("Needs a person", "Approve plan", true, 1, "needs-person");
|
|
117
117
|
if (Number(task["strikes"] ?? 0) >= 3)
|
|
118
118
|
return answer("Needs a person", "Review repeated failures", true, 1, "needs-person");
|
|
119
119
|
// An admitted attempt is already the work being watched. Its immutable
|
|
@@ -168,6 +168,15 @@ function queueReason(row) {
|
|
|
168
168
|
return "ready for a worker";
|
|
169
169
|
}
|
|
170
170
|
/** Whole-installation status, deliberately aggregated and bounded. */
|
|
171
|
+
/** The one-off repair on start. An older build left finished runs' process
|
|
172
|
+
* exits unrecorded and counted completed families as waiting; nothing about
|
|
173
|
+
* a status is stored, so recording every exit that can now be proven and
|
|
174
|
+
* re-deriving each open task's headline from the shared projection clears
|
|
175
|
+
* them. What can't be proven stays as it was. */
|
|
176
|
+
export function repairStaleStatuses(store, now) {
|
|
177
|
+
const exitsRecorded = store.recordFinishedRunExits(now);
|
|
178
|
+
return { exitsRecorded, readyForReview: workReadyForReview(store, now, { principal: "operator", repos: null, includeUnplaced: true }, 0).count };
|
|
179
|
+
}
|
|
171
180
|
export function installationStatus(store, now) {
|
|
172
181
|
const runningRows = store.handle.prepare(`SELECT run.id, run.role, run.phase, ref.external_id AS task
|
|
173
182
|
FROM run INDEXED BY work_unfinished
|
|
@@ -208,22 +217,10 @@ export function installationStatus(store, now) {
|
|
|
208
217
|
if (queuedTasks.length < 8)
|
|
209
218
|
queuedTasks.push({ task: String(row["id"]), reason });
|
|
210
219
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
AND NOT EXISTS (SELECT 1 FROM action_ledger INDEXED BY work_completion
|
|
216
|
-
WHERE action_ledger.task_id = task.id AND action_ledger.action = ? AND action_ledger.source = 'work'
|
|
217
|
-
AND action_ledger.run_id = (SELECT id FROM run INDEXED BY work_result WHERE task_ref = ref.id AND finished_at IS NOT NULL AND role IN ('builder','scout') ORDER BY id DESC LIMIT 1))`)
|
|
218
|
-
.get(BUILT_IN, COMPLETION_ACTION)?.["n"] ?? 0);
|
|
219
|
-
const waitingRows = store.handle.prepare(`SELECT task.id AS task, result.id, ${CHECK_COLUMNS}
|
|
220
|
-
FROM task INDEXED BY task_done_recent
|
|
221
|
-
JOIN task_ref AS ref INDEXED BY sqlite_autoindex_task_ref_1 ON ref.backend = ? AND ref.external_id = task.id
|
|
222
|
-
JOIN run AS result ON result.id = (SELECT id FROM run INDEXED BY work_result WHERE task_ref = ref.id AND finished_at IS NOT NULL AND role IN ('builder','scout') ORDER BY id DESC LIMIT 1)
|
|
223
|
-
LEFT JOIN run_check AS rc ON rc.run = result.id
|
|
224
|
-
WHERE task.state = 'done' AND NOT EXISTS (SELECT 1 FROM action_ledger INDEXED BY work_completion
|
|
225
|
-
WHERE action_ledger.task_id = task.id AND action_ledger.action = ? AND action_ledger.source = 'work' AND action_ledger.run_id = result.id)
|
|
226
|
-
ORDER BY task.updated_at DESC, task.id DESC LIMIT 5`).all(BUILT_IN, COMPLETION_ACTION);
|
|
220
|
+
// One per family, from the same projection as the Tasks list: a completed
|
|
221
|
+
// family (root or any revision) and a superseded version never count.
|
|
222
|
+
const ready = workReadyForReview(store, now, { principal: "operator", repos: null, includeUnplaced: true }, 5);
|
|
223
|
+
const checkRow = (run) => store.handle.prepare(`SELECT ${CHECK_COLUMNS} FROM run_check AS rc WHERE rc.run = ?`).get(run);
|
|
227
224
|
// Run ids grow with time, so the newest finished release check is the
|
|
228
225
|
// first finished row down the partial index.
|
|
229
226
|
const release = store.handle.prepare(`SELECT run.id, run.finished_at, ${CHECK_COLUMNS}, ref.external_id AS task
|
|
@@ -240,7 +237,7 @@ export function installationStatus(store, now) {
|
|
|
240
237
|
reasons: [...reasons].map(([reason, count]) => ({ reason, count })).sort((a, b) => b.count - a.count || a.reason.localeCompare(b.reason)),
|
|
241
238
|
tasks: queuedTasks,
|
|
242
239
|
},
|
|
243
|
-
waitingForReview: { count:
|
|
240
|
+
waitingForReview: { count: ready.count, results: ready.results.map(one => ({ task: one.taskId, run: one.run, check: checkOf(one.run === null ? undefined : checkRow(one.run)) })) },
|
|
244
241
|
releaseCheck: release === undefined ? null : { task: String(release["task"]), run: Number(release["id"]), finishedAt: release["finished_at"] == null ? null : String(release["finished_at"]), check: checkOf(release) },
|
|
245
242
|
planWindows: store.handle.prepare(`SELECT provider, window, used_percent, window_minutes, resets_at, reached, plan, observed_at
|
|
246
243
|
FROM provider_limit INDEXED BY sqlite_autoindex_provider_limit_1 ORDER BY provider, window`).all().map(row => ({
|
|
@@ -271,7 +268,7 @@ export function renderInstallationStatus(status) {
|
|
|
271
268
|
}
|
|
272
269
|
lines.push(status.running.count === 0 ? "Building: none" : `Building: ${status.running.count} — ${status.running.tasks.map(one => `${one.task} (#${one.run}, ${one.phase})`).join(", ")}${status.running.count > status.running.tasks.length ? ", …" : ""}`);
|
|
273
270
|
lines.push(status.queued.count === 0 ? "Queued: none" : `Queued: ${status.queued.count} — ${status.queued.tasks.map(one => `${one.task} (${one.reason})`).join(", ")}${status.queued.count > status.queued.tasks.length ? ", …" : ""}`);
|
|
274
|
-
lines.push(status.waitingForReview.count === 0 ? "Ready for review: none" : `Ready for review: ${status.waitingForReview.count} — ${status.waitingForReview.results.map(one => `${one.task} (#${one.run})`).join(", ")}${status.waitingForReview.count > status.waitingForReview.results.length ? ", …" : ""}`);
|
|
271
|
+
lines.push(status.waitingForReview.count === 0 ? "Ready for review: none" : `Ready for review: ${status.waitingForReview.count} — ${status.waitingForReview.results.map(one => one.run === null ? one.task : `${one.task} (#${one.run})`).join(", ")}${status.waitingForReview.count > status.waitingForReview.results.length ? ", …" : ""}`);
|
|
275
272
|
if (status.releaseCheck === null) {
|
|
276
273
|
lines.push("Release check: none recorded");
|
|
277
274
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/** Every reason a task, result or flow card can wait on a person, in one
|
|
2
|
+
* place: the plain sentence it shows and the one action that resolves it
|
|
3
|
+
* (task-status.ts reads it for every surface). A reason no person can act
|
|
4
|
+
* on is not Needs you: it is a Waiting reason, worded as what it waits for.
|
|
5
|
+
*
|
|
6
|
+
* The words never name the machine's internals; `plainSentence` keeps a
|
|
7
|
+
* recorded reason only when it is free of them. */
|
|
8
|
+
/** What a person is asked for. */
|
|
9
|
+
export type NeedKey = "approval" | "card" | "answer" | "questions" | "sign-in" | "confirm-stopped" | "check-stopped" | "hold" | "choose-project" | "define-task" | "fix-request" | "choose-agent" | "fix-dependency" | "add-requirement" | "connect-builder" | "start-builder" | "vanished" | "review-result" | "rebuild" | "earlier-version" | "other";
|
|
10
|
+
/** What the work waits for when no person can act. */
|
|
11
|
+
export type WaitKey = "build-stopping" | "other-computer" | "card-reply" | "card-time" | "card-ci";
|
|
12
|
+
/** The action's code: the WorkAction that owns the act, or one of the acts a surface renders itself. */
|
|
13
|
+
export type NeedActionCode = "approve-scope" | "approve-card" | "answer-decision" | "inspect-decisions" | "sign-in" | "confirm-stopped" | "inspect-hold" | "place-task" | "write-scope" | "select-agent" | "repair-dependency" | "repair-capability" | "start-worker" | "reconcile-run" | "open-result" | "retry-task" | "inspect-task";
|
|
14
|
+
export type NeedAction = {
|
|
15
|
+
code: NeedActionCode;
|
|
16
|
+
label: string;
|
|
17
|
+
};
|
|
18
|
+
/** `build`: the run a sentence names; `provider`: the agent that signed out. */
|
|
19
|
+
export type NeedContext = {
|
|
20
|
+
build?: number | null;
|
|
21
|
+
provider?: string | null;
|
|
22
|
+
};
|
|
23
|
+
/** `useReason`: which recorded reasons say what is needed better than the default words. */
|
|
24
|
+
type Need = {
|
|
25
|
+
sentence: (context: NeedContext) => string;
|
|
26
|
+
action: NeedAction;
|
|
27
|
+
useReason?: (reason: string) => boolean;
|
|
28
|
+
/** How a kept reason reads: by default the reason alone. */
|
|
29
|
+
withReason?: (reason: string) => string;
|
|
30
|
+
};
|
|
31
|
+
export declare const NEEDS: Readonly<Record<NeedKey, Need>>;
|
|
32
|
+
export declare const WAITS: Readonly<Record<WaitKey, (context: NeedContext) => string>>;
|
|
33
|
+
/** Words that name the machine's internals; no Needs you or Waiting sentence carries them. */
|
|
34
|
+
export declare const BANNED_WORDS: readonly ["witness", "unproven", "quiescence", "quiescent", "digest", "scope digest"];
|
|
35
|
+
export declare const hasInternalWords: (text: string) => boolean;
|
|
36
|
+
/** The sentence for a need: its recorded reason when the need keeps one and
|
|
37
|
+
* it reads plainly, otherwise the need's own words. */
|
|
38
|
+
export declare function needSentence(key: NeedKey, reason: string | null, context?: NeedContext): string;
|
|
39
|
+
export declare function waitSentence(key: WaitKey | undefined, reason: string | null, context?: NeedContext): string;
|
|
40
|
+
/** A finished run Toolroll can't prove stopped (store.stopQuiescenceFact):
|
|
41
|
+
* a person can confirm it only when nothing of it may still be running.
|
|
42
|
+
* `unknown`: Toolroll can't check, so the person confirms they checked. */
|
|
43
|
+
export declare function processNeedOf(fact: {
|
|
44
|
+
run: number;
|
|
45
|
+
kind: "open" | "alive" | "elsewhere" | "unprovable" | "unknown";
|
|
46
|
+
} | null): {
|
|
47
|
+
need: "confirm-stopped" | "check-stopped";
|
|
48
|
+
build: number;
|
|
49
|
+
} | {
|
|
50
|
+
wait: WaitKey;
|
|
51
|
+
build: number;
|
|
52
|
+
} | null;
|
|
53
|
+
/** Dispatch and work-index codes that wait on a person, as their need. */
|
|
54
|
+
export declare const CODE_NEED: Readonly<Record<string, NeedKey>>;
|
|
55
|
+
export {};
|