toolroll 0.9.5 → 0.9.7

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.
Files changed (92) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/dist/actor.d.ts +21 -0
  3. package/dist/actor.js +48 -0
  4. package/dist/assignment-adapters.d.ts +1 -0
  5. package/dist/assignment-presentation.d.ts +13 -2
  6. package/dist/assignment-presentation.js +22 -9
  7. package/dist/assignment-ui.d.ts +10 -15
  8. package/dist/assignment-ui.js +19 -18
  9. package/dist/assignment.d.ts +9 -0
  10. package/dist/assignment.js +62 -13
  11. package/dist/browser/workspace.css +2 -2
  12. package/dist/browser/workspace.js +29 -24
  13. package/dist/browser-workspace.d.ts +120 -12
  14. package/dist/builder.d.ts +1 -0
  15. package/dist/builder.js +33 -13
  16. package/dist/chat-acceptance.js +2 -2
  17. package/dist/chat-actions.js +2 -1
  18. package/dist/chat-delivery.js +2 -0
  19. package/dist/chat-quiet.js +30 -19
  20. package/dist/chat-task-actions.d.ts +13 -0
  21. package/dist/chat-task-actions.js +19 -1
  22. package/dist/check-levels-ui.d.ts +21 -0
  23. package/dist/check-levels-ui.js +70 -0
  24. package/dist/check-levels.d.ts +67 -0
  25. package/dist/check-levels.js +171 -0
  26. package/dist/claim.d.ts +22 -0
  27. package/dist/claim.js +43 -0
  28. package/dist/cli.d.ts +1 -1
  29. package/dist/cli.js +4 -0
  30. package/dist/demo-chat.d.ts +0 -5
  31. package/dist/demo-chat.js +14 -3
  32. package/dist/dispatch.js +7 -2
  33. package/dist/dispose.d.ts +8 -0
  34. package/dist/dispose.js +7 -1
  35. package/dist/export.js +3 -2
  36. package/dist/first-run.d.ts +34 -0
  37. package/dist/first-run.js +37 -1
  38. package/dist/flow-gallery-ui.d.ts +30 -0
  39. package/dist/flow-gallery-ui.js +85 -0
  40. package/dist/flow-gallery.d.ts +111 -0
  41. package/dist/flow-gallery.js +445 -0
  42. package/dist/flow-share.d.ts +102 -0
  43. package/dist/flow-share.js +614 -0
  44. package/dist/flow-starters.d.ts +2 -0
  45. package/dist/flow-starters.js +1 -1
  46. package/dist/flow-steps.js +7 -0
  47. package/dist/flow-triggers.js +2 -0
  48. package/dist/flows-cli.d.ts +56 -0
  49. package/dist/flows-cli.js +109 -3
  50. package/dist/flows-ui.d.ts +15 -1
  51. package/dist/flows-ui.js +31 -6
  52. package/dist/guides.js +27 -2
  53. package/dist/lead-context.js +6 -2
  54. package/dist/lead-status.d.ts +7 -0
  55. package/dist/lead-status.js +11 -2
  56. package/dist/mate-doors.d.ts +0 -15
  57. package/dist/mate-doors.js +5 -1
  58. package/dist/mate-tools.js +9 -4
  59. package/dist/models-ui.js +1 -1
  60. package/dist/operate.d.ts +4 -1
  61. package/dist/operate.js +601 -85
  62. package/dist/project-concurrency.d.ts +62 -0
  63. package/dist/project-concurrency.js +116 -0
  64. package/dist/project-delete-ui.d.ts +7 -0
  65. package/dist/project-delete-ui.js +19 -2
  66. package/dist/project-delete.js +4 -0
  67. package/dist/pull-request-flow.d.ts +9 -0
  68. package/dist/pull-request-flow.js +21 -3
  69. package/dist/result-follow-ups.d.ts +101 -0
  70. package/dist/result-follow-ups.js +333 -0
  71. package/dist/scope.js +11 -0
  72. package/dist/serve.d.ts +211 -2
  73. package/dist/serve.js +1070 -204
  74. package/dist/store.d.ts +76 -3
  75. package/dist/store.js +311 -22
  76. package/dist/surface.js +31 -8
  77. package/dist/tailnet.d.ts +11 -0
  78. package/dist/tailnet.js +47 -0
  79. package/dist/task-outcome-cli.js +2 -2
  80. package/dist/task-status.d.ts +186 -0
  81. package/dist/task-status.js +335 -0
  82. package/dist/team-cli.js +3 -0
  83. package/dist/telegram-progress.js +62 -100
  84. package/dist/telegram-status.js +21 -8
  85. package/dist/verification-evidence.js +21 -1
  86. package/dist/webhooks.js +5 -0
  87. package/dist/work-index.js +60 -11
  88. package/dist/work-summary.d.ts +0 -4
  89. package/dist/work-summary.js +2 -0
  90. package/dist/workspace-ui.d.ts +10 -0
  91. package/dist/workspace-ui.js +33 -2
  92. package/package.json +1 -1
