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/work-index.d.ts
CHANGED
|
@@ -66,3 +66,14 @@ export declare class WorkIndexCursorError extends Error {
|
|
|
66
66
|
* Cursors bind the admitted scope, project and view, never broaden access. */
|
|
67
67
|
export declare function workIndexPage(store: Store, now: Date, access: WorkSummaryAccess, options?: WorkIndexOptions): WorkIndexPage;
|
|
68
68
|
export declare function workCountsByProject(store: Store, now: Date, access: WorkSummaryAccess): WorkProjectCounts[];
|
|
69
|
+
/** Families whose headline is Ready for review, newest first: the Tasks
|
|
70
|
+
* list's own rows, so a completed family (root or any revision), a failed
|
|
71
|
+
* check and a superseded version never count. */
|
|
72
|
+
export declare function workReadyForReview(store: Store, now: Date, access: WorkSummaryAccess, limit?: number): {
|
|
73
|
+
count: number;
|
|
74
|
+
results: {
|
|
75
|
+
rootId: string;
|
|
76
|
+
taskId: string;
|
|
77
|
+
run: number | null;
|
|
78
|
+
}[];
|
|
79
|
+
};
|
package/dist/work-index.js
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
/** Paginated, permission-bound summaries of saved task facts. These reads do
|
|
2
|
-
* not verify artifacts
|
|
2
|
+
* not verify artifacts or authorize task operations; a build Toolroll can't
|
|
3
|
+
* yet confirm stopped is read through the store's own read-only probe. */
|
|
3
4
|
import { createHash } from 'node:crypto';
|
|
5
|
+
import { existsSync } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
4
7
|
import { readProjectAccess } from './project-access.js';
|
|
5
8
|
import { pauseOnTaskProviders, providerName, signInReason } from './provider-auth.js';
|
|
6
|
-
import { scopeTermsProblem } from './store.js';
|
|
9
|
+
import { scopeTermsProblem, stopFactOf } from './store.js';
|
|
7
10
|
import { plainReasonOf, replacedWords, stageOfCode, taskStatusOf, workToneOf } from './task-status.js';
|
|
8
11
|
import { isCheckLevel } from './check-levels.js';
|
|
12
|
+
import { MARKER } from './worktree.js';
|
|
9
13
|
import { withFollowUps } from './result-follow-ups.js';
|
|
14
|
+
import { NEEDS, processNeedOf } from './needs-you.js';
|
|
10
15
|
export const WORK_INDEX_PAGE_LIMIT = 40;
|
|
11
16
|
export const WORK_INDEX_MAX_LIMIT = 100;
|
|
12
17
|
const EMPTY = { all: 0, 'needs-you': 0, running: 0, completed: 0 };
|
|
@@ -20,6 +25,8 @@ const s = (row, key) => row[key] == null ? null : String(row[key]);
|
|
|
20
25
|
* MATERIALIZED prevents counts + page selection repeating correlated reads. */
|
|
21
26
|
/** A gate note counts only while its pause is on one of the task's current providers. */
|
|
22
27
|
const GATE_PAUSE_ON_TASK = pauseOnTaskProviders('p', 'c.ref_id');
|
|
28
|
+
/** A row whose build may still be running (the store's reading, custodyReadings): Waiting, not Needs you. */
|
|
29
|
+
const CUSTODY_WAITS = `(custody_unresolved AND code IN ('process-needs-attention','result-needs-attention') AND ref_id IN (SELECT value FROM json_each($waitingCustody)))`;
|
|
23
30
|
const PROJECTION = `WITH RECURSIVE admitted AS MATERIALIZED (
|
|
24
31
|
SELECT t.id, t.title, t.state, t.created_at, t.updated_at, r.id ref_id, r.repo,
|
|
25
32
|
r.revision_of, r.revision_brief_artifact, r.assigned_runner, r.plan,
|
|
@@ -55,7 +62,8 @@ const PROJECTION = `WITH RECURSIVE admitted AS MATERIALIZED (
|
|
|
55
62
|
(SELECT id FROM run WHERE task_ref=a.ref_id AND outcome IS NULL ORDER BY id DESC LIMIT 1) unfinished,
|
|
56
63
|
CASE WHEN a.state='done' THEN (SELECT id FROM run WHERE task_ref=a.ref_id AND finished_at IS NOT NULL AND role IN ('builder','scout') ORDER BY id DESC LIMIT 1) END result_id,
|
|
57
64
|
(SELECT h.id FROM hold h WHERE h.task_ref=a.ref_id AND (h.until IS NULL OR h.until>$now) ORDER BY h.held_at,h.id LIMIT 1) hold_id,
|
|
58
|
-
q.decision_id,cu.task_ref IS NOT NULL
|
|
65
|
+
q.decision_id,(cu.task_ref IS NOT NULL OR a.ref_id IN (SELECT value FROM json_each($workspaceCustody)))
|
|
66
|
+
AND a.ref_id NOT IN (SELECT value FROM json_each($settledCustody)) custody_unresolved
|
|
59
67
|
FROM admitted a LEFT JOIN lineage l ON l.ref_id=a.ref_id
|
|
60
68
|
LEFT JOIN questions q ON q.task_ref=a.ref_id LEFT JOIN custody cu ON cu.task_ref=a.ref_id
|
|
61
69
|
), family_heads AS MATERIALIZED (
|
|
@@ -197,9 +205,9 @@ const PROJECTION = `WITH RECURSIVE admitted AS MATERIALIZED (
|
|
|
197
205
|
ELSE 'queued' END code
|
|
198
206
|
FROM checked f
|
|
199
207
|
), ranked AS MATERIALIZED (
|
|
200
|
-
SELECT c.*,code NOT IN ('cancelled','complete','running','updating','retry-scheduled','waiting-dependency','worker-at-capacity','planning-ready','scouting-ready','queued') needs,
|
|
208
|
+
SELECT c.*,code NOT IN ('cancelled','complete','running','updating','retry-scheduled','waiting-dependency','worker-at-capacity','planning-ready','scouting-ready','queued') AND NOT ${CUSTODY_WAITS} needs,
|
|
201
209
|
CASE WHEN code='complete' THEN 3 WHEN code='cancelled' THEN 4 WHEN code='running' THEN 1
|
|
202
|
-
WHEN code IN ('updating','retry-scheduled','waiting-dependency','worker-at-capacity','planning-ready','scouting-ready','queued') THEN 2 ELSE 0 END rank,
|
|
210
|
+
WHEN ${CUSTODY_WAITS} OR code IN ('updating','retry-scheduled','waiting-dependency','worker-at-capacity','planning-ready','scouting-ready','queued') THEN 2 ELSE 0 END rank,
|
|
203
211
|
CASE WHEN code='complete' THEN completed.at ELSE family_updated END sort_at,
|
|
204
212
|
completed.actor checked_actor,completed.at checked_at,completed.outcome checked_digest
|
|
205
213
|
FROM classified c LEFT JOIN action_ledger completed ON completed.id=c.checked_id WHERE ($state IS NULL OR c.state=$state) AND ($leadId IS NULL OR EXISTS(SELECT 1 FROM team_task_owner own WHERE own.task_ref=c.root_ref AND own.lead=$leadId))
|
|
@@ -235,8 +243,38 @@ function registerValidators(store) {
|
|
|
235
243
|
registered.set(db, projection);
|
|
236
244
|
return projection;
|
|
237
245
|
}
|
|
238
|
-
|
|
239
|
-
|
|
246
|
+
/** The list can't see processes: each task whose build Toolroll can't yet
|
|
247
|
+
* confirm stopped, as the store reads it now (the task page's own reading).
|
|
248
|
+
* Null when it has since settled; `open` runs are the classification's own.
|
|
249
|
+
* `workspace`: a finished task whose last build's workspace note is still
|
|
250
|
+
* there, which only the store can read (a held or unreadable workspace). */
|
|
251
|
+
function custodyReadings(store) {
|
|
252
|
+
const refs = store.handle.prepare(`SELECT r.task_ref FROM held_session h JOIN run r ON r.id=h.run WHERE h.ended_at IS NULL
|
|
253
|
+
UNION SELECT r.task_ref FROM run r WHERE r.provider_started_at IS NOT NULL AND NOT EXISTS(SELECT 1 FROM run_process p WHERE p.run=r.id)
|
|
254
|
+
UNION SELECT r.task_ref FROM run_process p JOIN run r ON r.id=p.run WHERE p.exited_at IS NULL AND p.container_empty_at IS NULL`).all().map(row => Number(row['task_ref']));
|
|
255
|
+
const noted = store.handle.prepare(`SELECT r.task_ref,r.worktree FROM task t JOIN task_ref tr ON tr.external_id=t.id
|
|
256
|
+
JOIN run r ON r.id=(SELECT MAX(id) FROM run WHERE task_ref=tr.id AND role IN ('builder','scout') AND finished_at IS NOT NULL)
|
|
257
|
+
WHERE t.state='done' AND r.worktree IS NOT NULL`).all()
|
|
258
|
+
.filter(row => existsSync(join(String(row['worktree']), MARKER))).map(row => Number(row['task_ref']));
|
|
259
|
+
const readings = new Map();
|
|
260
|
+
for (const ref of new Set([...refs, ...noted])) {
|
|
261
|
+
let found = null;
|
|
262
|
+
for (const run of store.runsFor(ref)) {
|
|
263
|
+
const fact = stopFactOf(store, run.id);
|
|
264
|
+
if (fact !== null && fact.kind !== 'open') {
|
|
265
|
+
found = fact;
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
readings.set(ref, found);
|
|
270
|
+
}
|
|
271
|
+
return { readings, workspace: new Set(noted) };
|
|
272
|
+
}
|
|
273
|
+
function parameters(now, access, options, custody) {
|
|
274
|
+
const ids = (keep) => JSON.stringify([...custody.readings].filter(([ref, fact]) => keep(ref, fact)).map(([ref]) => ref));
|
|
275
|
+
return { $settledCustody: ids((_, fact) => fact === null), $waitingCustody: ids((_, fact) => fact !== null && 'wait' in (processNeedOf(fact) ?? {})),
|
|
276
|
+
$workspaceCustody: ids((ref, fact) => fact !== null && custody.workspace.has(ref)),
|
|
277
|
+
$now: now.toISOString(), $alive: new Date(now.getTime() - 180_000).toISOString(),
|
|
240
278
|
$all: access.repos === null ? 1 : 0, $repos: JSON.stringify(access.repos ?? []),
|
|
241
279
|
$unplaced: access.principal === 'operator' && access.includeUnplaced === true ? 1 : 0,
|
|
242
280
|
$projectSet: options.project != null ? 1 : 0, $project: options.project ?? null, $state: options.state ?? null, $leadId: options.leadId ?? null };
|
|
@@ -279,6 +317,7 @@ function readCursor(value, scope) {
|
|
|
279
317
|
* Cursors bind the admitted scope, project and view, never broaden access. */
|
|
280
318
|
export function workIndexPage(store, now, access, options = {}) {
|
|
281
319
|
const projection = registerValidators(store);
|
|
320
|
+
const custody = custodyReadings(store);
|
|
282
321
|
const view = VIEWS.includes(options.view ?? 'all') ? options.view ?? 'all' : 'all';
|
|
283
322
|
const limit = Number.isFinite(options.limit) ? Math.max(1, Math.min(WORK_INDEX_MAX_LIMIT, Math.floor(options.limit))) : WORK_INDEX_PAGE_LIMIT;
|
|
284
323
|
const scope = cursorScope(access, options, view), cursor = readCursor(options.cursor, scope);
|
|
@@ -309,21 +348,39 @@ export function workIndexPage(store, now, access, options = {}) {
|
|
|
309
348
|
'question_run',(SELECT run FROM decision WHERE id=page.question_id),'replaced_by',(SELECT successor FROM task_replacement WHERE task_ref=page.ref_id),
|
|
310
349
|
'completed_by_lead',(SELECT lead FROM task_act WHERE task_ref=page.root_ref AND act='completed' ORDER BY id DESC LIMIT 1),
|
|
311
350
|
'question_task',(SELECT r.external_id FROM decision d JOIN run ON run.id=d.run JOIN task_ref r ON r.id=run.task_ref WHERE d.id=page.question_id)) FROM page`)
|
|
312
|
-
.all({ ...parameters(now, access, options), $cursorRoot: cursor?.root ?? 0, $cursorRank: cursor?.rank ?? 0, $cursorAt: cursor?.at ?? '', $limit: limit + 1, $recent: new Date(now.getTime() - 86_400_000).toISOString() });
|
|
351
|
+
.all({ ...parameters(now, access, options, custody), $cursorRoot: cursor?.root ?? 0, $cursorRank: cursor?.rank ?? 0, $cursorAt: cursor?.at ?? '', $limit: limit + 1, $recent: new Date(now.getTime() - 86_400_000).toISOString() });
|
|
313
352
|
const selected = rows.slice(1).map(row => JSON.parse(String(row['row_json'])));
|
|
314
353
|
const page = selected.slice(0, limit), last = page.at(-1);
|
|
315
354
|
const nextCursor = selected.length > limit && last !== undefined ? Buffer.from(JSON.stringify({ version: 1, scope,
|
|
316
355
|
rank: n(last, 'rank'), at: s(last, 'sort_at'), root: n(last, 'root_ref') })).toString('base64url') : null;
|
|
317
356
|
const projects = JSON.parse(String(rows[0]?.['row_json'] ?? '[]')).map(row => ({ repo: s(row, 'repo'), totals: counts(row), queued: n(row, 'queued'), doneRecently: n(row, 'done_recently') }));
|
|
318
|
-
|
|
357
|
+
// A row whose build Toolroll can't confirm stopped reads the store's own answer, as the task page does.
|
|
358
|
+
const probe = (taskId) => {
|
|
359
|
+
const ref = store.lookupRef(taskId);
|
|
360
|
+
return ref === null ? null : custody.readings.get(ref.id) ?? null;
|
|
361
|
+
};
|
|
362
|
+
return { items: page.map(row => itemOf(row, access.principal, probe)), projects, totals: rows[0] === undefined ? { ...EMPTY } : counts(rows[0]), nextCursor, limit, view };
|
|
319
363
|
}
|
|
320
364
|
export function workCountsByProject(store, now, access) {
|
|
321
365
|
const projection = registerValidators(store);
|
|
322
366
|
return store.handle.prepare(`${projection} SELECT repo,${TOTALS},SUM(state='queued' AND family_running=0) queued,
|
|
323
367
|
SUM(state='done' AND family_running=0 AND updated_at>=$recent) done_recently FROM ranked GROUP BY repo ORDER BY repo`)
|
|
324
|
-
.all({ ...parameters(now, access, {}), $recent: new Date(now.getTime() - 86_400_000).toISOString() })
|
|
368
|
+
.all({ ...parameters(now, access, {}, custodyReadings(store)), $recent: new Date(now.getTime() - 86_400_000).toISOString() })
|
|
325
369
|
.map(row => ({ repo: s(row, 'repo'), totals: counts(row), queued: n(row, 'queued'), doneRecently: n(row, 'done_recently') }));
|
|
326
370
|
}
|
|
371
|
+
/** Families whose headline is Ready for review, newest first: the Tasks
|
|
372
|
+
* list's own rows, so a completed family (root or any revision), a failed
|
|
373
|
+
* check and a superseded version never count. */
|
|
374
|
+
export function workReadyForReview(store, now, access, limit = 5) {
|
|
375
|
+
const ready = [];
|
|
376
|
+
let cursor = null;
|
|
377
|
+
do {
|
|
378
|
+
const page = workIndexPage(store, now, access, { view: 'needs-you', limit: WORK_INDEX_MAX_LIMIT, cursor });
|
|
379
|
+
ready.push(...page.items.filter(one => one.status.label === 'Ready for review'));
|
|
380
|
+
cursor = page.nextCursor;
|
|
381
|
+
} while (cursor !== null);
|
|
382
|
+
return { count: ready.length, results: ready.slice(0, Math.max(0, limit)).map(one => ({ rootId: one.rootId, taskId: one.activeTaskId, run: one.resultRunId })) };
|
|
383
|
+
}
|
|
327
384
|
/** Labels stay navigation, never an authorization to mutate. Full task
|
|
328
385
|
* opening re-proves process custody, receipt bytes and approval terms. */
|
|
329
386
|
/** The list's Checks: the build's own result, its level, and follow-up checks on the same commit (result-follow-ups.ts). */
|
|
@@ -347,9 +404,16 @@ function listChecksOf(own, level, follows) {
|
|
|
347
404
|
return null;
|
|
348
405
|
return { status: read.status, exitCode: null, head: null, level: read.level, running: read.running };
|
|
349
406
|
}
|
|
350
|
-
|
|
407
|
+
/** Index codes that wait on a person, as their need (needs-you.ts); dispatch codes come from task-status's own table. */
|
|
408
|
+
const INDEX_NEED = {
|
|
409
|
+
'earlier-active': 'earlier-version', 'process-needs-attention': 'confirm-stopped', 'invalid-scope': 'define-task', 'decision-queue': 'questions',
|
|
410
|
+
};
|
|
411
|
+
function itemOf(row, principal, probe) {
|
|
351
412
|
const code = String(row['code']), id = String(row['id']);
|
|
352
|
-
const
|
|
413
|
+
const custody = code === 'process-needs-attention' || (code === 'result-needs-attention' && n(row, 'custody_unresolved') === 1);
|
|
414
|
+
const process = custody && probe !== undefined ? processNeedOf(probe(id)) : null;
|
|
415
|
+
const waiting = process !== null && 'wait' in process;
|
|
416
|
+
const need = n(row, 'needs') === 1 && !waiting, running = n(row, 'family_running') === 1;
|
|
353
417
|
const assignmentState = code === 'complete' ? 'complete' : code === 'ready-to-check' ? 'ready-to-check'
|
|
354
418
|
: code === 'cancelled' ? 'cancelled' : need ? 'needs-decision' : 'working';
|
|
355
419
|
const words = {
|
|
@@ -375,7 +439,7 @@ function itemOf(row, principal) {
|
|
|
375
439
|
'needs-scope': ['Needs your decision', 'Define the task before a worker can start.', 'write-scope', 'Define the task'],
|
|
376
440
|
'invalid-scope': ['Needs your decision', 'The saved task scope cannot be read safely.', 'write-scope', 'Review scope'],
|
|
377
441
|
'needs-agent-profile': ['Needs your decision', 'Choose an available agent for this task.', 'select-agent', 'Choose an agent'],
|
|
378
|
-
'needs-approval': ['Needs your decision', 'Review the current plan before a worker can start.', 'approve-scope', '
|
|
442
|
+
'needs-approval': ['Needs your decision', 'Review the current plan before a worker can start.', 'approve-scope', 'Approve plan'],
|
|
379
443
|
'missing-requirement': ['Needs your decision', 'A required capability is missing or expired.', 'repair-capability', 'Review missing requirement'],
|
|
380
444
|
'no-worker-registered': ['Needs your decision', 'No connected builder includes this project.', 'start-worker', 'Check connection'],
|
|
381
445
|
'no-worker-online': ['Needs your decision', 'The builder has stopped checking in.', 'start-worker', 'Check connection'],
|
|
@@ -409,6 +473,18 @@ function itemOf(row, principal) {
|
|
|
409
473
|
actionCode = 'open-result';
|
|
410
474
|
actionLabel = 'Open result';
|
|
411
475
|
}
|
|
476
|
+
if (process !== null && 'need' in process) {
|
|
477
|
+
actionCode = 'confirm-stopped';
|
|
478
|
+
actionLabel = NEEDS[process.need].action.label;
|
|
479
|
+
}
|
|
480
|
+
else if (custody && probe === undefined) {
|
|
481
|
+
actionCode = 'confirm-stopped';
|
|
482
|
+
actionLabel = NEEDS['confirm-stopped'].action.label;
|
|
483
|
+
}
|
|
484
|
+
if (code === 'signed-out') {
|
|
485
|
+
actionCode = 'sign-in';
|
|
486
|
+
actionLabel = NEEDS['sign-in'].action.label;
|
|
487
|
+
}
|
|
412
488
|
if (actionCode === null) {
|
|
413
489
|
actionCode = code === 'running' ? 'inspect-run' : 'inspect-task';
|
|
414
490
|
actionLabel = code === 'running' ? 'Watch the build' : 'View task details';
|
|
@@ -417,21 +493,32 @@ function itemOf(row, principal) {
|
|
|
417
493
|
const primaryAction = actionCode === null ? null : { code: actionCode, label: actionLabel,
|
|
418
494
|
target: { taskId: successor ?? (code === 'earlier-active' ? s(row, 'earlier_id') ?? id : actionCode === 'answer-decision' ? s(row, 'question_task') ?? id : id),
|
|
419
495
|
runId: actionCode === 'answer-decision' ? n(row, 'question_run') || null : actionCode === 'inspect-run' && code === 'running' ? n(row, 'live_run') || null : actionCode === 'open-result' || actionCode === 'inspect-run' ? n(row, 'result_id') || null
|
|
420
|
-
: actionCode === 'reconcile-run' ? n(row, 'unfinished') || null : ['resume-run', 'inspect-stop'].includes(actionCode) ? n(row, 'stop_run') || null
|
|
496
|
+
: actionCode === 'reconcile-run' ? n(row, 'unfinished') || null : ['resume-run', 'inspect-stop'].includes(actionCode) ? n(row, 'stop_run') || null
|
|
497
|
+
: actionCode === 'confirm-stopped' ? process?.build ?? (n(row, 'result_id') || null) : null,
|
|
421
498
|
decisionId: actionCode === 'answer-decision' ? n(row, 'question_id') || null : null },
|
|
422
499
|
access: read ? 'read' : principal === 'operator' ? 'operator-control' : ['answer-decision', 'write-scope', 'unhold'].includes(actionCode) ? 'proposal-only' : 'operator-handoff',
|
|
423
500
|
retry: actionCode === 'reconcile-run' ? 'reconcile-before-retry' : read ? 'read-again' : 'refresh-before-acting' };
|
|
424
501
|
// The one shared status: the headline and sentence every surface uses.
|
|
425
|
-
const
|
|
502
|
+
const coded = stageOfCode(code, { needsPerson: need, planning: s(row, 'live_role') === 'planner', operatorHold: s(row, 'hold_kind') === 'operator' });
|
|
503
|
+
const reading = process !== null
|
|
504
|
+
? 'wait' in process ? { stage: 'waiting', wait: process.wait } : { stage: 'needs-you', need: process.need }
|
|
505
|
+
: custody ? { stage: 'needs-you', need: 'confirm-stopped' }
|
|
506
|
+
: coded.stage === 'needs-you' && (coded.need === undefined || coded.need === 'other') && INDEX_NEED[code] !== undefined ? { stage: 'needs-you', need: INDEX_NEED[code] } : coded;
|
|
426
507
|
const checkStatus = s(row, 'check_status'), verdict = s(row, 'proof_verdict');
|
|
427
508
|
const finished = reading.stage === 'finished' || reading.stage === 'complete';
|
|
428
509
|
const own = checkStatus === 'failed' || n(row, 'proof_failed_check') === 1 ? 'failed' : checkStatus === 'passed' || verdict === 'verified' ? 'passed' : null;
|
|
429
|
-
const shared = taskStatusOf({ stage: reading.stage, ...(reading.need === undefined ? {} : { need: reading.need }),
|
|
510
|
+
const shared = taskStatusOf({ stage: reading.stage, ...(reading.need === undefined ? {} : { need: reading.need }), ...(reading.wait === undefined ? {} : { wait: reading.wait }),
|
|
511
|
+
...(process === null ? {} : { needContext: { build: process.build } }),
|
|
430
512
|
reason: finished ? null : plainReasonOf(reading.stage, code, detail || null), report: s(row, 'result_role') === 'scout',
|
|
431
513
|
checks: !finished ? null : listChecksOf(own, s(row, 'check_level'), row['check_follows']),
|
|
432
514
|
completedBy: code !== 'complete' ? null : n(row, 'completed_by_lead') === 1 ? 'the lead' : String(row['checked_actor']).replace(/^(?:operator|coordinator|lead):/, '') });
|
|
433
515
|
label = shared.headline;
|
|
434
516
|
detail = shared.sentence;
|
|
517
|
+
// Every Needs you action wears its need's own words.
|
|
518
|
+
if (shared.need != null && shared.need.key !== 'other' && shared.need.key !== 'review-result' && primaryAction !== null) {
|
|
519
|
+
actionLabel = shared.need.action.label;
|
|
520
|
+
primaryAction.label = actionLabel;
|
|
521
|
+
}
|
|
435
522
|
const views = ['all'];
|
|
436
523
|
if (need)
|
|
437
524
|
views.push('needs-you');
|
package/dist/work-summary.d.ts
CHANGED
|
@@ -8,7 +8,9 @@ export type WorkTarget = {
|
|
|
8
8
|
decisionId: number | null;
|
|
9
9
|
};
|
|
10
10
|
export type WorkAction = {
|
|
11
|
-
|
|
11
|
+
/** `confirm-stopped`: an approver confirms a finished build stopped (behind the password);
|
|
12
|
+
* `sign-in`: the agent's sign-in, in Settings. */
|
|
13
|
+
code: DispatchAction | "inspect-task" | "inspect-run" | "inspect-stop" | "inspect-decisions" | "reconcile-run" | "open-pr" | "confirm-stopped" | "sign-in";
|
|
12
14
|
label: string;
|
|
13
15
|
target: WorkTarget;
|
|
14
16
|
access: "read" | "operator-control" | "proposal-only" | "operator-handoff";
|
package/dist/work-summary.js
CHANGED
|
@@ -6,6 +6,7 @@ import { resultCheckLevel } from "./result-follow-ups.js";
|
|
|
6
6
|
import { diagnoseTaskDispatch } from "./dispatch.js";
|
|
7
7
|
import { isAlive } from "./runner.js";
|
|
8
8
|
import { taskControlOf } from "./task-control.js";
|
|
9
|
+
import { buildReviewOf } from "./review-switch.js";
|
|
9
10
|
import { reviewFactsOf, workStatusOf } from "./workspace-ui.js";
|
|
10
11
|
/** Call only after admitting the task's project. Keep the question's
|
|
11
12
|
* exact run and id; never substitute a family's newest attempt. Expiry
|
|
@@ -103,7 +104,7 @@ export function taskWorkSummaryOf(store, taskId, now, access) {
|
|
|
103
104
|
review: reviewFactsOf(store.reviewRetryStateOf(result.id), name => {
|
|
104
105
|
const runner = store.getRunner(name)?.runner;
|
|
105
106
|
return runner !== undefined && isAlive(runner, now);
|
|
106
|
-
}),
|
|
107
|
+
}, buildReviewOf(store, result.id)?.state === "pending"),
|
|
107
108
|
...(result.outcome !== "no-change" ? {} : { recordComplete: (() => {
|
|
108
109
|
const kinds = new Set(store.artifactsFor(result.id).map(one => one.kind));
|
|
109
110
|
return kinds.has("handoff") && kinds.has("terminal-diff");
|
package/dist/workspace-ui.d.ts
CHANGED
|
@@ -68,11 +68,13 @@ export type ReviewFacts = {
|
|
|
68
68
|
queuedOrigin?: "operator" | "automatic" | null;
|
|
69
69
|
/** Whether the live attempt's reviewer worker is answering. */
|
|
70
70
|
reviewerAlive: boolean;
|
|
71
|
+
/** The build's one automatic review is still pending (build-review.ts). */
|
|
72
|
+
automaticPending?: boolean;
|
|
71
73
|
};
|
|
72
74
|
/** The review facts from the store's bounded retry projection — the same
|
|
73
75
|
* reading `diagnoseTaskDispatch` makes, so every surface starts from one
|
|
74
76
|
* record. `reviewerAlive` answers for the live attempt's runner. */
|
|
75
|
-
export declare function reviewFactsOf(retry: ReviewRetryState | null, reviewerAlive: (runner: string) => boolean): ReviewFacts | null;
|
|
77
|
+
export declare function reviewFactsOf(retry: ReviewRetryState | null, reviewerAlive: (runner: string) => boolean, automaticPending?: boolean): ReviewFacts | null;
|
|
76
78
|
/** The tokens a review in flight wears — the dispatch codes, unchanged. */
|
|
77
79
|
export declare const REVIEW_TOKENS: ReadonlySet<string>;
|
|
78
80
|
/**
|
package/dist/workspace-ui.js
CHANGED
|
@@ -30,7 +30,7 @@ export function parseWorkView(raw) {
|
|
|
30
30
|
/** The review facts from the store's bounded retry projection — the same
|
|
31
31
|
* reading `diagnoseTaskDispatch` makes, so every surface starts from one
|
|
32
32
|
* record. `reviewerAlive` answers for the live attempt's runner. */
|
|
33
|
-
export function reviewFactsOf(retry, reviewerAlive) {
|
|
33
|
+
export function reviewFactsOf(retry, reviewerAlive, automaticPending = false) {
|
|
34
34
|
if (retry === null || retry.state === "unrequested")
|
|
35
35
|
return null;
|
|
36
36
|
const current = retry.live ?? (retry.state === "queued" ? null : retry.latest);
|
|
@@ -45,6 +45,7 @@ export function reviewFactsOf(retry, reviewerAlive) {
|
|
|
45
45
|
queuedBy: retry.openRequest?.requestedBy ?? null,
|
|
46
46
|
queuedOrigin: retry.openRequest?.origin ?? null,
|
|
47
47
|
reviewerAlive: retry.live !== null && reviewerAlive(retry.live.runner),
|
|
48
|
+
automaticPending,
|
|
48
49
|
};
|
|
49
50
|
}
|
|
50
51
|
/** The tokens a review in flight wears — the dispatch codes, unchanged. */
|
|
@@ -59,12 +60,12 @@ const retriesLeft = (count, explicit = true) => `${count}${explicit ? " explicit
|
|
|
59
60
|
* the v50 dispatch contract; nothing here changes the lifecycle.
|
|
60
61
|
*/
|
|
61
62
|
export function reviewStatusOf(review) {
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
return
|
|
63
|
+
// The one automatic review (build-review.ts) holds the result only while it
|
|
64
|
+
// is queued or running. A failed review never blocks: the result reads as
|
|
65
|
+
// it would unreviewed, marked "not reviewed" by the result itself.
|
|
66
|
+
if (review === null || review.automaticPending !== true || (review.state !== "queued" && review.state !== "running"))
|
|
67
|
+
return null;
|
|
68
|
+
return { token: "reviewing", label: "Reviewing", detail: "An automatic review is reading this result before it reaches you.", tone: "live", action: { label: "Open the result", kind: "open-result" } };
|
|
68
69
|
}
|
|
69
70
|
const FAILED_CHECK = /^the repository's approved verification command exited (-?[0-9]+)/;
|
|
70
71
|
export function evidenceProblemOf(verdict, reasons) {
|
|
@@ -123,7 +124,7 @@ export function publicationStatusOf(publication) {
|
|
|
123
124
|
* the detail).
|
|
124
125
|
*/
|
|
125
126
|
export function resultStatusOf(result, publication = null) {
|
|
126
|
-
return storedResultStatusOf(result, publication);
|
|
127
|
+
return (result?.role === "builder" ? reviewStatusOf(result.review ?? null) : null) ?? storedResultStatusOf(result, publication);
|
|
127
128
|
}
|
|
128
129
|
/** What the machine itself recorded about a result, in a clause. */
|
|
129
130
|
function machineVerdictWords(result) {
|
|
@@ -290,7 +291,7 @@ const DISPATCH_ACTION_LABELS = {
|
|
|
290
291
|
"place-task": "Choose a project",
|
|
291
292
|
"write-scope": "Define the task",
|
|
292
293
|
"select-agent": "Choose an agent",
|
|
293
|
-
"approve-scope": "
|
|
294
|
+
"approve-scope": "Approve plan",
|
|
294
295
|
"answer-decision": "Answer the question",
|
|
295
296
|
unhold: "Review hold",
|
|
296
297
|
"inspect-hold": "Review hold",
|
package/dist/worktree.d.ts
CHANGED
|
@@ -76,6 +76,8 @@ export type KeptWhy = "has changes" | "has commits" | "unreadable" | "git refuse
|
|
|
76
76
|
* something is in there.
|
|
77
77
|
*/
|
|
78
78
|
export declare const MARKER = ".standing-orders-lease";
|
|
79
|
+
/** What a kept checkout drops once its run ends (only where .gitignore hides it): dependencies and build output. */
|
|
80
|
+
export declare const SLIM_NAMES: readonly string[];
|
|
79
81
|
/** Creating a worktree copies a tree; it is local work but not instant. */
|
|
80
82
|
export declare const WORKTREE_TIMEOUT_MS = 60000;
|
|
81
83
|
/**
|
|
@@ -251,6 +253,20 @@ export declare class WorktreePool {
|
|
|
251
253
|
why: KeptWhy;
|
|
252
254
|
}[];
|
|
253
255
|
}>;
|
|
256
|
+
/**
|
|
257
|
+
* Drop a let-go, idle checkout's dependencies and build output: the folders .gitignore hides that are named like
|
|
258
|
+
* them (SLIM_NAMES). Its work, tracked or untracked, is untouched. The setup stamp goes first, so the next lease
|
|
259
|
+
* runs the project's setup again and restores them. `wanted` is asked again right before anything goes; `deps`
|
|
260
|
+
* false keeps node_modules (no setup would restore it).
|
|
261
|
+
*/
|
|
262
|
+
slim(row: WorktreeRow, wanted: (row: WorktreeRow) => boolean, deps?: boolean): Promise<{
|
|
263
|
+
ok: true;
|
|
264
|
+
dropped: string[];
|
|
265
|
+
bytes: number;
|
|
266
|
+
} | {
|
|
267
|
+
ok: false;
|
|
268
|
+
message: string;
|
|
269
|
+
}>;
|
|
254
270
|
/**
|
|
255
271
|
* Throw away a let-go checkout kept for its changes, on purpose: the working copy and what is uncommitted in it
|
|
256
272
|
* go; its branch, and every commit on it, stays. Refused while anything holds it.
|
package/dist/worktree.js
CHANGED
|
@@ -30,6 +30,7 @@ import { run } from "./exec.js";
|
|
|
30
30
|
import { currentBootId, provenDeadByBootChange } from "./boot-identity.js";
|
|
31
31
|
import { HANDOFF_PREFIX, MAILBOX_SUFFIX, looksLikeProtocolFile, readMailbox } from "./evidence.js";
|
|
32
32
|
import { parseHandoff } from "./decision.js";
|
|
33
|
+
import { diskBytes } from "./storage.js";
|
|
33
34
|
/**
|
|
34
35
|
* The note a lease leaves in the checkout, naming the process holding it.
|
|
35
36
|
*
|
|
@@ -40,6 +41,8 @@ import { parseHandoff } from "./decision.js";
|
|
|
40
41
|
export const MARKER = ".standing-orders-lease";
|
|
41
42
|
/** Checkouts this process is leasing or removing right now: a lease and storage retention never overlap on one. */
|
|
42
43
|
const busy = new Map();
|
|
44
|
+
/** What a kept checkout drops once its run ends (only where .gitignore hides it): dependencies and build output. */
|
|
45
|
+
export const SLIM_NAMES = ["node_modules", "dist", "build", "out", ".next", ".nuxt", ".svelte-kit", ".turbo", ".parcel-cache", "coverage", "target"];
|
|
43
46
|
/** Creating a worktree copies a tree; it is local work but not instant. */
|
|
44
47
|
export const WORKTREE_TIMEOUT_MS = 60_000;
|
|
45
48
|
const GIT = "git";
|
|
@@ -706,6 +709,50 @@ export class WorktreePool {
|
|
|
706
709
|
}
|
|
707
710
|
return { removed, kept };
|
|
708
711
|
}
|
|
712
|
+
/**
|
|
713
|
+
* Drop a let-go, idle checkout's dependencies and build output: the folders .gitignore hides that are named like
|
|
714
|
+
* them (SLIM_NAMES). Its work, tracked or untracked, is untouched. The setup stamp goes first, so the next lease
|
|
715
|
+
* runs the project's setup again and restores them. `wanted` is asked again right before anything goes; `deps`
|
|
716
|
+
* false keeps node_modules (no setup would restore it).
|
|
717
|
+
*/
|
|
718
|
+
async slim(row, wanted, deps = true) {
|
|
719
|
+
const idle = (now) => now !== null && now.runner === null && now.releasedAt !== null && now.releasedAt === row.releasedAt && !this.inUse(row.path).held;
|
|
720
|
+
if (busy.has(row.path) || !existsSync(row.path) || !idle(this.store.getWorktree(row.path)))
|
|
721
|
+
return { ok: false, message: `${row.path} is in use` };
|
|
722
|
+
const listed = await this.runner(GIT, [...READ_ONLY, "ls-files", "--others", "--ignored", "--exclude-standard", "--directory", "-z"], { cwd: row.path, timeoutMs: WORKTREE_TIMEOUT_MS });
|
|
723
|
+
if (listed.code !== 0)
|
|
724
|
+
return { ok: false, message: `git couldn't list ${row.path}` };
|
|
725
|
+
const drop = listed.stdout.split("\0").filter(one => one.endsWith("/")).map(one => one.slice(0, -1))
|
|
726
|
+
.filter(one => { const name = one.split("/").at(-1); return SLIM_NAMES.includes(name) && (deps || name !== "node_modules"); });
|
|
727
|
+
if (drop.length === 0)
|
|
728
|
+
return { ok: true, dropped: [], bytes: 0 };
|
|
729
|
+
busy.set(row.path, "removing");
|
|
730
|
+
const note = join(row.path, MARKER);
|
|
731
|
+
let held = false;
|
|
732
|
+
try {
|
|
733
|
+
const now = this.store.getWorktree(row.path);
|
|
734
|
+
if (!idle(now) || !wanted(now))
|
|
735
|
+
return { ok: false, message: `${row.path} is in use` };
|
|
736
|
+
// Held while it slims: another process's lease sees this process in it.
|
|
737
|
+
try {
|
|
738
|
+
writeFileSync(note, `${process.pid} storage-retention slimming ${currentBootId() ?? "unknown"} ${hostname()}\n`, "utf8");
|
|
739
|
+
held = true;
|
|
740
|
+
}
|
|
741
|
+
catch {
|
|
742
|
+
return { ok: false, message: `${row.path} couldn't be held` };
|
|
743
|
+
}
|
|
744
|
+
const sizes = diskBytes(drop.map(one => join(row.path, one)));
|
|
745
|
+
this.store.forgetWorktreeSetup(row.path);
|
|
746
|
+
for (const one of drop)
|
|
747
|
+
rmSync(join(row.path, one), { recursive: true, force: true, maxRetries: 3 });
|
|
748
|
+
return { ok: true, dropped: drop, bytes: [...sizes.values()].reduce((sum, one) => sum + one, 0) };
|
|
749
|
+
}
|
|
750
|
+
finally {
|
|
751
|
+
if (held)
|
|
752
|
+
rmSync(note, { force: true });
|
|
753
|
+
busy.delete(row.path);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
709
756
|
/**
|
|
710
757
|
* Throw away a let-go checkout kept for its changes, on purpose: the working copy and what is uncommitted in it
|
|
711
758
|
* go; its branch, and every commit on it, stays. Refused while anything holds it.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toolroll",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.9",
|
|
4
4
|
"description": "A control plane for unattended coding agents — queue tasks, walk away, come back to pull requests. Agents build in leased worktrees, never touch your default branch, and interrupt you only for decisions that need a human.",
|
|
5
5
|
"directories": {
|
|
6
6
|
"doc": "docs"
|