toolroll 0.9.101 → 0.9.102
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 +8 -0
- package/dist/browser/terminal.js +9 -9
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +38 -38
- package/dist/browser-workspace.d.ts +47 -13
- package/dist/browser-workspace.js +5 -3
- package/dist/desktop-update.d.ts +1 -1
- package/dist/page-work-contract.d.ts +21 -0
- package/dist/page-work-contract.js +5 -0
- package/dist/project-knowledge.d.ts +6 -6
- package/dist/run-efficiency.d.ts +3 -2
- package/dist/run-efficiency.js +4 -3
- package/dist/serve.js +51 -9
- package/dist/server/chrome.d.ts +10 -1
- package/dist/server/chrome.js +9 -0
- package/dist/server/console.css +19 -4
- package/dist/server/home.d.ts +27 -14
- package/dist/server/home.js +78 -63
- package/dist/server/http.d.ts +6 -0
- package/dist/server/http.js +8 -0
- package/dist/server/in-place.d.ts +15 -0
- package/dist/server/in-place.js +32 -0
- package/dist/server/pages.js +23 -2
- package/dist/server/render-tasks.js +14 -3
- package/dist/server/request-context.d.ts +14 -0
- package/dist/server/request-context.js +3 -0
- package/dist/server/task-views.js +6 -3
- package/dist/server/tasks.d.ts +8 -2
- package/dist/server/tasks.js +31 -17
- package/dist/work-index.d.ts +14 -1
- package/dist/work-index.js +24 -8
- package/package.json +1 -1
package/dist/server/tasks.js
CHANGED
|
@@ -80,7 +80,7 @@ import { buildProgressOf, parseWorkView } from "../workspace-ui.js";
|
|
|
80
80
|
import { requestContext } from "./request-context.js";
|
|
81
81
|
import { boardTickScript, oneLineOf, QUEUE_VIEW, refuse, regionScript, screen, transcriptScript, whenTime } from "./chrome.js";
|
|
82
82
|
import { retiredRedirect } from "./retired-routes.js";
|
|
83
|
-
import { chatReturnWithSaid, matchTaskPath, redirect, respond, reviewHref, safeChatReturn, safeReturn, SAFETY, taskChatHref, taskHref } from "./http.js";
|
|
83
|
+
import { answersInPlace, chatReturnWithSaid, matchTaskPath, redirect, respond, reviewHref, safeChatReturn, safeReturn, SAFETY, taskChatHref, taskHref } from "./http.js";
|
|
84
84
|
import { putAwayPage, boardBody, boardWorkers, QUEUE_FRONT, queueBody, queueScript, TASK_STATES, workPage } from "./render-pages.js";
|
|
85
85
|
import { runEfficiencyOf } from "../run-efficiency.js";
|
|
86
86
|
import { timingFor } from "../latency-timeline.js";
|
|
@@ -1165,7 +1165,7 @@ export function createTasksHandlers(runtime) {
|
|
|
1165
1165
|
const act = matchTaskPath(url.pathname, `/${verb}$`);
|
|
1166
1166
|
if (act === null)
|
|
1167
1167
|
return noPage(who, response);
|
|
1168
|
-
return taskMutation(response, who, act.taskId, verb, posted, now);
|
|
1168
|
+
return taskMutation(response, who, act.taskId, verb, posted, now, ctx.request);
|
|
1169
1169
|
};
|
|
1170
1170
|
// Cockpit E: Show in Finder — the checkout proved again right now (live, this machine's runner, inside the pool),
|
|
1171
1171
|
// then Finder asked to reveal it with a fixed argv. Nothing runs in it; a Mac only.
|
|
@@ -1632,7 +1632,9 @@ export function createTasksHandlers(runtime) {
|
|
|
1632
1632
|
}
|
|
1633
1633
|
}
|
|
1634
1634
|
// ---- mutations -----------------------------------------------------------
|
|
1635
|
-
function taskMutation(response, who, taskId, verb, posted, now
|
|
1635
|
+
function taskMutation(response, who, taskId, verb, posted, now,
|
|
1636
|
+
/** Read only to shape an answer in place (answersInPlace), after the act's proofs. */
|
|
1637
|
+
request = null) {
|
|
1636
1638
|
const ref = store.lookupRef(taskId);
|
|
1637
1639
|
if (ref === null || store.getTask(taskId) === null) {
|
|
1638
1640
|
return refuse(response, who, 404, "no such task", "/tasks");
|
|
@@ -2277,9 +2279,13 @@ export function createTasksHandlers(runtime) {
|
|
|
2277
2279
|
: safeChatReturn(requestedReturn);
|
|
2278
2280
|
// The release sheet's Approve release (trusted-release-reports.md A7): the same door, its own page for words.
|
|
2279
2281
|
const sheet = body.get("sheet") === "release";
|
|
2280
|
-
|
|
2282
|
+
// Each caller names its outcome (live-pages-and-home.md B2.3); none is inferred from the status.
|
|
2283
|
+
const approvalProblem = (outcome, message, status) => {
|
|
2281
2284
|
if (sheet)
|
|
2282
2285
|
return releaseSheet.sheetScreen({ who, response, project: null }, taskId, message, status);
|
|
2286
|
+
// Asked in place: the same proofs ran, only the answer's shape differs.
|
|
2287
|
+
if (answersInPlace(request))
|
|
2288
|
+
return answerApprovalInPlace(response, outcome, message, status);
|
|
2283
2289
|
if (requestedReturn !== null && requestedReturn !== "next") {
|
|
2284
2290
|
return redirect(response, chatReturnWithSaid(approvalBack, message));
|
|
2285
2291
|
}
|
|
@@ -2287,7 +2293,7 @@ export function createTasksHandlers(runtime) {
|
|
|
2287
2293
|
};
|
|
2288
2294
|
const approvingRef = store.lookupRef(taskId);
|
|
2289
2295
|
if (approvingRef?.plan === "requested") {
|
|
2290
|
-
return approvalProblem("approval is blocked while planning is in progress — review the drafted plan first", 409);
|
|
2296
|
+
return approvalProblem("refused", "approval is blocked while planning is in progress — review the drafted plan first", 409);
|
|
2291
2297
|
}
|
|
2292
2298
|
// Step-up: the session got you here; only the token agrees. The
|
|
2293
2299
|
// digest names what was seen; the nonce proves this exact form was
|
|
@@ -2296,25 +2302,25 @@ export function createTasksHandlers(runtime) {
|
|
|
2296
2302
|
const token = body.get("token") ?? "";
|
|
2297
2303
|
const nonce = body.get("nonce") ?? "";
|
|
2298
2304
|
if (!consumeApprovalNonce(nonce, who.name, taskId, digest)) {
|
|
2299
|
-
return approvalProblem("that approval form is stale — read it again", 409);
|
|
2305
|
+
return approvalProblem("refused", "that approval form is stale — read it again", 409);
|
|
2300
2306
|
}
|
|
2301
2307
|
// An empty password stands for an explicit identity-provider re-check not yet spent, or (cockpit B2) a password
|
|
2302
2308
|
// remembered on this session; approve() decides whether these terms may stand on a remembered one.
|
|
2303
2309
|
// Easy-start C5: a laptop's local owner approves eligible work with no password (approve() proves it again).
|
|
2304
2310
|
if (token === "" && !passwordlessSessionFor(store, who.name) && !ssoNowAvailable(who.name) && withFreshness("approve-standard", () => freshnessFor(who.name)) === null
|
|
2305
2311
|
&& !(who.authority === "local-owner" && localApprovalEligible(store, who.name, taskId).ok)) {
|
|
2306
|
-
return approvalProblem("approval requires your password, typed again", 400);
|
|
2312
|
+
return approvalProblem("refused", "approval requires your password, typed again", 400);
|
|
2307
2313
|
}
|
|
2308
2314
|
// The sheet binds the release card's digest (the nonce was minted for it): the shared card wrapper decides.
|
|
2309
2315
|
if (sheet)
|
|
2310
2316
|
return releaseSheet.approveFromSheet({ who, response, project: null }, taskId, digest, token);
|
|
2311
2317
|
const scopeRow = store.getScope(taskId);
|
|
2312
2318
|
if (scopeRow === null)
|
|
2313
|
-
return approvalProblem("this task has no scope to approve", 409);
|
|
2319
|
+
return approvalProblem("refused", "this task has no scope to approve", 409);
|
|
2314
2320
|
const planView = planViewOf(ref.id);
|
|
2315
2321
|
const expectedDigest = approvalFormDigest(scopeRow.digest, planView?.sha256 ?? null);
|
|
2316
2322
|
if (digest !== expectedDigest) {
|
|
2317
|
-
return approvalProblem("the scope or plan changed while this form was open — read the latest version and approve again", 409);
|
|
2323
|
+
return approvalProblem("refused", "the scope or plan changed while this form was open — read the latest version and approve again", 409);
|
|
2318
2324
|
}
|
|
2319
2325
|
// A revision approves ONLY against a brief that still verifies
|
|
2320
2326
|
// (Codex M5-M8 audit, IV-3): the batch the screen restated must be
|
|
@@ -2324,7 +2330,7 @@ export function createTasksHandlers(runtime) {
|
|
|
2324
2330
|
if (approvingRef !== null && approvingRef.revisionBriefArtifact !== null) {
|
|
2325
2331
|
const view = revisionViewOf(approvingRef);
|
|
2326
2332
|
if (view !== null && "problem" in view) {
|
|
2327
|
-
return approvalProblem(`approval is blocked: ${view.problem}`, 409);
|
|
2333
|
+
return approvalProblem("refused", `approval is blocked: ${view.problem}`, 409);
|
|
2328
2334
|
}
|
|
2329
2335
|
}
|
|
2330
2336
|
// A re-check is spent only if this yes is kept (sealed, or a protected-work vote recorded).
|
|
@@ -2332,11 +2338,11 @@ export function createTasksHandlers(runtime) {
|
|
|
2332
2338
|
if (!approved.ok) {
|
|
2333
2339
|
// v102: a first yes on protected work is recorded, not refused — the page says who else is needed.
|
|
2334
2340
|
if (approved.reason === "second-approver")
|
|
2335
|
-
return approvalProblem(gateWords({ verdict: "vote", have: approved.have, need: 2, already: approved.already }), 200);
|
|
2341
|
+
return approvalProblem("vote", gateWords({ verdict: "vote", have: approved.have, need: 2, already: approved.already }), 200);
|
|
2336
2342
|
if (approved.reason === "policy")
|
|
2337
|
-
return approvalProblem(approved.message, 403);
|
|
2343
|
+
return approvalProblem("refused", approved.message, 403);
|
|
2338
2344
|
const status = approved.reason === "changed" || approved.reason === "unrouted" ? 409 : 403;
|
|
2339
|
-
return approvalProblem(approveRefusalWords(approved.reason), status);
|
|
2345
|
+
return approvalProblem("refused", approveRefusalWords(approved.reason), status);
|
|
2340
2346
|
}
|
|
2341
2347
|
// Approve & build again (the plan was rewritten after a build): the same yes requeues, only for the build the
|
|
2342
2348
|
// sheet named, and only while it is still the task's latest result, made to an earlier plan than this one.
|
|
@@ -2348,10 +2354,12 @@ export function createTasksHandlers(runtime) {
|
|
|
2348
2354
|
const offered = named != null && named.taskRef === ref.id && named.role === "builder" && !!named.scopeDigest && named.scopeDigest !== scopeRow.digest;
|
|
2349
2355
|
const requeued = offered ? store.requeueTask(taskId, who.name, now, { run: named.id, scopeDigest: scopeRow.digest }) : null;
|
|
2350
2356
|
if (requeued === null || (!requeued.ok && requeued.reason === "moved"))
|
|
2351
|
-
return approvalProblem("Approved. The task changed while this page was open, so it wasn't built again — check its status.", 200);
|
|
2357
|
+
return approvalProblem("approved-not-rebuilt", "Approved. The task changed while this page was open, so it wasn't built again — check its status.", 200);
|
|
2352
2358
|
if (!requeued.ok)
|
|
2353
|
-
return approvalProblem(`Approved, but building again didn't start (${requeued.reason}). Use Build again.`, 200);
|
|
2359
|
+
return approvalProblem("approved-not-rebuilt", `Approved, but building again didn't start (${requeued.reason}). Use Build again.`, 200);
|
|
2354
2360
|
}
|
|
2361
|
+
if (!sheet && answersInPlace(request))
|
|
2362
|
+
return answerApprovalInPlace(response, "approved", "Approved.", 200);
|
|
2355
2363
|
return redirect(response, approvalBack);
|
|
2356
2364
|
}
|
|
2357
2365
|
case "accept-proof": {
|
|
@@ -2759,9 +2767,10 @@ export function createTasksHandlers(runtime) {
|
|
|
2759
2767
|
summary: detail.handoff?.conclusion ?? run.handoff,
|
|
2760
2768
|
},
|
|
2761
2769
|
detail,
|
|
2762
|
-
// How it ran: derived from the run's own records once it has finished, display only.
|
|
2770
|
+
// How it ran: derived from the run's own records once it has finished, display only. A read ahead of the page's
|
|
2771
|
+
// link (x-toolroll-read: prefetch) derives it and saves nothing.
|
|
2763
2772
|
efficiency: (() => { try {
|
|
2764
|
-
return runEfficiencyOf(store, evidenceRoot, run.id, now);
|
|
2773
|
+
return runEfficiencyOf(store, evidenceRoot, run.id, now, requestContext.getStore()?.readPurpose !== "prefetch");
|
|
2765
2774
|
}
|
|
2766
2775
|
catch {
|
|
2767
2776
|
return null;
|
|
@@ -3325,3 +3334,8 @@ export function createReleaseSheetHandlers(runtime) {
|
|
|
3325
3334
|
}
|
|
3326
3335
|
return { routes: { "task.release": showSheet, "task.release.go-on": goOn, "release.locate": locate }, sheetScreen, approveFromSheet };
|
|
3327
3336
|
}
|
|
3337
|
+
/** The answer to an approval asked in place: its named outcome and the same words the page would show, as JSON. Written
|
|
3338
|
+
* only after the approve route's proofs, by the caller that knows the outcome. */
|
|
3339
|
+
export function answerApprovalInPlace(response, outcome, words, status) {
|
|
3340
|
+
respond(response, status, "application/json; charset=utf-8", JSON.stringify({ outcome, words }));
|
|
3341
|
+
}
|
package/dist/work-index.d.ts
CHANGED
|
@@ -27,6 +27,8 @@ export type WorkIndexItem = {
|
|
|
27
27
|
ask: Ask | null;
|
|
28
28
|
/** The row's chip under its group: the specific ask (needs-you.ts askChipOf), or none. */
|
|
29
29
|
chip: AskChip | null;
|
|
30
|
+
/** What a Needs you row asks, in Home's five groups (ASK_GROUP_SQL); null for every other row. */
|
|
31
|
+
askGroup: WorkAskGroup | null;
|
|
30
32
|
primaryAction: WorkAction | null;
|
|
31
33
|
completion: {
|
|
32
34
|
actor: string;
|
|
@@ -45,7 +47,9 @@ export type WorkIndexItem = {
|
|
|
45
47
|
at: string;
|
|
46
48
|
};
|
|
47
49
|
};
|
|
48
|
-
/** `root`: the evidence root, so a result whose checks were Off can read the release check that covers it.
|
|
50
|
+
/** `root`: the evidence root, so a result whose checks were Off can read the release check that covers it.
|
|
51
|
+
* `completedSince` (an ISO time): the Completed view lists only what was completed at or after it, filtered before the
|
|
52
|
+
* page limit, and the page's `completedSince` counts them all. */
|
|
49
53
|
export type WorkIndexOptions = {
|
|
50
54
|
view?: WorkView;
|
|
51
55
|
limit?: number;
|
|
@@ -54,16 +58,25 @@ export type WorkIndexOptions = {
|
|
|
54
58
|
state?: TaskState;
|
|
55
59
|
leadId?: string;
|
|
56
60
|
root?: string;
|
|
61
|
+
completedSince?: string;
|
|
57
62
|
/** Only these families (by root task id): the sidebar reads its rows' own words this way. */
|
|
58
63
|
roots?: readonly string[];
|
|
59
64
|
};
|
|
60
65
|
/** How the list is grouped: the Needs you rows by their ask, then building, then the rest. */
|
|
61
66
|
export type WorkIndexGroup = Ask | 'building' | 'rest';
|
|
62
67
|
export type WorkIndexGroupCounts = Record<WorkIndexGroup, number>;
|
|
68
|
+
/** What a Needs you row asks, as Home groups it (docs/plans/live-pages-and-home.md B1.2): a plan to approve, a question
|
|
69
|
+
* to answer, something that went wrong, a result to review, or anything else that needs the person. */
|
|
70
|
+
export type WorkAskGroup = 'approve' | 'answer' | 'wrong' | 'review' | 'other';
|
|
71
|
+
export declare const WORK_ASK_GROUPS: readonly WorkAskGroup[];
|
|
72
|
+
/** `asks`: the exact Needs you totals per ask group, over every row in view (never only the page). `completedSince`:
|
|
73
|
+
* how many the Completed view lists from `options.completedSince` on (0 without it). */
|
|
63
74
|
export type WorkIndexPage = {
|
|
64
75
|
items: WorkIndexItem[];
|
|
65
76
|
totals: WorkIndexCounts;
|
|
66
77
|
groups: WorkIndexGroupCounts;
|
|
78
|
+
asks: Record<WorkAskGroup, number>;
|
|
79
|
+
completedSince: number;
|
|
67
80
|
projects: WorkProjectCounts[];
|
|
68
81
|
nextCursor: string | null;
|
|
69
82
|
limit: number;
|
package/dist/work-index.js
CHANGED
|
@@ -22,10 +22,12 @@ import { LANDED_ACTOR, landedReasonOf } from './result-completion.js';
|
|
|
22
22
|
import { ASKS, CODE_READING, STORAGE_HOLD_PREFIX, NEED_ASK, NEEDS, askChipOf, failedAttemptSentence, isWaitCode, processNeedOf, resultHoldUpSentence } from './needs-you.js';
|
|
23
23
|
import { retryWhenWords } from './dispatch.js';
|
|
24
24
|
import { LEAD_CLAIM_SQL, LEAD_IDLE_MS } from './lead-voice.js';
|
|
25
|
+
export const WORK_ASK_GROUPS = ['approve', 'answer', 'wrong', 'review', 'other'];
|
|
25
26
|
export const WORK_INDEX_PAGE_LIMIT = 40;
|
|
26
27
|
export const WORK_INDEX_MAX_LIMIT = 100;
|
|
27
28
|
const EMPTY = { all: 0, 'needs-you': 0, ready: 0, running: 0, queued: 0, completed: 0, archived: 0 };
|
|
28
29
|
const NO_GROUPS = { decide: 0, review: 0, unblock: 0, building: 0, rest: 0 };
|
|
30
|
+
const NO_ASKS = { approve: 0, answer: 0, wrong: 0, review: 0, other: 0 };
|
|
29
31
|
const FAMILY_PROBLEM = 'Task history is unavailable or incomplete. This task is shown separately.';
|
|
30
32
|
const VIEWS = ['all', 'needs-you', 'ready', 'running', 'queued', 'completed', 'archived'];
|
|
31
33
|
/** The family's current version is put away (v145): it waits on nobody and only the Archived view lists it. */
|
|
@@ -70,6 +72,11 @@ export function askOfCode(code, custody = false) {
|
|
|
70
72
|
const codesOf = (ask) => PERSON_CODES.filter(code => askOfCode(code) === ask).map(code => `'${code}'`).join(',');
|
|
71
73
|
/** The ask as its place in the list (ASKS order), read by the same code the row's status reads. */
|
|
72
74
|
const ASK_RANK = `CASE WHEN code IN (${codesOf('decide')}) THEN 0 WHEN code IN (${codesOf('review')}) AND NOT custody_unresolved THEN 1 ELSE 2 END`;
|
|
75
|
+
/** Home's ask group of a row, by its code alone, so a row's group and its group's count are one rule: a plan waiting
|
|
76
|
+
* for its yes, a question, a build that failed or stopped unexpectedly, a saved result to review (ASK_RANK's Review),
|
|
77
|
+
* and everything else that waits on a person. Read on Needs you rows only. */
|
|
78
|
+
const ASK_GROUP_SQL = `CASE WHEN code='needs-approval' THEN 'approve' WHEN code IN ('waiting-decision','decision-queue') THEN 'answer'
|
|
79
|
+
WHEN code IN ('failed','waiting-incident','vanished-run') THEN 'wrong' WHEN code IN (${codesOf('review')}) AND NOT custody_unresolved THEN 'review' ELSE 'other' END`;
|
|
73
80
|
/** Where a row sits in the list: Decide, Review, Unblock, then building, then the rest by recency. */
|
|
74
81
|
const ORDER = `(CASE WHEN rank=0 THEN ask_rank WHEN rank=1 THEN 3 ELSE 4 END)`;
|
|
75
82
|
/** A row whose build may still be running (the store's reading, custodyReadings): Waiting, not Needs you. */
|
|
@@ -261,7 +268,7 @@ const PROJECTION = `WITH RECURSIVE admitted AS MATERIALIZED (
|
|
|
261
268
|
${ARCHIVED} archived,
|
|
262
269
|
CASE WHEN code='complete' THEN 3 WHEN code='cancelled' THEN 4 WHEN code='running' THEN 1
|
|
263
270
|
WHEN ${CUSTODY_WAITS} OR code IN (${WAITING_SQL.replace(/'(?:cancelled|complete|running)',/g, '')}) THEN 2 ELSE 0 END rank,
|
|
264
|
-
${ASK_RANK} ask_rank,
|
|
271
|
+
${ASK_RANK} ask_rank,${ASK_GROUP_SQL} ask_group,
|
|
265
272
|
CASE WHEN code='complete' THEN completed.at ELSE family_updated END sort_at,
|
|
266
273
|
completed.actor checked_actor,completed.at checked_at,completed.outcome checked_digest,completed.detail checked_detail
|
|
267
274
|
FROM claimed 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))
|
|
@@ -364,6 +371,12 @@ const TOTALS = `COALESCE(SUM(archived=0),0) all_count,COALESCE(SUM(needs),0) nee
|
|
|
364
371
|
+ `COALESCE(SUM(${QUEUED}),0) queued_count,COALESCE(SUM(code='complete' AND archived=0),0) completed_count,COALESCE(SUM(archived),0) archived_count`;
|
|
365
372
|
const GROUP_TOTALS = `COALESCE(SUM(rank=0 AND ask_rank=0 AND archived=0),0) decide_count,COALESCE(SUM(rank=0 AND ask_rank=1 AND archived=0),0) review_count,COALESCE(SUM(rank=0 AND ask_rank=2 AND archived=0),0) unblock_count,`
|
|
366
373
|
+ `COALESCE(SUM(rank=1 AND archived=0),0) building_count,COALESCE(SUM(rank>1 AND archived=0),0) rest_count`;
|
|
374
|
+
/** The exact Needs you totals per ask group, and what was completed since the page's `completedSince`: page reads only. */
|
|
375
|
+
const ASK_TOTALS = WORK_ASK_GROUPS.map(group => `COALESCE(SUM(needs AND ask_group='${group}'),0) ask_${group}_count`).join(',')
|
|
376
|
+
+ `,COALESCE(SUM(code='complete' AND archived=0 AND $completedSince IS NOT NULL AND sort_at>=$completedSince),0) completed_since_count`;
|
|
377
|
+
function askCounts(row) {
|
|
378
|
+
return Object.fromEntries(WORK_ASK_GROUPS.map(group => [group, n(row, `ask_${group}_count`)]));
|
|
379
|
+
}
|
|
367
380
|
function groupCounts(row) {
|
|
368
381
|
return { decide: n(row, 'decide_count'), review: n(row, 'review_count'), unblock: n(row, 'unblock_count'), building: n(row, 'building_count'), rest: n(row, 'rest_count') };
|
|
369
382
|
}
|
|
@@ -374,7 +387,8 @@ function cursorScope(access, options, view) {
|
|
|
374
387
|
return createHash('sha256').update(JSON.stringify({ principal: access.principal,
|
|
375
388
|
repos: access.repos === null ? null : [...new Set(access.repos)].sort(),
|
|
376
389
|
unplaced: access.principal === 'operator' && access.includeUnplaced === true,
|
|
377
|
-
leadId: options.leadId ?? null, project: options.project ?? null, state: options.state ?? null, view
|
|
390
|
+
leadId: options.leadId ?? null, project: options.project ?? null, state: options.state ?? null, view,
|
|
391
|
+
...(options.completedSince === undefined ? {} : { completedSince: options.completedSince }) })).digest('hex');
|
|
378
392
|
}
|
|
379
393
|
function readCursor(value, scope) {
|
|
380
394
|
if (value == null)
|
|
@@ -413,18 +427,18 @@ function readWorkIndexPage(store, now, access, options, taskId = null) {
|
|
|
413
427
|
const view = VIEWS.includes(options.view ?? 'all') ? options.view ?? 'all' : 'all';
|
|
414
428
|
const limit = Number.isFinite(options.limit) ? Math.max(1, Math.min(WORK_INDEX_MAX_LIMIT, Math.floor(options.limit))) : WORK_INDEX_PAGE_LIMIT;
|
|
415
429
|
const scope = cursorScope(access, options, view), cursor = readCursor(options.cursor, scope);
|
|
416
|
-
const filter = view === 'archived' ? 'archived=1' : view === 'needs-you' ? 'needs=1' : view === 'ready' ? READY : view === 'running' ? 'family_running=1 AND archived=0' : view === 'queued' ? QUEUED : view === 'completed' ? "code='complete' AND archived=0" : 'archived=0';
|
|
430
|
+
const filter = view === 'archived' ? 'archived=1' : view === 'needs-you' ? 'needs=1' : view === 'ready' ? READY : view === 'running' ? 'family_running=1 AND archived=0' : view === 'queued' ? QUEUED : view === 'completed' ? "code='complete' AND archived=0 AND ($completedSince IS NULL OR sort_at>=$completedSince)" : 'archived=0';
|
|
417
431
|
const rows = prepared(store, `${projection}, selected_page AS MATERIALIZED (
|
|
418
432
|
SELECT * FROM ranked WHERE ($taskId IS NOT NULL OR ${filter}) AND ($taskId IS NULL OR root_ref IN (SELECT root_ref FROM members WHERE id=$taskId)) AND ($roots IS NULL OR root_id IN (SELECT value FROM json_each($roots))) AND ($cursorRoot=0 OR
|
|
419
433
|
${ORDER}>$cursorRank OR (${ORDER}=$cursorRank AND (sort_at<$cursorAt OR (sort_at=$cursorAt AND root_ref<$cursorRoot))))
|
|
420
434
|
ORDER BY ${ORDER},sort_at DESC,root_ref DESC LIMIT $limit
|
|
421
|
-
), page AS (SELECT p.*,COALESCE(p.result_id,(SELECT id FROM run WHERE task_ref=p.ref_id AND finished_at IS NOT NULL AND role IN ('builder','scout') ORDER BY id DESC LIMIT 1)) page_result_id FROM selected_page p), totals AS (SELECT ${TOTALS},${GROUP_TOTALS} FROM ranked), project_totals AS (
|
|
435
|
+
), page AS (SELECT p.*,COALESCE(p.result_id,(SELECT id FROM run WHERE task_ref=p.ref_id AND finished_at IS NOT NULL AND role IN ('builder','scout') ORDER BY id DESC LIMIT 1)) page_result_id FROM selected_page p), totals AS (SELECT ${TOTALS},${GROUP_TOTALS},${ASK_TOTALS} FROM ranked), project_totals AS (
|
|
422
436
|
SELECT repo,${TOTALS},SUM(state='queued' AND family_running=0 AND archived=0) queued,
|
|
423
437
|
SUM(state='done' AND family_running=0 AND archived=0 AND updated_at>=$recent) done_recently FROM ranked GROUP BY repo
|
|
424
438
|
)
|
|
425
439
|
SELECT 0 record_kind,totals.*,(SELECT json_group_array(json_object('repo',repo,'all_count',all_count,'needs_count',needs_count,'ready_count',ready_count,
|
|
426
440
|
'running_count',running_count,'queued_count',queued_count,'completed_count',completed_count,'archived_count',archived_count,'queued',queued,'done_recently',done_recently)) FROM project_totals) row_json FROM totals
|
|
427
|
-
UNION ALL SELECT 1,0,0,0,0,0,0,0,0,0,0,0,0,json_object('archived',archived,'archived_by',(SELECT by FROM task_archive WHERE task_ref=page.ref_id),
|
|
441
|
+
UNION ALL SELECT 1,0,0,0,0,0,0,0,0,0,0,0,0,${WORK_ASK_GROUPS.map(() => '0,').join('')}0,json_object('archived',archived,'archived_by',(SELECT by FROM task_archive WHERE task_ref=page.ref_id),
|
|
428
442
|
'archived_kind',(SELECT by_kind FROM task_archive WHERE task_ref=page.ref_id),'archived_reason',(SELECT reason FROM task_archive WHERE task_ref=page.ref_id),
|
|
429
443
|
'archived_at',(SELECT at FROM task_archive WHERE task_ref=page.ref_id),'root_ref',root_ref,'root_id',root_id,'id',id,'root_title',root_title,'repo',repo,
|
|
430
444
|
'state',state,'created_at',created_at,'family_updated',family_updated,'version_count',version_count,'earlier_active',earlier_active,
|
|
@@ -436,7 +450,7 @@ function readWorkIndexPage(store, now, access, options, taskId = null) {
|
|
|
436
450
|
'check_follows',(SELECT json_group_array(q.outcome||' '||COALESCE((SELECT substr(f.outcome,instr(f.outcome,':')+1) FROM action_ledger f WHERE f.repo IS page.repo AND f.run_id=page_result_id
|
|
437
451
|
AND f.action='checks finished' AND f.outcome LIKE q.id||':%' LIMIT 1),'open')) FROM (SELECT id,outcome FROM action_ledger WHERE repo IS page.repo AND run_id=page_result_id AND action='checks requested' ORDER BY id) q),
|
|
438
452
|
'proof_verdict',(SELECT COALESCE(machine_verdict,verdict) FROM proof_verdict WHERE run=page_result_id),
|
|
439
|
-
'proof_failed_check',(SELECT verdict='refuted' AND reasons_json LIKE '%approved verification command exited%' FROM proof_verdict WHERE run=page_result_id),'code',code,'needs',needs,'person_need',person_need,'lead_claim',lead_claim,'family_running',family_running,'rank',rank,'ask_rank',ask_rank,'list_order',${ORDER},'sort_at',sort_at,
|
|
453
|
+
'proof_failed_check',(SELECT verdict='refuted' AND reasons_json LIKE '%approved verification command exited%' FROM proof_verdict WHERE run=page_result_id),'code',code,'ask_group',ask_group,'needs',needs,'person_need',person_need,'lead_claim',lead_claim,'family_running',family_running,'rank',rank,'ask_rank',ask_rank,'list_order',${ORDER},'sort_at',sort_at,
|
|
440
454
|
'checked_actor',checked_actor,'checked_at',checked_at,'checked_digest',checked_digest,'checked_detail',checked_detail,'question_id',question_id,
|
|
441
455
|
'dependency_id',dependency_id,'dependency_state',dependency_state,'hold_until',hold_until,'attempt_run',(SELECT id FROM run WHERE task_ref IN (SELECT ref_id FROM members WHERE root_ref=page.root_ref) AND (finished_at IS NOT NULL OR outcome IS NOT NULL) AND role NOT IN ('planner','reviewer') ORDER BY id DESC LIMIT 1),'custody_unresolved',custody_unresolved,'hold_kind',hold_kind,'hold_reason',substr(hold_reason,1,600),'hold_id',hold_id,'live_lease',live_lease IS NOT NULL,'approved',COALESCE(approved,0),
|
|
442
456
|
'plan_changed',scope_digest IS NOT NULL AND digest IS NOT scope_digest,'head_ok',length(head_revision)=40 AND head_revision NOT GLOB '*[^a-f0-9]*','stop_run',stop_run,'unfinished',unfinished,'signed_out',signed_out,
|
|
@@ -444,7 +458,7 @@ function readWorkIndexPage(store, now, access, options, taskId = null) {
|
|
|
444
458
|
'agent_ask_run',(SELECT run FROM agent_question WHERE id=page.agent_ask),'agent_ask_text',(SELECT substr(question,1,300) FROM agent_question WHERE id=page.agent_ask),'replaced_by',(SELECT successor FROM task_replacement WHERE task_ref=page.ref_id),
|
|
445
459
|
'completed_by_lead',(SELECT lead FROM task_act WHERE task_ref=page.root_ref AND act='completed' ORDER BY id DESC LIMIT 1),'machine_check',COALESCE(machine_check,0),
|
|
446
460
|
'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`)
|
|
447
|
-
.all({ ...parameters(now, access, options, custody), $taskId: taskId, $roots: options.roots === undefined ? null : JSON.stringify(options.roots), $cursorRoot: cursor?.root ?? 0, $cursorRank: cursor?.rank ?? 0, $cursorAt: cursor?.at ?? '', $limit: limit + 1, $recent: new Date(now.getTime() - 86_400_000).toISOString() });
|
|
461
|
+
.all({ ...parameters(now, access, options, custody), $taskId: taskId, $roots: options.roots === undefined ? null : JSON.stringify(options.roots), $cursorRoot: cursor?.root ?? 0, $cursorRank: cursor?.rank ?? 0, $cursorAt: cursor?.at ?? '', $limit: limit + 1, $completedSince: options.completedSince ?? null, $recent: new Date(now.getTime() - 86_400_000).toISOString() });
|
|
448
462
|
const selected = rows.slice(1).map(row => JSON.parse(String(row['row_json'])));
|
|
449
463
|
const page = selected.slice(0, limit), last = page.at(-1);
|
|
450
464
|
const nextCursor = selected.length > limit && last !== undefined ? Buffer.from(JSON.stringify({ version: 2, scope,
|
|
@@ -462,7 +476,8 @@ function readWorkIndexPage(store, now, access, options, taskId = null) {
|
|
|
462
476
|
const memo = new Map();
|
|
463
477
|
const readiness = (row, checks) => resultReadinessFacts(store, now, options.root, row, checks, memo);
|
|
464
478
|
return { items: page.map(row => itemOf(row, access.principal, probe, leadName, readiness, options.root !== undefined, now)), projects, totals: rows[0] === undefined ? { ...EMPTY } : counts(rows[0]),
|
|
465
|
-
groups: rows[0] === undefined ? { ...NO_GROUPS } : groupCounts(rows[0]),
|
|
479
|
+
groups: rows[0] === undefined ? { ...NO_GROUPS } : groupCounts(rows[0]), asks: rows[0] === undefined ? { ...NO_ASKS } : askCounts(rows[0]),
|
|
480
|
+
completedSince: rows[0] === undefined ? 0 : n(rows[0], 'completed_since_count'), nextCursor, limit, view };
|
|
466
481
|
}
|
|
467
482
|
export function workCountsByProject(store, now, access) {
|
|
468
483
|
const projection = registerValidators(store);
|
|
@@ -757,6 +772,7 @@ function itemOf(row, principal, probe, leadName, readiness, readable = false, no
|
|
|
757
772
|
status: { token: `assignment-${assignmentState}`, label, detail, tone: workToneOf(shared.headline),
|
|
758
773
|
action: primaryAction === null ? null : { label: actionLabel, kind: actionCode === 'open-result' ? 'open-result' : 'open-task' }, views, rank: n(row, 'rank') },
|
|
759
774
|
ask: need ? ASKS[n(row, 'ask_rank')] ?? 'unblock' : null,
|
|
775
|
+
askGroup: need ? WORK_ASK_GROUPS.find(group => group === s(row, 'ask_group')) ?? 'other' : null,
|
|
760
776
|
chip: need ? askChipOf({ headline: shared.headline, need: shared.need?.key ?? null, planChanged, mismatch: verdict === 'refuted' && n(row, 'proof_failed_check') !== 1, ask: ASKS[n(row, 'ask_rank')] ?? 'unblock' }) : null,
|
|
761
777
|
primaryAction, completion: code === 'complete' ? { actor: String(row['checked_actor']), at: String(row['checked_at']), digest: String(row['checked_digest']), ...(landed === null ? {} : { landed }) } : null,
|
|
762
778
|
evidence: 'recorded', ...(lead === 'on-it' ? { lead: leadOnIt(leadName) } : {}), ...(archived === null ? {} : { archived }) };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toolroll",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.102",
|
|
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"
|