toolroll 0.9.56 → 0.9.57
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/assignment-adapters.d.ts +5 -0
- package/dist/assignment.d.ts +24 -3
- package/dist/assignment.js +36 -10
- package/dist/browser-workspace.js +1 -1
- package/dist/chat-delivery.js +3 -3
- package/dist/chat-voice.js +1 -1
- package/dist/check-levels.d.ts +3 -0
- package/dist/check-levels.js +5 -0
- package/dist/dispose.js +0 -5
- package/dist/lead-status.d.ts +1 -1
- package/dist/lead-status.js +2 -2
- package/dist/operate.js +17 -8
- package/dist/proof.d.ts +9 -0
- package/dist/proof.js +12 -0
- package/dist/release-coverage.d.ts +16 -0
- package/dist/release-coverage.js +92 -0
- package/dist/server/chat.js +8 -6
- package/dist/server/render-results.d.ts +4 -1
- package/dist/server/render-results.js +23 -9
- package/dist/server/tasks.js +28 -25
- package/dist/store.d.ts +3 -5
- package/dist/store.js +17 -2
- package/dist/task-outcome-cli.js +3 -1
- package/dist/task-status.d.ts +59 -7
- package/dist/task-status.js +132 -16
- package/dist/team-runtime.js +1 -1
- package/dist/telegram-status.d.ts +11 -4
- package/dist/telegram-status.js +17 -10
- package/dist/telegram.js +4 -4
- package/dist/work-index.d.ts +6 -1
- package/dist/work-index.js +50 -8
- package/dist/workspace-ui.js +2 -1
- package/package.json +1 -1
package/dist/task-status.js
CHANGED
|
@@ -27,6 +27,17 @@ export const CHECKS_LABEL = "Project checks";
|
|
|
27
27
|
export const PR_CI_LABEL = "PR CI";
|
|
28
28
|
/** The one verb for opening a saved result, on every surface. */
|
|
29
29
|
export const OPEN_RESULT = "Open result";
|
|
30
|
+
export const CHECKED_AT_RELEASE = "Checked at release";
|
|
31
|
+
export function releaseStateOf(checks) {
|
|
32
|
+
if (checks?.level !== "off" || checks.status === "passed" || checks.status === "failed")
|
|
33
|
+
return undefined;
|
|
34
|
+
return checks.release != null ? "covered" : "pending";
|
|
35
|
+
}
|
|
36
|
+
/** How the project check stands for requirements that rest on it, read now: `covered` once a check passed on this
|
|
37
|
+
* commit (its own, a follow-up, or a release check containing it), `pending` while checks Off wait for a release. */
|
|
38
|
+
export function checkBackingOf(checks) {
|
|
39
|
+
return checks?.status === "passed" ? "covered" : checksUnderway(checks) ? "running" : releaseStateOf(checks);
|
|
40
|
+
}
|
|
30
41
|
/** "T2", "T2 and T3", "T2, T3 and 2 more": short enough for one line. The full list is on the result. */
|
|
31
42
|
export function peerWords(peers, shown = 2) {
|
|
32
43
|
if (peers.length <= shown)
|
|
@@ -43,6 +54,23 @@ const BATCH_WHY = {
|
|
|
43
54
|
conflict: "It conflicted with another result when merged for a batch check, so it was checked on its own.",
|
|
44
55
|
};
|
|
45
56
|
const short = (sha) => sha !== null && /^[a-f0-9]{7,40}$/.test(sha) ? sha.slice(0, 7) : null;
|
|
57
|
+
/** A check waiting for its batch or running on this commit: the result reads Ready meanwhile, not Needs you. */
|
|
58
|
+
const checksUnderway = (checks) => checks?.batch?.state === "waiting" || checks?.running != null;
|
|
59
|
+
/** A finished result a person still has to act on before it can be complete: an unresolved HIGH finding or unmet
|
|
60
|
+
* requirement, a missing check, or a requirement only they can confirm. Failed checks read Failed instead. */
|
|
61
|
+
function resultAsk(facts) {
|
|
62
|
+
if (facts.report || facts.checks?.status === "failed")
|
|
63
|
+
return null;
|
|
64
|
+
const blocker = facts.blockers?.find(one => one.key !== "check-failed" && !(one.key === "check-missing" && checksUnderway(facts.checks)));
|
|
65
|
+
if (blocker !== undefined)
|
|
66
|
+
return blocker.message;
|
|
67
|
+
const req = facts.requirements;
|
|
68
|
+
if (req == null || facts.accepted === true || req.unverified === true)
|
|
69
|
+
return null;
|
|
70
|
+
if ((req.yours > 0 || (req.unshown ?? 0) > 0) && req.ask != null)
|
|
71
|
+
return req.ask;
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
46
74
|
export function headlineOf(facts) {
|
|
47
75
|
switch (facts.stage) {
|
|
48
76
|
case "queued": return "Queued";
|
|
@@ -57,7 +85,10 @@ export function headlineOf(facts) {
|
|
|
57
85
|
return "Ready for review";
|
|
58
86
|
if (facts.checks?.status === "failed")
|
|
59
87
|
return "Failed";
|
|
60
|
-
//
|
|
88
|
+
// A requirement a person checks, a missing screenshot, an unmet requirement or a HIGH finding: Needs you, with the ask.
|
|
89
|
+
if (resultAsk(facts) !== null)
|
|
90
|
+
return "Needs you";
|
|
91
|
+
// Off: ready like any build; the sentence and the Checks row say it is checked at release.
|
|
61
92
|
return "Ready for review";
|
|
62
93
|
case "complete": return "Complete";
|
|
63
94
|
case "failed": return "Failed";
|
|
@@ -71,15 +102,17 @@ function sentenceOf(headline, facts) {
|
|
|
71
102
|
case "Queued": return reason ?? "Waiting for a worker.";
|
|
72
103
|
case "Planning": return reason ?? "The lead is writing the plan.";
|
|
73
104
|
// One plain sentence per need (needs-you.ts); a recorded reason only where the need keeps one.
|
|
74
|
-
case "Needs you": return needSentence(facts.need ?? "other", reason, facts.needContext);
|
|
105
|
+
case "Needs you": return facts.stage === "finished" ? resultAsk(facts) ?? needSentence("review-result", reason, facts.needContext) : needSentence(facts.need ?? "other", reason, facts.needContext);
|
|
75
106
|
case "Waiting": return waitSentence(facts.wait, reason, facts.needContext);
|
|
76
107
|
case "Building": return facts.stage === "checking" ? "Checks are running on the change."
|
|
77
108
|
: facts.stage === "reviewing" ? "Reviewing the change before it reaches you." : reason ?? "An agent is working on it.";
|
|
78
109
|
case "Ready for review":
|
|
79
110
|
if (facts.report)
|
|
80
111
|
return "The report is ready to read. Read it, then mark it complete.";
|
|
112
|
+
if (releaseStateOf(facts.checks) === "covered")
|
|
113
|
+
return `Checked at release on ${short(facts.checks.release.head) ?? "its release"}. Review the change, then mark it complete.`;
|
|
81
114
|
if (facts.checks?.level === "off" && facts.checks.status !== "passed")
|
|
82
|
-
return "
|
|
115
|
+
return "Checked at release. Review the change, then mark it complete.";
|
|
83
116
|
if (facts.checks?.status === "passed") {
|
|
84
117
|
// The Checks row names the others and the batch commit; the sentence doesn't repeat them.
|
|
85
118
|
const peers = facts.checks.batch?.state === "together" ? facts.checks.batch.peers.length : 0;
|
|
@@ -135,8 +168,10 @@ function detailsOf(headline, facts) {
|
|
|
135
168
|
row("checks", CHECKS_LABEL, `${quick ? "Quick checks failed" : "Failed"}${checks.exitCode === null ? "" : ` (exit ${checks.exitCode})`}`, problem(headline), headline === "Failed" ? { href: checksHref } : { action: { label: "See what failed", href: checksHref } });
|
|
136
169
|
else if (checks.status === "running")
|
|
137
170
|
row("checks", CHECKS_LABEL, "Running", "running");
|
|
171
|
+
else if (checks.level === "off" && checks.release != null)
|
|
172
|
+
row("checks", CHECKS_LABEL, `Passed at release on ${short(checks.release.head) ?? `build #${checks.release.run}`}`, "ok", { href: checksHref });
|
|
138
173
|
else if (checks.level === "off")
|
|
139
|
-
row("checks", CHECKS_LABEL,
|
|
174
|
+
row("checks", CHECKS_LABEL, CHECKED_AT_RELEASE, "none", runChecks === null ? {} : { action: runChecks });
|
|
140
175
|
else if (checks.status === "not-run")
|
|
141
176
|
row("checks", CHECKS_LABEL, "Didn't run", "none", runChecks === null ? {} : { action: runChecks });
|
|
142
177
|
// A saved check that can't be read is run again where that is possible.
|
|
@@ -173,8 +208,9 @@ function detailsOf(headline, facts) {
|
|
|
173
208
|
else if (req != null && req.total > 0 && req.unverified === true)
|
|
174
209
|
row("requirements", "Requirements", "Unverified", "none");
|
|
175
210
|
else if (req != null && req.total > 0) {
|
|
176
|
-
const
|
|
177
|
-
const
|
|
211
|
+
const atRelease = req.atRelease ?? 0;
|
|
212
|
+
const unmet = req.total - req.met - req.yours - atRelease;
|
|
213
|
+
const text = `${req.met} of ${req.total} met${atRelease > 0 ? ` · ${atRelease} at release` : ""}${req.yours > 0 ? ` · You check ${req.yours}` : ""}`;
|
|
178
214
|
row("requirements", "Requirements", text, unmet > 0 ? problem(headline) : req.met === req.total ? "ok" : "none", unmet > 0 && headline !== "Failed" ? { action: { label: "See which", href: facts.links?.checks ?? facts.links?.result ?? null } } : {});
|
|
179
215
|
}
|
|
180
216
|
const evidence = facts.evidence;
|
|
@@ -203,19 +239,23 @@ function leadSentence(headline, facts) {
|
|
|
203
239
|
return `${facts.checks.level === "quick" ? "Quick checks" : "Checks"} failed${sha === null ? "" : ` on ${sha}`}. ${leadOnIt(facts.leadName)}`;
|
|
204
240
|
return leadOnIt(facts.leadName);
|
|
205
241
|
}
|
|
206
|
-
|
|
242
|
+
/** The one status every surface shows (console list, task and result pages, CLI, chat, phone, `status`). */
|
|
243
|
+
export function statusOf(facts) {
|
|
207
244
|
const read = headlineOf(facts);
|
|
208
245
|
const lead = facts.lead != null && LEAD_HEADLINES.has(read) ? facts.lead : null;
|
|
209
246
|
// The lead has it: nothing waits on the person, so a Needs you reads Waiting.
|
|
210
247
|
const headline = lead === "on-it" && read === "Needs you" ? "Waiting" : read;
|
|
211
|
-
// Every Needs you carries the one action that resolves it, worded the same everywhere.
|
|
212
|
-
const
|
|
248
|
+
// Every Needs you carries the one action that resolves it, worded the same everywhere; a finished result's is Open result.
|
|
249
|
+
const needKey = facts.stage === "finished" ? "review-result" : facts.need ?? "other";
|
|
250
|
+
const need = headline === "Needs you" ? { key: needKey, action: NEEDS[needKey].action } : null;
|
|
213
251
|
const ownWords = need !== null && (need.key === "other" || need.key === "review-result") && facts.action != null;
|
|
214
252
|
const primaryAction = need === null || ownWords ? facts.action ?? null : { label: need.action.label, href: facts.action?.href ?? null };
|
|
215
253
|
const sentence = lead === "on-it" ? leadSentence(headline, facts) : lead === "lapsed" ? `${leadLapsed(facts.leadName)} ${sentenceOf(headline, facts)}` : sentenceOf(headline, facts);
|
|
216
254
|
return { headline, tone: HEADLINE_TONE[headline], sentence, details: detailsOf(headline, facts),
|
|
217
255
|
primaryAction, why: [...new Set(facts.why ?? [])].filter(one => one.trim() !== ""), need };
|
|
218
256
|
}
|
|
257
|
+
/** The name most surfaces already import. */
|
|
258
|
+
export const taskStatusOf = statusOf;
|
|
219
259
|
// ---- reading the existing projections ----------------------------------------
|
|
220
260
|
/** Every dispatch or work-index code, as the stage it means. Unknown codes that
|
|
221
261
|
* wait on a person stay Needs you; everything else waits in the queue. */
|
|
@@ -316,19 +356,93 @@ export function assignmentStageOf(assignment, work, planning = false) {
|
|
|
316
356
|
}
|
|
317
357
|
/** Evidence that passed and only awaits the retired assessment step is met. */
|
|
318
358
|
const requirementMet = (row) => row.state === "pass" || (row.assessment?.evidenceState === "pass" && (row.review ?? null) === null);
|
|
319
|
-
|
|
359
|
+
/** A requirement whose only gap is the project check: every other kind it needs was shown, none is a person's check. */
|
|
360
|
+
const onlyCheckMissing = (row) => {
|
|
361
|
+
const needs = row.requiredEvidence ?? [];
|
|
362
|
+
const answered = new Set((row.answered ?? []).map(one => one.kind));
|
|
363
|
+
if (row.state !== "missing" || row.review != null || row.assessment?.evidenceState === "failed" ||
|
|
364
|
+
!needs.includes("check") || needs.includes("manual-review") || !needs.every(kind => kind === "check" || answered.has(kind)))
|
|
365
|
+
return false;
|
|
366
|
+
// `answered` contains claims, not validated evidence. In legacy matrices a missing
|
|
367
|
+
// check takes precedence over an invalid screenshot/changed path in `state`.
|
|
368
|
+
// Only the stored validator's check-only gaps establish that every other
|
|
369
|
+
// reference resolved. Direct assessment keeps these outcomes in its own detail.
|
|
370
|
+
const detail = row.assessment?.detail ?? row.detail ?? [];
|
|
371
|
+
const checkGaps = new Set([
|
|
372
|
+
`criterion "${row.id}" requires check evidence, which the proof does not reference`,
|
|
373
|
+
`criterion "${row.id}" is waiting for the final check`,
|
|
374
|
+
`criterion "${row.id}" needs a passing approved project check`,
|
|
375
|
+
]);
|
|
376
|
+
return (needs.every(kind => kind === "check") || detail.length > 0) && detail.every(one => checkGaps.has(one));
|
|
377
|
+
};
|
|
378
|
+
/** A required screenshot nobody captured. */
|
|
379
|
+
const screenshotMissing = (row) => (row.requiredEvidence ?? []).includes("screenshot") && !(row.answered ?? []).some(one => one.kind === "screenshot");
|
|
380
|
+
export function requirementWordOf(row, verdict, release) {
|
|
320
381
|
if (verdict === "refuted")
|
|
321
382
|
return "Unverified";
|
|
322
383
|
if (requirementMet(row))
|
|
323
384
|
return "Met";
|
|
324
|
-
|
|
385
|
+
if (release !== undefined && onlyCheckMissing(row))
|
|
386
|
+
return release === "covered" ? "Met" : release === "running" ? "Waiting for checks" : CHECKED_AT_RELEASE;
|
|
387
|
+
if (row.state === "manual-review")
|
|
388
|
+
return "You check";
|
|
389
|
+
return row.state === "missing" || screenshotMissing(row) ? "Not shown yet" : "Not met";
|
|
390
|
+
}
|
|
391
|
+
const ASK_STATEMENT = 140;
|
|
392
|
+
const quoted = (statement) => {
|
|
393
|
+
const one = (statement ?? "").replace(/\s+/g, " ").trim();
|
|
394
|
+
return one.length > ASK_STATEMENT ? `${one.slice(0, ASK_STATEMENT - 1)}…` : one;
|
|
395
|
+
};
|
|
396
|
+
/** The ask for one unresolved requirement: what is wrong with it, and the two ways on. */
|
|
397
|
+
function unresolvedAsk(row, word) {
|
|
398
|
+
const what = word === "Not shown yet" && screenshotMissing(row) ? "A screenshot is missing for"
|
|
399
|
+
: word === "Not shown yet" ? "Nothing shows this requirement is met" : word === "Unverified" ? "This requirement couldn't be verified" : "This requirement isn't met";
|
|
400
|
+
return `${what}: ${quoted(row.statement) || row.id || "a requirement"}. Ask for changes, or accept the result with a reason.`;
|
|
325
401
|
}
|
|
326
402
|
/** Requirements from the stored matrix: met, the ones only a person confirms, and the total. */
|
|
327
|
-
export function requirementsOf(matrix) {
|
|
403
|
+
export function requirementsOf(matrix, release) {
|
|
328
404
|
if (!matrix || matrix.length === 0)
|
|
329
405
|
return null;
|
|
330
|
-
|
|
331
|
-
|
|
406
|
+
const words = matrix.map(row => requirementWordOf(row, null, release));
|
|
407
|
+
const yours = matrix.find((_, index) => words[index] === "You check");
|
|
408
|
+
const unshown = matrix.filter((row, index) => words[index] === "Not shown yet" && screenshotMissing(row));
|
|
409
|
+
const ask = yours !== undefined ? `Check this requirement yourself, then mark it complete: ${quoted(yours.statement)}`
|
|
410
|
+
: unshown.length > 0 ? unresolvedAsk(unshown[0], "Not shown yet") : null;
|
|
411
|
+
return { met: words.filter(word => word === "Met").length, total: matrix.length, yours: words.filter(word => word === "You check").length,
|
|
412
|
+
missed: words.filter(word => word === "Not met" || word === "Not shown yet").length,
|
|
413
|
+
...(release !== "pending" ? {} : { atRelease: words.filter(word => word === CHECKED_AT_RELEASE).length }),
|
|
414
|
+
...(unshown.length === 0 ? {} : { unshown: unshown.length }),
|
|
415
|
+
...(ask === null ? {} : { ask }) };
|
|
416
|
+
}
|
|
417
|
+
/** What refuses Complete, for `task complete`, the console and chat completion and deploy-candidate alike: a failed
|
|
418
|
+
* check; a required check that is missing (checks deliberately Off are not missing: they are checked at release);
|
|
419
|
+
* an unresolved requirement (a person's own check is theirs to give by completing; a recorded acceptance resolves
|
|
420
|
+
* the rest); an unresolved HIGH review finding. A release check that covers the commit counts as passed. */
|
|
421
|
+
export function completionBlockersOf(f) {
|
|
422
|
+
const blockers = [];
|
|
423
|
+
const release = releaseStateOf(f.checks);
|
|
424
|
+
const backing = checkBackingOf(f.checks);
|
|
425
|
+
if (!f.report && f.checks?.status === "failed") {
|
|
426
|
+
blockers.push({ key: "check-failed", message: `Checks failed${f.checks.exitCode === null ? "" : ` (exit ${f.checks.exitCode})`}. Fix them and run checks again, or ask for changes.` });
|
|
427
|
+
}
|
|
428
|
+
else if (!f.report && f.checkRequired && f.checks?.status !== "passed" && release === undefined) {
|
|
429
|
+
blockers.push({ key: "check-missing", message: checksUnderway(f.checks) ? "Its project check hasn't finished yet. Mark it complete once it passes."
|
|
430
|
+
: "No passing project check is recorded for this result. Run checks on it, then mark it complete." });
|
|
431
|
+
}
|
|
432
|
+
if (!f.accepted && f.checks?.status !== "failed") {
|
|
433
|
+
const refuted = f.verdict === "refuted";
|
|
434
|
+
const unresolved = (f.matrix ?? []).map(row => ({ row, word: requirementWordOf(row, refuted ? "refuted" : null, backing) }))
|
|
435
|
+
.filter(one => one.word === "Not met" || one.word === "Not shown yet" || one.word === "Unverified");
|
|
436
|
+
if (unresolved.length === 1)
|
|
437
|
+
blockers.push({ key: "criteria", message: unresolvedAsk(unresolved[0].row, unresolved[0].word) });
|
|
438
|
+
else if (unresolved.length > 1) {
|
|
439
|
+
const ids = unresolved.map(one => one.row.id ?? "?");
|
|
440
|
+
blockers.push({ key: "criteria", message: `${unresolved.length} requirements ${unresolved.every(one => one.word === "Unverified") ? "couldn't be verified" : "aren't met yet"} (${ids.slice(0, 6).join(", ")}${ids.length > 6 ? ", …" : ""}). Ask for changes, or accept the result with a reason.` });
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
if (f.high > 0)
|
|
444
|
+
blockers.push({ key: "high", message: `The automatic review found ${f.high === 1 ? "a high-severity problem" : `${f.high} high-severity problems`}. Ask for changes before marking it complete.` });
|
|
445
|
+
return blockers;
|
|
332
446
|
}
|
|
333
447
|
/** A refuted report verifies none of its requirements, whatever it marked met (a person's acceptance doesn't change that). */
|
|
334
448
|
export function unverifiedWhenRefuted(requirements, verdict) {
|
|
@@ -355,7 +469,7 @@ export function assignmentStatusFacts(assignment, options = {}) {
|
|
|
355
469
|
const checks = receipt === null || !withResult ? null
|
|
356
470
|
: { status: receipt.checks.status, exitCode: receipt.checks.exitCode, head: receipt.head,
|
|
357
471
|
...(receipt.checks.level == null ? {} : { level: receipt.checks.level }), ...(receipt.checks.running == null ? {} : { running: receipt.checks.running }),
|
|
358
|
-
...(receipt.checks.batch == null ? {} : { batch: receipt.checks.batch }) };
|
|
472
|
+
...(receipt.checks.batch == null ? {} : { batch: receipt.checks.batch }), ...(receipt.checks.release == null ? {} : { release: receipt.checks.release }) };
|
|
359
473
|
const publication = assignment.publication;
|
|
360
474
|
const pullRequest = options.pullRequest !== undefined ? options.pullRequest
|
|
361
475
|
: publication === null ? (withResult && !report ? { state: "none", number: null, url: null, ci: null, error: null } : null)
|
|
@@ -368,7 +482,9 @@ export function assignmentStatusFacts(assignment, options = {}) {
|
|
|
368
482
|
stage, ...(need === undefined ? {} : { need }), ...(wait === undefined ? {} : { wait }), ...(build === null ? {} : { needContext: { build } }), reason, report,
|
|
369
483
|
checks,
|
|
370
484
|
pullRequest: withResult ? pullRequest : null,
|
|
371
|
-
requirements: withResult ? unverifiedWhenRefuted(requirementsOf(receipt?.proof?.matrix), receipt?.proof?.verdict) : null,
|
|
485
|
+
requirements: withResult ? unverifiedWhenRefuted(requirementsOf(receipt?.proof?.matrix, checkBackingOf(checks)), assignment.readiness?.verdict ?? receipt?.proof?.verdict) : null,
|
|
486
|
+
...(withResult && receipt?.proofAcceptance != null ? { accepted: true } : {}),
|
|
487
|
+
...(withResult && (assignment.readiness?.blockers.length ?? 0) > 0 ? { blockers: assignment.readiness.blockers } : {}),
|
|
372
488
|
evidence: withResult ? options.evidence ?? evidenceOf(receipt, [...(receipt?.caveats ?? []), ...assignment.attention]) : null,
|
|
373
489
|
completedBy: assignment.completion === null ? null : assignment.completion.lead === true ? "the lead" : assignment.completion.actor.replace(/^(?:operator|coordinator|lead):/, ""),
|
|
374
490
|
action: options.action ?? null,
|
package/dist/team-runtime.js
CHANGED
|
@@ -58,7 +58,7 @@ export function createTeamRuntime(options) {
|
|
|
58
58
|
const previous = pages.get(key);
|
|
59
59
|
if (revision && previous?.revision === revision && previous.expires > now.getTime())
|
|
60
60
|
return previous.page;
|
|
61
|
-
const page = workIndexPage(store, now, { principal: 'operator', repos: selected.projects }, { leadId: selected.leadId, limit: 40 });
|
|
61
|
+
const page = workIndexPage(store, now, { principal: 'operator', repos: selected.projects }, { leadId: selected.leadId, limit: 40, root: options.evidenceRoot });
|
|
62
62
|
if (revision && options.workspaceRevision) {
|
|
63
63
|
if (pages.size >= 64)
|
|
64
64
|
pages.delete(pages.keys().next().value);
|
|
@@ -17,9 +17,16 @@ export declare const PHONE_HELP: string;
|
|
|
17
17
|
/** The same display scrub for every phone-bound string a person or a model authored. */
|
|
18
18
|
export declare const phoneText: typeof publicChatText;
|
|
19
19
|
export declare function projectLabel(repo: string): string;
|
|
20
|
+
/** The phone's reading of saved results: `root` lets a finished result read its saved checks, requirements and
|
|
21
|
+
* review exactly as the console does. Without it, the dispatch diagnosis speaks. */
|
|
22
|
+
export type PhoneRead = {
|
|
23
|
+
root?: string | undefined;
|
|
24
|
+
now?: Date;
|
|
25
|
+
repos?: readonly string[];
|
|
26
|
+
};
|
|
20
27
|
/** repos is the transport's explicit enrollment ceiling, never opened-project
|
|
21
28
|
* history. This snapshot is intentionally bounded and advertises that bound. */
|
|
22
|
-
export declare function phoneStatus(store: Store, repos: readonly string[], now: Date, focused?: string | null): string;
|
|
29
|
+
export declare function phoneStatus(store: Store, repos: readonly string[], now: Date, focused?: string | null, root?: string): string;
|
|
23
30
|
/** One task a person can pick to talk about from a chat app. */
|
|
24
31
|
export type PhoneTaskChoice = {
|
|
25
32
|
id: string;
|
|
@@ -31,7 +38,7 @@ export type PhoneTaskChoice = {
|
|
|
31
38
|
* then work in progress, waiting work and recent results; one entry per
|
|
32
39
|
* task (its revisions share it), optionally narrowed by words in the id or
|
|
33
40
|
* title. Bounded, within the chat's own project ceiling. */
|
|
34
|
-
export declare function phoneTaskChoices(store: Store, repos: readonly string[], now: Date, query?: string | null, limit?: number): PhoneTaskChoice[];
|
|
41
|
+
export declare function phoneTaskChoices(store: Store, repos: readonly string[], now: Date, query?: string | null, limit?: number, root?: string): PhoneTaskChoice[];
|
|
35
42
|
/** `id` keys the chat's choice (the task's first version, where its chat
|
|
36
43
|
* lives); `view` is what to show: the exact version named, else the current one. */
|
|
37
44
|
export type PhoneTaskPick = {
|
|
@@ -63,10 +70,10 @@ export type PhoneTaskLink = {
|
|
|
63
70
|
};
|
|
64
71
|
/** Where the rest lives when no button can say so: the closing line of an unlinked `/task`. */
|
|
65
72
|
export declare const PHONE_CONSOLE_FOOTER = "Read-only status. Saved files and full actions are in the console.";
|
|
66
|
-
export declare function phoneTask(store: Store, repos: readonly string[], id: string, now: Date): string;
|
|
73
|
+
export declare function phoneTask(store: Store, repos: readonly string[], id: string, now: Date, root?: string): string;
|
|
67
74
|
/** One task's status for a chat app: the words, its one console link, and
|
|
68
75
|
* the saved result it shows (a reply to it is about that result). */
|
|
69
|
-
export declare function phoneTaskView(store: Store, repos: readonly string[], id: string, now: Date): {
|
|
76
|
+
export declare function phoneTaskView(store: Store, repos: readonly string[], id: string, now: Date, root?: string): {
|
|
70
77
|
text: string;
|
|
71
78
|
link: PhoneTaskLink | null;
|
|
72
79
|
run: number | null;
|
package/dist/telegram-status.js
CHANGED
|
@@ -4,7 +4,8 @@ import { manualReviewOnly } from "./proof.js";
|
|
|
4
4
|
* No provider calls, repository access, new workflow state, or inferred success. */
|
|
5
5
|
import { diagnoseTaskDispatch, withDispatchDiagnoses } from "./dispatch.js";
|
|
6
6
|
import { familyCompleted } from "./result-completion.js";
|
|
7
|
-
import { plainReasonOf, stageOfDispatch, taskStatusOf } from "./task-status.js";
|
|
7
|
+
import { assignmentStatusFacts, plainReasonOf, stageOfDispatch, taskStatusOf } from "./task-status.js";
|
|
8
|
+
import { assignmentOf } from "./assignment.js";
|
|
8
9
|
import { CHAT_CONTROLS, chatControlHref, chatResultHref } from "./chat-controls.js";
|
|
9
10
|
export function phoneCommand(text) {
|
|
10
11
|
// Slash forms everywhere; the bare words too, because Slack and Discord
|
|
@@ -53,10 +54,16 @@ export function projectLabel(repo) {
|
|
|
53
54
|
return plain(repo.split(/[\\/]/).filter(Boolean).pop() ?? "project", 48);
|
|
54
55
|
}
|
|
55
56
|
/** The shared headline (task-status.ts) for a dispatch diagnosis on the phone. */
|
|
56
|
-
function headlineFor(store, id, d) {
|
|
57
|
+
function headlineFor(store, id, d, read_ = {}) {
|
|
57
58
|
// Any version of a completed family reads Complete, as taskWaitSnapshot says.
|
|
58
59
|
const completed = d.code === "complete" && familyCompleted(store, id);
|
|
59
60
|
const read = stageOfDispatch(d, { completed });
|
|
61
|
+
// A finished result reads the console's own facts: its requirements, checks Off at release, and what holds it back.
|
|
62
|
+
if (read.stage === "finished" && read_.root !== undefined && read_.now !== undefined && read_.repos !== undefined) {
|
|
63
|
+
const assignment = assignmentOf(store, id, read_.now, { principal: "operator", repos: read_.repos }, read_.root);
|
|
64
|
+
if (assignment !== null && assignment.state === "ready-to-check")
|
|
65
|
+
return taskStatusOf(assignmentStatusFacts(assignment));
|
|
66
|
+
}
|
|
60
67
|
const finished = read.stage === "finished" || read.stage === "complete" || read.stage === "failed";
|
|
61
68
|
return taskStatusOf({ stage: read.stage, ...(read.need === undefined ? {} : { need: read.need }), reason: finished ? null : plainReasonOf(read.stage, d.code, d.detail),
|
|
62
69
|
checks: read.stage === "failed" && d.code === "proof-refuted" ? { status: "failed", exitCode: null, head: null } : null });
|
|
@@ -95,7 +102,7 @@ function nextStep(d) {
|
|
|
95
102
|
}
|
|
96
103
|
/** repos is the transport's explicit enrollment ceiling, never opened-project
|
|
97
104
|
* history. This snapshot is intentionally bounded and advertises that bound. */
|
|
98
|
-
export function phoneStatus(store, repos, now, focused = null) {
|
|
105
|
+
export function phoneStatus(store, repos, now, focused = null, root) {
|
|
99
106
|
if (repos.length === 0)
|
|
100
107
|
return "No connected projects are available to this bridge. Add a project in Toolroll, then send /status again.";
|
|
101
108
|
return store.transact(() => {
|
|
@@ -111,7 +118,7 @@ export function phoneStatus(store, repos, now, focused = null) {
|
|
|
111
118
|
continue;
|
|
112
119
|
lines.push(`${group} · ${rows.length}${snapshot.tasksSaturated ? " in this snapshot" : ""}`);
|
|
113
120
|
for (const task of rows.slice(0, 2)) {
|
|
114
|
-
lines.push(`${plain(task.id, 64)} · ${projectLabel(repos[task.repoIndex])}`, ` ${plain(task.title, 64)} — ${headlineFor(store, task.id, task.dispatch).headline}`);
|
|
121
|
+
lines.push(`${plain(task.id, 64)} · ${projectLabel(repos[task.repoIndex])}`, ` ${plain(task.title, 64)} — ${headlineFor(store, task.id, task.dispatch, { root, now, repos }).headline}`);
|
|
115
122
|
}
|
|
116
123
|
if (rows.length > 2)
|
|
117
124
|
lines.push(` +${rows.length - 2} more in the console`);
|
|
@@ -126,7 +133,7 @@ const PICK_GROUPS = ["Needs attention", "Working", "Waiting / next up", "Finishe
|
|
|
126
133
|
* then work in progress, waiting work and recent results; one entry per
|
|
127
134
|
* task (its revisions share it), optionally narrowed by words in the id or
|
|
128
135
|
* title. Bounded, within the chat's own project ceiling. */
|
|
129
|
-
export function phoneTaskChoices(store, repos, now, query = null, limit = 8) {
|
|
136
|
+
export function phoneTaskChoices(store, repos, now, query = null, limit = 8, root) {
|
|
130
137
|
if (repos.length === 0)
|
|
131
138
|
return [];
|
|
132
139
|
return store.transact(() => {
|
|
@@ -142,7 +149,7 @@ export function phoneTaskChoices(store, repos, now, query = null, limit = 8) {
|
|
|
142
149
|
if (seen.has(id) || !words.every(word => `${id} ${row.title}`.toLowerCase().includes(word)))
|
|
143
150
|
continue;
|
|
144
151
|
seen.add(id);
|
|
145
|
-
choices.push({ id, title: plain(row.title, 64), label: headlineFor(store, row.id, row.dispatch).headline, group: groupOf(row.dispatch) });
|
|
152
|
+
choices.push({ id, title: plain(row.title, 64), label: headlineFor(store, row.id, row.dispatch, { root, now, repos }).headline, group: groupOf(row.dispatch) });
|
|
146
153
|
if (choices.length >= limit)
|
|
147
154
|
break;
|
|
148
155
|
}
|
|
@@ -210,12 +217,12 @@ function taskLinkFor(id, d, result) {
|
|
|
210
217
|
default: return control("task");
|
|
211
218
|
}
|
|
212
219
|
}
|
|
213
|
-
export function phoneTask(store, repos, id, now) {
|
|
214
|
-
return phoneTaskView(store, repos, id, now).text;
|
|
220
|
+
export function phoneTask(store, repos, id, now, root) {
|
|
221
|
+
return phoneTaskView(store, repos, id, now, root).text;
|
|
215
222
|
}
|
|
216
223
|
/** One task's status for a chat app: the words, its one console link, and
|
|
217
224
|
* the saved result it shows (a reply to it is about that result). */
|
|
218
|
-
export function phoneTaskView(store, repos, id, now) {
|
|
225
|
+
export function phoneTaskView(store, repos, id, now, root) {
|
|
219
226
|
return store.transact(() => {
|
|
220
227
|
const ref = store.lookupRef(id);
|
|
221
228
|
// Check admission before reading a title, run, proof, or diagnosis.
|
|
@@ -225,7 +232,7 @@ export function phoneTaskView(store, repos, id, now) {
|
|
|
225
232
|
const d = diagnoseTaskDispatch(store, id, now);
|
|
226
233
|
if (d === null)
|
|
227
234
|
return { text: "This task's status is unavailable. Open it in the console before retrying.", link: null, run: null };
|
|
228
|
-
const shared = headlineFor(store, id, d);
|
|
235
|
+
const shared = headlineFor(store, id, d, { root, now, repos });
|
|
229
236
|
const lines = [plain(task.title, 140), `${plain(id, 64)} · ${projectLabel(ref.repo)}`, `As of ${now.toISOString().replace("T", " ").slice(0, 19)} UTC`, "", shared.headline];
|
|
230
237
|
const blocker = d.blockerTaskId === null ? null : store.lookupRef(d.blockerTaskId);
|
|
231
238
|
const hiddenDependency = blocker !== null && (blocker.repo === null || !repos.includes(blocker.repo));
|
package/dist/telegram.js
CHANGED
|
@@ -1884,9 +1884,9 @@ function applyPhoneRead(context, update, effects) {
|
|
|
1884
1884
|
const repos = telegramConversationRepos(store, binding.approver, registry);
|
|
1885
1885
|
const focused = focusedTitle(store, binding);
|
|
1886
1886
|
if (command.kind === "status")
|
|
1887
|
-
response = phoneStatus(store, repos, clock(), focused?.title ?? null);
|
|
1887
|
+
response = phoneStatus(store, repos, clock(), focused?.title ?? null, context.conversation?.evidenceRoot);
|
|
1888
1888
|
else if (command.kind === "tasks") {
|
|
1889
|
-
const choices = phoneTaskChoices(store, repos, clock());
|
|
1889
|
+
const choices = phoneTaskChoices(store, repos, clock(), null, 8, context.conversation?.evidenceRoot);
|
|
1890
1890
|
response = choices.length === 0 ? phoneTaskListText([], null) : `${focused === null ? "" : `Talking about: ${focused.title}\n\n`}Pick a task to talk about:`;
|
|
1891
1891
|
if (choices.length > 0)
|
|
1892
1892
|
keyboard = pickKeyboard(store, choices, focused !== null);
|
|
@@ -1901,7 +1901,7 @@ function applyPhoneRead(context, update, effects) {
|
|
|
1901
1901
|
response = PHONE_NO_MATCH;
|
|
1902
1902
|
else {
|
|
1903
1903
|
store.setChatFocus("telegram", binding.id, pick.id, clock());
|
|
1904
|
-
const view = phoneTaskView(store, repos, pick.view, clock());
|
|
1904
|
+
const view = phoneTaskView(store, repos, pick.view, clock(), context.conversation?.evidenceRoot);
|
|
1905
1905
|
shown = { task: pick.view, run: view.run };
|
|
1906
1906
|
button = phoneLinkButton(context.conversation?.phoneOrigin?.() ?? null, view.link);
|
|
1907
1907
|
// A destination with no trusted origin to carry it: the words say where instead.
|
|
@@ -2124,7 +2124,7 @@ function applyCallback(context, update, effects) {
|
|
|
2124
2124
|
store.setChatFocus("telegram", binding.id, id, clock());
|
|
2125
2125
|
ack(`Talking about: ${title}`.slice(0, 190));
|
|
2126
2126
|
const current = store.taskFamilyOf(taskId, repos, false)?.current.id ?? id;
|
|
2127
|
-
const view = phoneTaskView(store, repos, current, clock());
|
|
2127
|
+
const view = phoneTaskView(store, repos, current, clock(), context.conversation?.evidenceRoot);
|
|
2128
2128
|
store.recordTelegramTaskMessage(binding, String(message.message_id), current, view.run, clock());
|
|
2129
2129
|
editText(phoneFocusText(view.text), [[telegramButton("Back to the lead", "pick:lead")]]);
|
|
2130
2130
|
return;
|
package/dist/work-index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Store, type TaskState } from './store.js';
|
|
2
|
-
import type
|
|
2
|
+
import { type AssignmentSnapshot } from './assignment.js';
|
|
3
3
|
import type { WorkAction, WorkSummaryAccess } from './work-summary.js';
|
|
4
4
|
import type { WorkStatus, WorkView } from './workspace-ui.js';
|
|
5
5
|
import { type Ask, type AskChip } from './needs-you.js';
|
|
@@ -37,6 +37,7 @@ export type WorkIndexItem = {
|
|
|
37
37
|
/** The viewer's own lead took it on (lead-voice.ts): "<name> is on it.", by the name they gave it, read now. */
|
|
38
38
|
lead?: string;
|
|
39
39
|
};
|
|
40
|
+
/** `root`: the evidence root, so a result whose checks were Off can read the release check that covers it. */
|
|
40
41
|
export type WorkIndexOptions = {
|
|
41
42
|
view?: WorkView;
|
|
42
43
|
limit?: number;
|
|
@@ -44,6 +45,7 @@ export type WorkIndexOptions = {
|
|
|
44
45
|
project?: string | null;
|
|
45
46
|
state?: TaskState;
|
|
46
47
|
leadId?: string;
|
|
48
|
+
root?: string;
|
|
47
49
|
};
|
|
48
50
|
/** How the list is grouped: the Needs you rows by their ask, then building, then the rest. */
|
|
49
51
|
export type WorkIndexGroup = Ask | 'building' | 'rest';
|
|
@@ -78,6 +80,9 @@ export declare class WorkIndexCursorError extends Error {
|
|
|
78
80
|
* Project counters share the page's admitted project/state filter.
|
|
79
81
|
* Cursors bind the admitted scope, project and view, never broaden access. */
|
|
80
82
|
export declare function workIndexPage(store: Store, now: Date, access: WorkSummaryAccess, options?: WorkIndexOptions): WorkIndexPage;
|
|
83
|
+
/** An exact family lookup through the list's saved-fact projection. It remains
|
|
84
|
+
* permission-bound and bypasses pagination, never action-time verification. */
|
|
85
|
+
export declare function workIndexTask(store: Store, taskId: string, now: Date, access: WorkSummaryAccess, root?: string): WorkIndexItem | null;
|
|
81
86
|
export declare function workCountsByProject(store: Store, now: Date, access: WorkSummaryAccess): WorkProjectCounts[];
|
|
82
87
|
/** Families whose headline is Ready for review, newest first: the Tasks
|
|
83
88
|
* list's own rows, so a completed family (root or any revision), a failed
|
package/dist/work-index.js
CHANGED
|
@@ -8,8 +8,12 @@ import { join } from 'node:path';
|
|
|
8
8
|
import { readProjectAccess } from './project-access.js';
|
|
9
9
|
import { pauseOnTaskProviders, providerName, signInReason } from './provider-auth.js';
|
|
10
10
|
import { scopeTermsProblem, stopFactOf } from './store.js';
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
11
|
+
import { assignmentChecksForRun } from './assignment.js';
|
|
12
|
+
import { checkBackingOf, completionBlockersOf, leadOnIt, plainReasonOf, replacedWords, requirementsOf, stageOfCode, taskStatusOf, unverifiedWhenRefuted, workToneOf } from './task-status.js';
|
|
13
|
+
import { releaseCoverageOf } from './release-coverage.js';
|
|
14
|
+
import { buildReviewOf } from './review-switch.js';
|
|
15
|
+
import { shownVerdictOf } from './proof.js';
|
|
16
|
+
import { isCheckLevel, requiredCheckCommandFor } from './check-levels.js';
|
|
13
17
|
import { MARKER } from './worktree.js';
|
|
14
18
|
import { withFollowUps } from './result-follow-ups.js';
|
|
15
19
|
import { ASKS, NEED_ASK, NEEDS, askChipOf, failedAttemptSentence, processNeedOf, resultHoldUpSentence } from './needs-you.js';
|
|
@@ -359,6 +363,14 @@ function readCursor(value, scope) {
|
|
|
359
363
|
* Project counters share the page's admitted project/state filter.
|
|
360
364
|
* Cursors bind the admitted scope, project and view, never broaden access. */
|
|
361
365
|
export function workIndexPage(store, now, access, options = {}) {
|
|
366
|
+
return readWorkIndexPage(store, now, access, options);
|
|
367
|
+
}
|
|
368
|
+
/** An exact family lookup through the list's saved-fact projection. It remains
|
|
369
|
+
* permission-bound and bypasses pagination, never action-time verification. */
|
|
370
|
+
export function workIndexTask(store, taskId, now, access, root) {
|
|
371
|
+
return readWorkIndexPage(store, now, access, { limit: 1, ...(root === undefined ? {} : { root }) }, taskId).items[0] ?? null;
|
|
372
|
+
}
|
|
373
|
+
function readWorkIndexPage(store, now, access, options, taskId = null) {
|
|
362
374
|
const projection = registerValidators(store);
|
|
363
375
|
const custody = custodyReadings(store);
|
|
364
376
|
const view = VIEWS.includes(options.view ?? 'all') ? options.view ?? 'all' : 'all';
|
|
@@ -366,7 +378,7 @@ export function workIndexPage(store, now, access, options = {}) {
|
|
|
366
378
|
const scope = cursorScope(access, options, view), cursor = readCursor(options.cursor, scope);
|
|
367
379
|
const filter = view === 'needs-you' ? 'needs=1' : view === 'running' ? 'family_running=1' : view === 'completed' ? "code='complete'" : '1';
|
|
368
380
|
const rows = prepared(store, `${projection}, selected_page AS MATERIALIZED (
|
|
369
|
-
SELECT * FROM ranked WHERE ${filter} AND ($cursorRoot=0 OR
|
|
381
|
+
SELECT * FROM ranked WHERE ${filter} AND ($taskId IS NULL OR root_ref IN (SELECT root_ref FROM members WHERE id=$taskId)) AND ($cursorRoot=0 OR
|
|
370
382
|
${ORDER}>$cursorRank OR (${ORDER}=$cursorRank AND (sort_at<$cursorAt OR (sort_at=$cursorAt AND root_ref<$cursorRoot))))
|
|
371
383
|
ORDER BY ${ORDER},sort_at DESC,root_ref DESC LIMIT $limit
|
|
372
384
|
), 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 (
|
|
@@ -379,7 +391,7 @@ export function workIndexPage(store, now, access, options = {}) {
|
|
|
379
391
|
'state',state,'created_at',created_at,'family_updated',family_updated,'version_count',version_count,'earlier_active',earlier_active,
|
|
380
392
|
'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),'attempt_reason',(SELECT substr(reason,1,1000) 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),
|
|
381
393
|
'publication_url',(SELECT pr_url FROM publication WHERE run=page_result_id),
|
|
382
|
-
'result_role',(SELECT role FROM run WHERE id=page_result_id),'live_role',(SELECT role FROM run WHERE id=page.live_run),
|
|
394
|
+
'result_role',(SELECT role FROM run WHERE id=page_result_id),'result_head',(SELECT head_revision FROM run WHERE id=page_result_id),'live_role',(SELECT role FROM run WHERE id=page.live_run),
|
|
383
395
|
'check_status',(SELECT status FROM run_check WHERE run=page_result_id),
|
|
384
396
|
'check_level',(SELECT outcome FROM action_ledger WHERE repo IS page.repo AND run_id=page_result_id AND action='checks used' ORDER BY id DESC LIMIT 1),
|
|
385
397
|
'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
|
|
@@ -392,7 +404,7 @@ export function workIndexPage(store, now, access, options = {}) {
|
|
|
392
404
|
'question_run',(SELECT run FROM decision WHERE id=page.question_id),'replaced_by',(SELECT successor FROM task_replacement WHERE task_ref=page.ref_id),
|
|
393
405
|
'completed_by_lead',(SELECT lead FROM task_act WHERE task_ref=page.root_ref AND act='completed' ORDER BY id DESC LIMIT 1),
|
|
394
406
|
'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`)
|
|
395
|
-
.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() });
|
|
407
|
+
.all({ ...parameters(now, access, options, custody), $taskId: taskId, $cursorRoot: cursor?.root ?? 0, $cursorRank: cursor?.rank ?? 0, $cursorAt: cursor?.at ?? '', $limit: limit + 1, $recent: new Date(now.getTime() - 86_400_000).toISOString() });
|
|
396
408
|
const selected = rows.slice(1).map(row => JSON.parse(String(row['row_json'])));
|
|
397
409
|
const page = selected.slice(0, limit), last = page.at(-1);
|
|
398
410
|
const nextCursor = selected.length > limit && last !== undefined ? Buffer.from(JSON.stringify({ version: 2, scope,
|
|
@@ -405,7 +417,11 @@ export function workIndexPage(store, now, access, options = {}) {
|
|
|
405
417
|
};
|
|
406
418
|
// A claimed row says the viewer's own name for their lead.
|
|
407
419
|
const leadName = page.some(row => n(row, 'lead_claim') !== 0) && access.principal === 'operator' && access.viewer != null ? leadNameOf(store, access.viewer) : undefined;
|
|
408
|
-
|
|
420
|
+
// A finished result reads the same readiness the task page does (task-status.ts): its requirements, acceptance,
|
|
421
|
+
// automatic review and, with checks Off, the release check that covers it.
|
|
422
|
+
const memo = new Map();
|
|
423
|
+
const readiness = (row, checks) => resultReadinessFacts(store, now, options.root, row, checks, memo);
|
|
424
|
+
return { items: page.map(row => itemOf(row, access.principal, probe, leadName, readiness)), projects, totals: rows[0] === undefined ? { ...EMPTY } : counts(rows[0]),
|
|
409
425
|
groups: rows[0] === undefined ? { ...NO_GROUPS } : groupCounts(rows[0]), nextCursor, limit, view };
|
|
410
426
|
}
|
|
411
427
|
export function workCountsByProject(store, now, access) {
|
|
@@ -451,7 +467,30 @@ function listChecksOf(own, level, follows) {
|
|
|
451
467
|
return null;
|
|
452
468
|
return { status: read.status, exitCode: null, head: null, level: read.level, running: read.running };
|
|
453
469
|
}
|
|
454
|
-
|
|
470
|
+
/** A ready result's requirements, acceptance and completion blockers from its saved records, as assignmentOf reads them. */
|
|
471
|
+
function resultReadinessFacts(store, now, root, row, recorded, memo) {
|
|
472
|
+
const run = n(row, 'result_id'), repo = s(row, 'repo');
|
|
473
|
+
if (!run)
|
|
474
|
+
return { checks: recorded };
|
|
475
|
+
const report = s(row, 'result_role') === 'scout';
|
|
476
|
+
// No recorded check result (a build from before results were recorded, or none ran): read its saved receipt, as the page does.
|
|
477
|
+
const saved = root !== undefined && (recorded === null || recorded.status === 'not-run') ? assignmentChecksForRun(store, root, run, now) : null;
|
|
478
|
+
const listed = saved === null ? recorded : { status: saved.status, exitCode: saved.exitCode, head: s(row, 'result_head'),
|
|
479
|
+
...(saved.level == null ? {} : { level: saved.level }), ...(saved.running == null ? {} : { running: saved.running }), ...(saved.batch == null ? {} : { batch: saved.batch }) };
|
|
480
|
+
const checks = listed?.level === 'off' && listed.status !== 'passed' && listed.status !== 'failed'
|
|
481
|
+
? { ...listed, release: releaseCoverageOf(store, root, repo, s(row, 'result_head'), { memo }) } : listed;
|
|
482
|
+
const proof = store.proofVerdictFor(run), accepted = store.proofAcceptance(run) !== null;
|
|
483
|
+
const review = buildReviewOf(store, run);
|
|
484
|
+
const high = review === null || review.sentBackAs !== null || review.state !== 'reviewed' ? 0 : review.high.length;
|
|
485
|
+
const checkRequired = requiredCheckCommandFor(store, repo, String(row['id']), checks?.level) !== null;
|
|
486
|
+
const backing = checkBackingOf(checks);
|
|
487
|
+
const requirements = requirementsOf(proof?.matrix, backing);
|
|
488
|
+
const verdict = shownVerdictOf(proof?.verdict ?? null, { checkFailed: !report && checks?.status === 'failed', highFindings: high, reviewPending: review?.state === 'pending',
|
|
489
|
+
criteriaPassed: requirements === null || requirements.met === requirements.total });
|
|
490
|
+
const blockers = completionBlockersOf({ report, checks, checkRequired, matrix: proof?.matrix, verdict: proof?.verdict ?? null, accepted, high });
|
|
491
|
+
return { checks, requirements: unverifiedWhenRefuted(requirements, verdict), ...(accepted ? { accepted } : {}), ...(blockers.length === 0 ? {} : { blockers }) };
|
|
492
|
+
}
|
|
493
|
+
function itemOf(row, principal, probe, leadName, readiness) {
|
|
455
494
|
const code = String(row['code']), id = String(row['id']);
|
|
456
495
|
const custody = code === 'process-needs-attention' || (code === 'result-needs-attention' && n(row, 'custody_unresolved') === 1);
|
|
457
496
|
const process = custody && probe !== undefined ? processNeedOf(probe(id)) : null;
|
|
@@ -564,10 +603,13 @@ function itemOf(row, principal, probe, leadName) {
|
|
|
564
603
|
const checkStatus = s(row, 'check_status'), verdict = s(row, 'proof_verdict');
|
|
565
604
|
const finished = reading.stage === 'finished' || reading.stage === 'complete';
|
|
566
605
|
const own = checkStatus === 'failed' || n(row, 'proof_failed_check') === 1 ? 'failed' : checkStatus === 'passed' || verdict === 'verified' ? 'passed' : null;
|
|
606
|
+
const listedChecks = !finished ? null : listChecksOf(own, s(row, 'check_level'), row['check_follows']);
|
|
607
|
+
const ready = reading.stage === 'finished' && readiness !== undefined ? readiness(row, listedChecks) : null;
|
|
567
608
|
const shared = taskStatusOf({ stage: reading.stage, ...(reading.need === undefined ? {} : { need: reading.need }), ...(reading.wait === undefined ? {} : { wait: reading.wait }),
|
|
568
609
|
...(process === null ? {} : { needContext: { build: process.build } }),
|
|
569
610
|
reason: finished ? null : plainReasonOf(reading.stage, code, detail || null), report: s(row, 'result_role') === 'scout',
|
|
570
|
-
checks:
|
|
611
|
+
checks: ready?.checks ?? listedChecks,
|
|
612
|
+
...(ready === null ? {} : { requirements: ready.requirements ?? null, ...(ready.accepted ? { accepted: true } : {}), ...(ready.blockers === undefined ? {} : { blockers: ready.blockers }) }),
|
|
571
613
|
completedBy: code !== 'complete' ? null : n(row, 'completed_by_lead') === 1 ? 'the lead' : String(row['checked_actor']).replace(/^(?:operator|coordinator|lead):/, ''),
|
|
572
614
|
...(lead === null ? {} : { lead, ...(leadName === undefined ? {} : { leadName }) }) });
|
|
573
615
|
label = shared.headline;
|
package/dist/workspace-ui.js
CHANGED
|
@@ -395,7 +395,8 @@ export function acceptWordsOf(facts) {
|
|
|
395
395
|
const plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
|
|
396
396
|
const parts = [
|
|
397
397
|
...(facts.proof ? [] : ["nothing on record says what was met"]),
|
|
398
|
-
|
|
398
|
+
// Checks deliberately Off are checked at release (task-status.ts): not a gap here.
|
|
399
|
+
...(facts.checks === "passed" || facts.checks === "off" ? [] : [facts.checks === "failed" ? "checks failed"
|
|
399
400
|
: facts.checks === "running" ? "checks are still running" : facts.checks === "unavailable" ? "saved checks can't be read" : "checks didn't run"]),
|
|
400
401
|
...(facts.unmet > 0 ? [`${plural(facts.unmet, "requirement isn't", "requirements aren't")} met`] : []),
|
|
401
402
|
];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toolroll",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.57",
|
|
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"
|