toolroll 0.9.5 → 0.9.6
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 +11 -0
- package/dist/assignment-presentation.d.ts +13 -2
- package/dist/assignment-presentation.js +22 -9
- package/dist/assignment-ui.d.ts +10 -15
- package/dist/assignment-ui.js +17 -18
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +21 -20
- package/dist/browser-workspace.d.ts +3 -10
- package/dist/chat-acceptance.js +2 -2
- package/dist/chat-quiet.js +20 -19
- package/dist/demo-chat.d.ts +0 -5
- package/dist/demo-chat.js +10 -2
- package/dist/lead-status.d.ts +7 -0
- package/dist/lead-status.js +11 -2
- package/dist/serve.js +64 -66
- package/dist/task-status.d.ts +179 -0
- package/dist/task-status.js +317 -0
- package/dist/telegram-progress.js +62 -100
- package/dist/telegram-status.js +19 -6
- package/dist/work-index.js +25 -9
- package/dist/workspace-ui.d.ts +7 -0
- package/dist/workspace-ui.js +30 -1
- package/package.json +1 -1
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
/** A compact projection of saved work, never a percentage or an ETA. The
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
|
|
2
|
+
* headline, sentence and detail rows come from the shared task status
|
|
3
|
+
* (task-status.ts), read from the same assignment record the console and the
|
|
4
|
+
* CLI show: Ready for review once the saved result can be inspected, Complete
|
|
5
|
+
* once the lead or a person has marked that exact result. Slack, Discord and
|
|
6
|
+
* Teams send this same card. */
|
|
7
7
|
import { assignmentOf } from "./assignment.js";
|
|
8
8
|
import { chatResultHref, chatControlHref } from "./chat-controls.js";
|
|
9
9
|
import { phoneText, projectLabel } from "./telegram-status.js";
|
|
10
|
+
import { assignmentStatusFacts, headlineEmoji, pullRequestFactOf, requirementsOf, statusDetailLines, taskStatusOf } from "./task-status.js";
|
|
11
|
+
import { manualReviewOnly } from "./proof.js";
|
|
12
|
+
import { failedCheckExit } from "./workspace-ui.js";
|
|
10
13
|
/** The saved assignment for this exact result, or null when the run is not
|
|
11
14
|
* the family's current result (an older attempt keeps its own plain words). */
|
|
12
15
|
function assignmentFor(store, run, taskId, project, now, root) {
|
|
@@ -19,111 +22,70 @@ function assignmentFor(store, run, taskId, project, now, root) {
|
|
|
19
22
|
}
|
|
20
23
|
}
|
|
21
24
|
export function telegramProgressCard(store, run, taskId, project, now = new Date(), root) {
|
|
22
|
-
const proof = store.proofVerdictFor(run.id);
|
|
23
|
-
const rows = proof?.matrix ?? [];
|
|
24
|
-
const direct = rows.length > 0 && rows.every(row => row.assessment !== undefined);
|
|
25
|
-
const passed = rows.filter(row => row.state === "pass").length;
|
|
26
|
-
const human = manualReviewOnly(proof);
|
|
27
|
-
const accepted = store.proofAcceptance(run.id) !== null;
|
|
28
25
|
const built = run.finishedAt !== null && (run.outcome === "built" || run.outcome === "no-change");
|
|
29
26
|
const assignment = built ? assignmentFor(store, run, taskId, project, now, root) : null;
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
let icon = "⏳";
|
|
33
|
-
let next = "";
|
|
34
|
-
let build = built ? "✓ Build saved" : "● Build in progress";
|
|
35
|
-
let checks = rows.length ? `${passed === rows.length ? "✓" : "○"} Checks · ${passed}/${rows.length} requirements` : "○ Checks pending";
|
|
36
|
-
if (direct)
|
|
37
|
-
checks = proof?.machineVerdict === "verified" ? "✓ Checks passed" : proof?.machineVerdict === "attested" ? "○ Checks not configured" : "! Checks need attention";
|
|
38
|
-
let label = built ? "Open result" : "Open task";
|
|
39
|
-
if (run.outcome === null && run.phase !== null && run.phase !== "agent-running") {
|
|
40
|
-
status = "Checking the result";
|
|
41
|
-
build = "✓ Draft prepared";
|
|
42
|
-
checks = "● Checks in progress";
|
|
43
|
-
}
|
|
44
|
-
if (built) {
|
|
45
|
-
status = run.committed === false ? "Finished without a commit" : "Result saved";
|
|
46
|
-
if (proof === null)
|
|
47
|
-
checks = "○ Checks not recorded";
|
|
48
|
-
const recorded = assignment?.receipt?.checks;
|
|
49
|
-
if (recorded !== undefined && recorded.status !== "unavailable") {
|
|
50
|
-
checks = recorded.status === "passed" ? "✓ Checks passed"
|
|
51
|
-
: recorded.status === "failed" ? `! Checks failed${recorded.exitCode === null ? "" : ` (exit ${recorded.exitCode})`}`
|
|
52
|
-
: "○ Checks did not run";
|
|
53
|
-
}
|
|
54
|
-
if (assignment?.state === "ready-to-check") {
|
|
55
|
-
status = "Ready";
|
|
56
|
-
icon = checksFailed ? "⚠️" : "✅";
|
|
57
|
-
next = checksFailed ? "Next: inspect the failed check, then mark it complete or request changes." : "Next: open the result, then mark it complete or request changes.";
|
|
58
|
-
}
|
|
59
|
-
else if (assignment?.state === "complete") {
|
|
60
|
-
status = "Complete";
|
|
61
|
-
icon = "✅";
|
|
62
|
-
next = phoneText(assignment.detail, 200);
|
|
63
|
-
}
|
|
64
|
-
else if (assignment?.state === "needs-decision") {
|
|
65
|
-
status = "Needs your decision";
|
|
66
|
-
icon = "💬";
|
|
67
|
-
next = phoneText(assignment.detail, 200);
|
|
68
|
-
}
|
|
69
|
-
else if (assignment?.state === "cancelled") {
|
|
70
|
-
status = "Cancelled";
|
|
71
|
-
icon = "⏹";
|
|
72
|
-
}
|
|
73
|
-
else if (assignment !== null) {
|
|
74
|
-
next = phoneText(assignment.detail, 200);
|
|
75
|
-
}
|
|
76
|
-
else if (run.committed !== false) {
|
|
77
|
-
next = "Open the task for its current status.";
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
else if (run.outcome !== null) {
|
|
81
|
-
status = run.outcome === "parked" ? "Decision needed" : run.outcome === "interrupted" || run.reason === "interrupted" ? "Stopped" : "Build needs attention";
|
|
82
|
-
icon = run.outcome === "parked" ? "💬" : "⏸";
|
|
83
|
-
build = "! Build unfinished";
|
|
84
|
-
next = "Next: inspect the blocker.";
|
|
85
|
-
label = "Open task";
|
|
86
|
-
}
|
|
27
|
+
const publication = store.publicationForRun(run.id);
|
|
28
|
+
const pullRequest = publication === null ? undefined : pullRequestFactOf(publication);
|
|
87
29
|
const stop = store.stopOf(run.id);
|
|
88
|
-
if (run.outcome === null && stop !== null && stop.resumedAt === null) {
|
|
89
|
-
status = stop.settledAt === null ? "Stopping" : "Stopped";
|
|
90
|
-
icon = "⏸";
|
|
91
|
-
build = stop.settledAt === null ? "● Build stopping" : "! Build stopped";
|
|
92
|
-
next = stop.settledAt === null ? "Waiting for the running process to stop." : "Next: inspect the saved work, then resume.";
|
|
93
|
-
}
|
|
94
30
|
const holds = store.activeHolds(run.taskRef, now);
|
|
95
31
|
const operatorHold = holds.find(hold => hold.ownerKind === "operator");
|
|
96
32
|
const accessBlocked = run.reason === "retryable-infra" && /^could not re-read (?:the branch|HEAD) in /i.test(run.handoff ?? "");
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
label = "Open task";
|
|
33
|
+
// The one shared status: the same headline, sentence and rows as the console.
|
|
34
|
+
let facts;
|
|
35
|
+
if (assignment !== null && assignment.state !== "working" && assignment.state !== "checking") {
|
|
36
|
+
facts = assignmentStatusFacts(assignment, pullRequest === undefined ? {} : { pullRequest });
|
|
102
37
|
}
|
|
103
|
-
if (
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
38
|
+
else if (built) {
|
|
39
|
+
// A saved result is Ready even in the moment before its worker lets go of
|
|
40
|
+
// the task. Without its receipt yet, the stored machine verdict speaks.
|
|
41
|
+
const recorded = assignment?.receipt?.checks ?? null;
|
|
42
|
+
const proof = store.proofVerdictFor(run.id);
|
|
43
|
+
const machine = proof?.machineVerdict ?? proof?.verdict ?? null;
|
|
44
|
+
const exit = failedCheckExit(proof?.reasons ?? []);
|
|
45
|
+
const checks = recorded !== null ? { status: recorded.status, exitCode: recorded.exitCode, head: run.headRevision }
|
|
46
|
+
: exit !== null ? { status: "failed", exitCode: exit, head: run.headRevision }
|
|
47
|
+
: machine === "verified" ? { status: "passed", exitCode: null, head: run.headRevision }
|
|
48
|
+
: machine === "attested" ? { status: "not-run", exitCode: null, head: null } : null;
|
|
49
|
+
facts = { stage: "finished", report: run.role === "scout", checks, requirements: requirementsOf(proof?.matrix),
|
|
50
|
+
...(pullRequest === undefined ? {} : { pullRequest }) };
|
|
110
51
|
}
|
|
111
|
-
else if (accessBlocked
|
|
112
|
-
|
|
52
|
+
else if (accessBlocked) {
|
|
53
|
+
facts = { stage: "needs-you", need: "other", reason: holds.some(hold => hold.ownerKind === "backoff")
|
|
54
|
+
? "The worker can't read the project folder. It retries after a short pause; check its folder access."
|
|
55
|
+
: "The worker can't read the project folder. Restore its access, then resume." };
|
|
113
56
|
}
|
|
57
|
+
else if (run.outcome === null && stop !== null && stop.resumedAt === null) {
|
|
58
|
+
facts = { stage: "stopped", reason: stop.settledAt === null ? "Stopping. Waiting for the running process to end." : "Paused by a person. Inspect the saved work, then resume." };
|
|
59
|
+
}
|
|
60
|
+
else if (operatorHold) {
|
|
61
|
+
facts = { stage: "stopped", reason: `On hold: ${phoneText(operatorHold.reason, 150)}` };
|
|
62
|
+
}
|
|
63
|
+
else if (run.outcome === null) {
|
|
64
|
+
facts = { stage: run.phase !== null && run.phase !== "agent-running" ? "checking" : run.role === "planner" ? "planning" : "building" };
|
|
65
|
+
}
|
|
66
|
+
else if (run.outcome === "parked") {
|
|
67
|
+
facts = { stage: "needs-you", need: "answer" };
|
|
68
|
+
}
|
|
69
|
+
else if (run.outcome === "interrupted" || run.reason === "interrupted") {
|
|
70
|
+
facts = { stage: "stopped", reason: "The attempt was interrupted. Inspect the saved work, then resume." };
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
facts = { stage: "failed", reason: "The build stopped before it finished. Inspect the blocker, then retry." };
|
|
74
|
+
}
|
|
75
|
+
const status = taskStatusOf(facts);
|
|
114
76
|
const checkProgress = store.checkProgress(run.id);
|
|
115
|
-
|
|
116
|
-
checks = checkProgress.line;
|
|
117
|
-
const publication = store.publicationForRun(run.id);
|
|
118
|
-
const delivery = publication?.remoteState === "MERGED" ? "Merged · Installation not confirmed" : publication?.remoteState === "CLOSED" ? "Pull request closed without merging" : publication?.state === "opened" ? "Pull request open · Not merged" : publication?.state === "pushed" ? "Branch pushed · Pull request pending" : publication?.state === "intended" ? "Publication queued" : publication?.state === "failed" ? "Publication failed · Local result preserved" : built ? "Saved locally · Not published" : null;
|
|
119
|
-
const acceptance = accepted ? (human ? "Accepted by a person · Recorded checks unchanged" : "Accepted with an exception · Recorded checks unchanged") : null;
|
|
77
|
+
const progress = status.headline === "Building" && checkProgress !== null ? [`● ${checkProgress.line}`] : [];
|
|
120
78
|
const title = phoneText(store.getTask(taskId)?.title ?? taskId, 88);
|
|
121
|
-
const heading = `${
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
79
|
+
const heading = `${headlineEmoji(status.headline)} ${status.headline}`;
|
|
80
|
+
const next = phoneText(status.sentence, 200);
|
|
81
|
+
// A person's acceptance is its own recorded decision; it never changes a check.
|
|
82
|
+
const acceptance = store.proofAcceptance(run.id) === null ? [] : [manualReviewOnly(store.proofVerdictFor(run.id))
|
|
83
|
+
? "Accepted by a person · Recorded checks unchanged" : "Accepted with an exception · Recorded checks unchanged"];
|
|
84
|
+
const rows = [...statusDetailLines(status), ...progress, ...acceptance];
|
|
85
|
+
const text = [title, heading, next, ...(rows.length === 0 ? [] : ["", ...rows]), "", `${projectLabel(project)} · #${run.id}`].join("\n");
|
|
86
|
+
const failedChecks = status.details.some(one => one.key === "checks" && (one.mark === "failed" || one.mark === "note"));
|
|
87
|
+
const recovery = operatorHold !== undefined || accessBlocked || (!built && run.outcome !== null);
|
|
126
88
|
return { text, next, entities: [{ type: "bold", offset: 0, length: title.length }, { type: "bold", offset: title.length + 1, length: heading.length }],
|
|
127
|
-
link: { label
|
|
128
|
-
? chatControlHref("recovery", taskId) : built ? chatResultHref(taskId, run.id,
|
|
89
|
+
link: { label: built ? "Open result" : "Open task", path: recovery
|
|
90
|
+
? chatControlHref("recovery", taskId) : built ? chatResultHref(taskId, run.id, failedChecks ? "checks" : "summary") : chatControlHref("task", taskId) } };
|
|
129
91
|
}
|
package/dist/telegram-status.js
CHANGED
|
@@ -3,6 +3,8 @@ import { manualReviewOnly } from "./proof.js";
|
|
|
3
3
|
/** Read-only phone views over the same dispatch and proof records as chat.
|
|
4
4
|
* No provider calls, repository access, new workflow state, or inferred success. */
|
|
5
5
|
import { diagnoseTaskDispatch, withDispatchDiagnoses } from "./dispatch.js";
|
|
6
|
+
import { COMPLETION_ACTION } from "./result-completion.js";
|
|
7
|
+
import { plainReasonOf, stageOfDispatch, taskStatusOf } from "./task-status.js";
|
|
6
8
|
import { CHAT_CONTROLS, chatControlHref, chatResultHref } from "./chat-controls.js";
|
|
7
9
|
export function phoneCommand(text) {
|
|
8
10
|
// Slash forms everywhere; the bare words too, because Slack and Discord
|
|
@@ -50,6 +52,15 @@ export const phoneText = plain;
|
|
|
50
52
|
export function projectLabel(repo) {
|
|
51
53
|
return plain(repo.split(/[\\/]/).filter(Boolean).pop() ?? "project", 48);
|
|
52
54
|
}
|
|
55
|
+
/** The shared headline (task-status.ts) for a dispatch diagnosis on the phone. */
|
|
56
|
+
function headlineFor(store, id, d) {
|
|
57
|
+
const completed = d.code === "complete" && store.handle.prepare("SELECT 1 FROM action_ledger WHERE task_id = ? AND action = ? AND source = 'work' LIMIT 1")
|
|
58
|
+
.get(store.taskFamilyOf(id, null, true)?.root.id ?? id, COMPLETION_ACTION) !== undefined;
|
|
59
|
+
const read = stageOfDispatch(d, { completed });
|
|
60
|
+
const finished = read.stage === "finished" || read.stage === "complete" || read.stage === "failed";
|
|
61
|
+
return taskStatusOf({ stage: read.stage, ...(read.need === undefined ? {} : { need: read.need }), reason: finished ? null : plainReasonOf(read.stage, d.code, d.detail),
|
|
62
|
+
checks: read.stage === "failed" && d.code === "proof-refuted" ? { status: "failed", exitCode: null, head: null } : null });
|
|
63
|
+
}
|
|
53
64
|
function groupOf(d) {
|
|
54
65
|
if (d.condition === "running")
|
|
55
66
|
return "Working";
|
|
@@ -100,7 +111,7 @@ export function phoneStatus(store, repos, now, focused = null) {
|
|
|
100
111
|
continue;
|
|
101
112
|
lines.push(`${group} · ${rows.length}${snapshot.tasksSaturated ? " in this snapshot" : ""}`);
|
|
102
113
|
for (const task of rows.slice(0, 2)) {
|
|
103
|
-
lines.push(`${plain(task.id, 64)} · ${projectLabel(repos[task.repoIndex])}`, ` ${plain(task.title, 64)} — ${
|
|
114
|
+
lines.push(`${plain(task.id, 64)} · ${projectLabel(repos[task.repoIndex])}`, ` ${plain(task.title, 64)} — ${headlineFor(store, task.id, task.dispatch).headline}`);
|
|
104
115
|
}
|
|
105
116
|
if (rows.length > 2)
|
|
106
117
|
lines.push(` +${rows.length - 2} more in the console`);
|
|
@@ -131,7 +142,7 @@ export function phoneTaskChoices(store, repos, now, query = null, limit = 8) {
|
|
|
131
142
|
if (seen.has(id) || !words.every(word => `${id} ${row.title}`.toLowerCase().includes(word)))
|
|
132
143
|
continue;
|
|
133
144
|
seen.add(id);
|
|
134
|
-
choices.push({ id, title: plain(row.title, 64), label:
|
|
145
|
+
choices.push({ id, title: plain(row.title, 64), label: headlineFor(store, row.id, row.dispatch).headline, group: groupOf(row.dispatch) });
|
|
135
146
|
if (choices.length >= limit)
|
|
136
147
|
break;
|
|
137
148
|
}
|
|
@@ -214,10 +225,11 @@ export function phoneTaskView(store, repos, id, now) {
|
|
|
214
225
|
const d = diagnoseTaskDispatch(store, id, now);
|
|
215
226
|
if (d === null)
|
|
216
227
|
return { text: "This task's status is unavailable. Open it in the console before retrying.", link: null, run: null };
|
|
217
|
-
const
|
|
228
|
+
const shared = headlineFor(store, id, d);
|
|
229
|
+
const lines = [plain(task.title, 140), `${plain(id, 64)} · ${projectLabel(ref.repo)}`, `As of ${now.toISOString().replace("T", " ").slice(0, 19)} UTC`, "", shared.headline];
|
|
218
230
|
const blocker = d.blockerTaskId === null ? null : store.lookupRef(d.blockerTaskId);
|
|
219
231
|
const hiddenDependency = blocker !== null && (blocker.repo === null || !repos.includes(blocker.repo));
|
|
220
|
-
lines.push(hiddenDependency ? "A required task outside this phone view has not finished. Open the dependency in the console." : plain(
|
|
232
|
+
lines.push(hiddenDependency ? "A required task outside this phone view has not finished. Open the dependency in the console." : plain(shared.sentence, 650));
|
|
221
233
|
if (d.nextAt !== null)
|
|
222
234
|
lines.push(`Earliest recorded wake: ${plain(d.nextAt, 40)} (a connected worker is still required).`);
|
|
223
235
|
const runs = store.runsFor(ref.id);
|
|
@@ -247,8 +259,9 @@ export function phoneTaskView(store, repos, id, now) {
|
|
|
247
259
|
if (accepted && !manualReviewOnly(proof))
|
|
248
260
|
lines.push("An operator accepted this result; that does not change its recorded checks.");
|
|
249
261
|
const publication = store.publicationForRun(result.id);
|
|
250
|
-
const delivery = publication?.remoteState === "MERGED" ?
|
|
251
|
-
|
|
262
|
+
const delivery = publication?.remoteState === "MERGED" ? `#${publication.prNumber ?? "?"} merged` : publication?.remoteState === "CLOSED" ? `#${publication.prNumber ?? "?"} closed without merging` : publication?.state === "opened" ? `#${publication.prNumber ?? "?"} open` : publication?.state === "pushed" || publication?.state === "intended" ? "Opening…" : publication?.state === "failed" ? "Couldn't open; the commit is safe locally" : null;
|
|
263
|
+
if (delivery !== null)
|
|
264
|
+
lines.push(`Pull request: ${delivery}.`);
|
|
252
265
|
}
|
|
253
266
|
if (result === undefined)
|
|
254
267
|
link = taskLinkFor(id, d, null);
|
package/dist/work-index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { createHash } from 'node:crypto';
|
|
|
4
4
|
import { readProjectAccess } from './project-access.js';
|
|
5
5
|
import { pauseOnTaskProviders, providerName, signInReason } from './provider-auth.js';
|
|
6
6
|
import { scopeTermsProblem } from './store.js';
|
|
7
|
+
import { plainReasonOf, stageOfCode, taskStatusOf, workToneOf } from './task-status.js';
|
|
7
8
|
export const WORK_INDEX_PAGE_LIMIT = 40;
|
|
8
9
|
export const WORK_INDEX_MAX_LIMIT = 100;
|
|
9
10
|
const EMPTY = { all: 0, 'needs-you': 0, running: 0, completed: 0 };
|
|
@@ -194,9 +195,9 @@ const PROJECTION = `WITH RECURSIVE admitted AS MATERIALIZED (
|
|
|
194
195
|
ELSE 'queued' END code
|
|
195
196
|
FROM checked f
|
|
196
197
|
), ranked AS MATERIALIZED (
|
|
197
|
-
SELECT c.*,code NOT IN ('cancelled','complete','running','updating','retry-scheduled','waiting-dependency','worker-at-capacity','
|
|
198
|
+
SELECT c.*,code NOT IN ('cancelled','complete','running','updating','retry-scheduled','waiting-dependency','worker-at-capacity','planning-ready','scouting-ready','queued') needs,
|
|
198
199
|
CASE WHEN code='complete' THEN 3 WHEN code='cancelled' THEN 4 WHEN code='running' THEN 1
|
|
199
|
-
WHEN code IN ('updating','retry-scheduled','waiting-dependency','worker-at-capacity','
|
|
200
|
+
WHEN code IN ('updating','retry-scheduled','waiting-dependency','worker-at-capacity','planning-ready','scouting-ready','queued') THEN 2 ELSE 0 END rank,
|
|
200
201
|
CASE WHEN code='complete' THEN completed.at ELSE family_updated END sort_at,
|
|
201
202
|
completed.actor checked_actor,completed.at checked_at,completed.outcome checked_digest
|
|
202
203
|
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))
|
|
@@ -293,7 +294,11 @@ export function workIndexPage(store, now, access, options = {}) {
|
|
|
293
294
|
UNION ALL SELECT 1,0,0,0,0,json_object('root_ref',root_ref,'root_id',root_id,'id',id,'root_title',root_title,'repo',repo,
|
|
294
295
|
'state',state,'created_at',created_at,'family_updated',family_updated,'version_count',version_count,'earlier_active',earlier_active,
|
|
295
296
|
'earlier_id',earlier_id,'broken',broken,'live_run',live_run,'result_id',page_result_id,'result_outcome',(SELECT outcome FROM run WHERE id=page_result_id),
|
|
296
|
-
'publication_url',(SELECT pr_url FROM publication WHERE run=page_result_id),
|
|
297
|
+
'publication_url',(SELECT pr_url FROM publication WHERE run=page_result_id),
|
|
298
|
+
'result_role',(SELECT role FROM run WHERE id=page_result_id),'live_role',(SELECT role FROM run WHERE id=page.live_run),
|
|
299
|
+
'check_status',(SELECT status FROM run_check WHERE run=page_result_id),
|
|
300
|
+
'proof_verdict',(SELECT COALESCE(machine_verdict,verdict) FROM proof_verdict WHERE run=page_result_id),
|
|
301
|
+
'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,'family_running',family_running,'rank',rank,'sort_at',sort_at,
|
|
297
302
|
'checked_actor',checked_actor,'checked_at',checked_at,'checked_digest',checked_digest,'question_id',question_id,
|
|
298
303
|
'dependency_id',dependency_id,'dependency_state',dependency_state,'custody_unresolved',custody_unresolved,'hold_kind',hold_kind,'hold_reason',substr(hold_reason,1,300),'stop_run',stop_run,'unfinished',unfinished,'signed_out',signed_out,
|
|
299
304
|
'question_run',(SELECT run FROM decision WHERE id=page.question_id),
|
|
@@ -326,15 +331,15 @@ function itemOf(row, principal) {
|
|
|
326
331
|
'ready-to-check': ['Ready', 'The saved result is ready to review.', 'open-result', 'Open result'],
|
|
327
332
|
complete: ['Complete', 'Completion is recorded for this result. Open it to inspect the saved evidence.', 'open-result', 'Open result'],
|
|
328
333
|
cancelled: ['Cancelled', 'This assignment was cancelled.', 'inspect-task', 'View assignment'],
|
|
329
|
-
running: ['Running now', '
|
|
334
|
+
running: ['Running now', '', null, ''],
|
|
330
335
|
'waiting-decision': ['Needs your decision', 'An unanswered question is waiting.', n(row, 'question_id') ? 'answer-decision' : 'inspect-decisions', 'Answer question'],
|
|
331
336
|
'decision-queue': ['Needs your decision', 'The decision queue is full.', 'inspect-decisions', 'Review open questions'],
|
|
332
337
|
'result-needs-attention': ['Needs your decision', 'Inspect the saved result and resolve its remaining execution or scope issue.', 'inspect-run', 'Inspect run'],
|
|
333
338
|
'process-needs-attention': ['Needs your decision', 'A process exit is not recorded. Open the attempt to check whether its work has stopped.', 'inspect-run', 'Inspect run'],
|
|
334
339
|
failed: ['Needs your decision', 'The last attempt stopped; review its incident before retrying.', 'retry-task', 'Review and retry'],
|
|
335
340
|
'vanished-run': ['Needs your decision', 'An unfinished attempt has no current live claim.', 'reconcile-run', 'Check the unfinished attempt'],
|
|
336
|
-
stopping: ['Needs your decision', 'The
|
|
337
|
-
stopped: ['Needs your decision', 'The
|
|
341
|
+
stopping: ['Needs your decision', 'Stopping. The work so far is kept.', 'inspect-stop', 'View stop details'],
|
|
342
|
+
stopped: ['Needs your decision', 'Paused by a person. The work so far is kept.', 'resume-run', 'Review pause'],
|
|
338
343
|
held: ['Needs your decision', s(row, 'hold_reason') ?? 'This task is on hold.', s(row, 'hold_kind') === 'operator' ? 'unhold' : 'inspect-hold', 'Review hold'],
|
|
339
344
|
'waiting-incident': ['Needs your decision', 'An unresolved incident holds the next attempt.', 'retry-task', 'Review and retry'],
|
|
340
345
|
'terminal-dependency': ['Needs your decision', 'A required task did not finish.', 'repair-dependency', 'Review required task'],
|
|
@@ -353,13 +358,13 @@ function itemOf(row, principal) {
|
|
|
353
358
|
updating: ['Waiting for app update', 'New work resumes after the update.', null, ''],
|
|
354
359
|
'planning-ready': ['Planner ready', 'A connected worker can draft the plan.', null, ''],
|
|
355
360
|
'scouting-ready': ['Scout ready', 'A connected worker can produce the report.', null, ''],
|
|
356
|
-
queued: ['
|
|
361
|
+
queued: ['Queued', '', null, ''],
|
|
357
362
|
};
|
|
358
363
|
let [label, detail, actionCode, actionLabel] = words[code];
|
|
359
364
|
if (code === 'signed-out') {
|
|
360
365
|
const [provider = '', authMode = 'subscription'] = (s(row, 'signed_out') ?? '').split(':');
|
|
361
366
|
label = signInReason({ provider: provider, authMode: authMode === 'api-key' ? 'api-key' : 'subscription' });
|
|
362
|
-
detail =
|
|
367
|
+
detail = `${label}. It starts again on its own once ${providerName(provider)} works.`;
|
|
363
368
|
}
|
|
364
369
|
if (code === 'running' && n(row, 'version_count') > 1)
|
|
365
370
|
label = 'Revising';
|
|
@@ -383,6 +388,17 @@ function itemOf(row, principal) {
|
|
|
383
388
|
decisionId: actionCode === 'answer-decision' ? n(row, 'question_id') || null : null },
|
|
384
389
|
access: read ? 'read' : principal === 'operator' ? 'operator-control' : ['answer-decision', 'write-scope', 'unhold'].includes(actionCode) ? 'proposal-only' : 'operator-handoff',
|
|
385
390
|
retry: actionCode === 'reconcile-run' ? 'reconcile-before-retry' : read ? 'read-again' : 'refresh-before-acting' };
|
|
391
|
+
// The one shared status: the headline and sentence every surface uses.
|
|
392
|
+
const reading = stageOfCode(code, { needsPerson: need, planning: s(row, 'live_role') === 'planner', operatorHold: s(row, 'hold_kind') === 'operator' });
|
|
393
|
+
const checkStatus = s(row, 'check_status'), verdict = s(row, 'proof_verdict');
|
|
394
|
+
const finished = reading.stage === 'finished' || reading.stage === 'complete';
|
|
395
|
+
const shared = taskStatusOf({ stage: reading.stage, ...(reading.need === undefined ? {} : { need: reading.need }),
|
|
396
|
+
reason: finished ? null : plainReasonOf(reading.stage, code, detail || null), report: s(row, 'result_role') === 'scout',
|
|
397
|
+
checks: !finished ? null : checkStatus === 'failed' || n(row, 'proof_failed_check') === 1 ? { status: 'failed', exitCode: null, head: null }
|
|
398
|
+
: checkStatus === 'passed' || verdict === 'verified' ? { status: 'passed', exitCode: null, head: null } : null,
|
|
399
|
+
completedBy: code === 'complete' ? String(row['checked_actor']).replace(/^(?:operator|coordinator|lead):/, '') : null });
|
|
400
|
+
label = shared.headline;
|
|
401
|
+
detail = shared.sentence;
|
|
386
402
|
const views = ['all'];
|
|
387
403
|
if (need)
|
|
388
404
|
views.push('needs-you');
|
|
@@ -395,7 +411,7 @@ function itemOf(row, principal) {
|
|
|
395
411
|
versionCount: n(row, 'version_count'), earlierActiveCount: n(row, 'earlier_active'), familyProblem: n(row, 'broken') ? FAMILY_PROBLEM : null,
|
|
396
412
|
liveRunId: n(row, 'live_run') || null, unfinishedRunId: n(row, 'unfinished') || null, resultRunId: n(row, 'result_id') || null, resultTaskId: n(row, 'result_id') ? id : null,
|
|
397
413
|
resultOutcome: s(row, 'result_outcome'), publicationUrl: s(row, 'publication_url'),
|
|
398
|
-
status: { token: `assignment-${assignmentState}`, label, detail, tone:
|
|
414
|
+
status: { token: `assignment-${assignmentState}`, label, detail, tone: workToneOf(shared.headline),
|
|
399
415
|
action: primaryAction === null ? null : { label: actionLabel, kind: actionCode === 'open-result' ? 'open-result' : 'open-task' }, views, rank: n(row, 'rank') },
|
|
400
416
|
primaryAction, completion: code === 'complete' ? { actor: String(row['checked_actor']), at: String(row['checked_at']), digest: String(row['checked_digest']) } : null,
|
|
401
417
|
evidence: 'recorded' };
|
package/dist/workspace-ui.d.ts
CHANGED
|
@@ -182,6 +182,13 @@ export type WorkStatus = DisplayStatus & {
|
|
|
182
182
|
* concrete act — never every queued task indiscriminately. */
|
|
183
183
|
export declare function needsPerson(dispatch: DispatchDiagnosis | null): boolean;
|
|
184
184
|
export declare function dispatchActionLabel(dispatch: DispatchDiagnosis | null): string;
|
|
185
|
+
/** A finished result's display status in the shared headline's words: a
|
|
186
|
+
* failed project check is Failed, a result missing its record needs a
|
|
187
|
+
* person, every other saved result is Ready for review. Token and action stay. */
|
|
188
|
+
export declare function resultHeadlineOf<T extends DisplayStatus>(status: T): T;
|
|
189
|
+
/** The one shared headline (task-status.ts) over the work status below: the
|
|
190
|
+
* token, views, rank, action and diagnostics stay; the words and tone are
|
|
191
|
+
* the eight headlines'. */
|
|
185
192
|
export declare function workStatusOf(facts: WorkFacts, resultDisplay?: DisplayStatus): WorkStatus;
|
|
186
193
|
/** The counts each view tab wears — from the same rows the page lists. */
|
|
187
194
|
export declare function workCounts(rows: readonly {
|
package/dist/workspace-ui.js
CHANGED
|
@@ -17,10 +17,11 @@
|
|
|
17
17
|
* claimed without one.
|
|
18
18
|
*/
|
|
19
19
|
import { GOAL_ASSESSMENT_PENDING, manualReviewOnly } from "./proof.js";
|
|
20
|
+
import { plainReasonOf, stageOfCode, taskStatusOf, workToneOf } from "./task-status.js";
|
|
20
21
|
export const WORK_VIEWS = [
|
|
21
22
|
{ key: "all", label: "All", hint: "Every task in view, most urgent first.", empty: "Nothing is in progress. Describe work in chat or add a task, and it appears here." },
|
|
22
23
|
{ key: "needs-you", label: "Needs you", hint: "Tasks waiting on an answer, approval, or your inspection.", empty: "Nothing needs you right now. Queued and running work continues on its own." },
|
|
23
|
-
{ key: "running", label: "
|
|
24
|
+
{ key: "running", label: "Building", hint: "Attempts a builder owns right now.", empty: "Nothing is building right now. Approved tasks start when a builder with capacity is connected." },
|
|
24
25
|
{ key: "completed", label: "Complete", hint: "Tasks the lead or user has marked complete. Recorded check results remain available.", empty: "No tasks have been marked complete in this view." },
|
|
25
26
|
];
|
|
26
27
|
export function parseWorkView(raw) {
|
|
@@ -302,7 +303,35 @@ const DISPATCH_ACTION_LABELS = {
|
|
|
302
303
|
export function dispatchActionLabel(dispatch) {
|
|
303
304
|
return dispatch?.action == null ? "View task details" : DISPATCH_ACTION_LABELS[dispatch.action];
|
|
304
305
|
}
|
|
306
|
+
/** Result tokens a done task can wear, as the shared stage they mean. A
|
|
307
|
+
* result that is missing its record waits on a person; a failed check fails. */
|
|
308
|
+
const RESULT_NEEDS_YOU = new Set(["no-build-record", "verification-needed", "evidence-mismatch", "record-incomplete", "evidence-damaged"]);
|
|
309
|
+
/** Result tokens that only follow a passing project check. */
|
|
310
|
+
const VERIFIED_RESULT = new Set(["ready-to-review", "pr-opened", "merge-observed", "pr-closed"]);
|
|
311
|
+
/** A finished result's display status in the shared headline's words: a
|
|
312
|
+
* failed project check is Failed, a result missing its record needs a
|
|
313
|
+
* person, every other saved result is Ready for review. Token and action stay. */
|
|
314
|
+
export function resultHeadlineOf(status) {
|
|
315
|
+
const read = RESULT_NEEDS_YOU.has(status.token) ? { stage: "needs-you", need: "other" }
|
|
316
|
+
: status.token === "checks-failed" ? { stage: "failed" } : { stage: "finished" };
|
|
317
|
+
const shared = taskStatusOf({ ...read, reason: status.detail, checks: read.stage === "failed" ? { status: "failed", exitCode: null, head: null }
|
|
318
|
+
: VERIFIED_RESULT.has(status.token) ? { status: "passed", exitCode: null, head: null } : null });
|
|
319
|
+
return { ...status, label: shared.headline, tone: workToneOf(shared.headline) };
|
|
320
|
+
}
|
|
321
|
+
/** The one shared headline (task-status.ts) over the work status below: the
|
|
322
|
+
* token, views, rank, action and diagnostics stay; the words and tone are
|
|
323
|
+
* the eight headlines'. */
|
|
305
324
|
export function workStatusOf(facts, resultDisplay) {
|
|
325
|
+
const status = workStatusWordsOf(facts, resultDisplay);
|
|
326
|
+
const done = facts.state === "done" && status.token !== "waiting-decision";
|
|
327
|
+
const read = done ? RESULT_NEEDS_YOU.has(status.token) ? { stage: "needs-you", need: "other" } : stageOfCode(status.token, { needsPerson: false })
|
|
328
|
+
: stageOfCode(status.token, { needsPerson: status.views.includes("needs-you"), planning: facts.dispatch?.code === "running" && facts.dispatch.role === "planner", operatorHold: facts.dispatch?.action === "unhold" });
|
|
329
|
+
const stage = done && read.stage !== "needs-you" && read.stage !== "failed" ? "finished" : read.stage;
|
|
330
|
+
const shared = taskStatusOf({ stage, ...(read.need === undefined ? {} : { need: read.need }), reason: plainReasonOf(stage, status.token, status.detail),
|
|
331
|
+
report: facts.result?.role === "scout", checks: stage === "finished" && VERIFIED_RESULT.has(status.token) ? { status: "passed", exitCode: null, head: null } : null });
|
|
332
|
+
return { ...status, label: shared.headline, tone: workToneOf(shared.headline), detail: stage === "finished" ? status.detail : shared.sentence };
|
|
333
|
+
}
|
|
334
|
+
function workStatusWordsOf(facts, resultDisplay) {
|
|
306
335
|
const dispatch = facts.dispatch;
|
|
307
336
|
const result = facts.state === "done" ? resultDisplay ?? resultStatusOf(facts.result, facts.publication) : null;
|
|
308
337
|
const running = facts.liveRunId !== null || (dispatch?.condition === "running" && !REVIEW_TOKENS.has(dispatch.code));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toolroll",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.6",
|
|
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"
|