toolroll 0.9.7 → 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 +48 -1
- package/dist/assignment-adapters.d.ts +1 -0
- package/dist/assignment-adapters.js +2 -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 +15 -0
- package/dist/assignment.js +56 -9
- 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/build-review.d.ts +38 -0
- package/dist/build-review.js +341 -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-levels-ui.d.ts +5 -0
- package/dist/check-levels-ui.js +16 -2
- package/dist/check-progress.js +4 -3
- package/dist/checkout-cleanup.d.ts +40 -2
- package/dist/checkout-cleanup.js +129 -9
- package/dist/cli.js +2 -0
- package/dist/desktop-update.d.ts +52 -0
- package/dist/desktop-update.js +147 -5
- package/dist/dispatch.js +2 -1
- package/dist/dispose.d.ts +4 -3
- package/dist/dispose.js +12 -4
- package/dist/evidence.d.ts +6 -0
- package/dist/evidence.js +8 -0
- package/dist/exec.d.ts +5 -0
- package/dist/exec.js +30 -4
- package/dist/first-run.d.ts +1 -1
- package/dist/first-run.js +8 -6
- package/dist/invoke.js +5 -0
- 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/modes.d.ts +3 -1
- package/dist/modes.js +3 -1
- 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 +231 -36
- package/dist/plane-review.js +4 -0
- package/dist/process-custody.js +8 -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/provider.js +16 -0
- package/dist/result-completion.d.ts +4 -0
- package/dist/result-completion.js +11 -0
- package/dist/retention-ui.d.ts +3 -1
- package/dist/retention-ui.js +4 -4
- package/dist/retention.d.ts +13 -6
- package/dist/retention.js +50 -12
- package/dist/review-context.d.ts +3 -0
- package/dist/review-context.js +45 -5
- package/dist/review-switch.d.ts +41 -0
- package/dist/review-switch.js +88 -0
- package/dist/reviewer.d.ts +24 -2
- package/dist/reviewer.js +49 -1
- 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 +158 -18
- 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 +72 -1
- package/dist/store.js +258 -31
- package/dist/surface.js +5 -1
- package/dist/task-status.d.ts +22 -5
- package/dist/task-status.js +45 -24
- 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/work-summary.js +2 -1
- package/dist/workspace-ui.d.ts +3 -1
- package/dist/workspace-ui.js +10 -9
- 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/dispatch.js
CHANGED
|
@@ -13,6 +13,7 @@ import { plannerSourceProblemOf } from "./planner-source.js";
|
|
|
13
13
|
import { authPauseOf, authWaitOf, providerName, signInReason } from "./provider-auth.js";
|
|
14
14
|
import { REVIEW_TOKENS, resultStatusOf, reviewFactsOf } from "./workspace-ui.js";
|
|
15
15
|
import { replacedWords } from "./task-status.js";
|
|
16
|
+
import { buildReviewOf } from "./review-switch.js";
|
|
16
17
|
import { BUILT_IN, parseCapabilityKey } from "./store.js";
|
|
17
18
|
export const DEFAULT_MAX_OPEN_DECISIONS = 5;
|
|
18
19
|
/**
|
|
@@ -187,7 +188,7 @@ export function diagnoseTaskDispatch(store, taskId, now) {
|
|
|
187
188
|
const reviewFacts = reviewFactsOf(retry, runner => {
|
|
188
189
|
const reviewer = store.getRunner(runner)?.runner;
|
|
189
190
|
return reviewer !== undefined && isAlive(reviewer, now);
|
|
190
|
-
});
|
|
191
|
+
}, result !== undefined && buildReviewOf(store, result.id)?.state === "pending");
|
|
191
192
|
// A no-change conclusion owes no proof beyond its handoff and sealed
|
|
192
193
|
// diff (the task page's own "attested floor"); with both on record it
|
|
193
194
|
// is complete, not verification-needed.
|
package/dist/dispose.d.ts
CHANGED
|
@@ -85,9 +85,10 @@ export type Disposition = {
|
|
|
85
85
|
kind: "invariant";
|
|
86
86
|
reason: string;
|
|
87
87
|
};
|
|
88
|
-
/**
|
|
89
|
-
*
|
|
90
|
-
|
|
88
|
+
/** A committed build whose approved check passed gets the project's one
|
|
89
|
+
* automatic review when its review switch is on (build-review.ts). No-change
|
|
90
|
+
* results and failed checks go straight to the person, as before. */
|
|
91
|
+
export declare function maybeRequestAutoReview(store: Store, repo: string, runId: number, committed: boolean, noChange: boolean, now: Date, root?: string): void;
|
|
91
92
|
export declare function disposeBuildOutcome(context: DisposeContext, result: BuildResult): Disposition;
|
|
92
93
|
/**
|
|
93
94
|
* The stale-approval HOLD, shared by the post-build disposition above and
|
package/dist/dispose.js
CHANGED
|
@@ -17,7 +17,9 @@ import { createHash } from "node:crypto";
|
|
|
17
17
|
import { completeFenced, finalizeFailureFenced, finalizeHandBackFenced, finalizeMalformedFenced, finalizeParkFenced, interruptIfStopped, release, } from "./claim.js";
|
|
18
18
|
import { bodyHashOf, publicationBody } from "./publish.js";
|
|
19
19
|
import { modeTermsFromJson } from "./modes.js";
|
|
20
|
-
import { readVerifiedArtifact, writeEvidenceFile } from "./evidence.js";
|
|
20
|
+
import { evidenceRoot, readVerifiedArtifact, writeEvidenceFile } from "./evidence.js";
|
|
21
|
+
import { queueBuildReview } from "./build-review.js";
|
|
22
|
+
import { homedir } from "node:os";
|
|
21
23
|
import {} from "./proof.js";
|
|
22
24
|
import { verificationEvidence, failedVerificationEvidence } from "./verification-evidence.js";
|
|
23
25
|
import { classifyGateFailure, describeGateFailure, gateFailureSummary } from "./gate-failure.js";
|
|
@@ -55,9 +57,14 @@ const STANDALONE_BROKE_REASONS = new Set([
|
|
|
55
57
|
"timeout",
|
|
56
58
|
"git",
|
|
57
59
|
]);
|
|
58
|
-
/**
|
|
59
|
-
*
|
|
60
|
-
|
|
60
|
+
/** A committed build whose approved check passed gets the project's one
|
|
61
|
+
* automatic review when its review switch is on (build-review.ts). No-change
|
|
62
|
+
* results and failed checks go straight to the person, as before. */
|
|
63
|
+
export function maybeRequestAutoReview(store, repo, runId, committed, noChange, now, root = evidenceRoot(homedir())) {
|
|
64
|
+
if (!committed || noChange)
|
|
65
|
+
return;
|
|
66
|
+
queueBuildReview(store, repo, runId, root, now);
|
|
67
|
+
}
|
|
61
68
|
export function disposeBuildOutcome(context, result) {
|
|
62
69
|
// Stop and every terminal side effect compete under the same SQLite
|
|
63
70
|
// write lock. A separate preliminary transaction leaves a completion gap.
|
|
@@ -238,6 +245,7 @@ function disposeBuildOutcomeLocked(context, result) {
|
|
|
238
245
|
// proves the credential, clearing any quota stamp.
|
|
239
246
|
store.resetStrikes(taskRef);
|
|
240
247
|
store.clearQuota(runner, provider, model ?? "");
|
|
248
|
+
maybeRequestAutoReview(store, repo, runId, result.committed, result.noChange === true, clock(), context.evidenceRoot);
|
|
241
249
|
return { kind: "built", committed: result.committed, noChange: result.noChange === true };
|
|
242
250
|
}
|
|
243
251
|
store.transact(() => {
|
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/exec.d.ts
CHANGED
|
@@ -113,6 +113,11 @@ export type RunOptions = ProcessTreeObserver & {
|
|
|
113
113
|
/** Fires once the child exists, with its pid (the process-group id when
|
|
114
114
|
* processGroup is set) — the slot ledger records it (v14 finding 26). */
|
|
115
115
|
onSpawn?: (pid: number) => void;
|
|
116
|
+
/** Fires instead of onSpawn when a spawn that beforeSpawn admitted made no
|
|
117
|
+
* process at all (the OS call threw — ENOENT, EAGAIN — returned no pid, or
|
|
118
|
+
* its OS object could not be made), so its reserved witness can be settled
|
|
119
|
+
* as never started in the same step. Never fires once a child exists. */
|
|
120
|
+
onSpawnFailed?: () => void;
|
|
116
121
|
/** Fires before any target spawn, so a crash cannot lose its OS object. */
|
|
117
122
|
onContainer?: (info: {
|
|
118
123
|
backend: ContainmentBackendId;
|
package/dist/exec.js
CHANGED
|
@@ -84,9 +84,24 @@ function spawnContained(file, args, spawnOptions, label, bag, contain = true) {
|
|
|
84
84
|
throw new ContainmentRefusal(effective.refusal);
|
|
85
85
|
if (bag.beforeSpawn?.() === false)
|
|
86
86
|
throw new Error("the attempt stopped before this process could spawn");
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
// From here a witness may be reserved: every road that makes no process
|
|
88
|
+
// says so through onSpawnFailed before it returns or throws.
|
|
89
|
+
const neverStarted = () => { try {
|
|
90
|
+
bag.onSpawnFailed?.();
|
|
91
|
+
}
|
|
92
|
+
catch { /* the reservation stays unproven */ } };
|
|
93
|
+
let made;
|
|
94
|
+
try {
|
|
95
|
+
made = effective === null ? { container: null } : createContainer(effective, label);
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
neverStarted();
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
if ("refused" in made) {
|
|
102
|
+
neverStarted();
|
|
89
103
|
throw new ContainmentRefusal(made.refused);
|
|
104
|
+
}
|
|
90
105
|
const container = made.container;
|
|
91
106
|
try {
|
|
92
107
|
if (container !== null)
|
|
@@ -94,6 +109,7 @@ function spawnContained(file, args, spawnOptions, label, bag, contain = true) {
|
|
|
94
109
|
}
|
|
95
110
|
catch (error) {
|
|
96
111
|
container?.release();
|
|
112
|
+
neverStarted();
|
|
97
113
|
throw error;
|
|
98
114
|
}
|
|
99
115
|
let launch;
|
|
@@ -102,6 +118,7 @@ function spawnContained(file, args, spawnOptions, label, bag, contain = true) {
|
|
|
102
118
|
}
|
|
103
119
|
catch (error) {
|
|
104
120
|
container?.release();
|
|
121
|
+
neverStarted();
|
|
105
122
|
throw error;
|
|
106
123
|
}
|
|
107
124
|
let child;
|
|
@@ -123,6 +140,7 @@ function spawnContained(file, args, spawnOptions, label, bag, contain = true) {
|
|
|
123
140
|
}
|
|
124
141
|
catch { }
|
|
125
142
|
container?.release();
|
|
143
|
+
neverStarted();
|
|
126
144
|
throw error;
|
|
127
145
|
}
|
|
128
146
|
if (container !== null) {
|
|
@@ -143,6 +161,12 @@ function spawnContained(file, args, spawnOptions, label, bag, contain = true) {
|
|
|
143
161
|
if (child.pid !== undefined) {
|
|
144
162
|
bag.onSpawn?.(child.pid);
|
|
145
163
|
}
|
|
164
|
+
else if (container === null) {
|
|
165
|
+
// No pid: the OS refused the spawn (ENOENT, EACCES) and only an
|
|
166
|
+
// 'error' event follows. A contained spawn settles once its object is
|
|
167
|
+
// proven empty, and its transport's return finishes the witness.
|
|
168
|
+
neverStarted();
|
|
169
|
+
}
|
|
146
170
|
}
|
|
147
171
|
catch (error) {
|
|
148
172
|
throw new SpawnCustodyFailure(child, error);
|
|
@@ -201,7 +225,7 @@ function containmentOf(container, empty, attachFailure) {
|
|
|
201
225
|
}
|
|
202
226
|
/** The streaming transports' spawn: contained when the spawn is a provider (process group), plain otherwise. */
|
|
203
227
|
function spawnStream(file, args, options, stdio, childEnv) {
|
|
204
|
-
return spawnContained(file, args, { cwd: options.cwd, env: childEnv, stdio, detached: options.processGroup === true && process.platform !== "win32" }, options.owner ?? "stream", { beforeSpawn: options.beforeSpawn, onSpawn: options.onSpawn, onContainer: options.onContainer, onContainerEmpty: options.onContainerEmpty, onUnknown: options.onUnknown, fence: options.fence }, options.processGroup === true);
|
|
228
|
+
return spawnContained(file, args, { cwd: options.cwd, env: childEnv, stdio, detached: options.processGroup === true && process.platform !== "win32" }, options.owner ?? "stream", { beforeSpawn: options.beforeSpawn, onSpawn: options.onSpawn, onSpawnFailed: options.onSpawnFailed, onContainer: options.onContainer, onContainerEmpty: options.onContainerEmpty, onUnknown: options.onUnknown, fence: options.fence }, options.processGroup === true);
|
|
205
229
|
}
|
|
206
230
|
/** The refusal-before-spawn shape shared by every containable transport. */
|
|
207
231
|
function refusedResult(error) {
|
|
@@ -506,6 +530,7 @@ export function run(file, args, options = {}) {
|
|
|
506
530
|
...(cwd === undefined ? {} : { cwd }),
|
|
507
531
|
...(childEnv === undefined ? {} : { childEnv }),
|
|
508
532
|
...(options.onSpawn === undefined ? {} : { onSpawn: options.onSpawn }),
|
|
533
|
+
...(options.onSpawnFailed === undefined ? {} : { onSpawnFailed: options.onSpawnFailed }),
|
|
509
534
|
...(options.owner === undefined ? {} : { owner: options.owner }),
|
|
510
535
|
...(options.beforeSpawn === undefined ? {} : { beforeSpawn: options.beforeSpawn }),
|
|
511
536
|
...(options.onStdout === undefined ? {} : { onStdout: options.onStdout }),
|
|
@@ -553,7 +578,7 @@ function runBufferedGroup(file, args, bag) {
|
|
|
553
578
|
let container = null;
|
|
554
579
|
let attached;
|
|
555
580
|
try {
|
|
556
|
-
const spawned = spawnContained(file, args, { cwd: bag.cwd, env: bag.childEnv, stdio: [bag.stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"], detached: process.platform !== "win32" }, bag.owner ?? "buffered", { beforeSpawn: bag.beforeSpawn, onSpawn: bag.onSpawn, onContainer: bag.onContainer, onContainerEmpty: bag.onContainerEmpty, onUnknown: bag.onUnknown, fence: bag.fence });
|
|
581
|
+
const spawned = spawnContained(file, args, { cwd: bag.cwd, env: bag.childEnv, stdio: [bag.stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"], detached: process.platform !== "win32" }, bag.owner ?? "buffered", { beforeSpawn: bag.beforeSpawn, onSpawn: bag.onSpawn, onSpawnFailed: bag.onSpawnFailed, onContainer: bag.onContainer, onContainerEmpty: bag.onContainerEmpty, onUnknown: bag.onUnknown, fence: bag.fence });
|
|
557
582
|
child = spawned.child;
|
|
558
583
|
container = spawned.container;
|
|
559
584
|
attached = spawned.attached;
|
|
@@ -1578,6 +1603,7 @@ export function startClaudeHeldSession(file, args, options) {
|
|
|
1578
1603
|
}, options.owner ?? "held", {
|
|
1579
1604
|
beforeSpawn: options.beforeSpawn,
|
|
1580
1605
|
onSpawn: options.onSpawn,
|
|
1606
|
+
onSpawnFailed: options.onSpawnFailed,
|
|
1581
1607
|
onContainer: options.onContainer,
|
|
1582
1608
|
onContainerEmpty: options.onContainerEmpty,
|
|
1583
1609
|
onUnknown: options.onUnknown,
|
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" | "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
|
@@ -129,13 +129,15 @@ export const JOURNEY_STEPS = [["plan", "Plan"], ["approve", "You approve"], ["bu
|
|
|
129
129
|
*/
|
|
130
130
|
export function firstTaskJourney(read, approved, built = false) {
|
|
131
131
|
const at = read.stage === "finished" || read.stage === "complete" ? { index: 5, stuck: false }
|
|
132
|
-
: read.stage === "checking" ? { index: 3, stuck: false }
|
|
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/invoke.js
CHANGED
|
@@ -646,6 +646,11 @@ export async function invokeHeldAgent(store, runId, spec, argv, options) {
|
|
|
646
646
|
store.recordRunProcess(runId, pid, clock(), false, heldWitness);
|
|
647
647
|
runOptions.onSpawn?.(pid);
|
|
648
648
|
},
|
|
649
|
+
onSpawnFailed: () => {
|
|
650
|
+
if (heldWitness !== undefined)
|
|
651
|
+
store.finishUnspawnedProcess(heldWitness, clock());
|
|
652
|
+
runOptions.onSpawnFailed?.();
|
|
653
|
+
},
|
|
649
654
|
onContainer: info => {
|
|
650
655
|
nativeHeld = true;
|
|
651
656
|
if (heldWitness !== undefined)
|
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;
|