@@ -0,0 +1,335 @@
1
+ /** A cancelled task with a successor reads "Replaced by <id>" everywhere, never "Cancelled". */
2
+ export const replacedWords = (successor) => `Replaced by ${successor}`;
3
+ const REPLACED = /^Replaced by \S+/;
4
+ export const HEADLINES = ["Queued", "Planning", "Needs you", "Building", "Ready for review", "Complete", "Failed", "Stopped"];
5
+ export const HEADLINE_TONE = {
6
+ Queued: "neutral", Planning: "live", "Needs you": "attention", Building: "live",
7
+ "Ready for review": "ready", Complete: "success", Failed: "danger", Stopped: "neutral",
8
+ };
9
+ const short = (sha) => sha !== null && /^[a-f0-9]{7,40}$/.test(sha) ? sha.slice(0, 7) : null;
10
+ export function headlineOf(facts) {
11
+ switch (facts.stage) {
12
+ case "queued": return "Queued";
13
+ case "planning": return "Planning";
14
+ case "needs-you": return "Needs you";
15
+ case "building":
16
+ case "checking": return "Building";
17
+ case "finished":
18
+ if (facts.report)
19
+ return "Ready for review";
20
+ if (facts.checks?.status === "failed")
21
+ return "Failed";
22
+ // Off: ready like any build; the sentence and the Checks row say no check ran.
23
+ return "Ready for review";
24
+ case "complete": return "Complete";
25
+ case "failed": return "Failed";
26
+ case "stopped": return "Stopped";
27
+ }
28
+ }
29
+ function sentenceOf(headline, facts) {
30
+ const reason = facts.reason?.trim() || null;
31
+ const sha = short(facts.checks?.head ?? null);
32
+ switch (headline) {
33
+ case "Queued": return reason ?? "Waiting for a worker.";
34
+ case "Planning": return reason ?? "The lead is writing the plan.";
35
+ // An approval always reads the same way; the plan itself names the work.
36
+ case "Needs you": return facts.need === "approval" ? "Review the plan and approve it to start." : reason ?? (facts.need === "answer" ? "Answer the question so the work can continue."
37
+ : facts.need === "sign-in" ? "Sign in again; the task starts on its own after."
38
+ : "Something needs your decision before the work can continue.");
39
+ case "Building": return facts.stage === "checking" ? "Checks are running on the change." : reason ?? "An agent is working on it.";
40
+ case "Ready for review":
41
+ if (facts.report)
42
+ return "The report is ready to read. Read it, then mark it complete.";
43
+ if (facts.checks?.level === "off" && facts.checks.status !== "passed")
44
+ return "Checks are off for this project. Review the change, then mark it complete.";
45
+ if (facts.checks?.status === "passed")
46
+ return `${facts.checks.level === "quick" ? "Quick checks" : "Checks"} passed${sha === null ? "" : ` on ${sha}`}. Review the change, then mark it complete.`;
47
+ // Never claim a check that isn't known to have passed.
48
+ if (facts.checks == null)
49
+ return "Review the change, then mark it complete.";
50
+ return "Built without a passing project check. Review the change, then mark it complete.";
51
+ case "Complete": {
52
+ const by = facts.completedBy ? `Marked complete by ${facts.completedBy}.` : "Marked complete.";
53
+ const pr = facts.pullRequest;
54
+ return pr?.state === "merged" ? `${by} ${pr.number === null ? "Its pull request" : `Pull request #${pr.number}`} merged.` : by;
55
+ }
56
+ case "Failed":
57
+ if (facts.checks?.status === "failed")
58
+ return `${facts.checks.level === "quick" ? "Quick checks" : "Checks"} failed${sha === null ? "" : ` on ${sha}`}. See what broke, then retry or ask for changes.`;
59
+ return reason ?? "The last attempt stopped before it finished. Review it, then retry.";
60
+ case "Stopped": return reason ?? "Stopped by a person. The work so far is kept.";
61
+ }
62
+ }
63
+ /** A detail problem: red only under Failed, otherwise an amber note. */
64
+ const problem = (headline) => headline === "Failed" ? "failed" : "note";
65
+ function detailsOf(headline, facts) {
66
+ const rows = [];
67
+ const row = (key, label, text, mark, extra = {}) => rows.push({ key, label, text, mark, href: extra.href ?? null, action: extra.action ?? null, why: extra.why ?? null });
68
+ const checks = facts.checks;
69
+ if (checks != null && !facts.report) {
70
+ const sha = short(checks.head);
71
+ const checksHref = facts.links?.checks ?? facts.links?.result ?? null;
72
+ const runChecks = facts.links?.runChecks === undefined ? null : { label: "Run checks", href: facts.links.runChecks };
73
+ const quick = checks.level === "quick";
74
+ if (checks.running != null)
75
+ row("checks", "Checks", `${checks.running === "quick" ? "Quick" : "Full"} checks running`, "running", { href: checksHref });
76
+ else if (checks.status === "passed")
77
+ row("checks", "Checks", `${quick ? "Quick checks passed" : "Passed"}${sha === null ? "" : ` on ${sha}`}`, "ok", quick && runChecks !== null && headline !== "Complete" ? { href: checksHref, action: { label: "Run full checks", href: runChecks.href } } : { href: checksHref });
78
+ else if (checks.status === "failed")
79
+ row("checks", "Checks", `${quick ? "Quick checks failed" : "Failed"}${checks.exitCode === null ? "" : ` (exit ${checks.exitCode})`}`, problem(headline), headline === "Failed" ? { href: checksHref } : { action: { label: "See what failed", href: checksHref } });
80
+ else if (checks.status === "running")
81
+ row("checks", "Checks", "Running", "running");
82
+ else if (checks.level === "off")
83
+ row("checks", "Checks", "Off for this project", "none", runChecks === null ? {} : { action: runChecks });
84
+ else if (checks.status === "not-run")
85
+ row("checks", "Checks", "Didn't run", "none", runChecks === null ? {} : { action: runChecks });
86
+ else
87
+ row("checks", "Checks", "Couldn't be read", "note", { action: { label: "Open the result", href: facts.links?.result ?? null } });
88
+ }
89
+ const pr = facts.pullRequest;
90
+ if (pr != null) {
91
+ const name = pr.number === null ? "Pull request" : `#${pr.number}`;
92
+ const github = pr.url !== null && /^https:\/\/github\.com\//.test(pr.url) ? pr.url : null;
93
+ if (pr.state === "none")
94
+ row("pull-request", "Pull request", "None", "none");
95
+ else if (pr.state === "opening")
96
+ row("pull-request", "Pull request", "Opening…", "running", { why: pr.error });
97
+ else if (pr.state === "merged")
98
+ row("pull-request", "Pull request", `${name} merged`, "ok", { href: github, why: pr.note ?? null });
99
+ else if (pr.state === "closed")
100
+ row("pull-request", "Pull request", `${name} closed without merging`, "none", { href: github });
101
+ else if (pr.state === "failed")
102
+ row("pull-request", "Pull request", "Couldn't open", "note", { action: pr.compareUrl ? { label: "Open it on GitHub", href: pr.compareUrl } : { label: "See why", href: facts.links?.pullRequest ?? null }, why: pr.error === null ? "Publishing gave up. The commit is safe locally." : `${pr.error} The commit is safe locally.` });
103
+ else if (pr.ci === "running")
104
+ row("pull-request", "Pull request", `${name} open, CI running`, "running", { href: github });
105
+ else if (pr.ci === "passing")
106
+ row("pull-request", "Pull request", `${name} open, CI passed`, "ok", { href: github });
107
+ else if (pr.ci === "failing")
108
+ row("pull-request", "Pull request", `${name} open, CI failing`, "note", { action: { label: "Open it on GitHub", href: github } });
109
+ else
110
+ row("pull-request", "Pull request", `${name} open`, "none", { href: github });
111
+ }
112
+ const req = facts.requirements;
113
+ if (req != null && req.total > 0) {
114
+ const unmet = req.total - req.met - req.yours;
115
+ const text = `${req.met} of ${req.total} met${req.yours > 0 ? ` · You check ${req.yours}` : ""}`;
116
+ 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 } } : {});
117
+ }
118
+ const evidence = facts.evidence;
119
+ if (evidence != null) {
120
+ const action = { label: "See what was kept", href: facts.links?.result ?? null };
121
+ if (evidence.damaged > 0)
122
+ row("evidence", "Saved evidence", "Some saved files changed", problem(headline), headline === "Failed" ? {} : { action });
123
+ else if (evidence.missing > 0)
124
+ row("evidence", "Saved evidence", "Some saved files are missing", problem(headline), headline === "Failed" ? {} : { action });
125
+ else if (evidence.shortened > 0)
126
+ row("evidence", "Saved evidence", "Some output shortened", "note", { action });
127
+ else
128
+ row("evidence", "Saved evidence", "Complete", "ok");
129
+ }
130
+ return rows;
131
+ }
132
+ export function taskStatusOf(facts) {
133
+ const headline = headlineOf(facts);
134
+ return { headline, tone: HEADLINE_TONE[headline], sentence: sentenceOf(headline, facts), details: detailsOf(headline, facts),
135
+ primaryAction: facts.action ?? null, why: [...new Set(facts.why ?? [])].filter(one => one.trim() !== "") };
136
+ }
137
+ // ---- reading the existing projections ----------------------------------------
138
+ /** Every dispatch or work-index code, as the stage it means. Unknown codes that
139
+ * wait on a person stay Needs you; everything else waits in the queue. */
140
+ const CODE_STAGE = {
141
+ queued: ["queued"], ready: ["queued"], "worker-at-capacity": ["queued"], "retry-scheduled": ["queued"], updating: ["queued"],
142
+ "waiting-dependency": ["queued"], "scouting-ready": ["queued"], "planning-ready": ["queued"], "provider-quota": ["queued"], "planner-source": ["queued"],
143
+ running: ["building"], reviewing: ["building"], "review-pending": ["building"],
144
+ "signed-out": ["needs-you", "sign-in"],
145
+ "needs-approval": ["needs-you", "approval"],
146
+ "waiting-decision": ["needs-you", "answer"], "decision-queue": ["needs-you", "answer"],
147
+ stopping: ["stopped"], stopped: ["stopped"], paused: ["stopped"], cancelled: ["stopped"],
148
+ failed: ["failed"], "waiting-incident": ["failed"], "checks-failed": ["failed"],
149
+ "ready-to-check": ["finished"], "ready-to-review": ["finished"], "report-ready": ["finished"], "no-change": ["finished"],
150
+ "pr-opened": ["finished"], "merge-observed": ["finished"], "pr-closed": ["finished"], "agent-attested": ["finished"], "accepted-exception": ["finished"],
151
+ complete: ["complete"],
152
+ };
153
+ /** Queue reasons worth a sentence of their own; any other queued or
154
+ * building reason is scheduler wording, and the default sentence is plainer. */
155
+ const QUEUED_REASONS = new Set(["worker-at-capacity", "waiting-dependency", "retry-scheduled", "updating", "planning-ready", "scouting-ready", "provider-quota"]);
156
+ export function plainReasonOf(stage, code, detail) {
157
+ if (stage === "building" || stage === "checking" || stage === "planning")
158
+ return null;
159
+ if (stage === "queued")
160
+ return QUEUED_REASONS.has(code) ? detail ?? null : null;
161
+ return detail ?? null;
162
+ }
163
+ export function stageOfCode(code, options = {}) {
164
+ if (code === "running" && options.planning)
165
+ return { stage: "planning" };
166
+ if (code === "held")
167
+ return options.operatorHold === false ? { stage: "needs-you", need: "other" } : { stage: "stopped" };
168
+ const known = CODE_STAGE[code];
169
+ if (known !== undefined)
170
+ return known[1] === undefined ? { stage: known[0] } : { stage: known[0], need: known[1] };
171
+ return options.needsPerson === false ? { stage: "queued" } : { stage: "needs-you", need: "other" };
172
+ }
173
+ /** The pull request as the task page's follower reports it, or the bare publication row. */
174
+ export function pullRequestFactOf(publication) {
175
+ if (publication === null)
176
+ return null;
177
+ const safe = (part) => part !== undefined && /^[A-Za-z0-9_.\/-]+$/.test(part) && !part.includes("..") ? part : null;
178
+ const repo = safe(publication.githubRepo), from = safe(publication.base), to = safe(publication.head);
179
+ const compareUrl = repo !== null && /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repo) && from !== null && to !== null ? `https://github.com/${repo}/compare/${from}...${to}?expand=1` : null;
180
+ const seen = publication.lastCheckState;
181
+ const ci = seen === "running" || seen === "passing" || seen === "failing" ? seen : null;
182
+ const fromUrl = /\/pull\/(\d+)$/.exec(publication.prUrl ?? "");
183
+ const number = publication.prNumber ?? (fromUrl === null ? null : Number(fromUrl[1]));
184
+ const base = { number, url: publication.prUrl, ci, error: publication.lastError ?? null, compareUrl };
185
+ if (publication.merged === true || publication.remoteState === "MERGED")
186
+ return { ...base, state: "merged" };
187
+ if (publication.remoteState === "CLOSED")
188
+ return { ...base, state: "closed" };
189
+ if (publication.state === "failed")
190
+ return { ...base, state: "failed" };
191
+ if (publication.state === "opened")
192
+ return { ...base, state: "open" };
193
+ return { ...base, state: "opening" };
194
+ }
195
+ /** The assignment's stage. The work status, when the caller read one, names
196
+ * the exact reason (a running planner, a pause, a failed attempt, a sign-in). */
197
+ export function assignmentStageOf(assignment, work, planning = false) {
198
+ switch (assignment.state) {
199
+ case "cancelled": return { stage: "stopped" };
200
+ case "complete": return { stage: "complete" };
201
+ case "ready-to-check": return { stage: "finished" };
202
+ case "checking": return { stage: "checking" };
203
+ case "working": {
204
+ const token = work?.token ?? "running";
205
+ const read = stageOfCode(token, { needsPerson: work?.views?.includes("needs-you") ?? false, planning });
206
+ return read.stage === "finished" || read.stage === "complete" ? { stage: "building" } : read;
207
+ }
208
+ case "needs-decision": {
209
+ const action = assignment.primaryAction?.code;
210
+ if (action === "approve-scope")
211
+ return { stage: "needs-you", need: "approval" };
212
+ if (action === "answer-decision" || action === "inspect-decisions")
213
+ return { stage: "needs-you", need: "answer" };
214
+ const token = work?.token ?? "";
215
+ // A failed attempt is Failed. A saved result that still needs a decision
216
+ // (its scope, a hold, a process exit) is Needs you, even if its checks
217
+ // failed: the decision is the next step, and the Checks row says the rest.
218
+ if (token === "failed" || token === "waiting-incident")
219
+ return { stage: "failed" };
220
+ const read = stageOfCode(token, { needsPerson: true, operatorHold: action === "unhold" });
221
+ return read.stage === "stopped" ? read : { stage: "needs-you", need: read.need ?? "other" };
222
+ }
223
+ }
224
+ }
225
+ /** Requirements from the stored matrix: met, the ones only a person confirms, and the total. */
226
+ export function requirementsOf(matrix) {
227
+ if (!matrix || matrix.length === 0)
228
+ return null;
229
+ // Evidence that passed and only awaits the retired assessment step is met.
230
+ const met = (row) => row.state === "pass" || (row.assessment?.evidenceState === "pass" && (row.review ?? null) === null);
231
+ return { met: matrix.filter(met).length, total: matrix.length, yours: matrix.filter(row => !met(row) && row.state === "manual-review").length };
232
+ }
233
+ /** Saved evidence health from the receipt's artifact list and its caveats. */
234
+ export function evidenceOf(receipt, problems = receipt?.caveats ?? []) {
235
+ if (receipt === null)
236
+ return null;
237
+ const shortened = receipt.artifacts.filter(one => !one.complete).length + problems.filter(one => /shortened|cut short/.test(one)).length;
238
+ const damaged = problems.filter(one => /unavailable or changed|no longer verif|cannot be read|could not be read|failed capture|altered|damaged/i.test(one)).length;
239
+ const missing = problems.filter(one => /missing|ambiguous|no longer exists/i.test(one) && !/unavailable or changed/.test(one)).length;
240
+ return { shortened, missing, damaged };
241
+ }
242
+ /** The status facts of one assignment, as every surface reads them. */
243
+ export function assignmentStatusFacts(assignment, options = {}) {
244
+ const { stage, need } = assignmentStageOf(assignment, options.work ?? null, options.planning ?? false);
245
+ const receipt = assignment.receipt;
246
+ const finished = stage === "finished" || stage === "complete";
247
+ // A saved result's rows show whenever it is the reason a person is needed, too.
248
+ const withResult = finished || stage === "failed" || (stage === "needs-you" && receipt !== null);
249
+ const report = receipt?.completionKind === "research-report";
250
+ const checks = receipt === null || !withResult ? null
251
+ : { status: receipt.checks.status, exitCode: receipt.checks.exitCode, head: receipt.head,
252
+ ...(receipt.checks.level == null ? {} : { level: receipt.checks.level }), ...(receipt.checks.running == null ? {} : { running: receipt.checks.running }) };
253
+ const publication = assignment.publication;
254
+ const pullRequest = options.pullRequest !== undefined ? options.pullRequest
255
+ : publication === null ? (withResult && !report ? { state: "none", number: null, url: null, ci: null, error: null } : null)
256
+ : pullRequestFactOf(publication);
257
+ // Ready, Complete and Failed speak for themselves; every other stage keeps its recorded reason.
258
+ const reason = stage === "needs-you" || stage === "stopped" || stage === "queued" || (stage === "failed" && checks?.status !== "failed")
259
+ ? stage === "stopped" && assignment.state === "cancelled" ? REPLACED.test(assignment.detail) ? assignment.detail : "Cancelled. Nothing else will run." : plainReasonOf(stage, options.work?.token ?? "", options.work?.detail ?? assignment.detail)
260
+ : null;
261
+ return {
262
+ stage, ...(need === undefined ? {} : { need }), reason, report,
263
+ checks,
264
+ pullRequest: withResult ? pullRequest : null,
265
+ requirements: withResult ? requirementsOf(receipt?.proof?.matrix) : null,
266
+ evidence: withResult ? options.evidence ?? evidenceOf(receipt, [...(receipt?.caveats ?? []), ...assignment.attention]) : null,
267
+ completedBy: assignment.completion === null ? null : assignment.completion.lead === true ? "the lead" : assignment.completion.actor.replace(/^(?:operator|coordinator|lead):/, ""),
268
+ action: options.action ?? null,
269
+ ...(options.links === undefined ? {} : { links: options.links }),
270
+ why: options.why ?? [],
271
+ };
272
+ }
273
+ // ---- rendering helpers shared by the server pages and chat cards ------------
274
+ /** The legacy work tone each headline wears where a surface still keys off StatusTone. */
275
+ export function workToneOf(headline) {
276
+ switch (headline) {
277
+ case "Needs you": return "attention";
278
+ case "Failed": return "problem";
279
+ case "Building":
280
+ case "Planning": return "live";
281
+ case "Ready for review": return "ready";
282
+ case "Complete": return "done";
283
+ default: return "muted";
284
+ }
285
+ }
286
+ /** One plain-text line per detail for chat cards: an icon, the label, the words. */
287
+ export function statusDetailLines(status) {
288
+ const icon = { ok: "✓", running: "●", none: "○", note: "⚠", failed: "✕" };
289
+ return status.details.map(one => `${icon[one.mark]} ${one.label} · ${one.text}${one.action === null ? "" : ` — ${one.action.label}`}`);
290
+ }
291
+ /** The headline's emoji for chat cards (colour is never the only signal: the words follow). */
292
+ export function headlineEmoji(headline) {
293
+ return { Queued: "🕓", Planning: "📝", "Needs you": "👋", Building: "⏳", "Ready for review": "✅", Complete: "✅", Failed: "❌", Stopped: "⏹" }[headline];
294
+ }
295
+ const escapeHtml = (value) => value.replace(/[&<>"']/g, char => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[char]);
296
+ const ICON_PATHS = {
297
+ ok: `<path d="M20 6 9 17l-5-5"/>`,
298
+ running: `<circle cx="12" cy="12" r="4"/>`,
299
+ none: `<circle cx="12" cy="12" r="3"/>`,
300
+ note: `<path d="M12 8v5"/><path d="M12 16.5h.01"/><path d="M10.3 3.9 2.4 18a2 2 0 0 0 1.7 3h15.8a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z"/>`,
301
+ failed: `<path d="M18 6 6 18M6 6l12 12"/>`,
302
+ };
303
+ export const statusIconSvg = (mark) => `<svg class="status-icon status-icon--${mark}" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.25" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${ICON_PATHS[mark]}</svg>`;
304
+ /** The detail rows as server HTML: quiet rows, colour only on the icon. */
305
+ export function statusDetailsHtml(status) {
306
+ if (status.details.length === 0)
307
+ return "";
308
+ return `<ul class="status-details" aria-label="Details">` + status.details.map(one => {
309
+ const text = one.href === null ? escapeHtml(one.text) : `<a href="${escapeHtml(one.href)}">${escapeHtml(one.text)}</a>`;
310
+ const action = one.action === null ? "" : one.action.href === null ? `<span class="status-detail-act">${escapeHtml(one.action.label)}</span>` : `<a class="status-detail-act" href="${escapeHtml(one.action.href)}">${escapeHtml(one.action.label)}</a>`;
311
+ return `<li class="status-detail status-detail--${one.mark}" data-status-detail="${one.key}" data-mark="${one.mark}">${statusIconSvg(one.mark)}<span class="status-detail-label">${escapeHtml(one.label)}</span><span class="status-detail-text"><span>${text}</span>${action}</span></li>`;
312
+ }).join("") + `</ul>`;
313
+ }
314
+ /** The technical reasons, one tap away. */
315
+ export function statusWhyHtml(status, extra = [], diagnostics = []) {
316
+ const lines = [...new Set([...status.details.flatMap(one => one.why === null ? [] : [`${one.label}: ${one.why}`]), ...status.why, ...extra])];
317
+ if (lines.length === 0 && diagnostics.length === 0)
318
+ return "";
319
+ return `<details class="status-why"><summary>Details</summary>${lines.map(one => `<p class="meta">${escapeHtml(one)}</p>`).join("")}` +
320
+ diagnostics.map(one => `<p class="meta" data-work-diagnostic="${escapeHtml(one.token)}">${escapeHtml(one.label)} · ${escapeHtml(one.detail)}</p>`).join("") + `</details>`;
321
+ }
322
+ /** Shared CSS for the server-rendered status: neutral rows, coloured icons. */
323
+ export const TASK_STATUS_CSS = `.status-headline{display:flex;align-items:center;gap:.6rem;margin:0;font-size:1.125rem;font-weight:600;color:var(--foreground)}.status-headline i{width:.625rem;height:.625rem;border-radius:999px;flex-shrink:0;background:var(--muted-foreground)}[data-headline-tone=live] .status-headline i{background:var(--so-info,#0d74ce)}[data-headline-tone=attention] .status-headline i{background:var(--so-attention,var(--attention))}[data-headline-tone=ready] .status-headline i{background:transparent;box-shadow:inset 0 0 0 2px var(--so-success,#218358)}[data-headline-tone=success] .status-headline i{background:var(--so-success,#218358)}[data-headline-tone=danger] .status-headline i{background:var(--so-danger,#c4320a)}.status-sentence{margin:.35rem 0 0;color:var(--muted-foreground)}.status-details{list-style:none;margin:.75rem 0 0;padding:.25rem 0 0;border-top:1px solid var(--so-line,var(--border))}.status-detail{display:grid;grid-template-columns:14px 7.5rem minmax(0,1fr);align-items:center;gap:.6rem;padding:.4rem 0;font-size:.8125rem;color:var(--foreground)}.status-detail .status-icon{align-self:center;color:var(--muted-foreground)}.status-detail--ok .status-icon{color:var(--so-success,#218358)}.status-detail--running .status-icon{color:var(--so-info,#0d74ce)}.status-detail--note .status-icon{color:var(--so-warning,#ab6400)}.status-detail--failed .status-icon{color:var(--so-danger,#c4320a)}.status-detail-label{color:var(--muted-foreground)}.status-detail-text{display:flex;flex-wrap:wrap;align-items:baseline;gap:0 .6rem;min-width:0;overflow-wrap:anywhere}.status-detail-text a{color:inherit}.status-detail-act{font-weight:600;white-space:nowrap}.status-detail--note .status-detail-act{color:var(--so-warning,#ab6400)}@media(max-width:480px){.status-detail{grid-template-columns:14px 6.5rem minmax(0,1fr);min-height:2.75rem;padding:.25rem 0}.status-detail-text a{display:inline-block;padding:.75rem 0;margin:-.75rem 0}}.status-why{border:0;padding:0;margin:.25rem 0 0;background:transparent;box-shadow:none}.status-why>summary{cursor:pointer;min-height:2.75rem;display:list-item;align-content:center;font-size:.8125rem;color:var(--muted-foreground)}.status-why p{margin:.25rem 0 .5rem}.receipt-note{display:flex;align-items:center;gap:.45rem;font-size:.8125rem;color:var(--foreground)}.receipt-note .status-icon{color:var(--so-warning,#ab6400)}`;
324
+ /** A dispatch diagnosis as its stage, for the phone and CLI reads that hold
325
+ * no assignment. A done task is Complete once a person marked its result;
326
+ * a refuted result whose project check failed is Failed. */
327
+ export function stageOfDispatch(d, options = {}) {
328
+ if (d.code === "complete" || d.code.startsWith("review-") || d.code === "reviewing")
329
+ return { stage: options.completed ? "complete" : "finished" };
330
+ if (d.code === "proof-refuted")
331
+ return /approved check failed|checks failed/i.test(d.detail ?? "") ? { stage: "failed" } : { stage: "needs-you", need: "other" };
332
+ if (d.code === "needs-verification")
333
+ return { stage: "needs-you", need: "other" };
334
+ return stageOfCode(d.code, { needsPerson: d.condition === "waiting" && d.action !== null, planning: d.condition === "running" && d.role === "planner", operatorHold: d.action === "unhold" });
335
+ }
package/dist/team-cli.js CHANGED
@@ -305,6 +305,9 @@ function render(reply, listing) {
305
305
  /** null means preserve the existing local command path. Nothing here opens a database. */
306
306
  export async function maybeRunTeamCommand(argv, write, options = {}) {
307
307
  const command = argv[0];
308
+ // `lead token` is local: the credential a lead agent acts with on this computer.
309
+ if (command === 'lead' && argv[1] === 'token')
310
+ return null;
308
311
  if (!['connect', 'lead', 'conversation', 'chat', 'brief'].includes(command ?? '') && !argv.includes('--profile'))
309
312
  return null;
310
313
  const json = argv.includes('--json');
@@ -1,12 +1,15 @@
1
1
  /** A compact projection of saved work, never a percentage or an ETA. The
2
- * words come from the same assignment record the console and the CLI show:
3
- * a finished build is Ready once its saved result can be inspected, and
4
- * Complete once the lead or a person has marked that exact result. No model
5
- * reviewer is waited for or announced. */
6
- import { manualReviewOnly } from "./proof.js";
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 checksFailed = assignment?.receipt?.checks.status === "failed";
31
- let status = "Working";
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
- if (accessBlocked) {
98
- status = "Worker needs project access";
99
- icon = "⏸";
100
- next = "Next: restore the worker’s folder access, then resume.";
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 (operatorHold) {
104
- icon = "⏸";
105
- if (!accessBlocked) {
106
- status = run.outcome === null ? "Working · Next attempt paused" : "Paused";
107
- next = phoneText(operatorHold.reason, 150);
108
- }
109
- label = "Open task";
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 && holds.some(hold => hold.ownerKind === "backoff")) {
112
- next = "The worker will retry after a short pause. Check its folder access.";
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
- if (checkProgress !== null)
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 = `${icon} ${status}`;
122
- const text = [title, heading, "", build, checks,
123
- ...(next ? ["", next] : []),
124
- ...(acceptance === null ? [] : [acceptance]),
125
- ...(delivery === null ? [] : ["", delivery]), "", `${projectLabel(project)} · #${run.id}`].join("\n");
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, path: operatorHold || accessBlocked || (!built && run.outcome !== null)
128
- ? chatControlHref("recovery", taskId) : built ? chatResultHref(taskId, run.id, checksFailed ? "checks" : "summary") : chatControlHref("task", taskId) } };
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
  }