toolroll 0.9.14 → 0.9.16

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 (84) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/assignment-brief.d.ts +10 -0
  3. package/dist/assignment-brief.js +49 -26
  4. package/dist/assignment-ui.d.ts +1 -0
  5. package/dist/assignment-ui.js +6 -4
  6. package/dist/assignment.d.ts +34 -1
  7. package/dist/assignment.js +86 -7
  8. package/dist/browser/workspace.css +2 -2
  9. package/dist/browser/workspace.js +38 -38
  10. package/dist/browser-crew.d.ts +2 -1
  11. package/dist/browser-crew.js +3 -1
  12. package/dist/browser-workspace.d.ts +93 -18
  13. package/dist/chat-actions.d.ts +1 -1
  14. package/dist/chat-actions.js +6 -4
  15. package/dist/chat-ask.d.ts +28 -0
  16. package/dist/chat-ask.js +77 -0
  17. package/dist/chat-channel.js +6 -1
  18. package/dist/chat-controls.d.ts +8 -0
  19. package/dist/chat-controls.js +3 -0
  20. package/dist/chat-delivery-state.d.ts +7 -2
  21. package/dist/chat-delivery-state.js +15 -2
  22. package/dist/chat-delivery.js +20 -3
  23. package/dist/chat-quiet.js +4 -2
  24. package/dist/chat-voice.d.ts +6 -9
  25. package/dist/chat-voice.js +11 -5
  26. package/dist/demo.js +82 -2
  27. package/dist/disclosure.d.ts +6 -0
  28. package/dist/disclosure.js +13 -0
  29. package/dist/discord-chat.js +10 -0
  30. package/dist/lead-commitments.d.ts +84 -0
  31. package/dist/lead-commitments.js +208 -0
  32. package/dist/lead-context.d.ts +32 -3
  33. package/dist/lead-context.js +133 -19
  34. package/dist/lead-follow.js +3 -0
  35. package/dist/lead-identity.d.ts +23 -0
  36. package/dist/lead-identity.js +33 -0
  37. package/dist/lead-status.js +2 -2
  38. package/dist/lead-voice.d.ts +12 -9
  39. package/dist/lead-voice.js +25 -16
  40. package/dist/ledger-view.js +3 -2
  41. package/dist/mate-cli.d.ts +1 -1
  42. package/dist/mate-cli.js +8 -2
  43. package/dist/mate-contract.d.ts +1 -1
  44. package/dist/mate-contract.js +11 -4
  45. package/dist/mate-progress.js +5 -0
  46. package/dist/mate-tools.d.ts +18 -1
  47. package/dist/mate-tools.js +250 -6
  48. package/dist/mate.d.ts +9 -0
  49. package/dist/mate.js +34 -38
  50. package/dist/needs-you.d.ts +48 -0
  51. package/dist/needs-you.js +93 -1
  52. package/dist/operate.js +2 -1
  53. package/dist/project-delete.js +3 -1
  54. package/dist/result-acts.d.ts +63 -5
  55. package/dist/result-acts.js +53 -3
  56. package/dist/result-review.d.ts +4 -3
  57. package/dist/result-review.js +7 -3
  58. package/dist/retention.js +6 -0
  59. package/dist/serve.d.ts +3 -0
  60. package/dist/serve.js +1220 -694
  61. package/dist/slack-chat.js +11 -1
  62. package/dist/store.d.ts +51 -0
  63. package/dist/store.js +115 -1
  64. package/dist/surface.js +2 -2
  65. package/dist/task-status.d.ts +26 -19
  66. package/dist/task-status.js +59 -21
  67. package/dist/team-runtime.js +2 -1
  68. package/dist/teams-chat.js +4 -1
  69. package/dist/telegram-mate.d.ts +4 -1
  70. package/dist/telegram-mate.js +17 -1
  71. package/dist/telegram-progress.js +3 -2
  72. package/dist/telegram.js +43 -3
  73. package/dist/test-temp.js +1 -1
  74. package/dist/transitions-recipes.d.ts +1 -1
  75. package/dist/transitions-recipes.js +5 -9
  76. package/dist/when-html.d.ts +24 -2
  77. package/dist/when-html.js +98 -11
  78. package/dist/work-index.d.ts +5 -1
  79. package/dist/work-index.js +22 -9
  80. package/dist/work-summary.d.ts +1 -1
  81. package/dist/workspace-motion.js +3 -3
  82. package/dist/workspace-ui.d.ts +44 -20
  83. package/dist/workspace-ui.js +63 -37
  84. package/package.json +1 -1
@@ -17,8 +17,8 @@
17
17
  * claimed without one.
18
18
  */
19
19
  import type { DispatchDiagnosis } from "./dispatch.js";
20
- import { ACCEPT_NEEDS_REASON } from "./result-acts.js";
21
- export { ACCEPT_NEEDS_REASON };
20
+ import { ACCEPT_NEEDS_REASON, cantAcceptYetOf, evidenceProblemOf, type AcceptLabel, type EvidenceProblem } from "./result-acts.js";
21
+ export { ACCEPT_NEEDS_REASON, cantAcceptYetOf, evidenceProblemOf, type EvidenceProblem };
22
22
  import { type ProofVerdict } from "./proof.js";
23
23
  import type { ReviewRetryState, TaskState } from "./store.js";
24
24
  import type { TaskControlView } from "./task-control.js";
@@ -113,14 +113,6 @@ export type DisplayStatus = {
113
113
  tone: StatusTone;
114
114
  action: NextAction | null;
115
115
  };
116
- /** What kind of evidence problem a verdict records: a check the machine
117
- * actually ran and saw fail, evidence that contradicts the sealed record
118
- * (a mismatched changed-path claim, an altered criterion, a caveat that
119
- * contradicts a met verdict), or evidence that is simply absent. A generic
120
- * refuted verdict is NOT a failed test claim — only the verify-command
121
- * reasons say a check failed. */
122
- export type EvidenceProblem = "checks-failed" | "mismatched" | "missing" | "none";
123
- export declare function evidenceProblemOf(verdict: ProofVerdict | null, reasons: readonly string[]): EvidenceProblem;
124
116
  /** The exit code of the failed check, when the recorded reason names one. */
125
117
  export declare function failedCheckExit(reasons: readonly string[]): number | null;
126
118
  /** The observed publication record in words. Each state is a distinct
@@ -192,10 +184,6 @@ export declare function dispatchActionLabel(dispatch: DispatchDiagnosis | null):
192
184
  /** A finished result's display status in the shared headline's words: a
193
185
  * failed project check is Failed, a result missing its record needs a
194
186
  * person, every other saved result is Ready for review. Token and action stay. */
195
- /** Why a result can't be accepted as it stands, in one plain line, or null
196
- * when nothing refutes it (a person's acceptance settles it). The result page
197
- * shows it before Request changes, its one ink act. */
198
- export declare function cantAcceptYetOf(verdict: ProofVerdict | null, reasons: readonly string[], accepted: boolean): string | null;
199
187
  /** The result's headline while its report disagrees with its saved changes. */
200
188
  export declare const MISMATCH_HEADLINE = "The report doesn't match the changes";
201
189
  /** One way the report disagrees with the saved changes, in plain words: the
@@ -241,15 +229,15 @@ export declare function lastErrorLineOf(log: string): {
241
229
  /** The result page's own sentence when the decision is on it. */
242
230
  export declare const RESULT_DECISION_SENTENCE = "Review the change, then accept it or ask for changes.";
243
231
  /** What the result page's Accept says, from facts it already shows: the
244
- * recorded checks, the requirements not met, the ones a person still checks,
245
- * whether the saved proof reads, and what the button posts. "Accept" only when everything is met and the
246
- * checks passed; otherwise "Accept without checks" and one line naming what.
232
+ * recorded checks, the requirements not met, whether the saved proof reads,
233
+ * and what the button posts. "Accept and finish" only when everything is met
234
+ * and the checks passed; otherwise "Accept without checks" and one line naming
235
+ * what. The person's own checks are the page's to add (acceptWithChecksOf).
247
236
  * `effect` says what pressing it does, in the action's own terms. */
248
237
  export type AcceptFacts = {
249
238
  checks: "passed" | "failed" | "not-run" | "off" | "running" | "unavailable" | null;
250
239
  unmet: number;
251
- yours: number;
252
- /** `complete`: marks the exact result complete; `accept`: records a person's acceptance only. */
240
+ /** `complete`: accepts and finishes the exact result in one request; `accept`: records a person's acceptance only (nothing here can finish it). */
253
241
  action: "complete" | "accept";
254
242
  /** `pull-request`: one can be opened from the task after; `off`: publishing isn't set up; `other`: neither is said. Accept itself never publishes. */
255
243
  publishing: "pull-request" | "off" | "other";
@@ -257,7 +245,7 @@ export type AcceptFacts = {
257
245
  proof: boolean;
258
246
  };
259
247
  export type AcceptWords = {
260
- label: "Accept" | "Accept without checks";
248
+ label: AcceptLabel;
261
249
  ready: boolean;
262
250
  why: string | null;
263
251
  effect: string;
@@ -287,3 +275,39 @@ export type PrimaryDestination = "chat" | "work" | "projects" | "flows" | "setti
287
275
  export declare function primaryDestinationOf(active: string): PrimaryDestination;
288
276
  /** Learning stays in Settings and a useful, closed result disclosure. */
289
277
  export declare function learningHtml(view: import('./project-learning.js').LearningView, csrf: string, canManage: boolean, source?: number): string;
278
+ /** The first signed requirement a finished attempt missed (failed or unanswered; a person's own check is not a miss),
279
+ * with the evidence line behind it in plain words. Null when every requirement held. */
280
+ export declare function missedRequirementOf(matrix: readonly {
281
+ id: string;
282
+ statement: string;
283
+ state: string;
284
+ detail: readonly string[];
285
+ }[]): {
286
+ id: string;
287
+ statement: string;
288
+ evidence: string | null;
289
+ } | null;
290
+ /** One milestone of a live build's plan, as the agent last reported it; its words are null where only the recorded
291
+ * progress was read (a list row reads no plan file). */
292
+ export type BuildStep = {
293
+ description: string | null;
294
+ state: "pending" | "current" | "completed" | "blocked";
295
+ note: string | null;
296
+ };
297
+ /** Where a live build is, in one line: "Step 3 of 6: …", or, when a step is blocked, "Stuck on step 4 of 6: <why>" in
298
+ * its place. `stuck` says which step and why; null while nothing is blocked. */
299
+ export type BuildProgress = {
300
+ step: number;
301
+ total: number;
302
+ line: string;
303
+ stuck: {
304
+ step: number;
305
+ why: string | null;
306
+ line: string;
307
+ } | null;
308
+ };
309
+ /** A live build's progress: the step in progress (else the first one not done) as "Step N of M: <step>"; a blocked
310
+ * step replaces it as "Stuck on step N of M: <why>" (its note, else the step itself). Null with no plan steps. */
311
+ export declare function buildProgressOf(steps: readonly BuildStep[] | null | undefined): BuildProgress | null;
312
+ /** Earlier attempts that stopped before the one now running, in one quiet line. */
313
+ export declare function earlierAttemptsWords(count: number): string;
@@ -16,10 +16,11 @@
16
16
  * observed" name observed publication records, and deployment is never
17
17
  * claimed without one.
18
18
  */
19
- import { ACCEPT_NEEDS_REASON } from "./result-acts.js";
20
- export { ACCEPT_NEEDS_REASON };
19
+ import { ACCEPT_NEEDS_REASON, FAILED_CHECK, cantAcceptYetOf, evidenceProblemOf } from "./result-acts.js";
20
+ export { ACCEPT_NEEDS_REASON, cantAcceptYetOf, evidenceProblemOf };
21
21
  import { GOAL_ASSESSMENT_PENDING, manualReviewOnly, plainReasonWords } from "./proof.js";
22
- import { plainReasonOf, stageOfCode, taskStatusOf, workToneOf } from "./task-status.js";
22
+ import { OPEN_RESULT, plainReasonOf, stageOfCode, taskStatusOf, workToneOf } from "./task-status.js";
23
+ import { whenUtc } from "./when-html.js";
23
24
  export const WORK_VIEWS = [
24
25
  { key: "all", label: "All", hint: "Every task in view, most urgent first.", empty: "Nothing is in progress. Describe work in chat or add a task, and it appears here." },
25
26
  { key: "needs-you", label: "Needs you", hint: "Tasks waiting on an answer, approval, or your inspection.", empty: "Nothing needs you right now. Queued and running work continues on its own." },
@@ -67,15 +68,7 @@ export function reviewStatusOf(review) {
67
68
  // it would unreviewed, marked "not reviewed" by the result itself.
68
69
  if (review === null || review.automaticPending !== true || (review.state !== "queued" && review.state !== "running"))
69
70
  return null;
70
- return { token: "reviewing", label: "Reviewing", detail: "An automatic review is reading this result before it reaches you.", tone: "live", action: { label: "Open the result", kind: "open-result" } };
71
- }
72
- const FAILED_CHECK = /^the repository's approved verification command exited (-?[0-9]+)/;
73
- export function evidenceProblemOf(verdict, reasons) {
74
- if (verdict === "verified" || verdict === "attested")
75
- return "none";
76
- if (verdict === "refuted")
77
- return reasons.some(reason => FAILED_CHECK.test(reason)) ? "checks-failed" : "mismatched";
78
- return "missing";
71
+ return { token: "reviewing", label: "Reviewing", detail: "An automatic review is reading this result before it reaches you.", tone: "live", action: { label: OPEN_RESULT, kind: "open-result" } };
79
72
  }
80
73
  /** The exit code of the failed check, when the recorded reason names one. */
81
74
  export function failedCheckExit(reasons) {
@@ -193,11 +186,11 @@ function storedResultStatusOf(result, publication) {
193
186
  // weighed, so it says nothing about whether that check passed.
194
187
  detail: withPublication("What the agent reported doesn't match the changes it saved."),
195
188
  tone: "problem",
196
- action: { label: "Open the result", kind: "open-review" },
189
+ action: { label: OPEN_RESULT, kind: "open-review" },
197
190
  };
198
191
  }
199
192
  if (result.verdict === "short" && humanReview) {
200
- return { token: "verification-needed", label: "Ready to inspect", detail: withPublication("The remaining requirements need a person's inspection."), tone: "attention", action: { label: "Inspect the result", kind: "open-review" } };
193
+ return { token: "verification-needed", label: "Ready to inspect", detail: withPublication("The remaining requirements need a person's inspection."), tone: "attention", action: { label: OPEN_RESULT, kind: "open-review" } };
201
194
  }
202
195
  if (result.verdict === "short") {
203
196
  return {
@@ -214,7 +207,7 @@ function storedResultStatusOf(result, publication) {
214
207
  // presence facts here.
215
208
  return result.recordComplete === false && result.verdict === null
216
209
  ? { token: "record-incomplete", label: "No-change result, record incomplete", detail: "The build concluded nothing needed to change, but its handoff or sealed diff is missing.", tone: "problem", action: { label: "Open the record", kind: "open-run" } }
217
- : { token: "no-change", label: "No changes were needed", detail: withPublication("The build concluded nothing needed to change; its handoff and sealed diff are on record."), tone: "done", action: { label: "Open the result", kind: "open-result" } };
210
+ : { token: "no-change", label: "No changes were needed", detail: withPublication("The build concluded nothing needed to change; its handoff and sealed diff are on record."), tone: "done", action: { label: OPEN_RESULT, kind: "open-result" } };
218
211
  }
219
212
  if (result.verdict === null) {
220
213
  return {
@@ -231,7 +224,7 @@ function storedResultStatusOf(result, publication) {
231
224
  label: "Result saved — checks reported by the agent",
232
225
  detail: withPublication("No independent project check ran; the checks listed are the agent's own report."),
233
226
  tone: "neutral",
234
- action: { label: "Open the result", kind: "open-result" },
227
+ action: { label: OPEN_RESULT, kind: "open-result" },
235
228
  };
236
229
  }
237
230
  if (published !== null && (published.token === "merge-observed" || published.token === "pr-opened" || published.token === "pr-closed")) {
@@ -240,7 +233,7 @@ function storedResultStatusOf(result, publication) {
240
233
  label: published.label,
241
234
  detail: `The approved check passed against this result. ${published.detail}`,
242
235
  tone: published.token === "pr-closed" ? "muted" : "done",
243
- action: published.token === "merge-observed" ? { label: "Open the result", kind: "open-result" } : { label: "Open the pull request", kind: "open-pr" },
236
+ action: published.token === "merge-observed" ? { label: OPEN_RESULT, kind: "open-result" } : { label: "Open the pull request", kind: "open-pr" },
244
237
  };
245
238
  }
246
239
  return {
@@ -248,7 +241,7 @@ function storedResultStatusOf(result, publication) {
248
241
  label: "Ready",
249
242
  detail: withPublication("The approved check passed against this result."),
250
243
  tone: "ready",
251
- action: { label: "Open the result", kind: "open-result" },
244
+ action: { label: OPEN_RESULT, kind: "open-result" },
252
245
  };
253
246
  }
254
247
  /** The completion receipt's heading: what the record actually supports.
@@ -288,7 +281,7 @@ export function needsPerson(dispatch) {
288
281
  /** Navigation labels describe the available help, not a mutation: opening
289
282
  * a hold or pause must never promise that the task has already resumed. */
290
283
  const DISPATCH_ACTION_LABELS = {
291
- "open-result": "Open the result",
284
+ "open-result": OPEN_RESULT,
292
285
  "retry-task": "Review and retry",
293
286
  "place-task": "Choose a project",
294
287
  "write-scope": "Define the task",
@@ -314,17 +307,6 @@ const VERIFIED_RESULT = new Set(["ready-to-review", "pr-opened", "merge-observed
314
307
  /** A finished result's display status in the shared headline's words: a
315
308
  * failed project check is Failed, a result missing its record needs a
316
309
  * person, every other saved result is Ready for review. Token and action stay. */
317
- /** Why a result can't be accepted as it stands, in one plain line, or null
318
- * when nothing refutes it (a person's acceptance settles it). The result page
319
- * shows it before Request changes, its one ink act. */
320
- export function cantAcceptYetOf(verdict, reasons, accepted) {
321
- if (accepted || verdict !== "refuted")
322
- return null;
323
- // A mismatch is the status card's own headline; here only what accepting takes.
324
- return evidenceProblemOf(verdict, reasons) === "checks-failed"
325
- ? "Can't accept yet: the project's check failed on these changes."
326
- : ACCEPT_NEEDS_REASON;
327
- }
328
310
  /** The result's headline while its report disagrees with its saved changes. */
329
311
  export const MISMATCH_HEADLINE = "The report doesn't match the changes";
330
312
  const OVERCLAIMED = /^claimed changed paths? not in the sealed diff: ([\s\S]+)$/;
@@ -413,15 +395,14 @@ export function acceptWordsOf(facts) {
413
395
  ...(facts.checks === "passed" ? [] : [facts.checks === "failed" ? "checks failed" : facts.checks === "off" ? "checks are off for this project"
414
396
  : facts.checks === "running" ? "checks are still running" : facts.checks === "unavailable" ? "saved checks can't be read" : "checks didn't run"]),
415
397
  ...(facts.unmet > 0 ? [`${plural(facts.unmet, "requirement isn't", "requirements aren't")} met`] : []),
416
- ...(facts.yours > 0 ? [`${plural(facts.yours, "item still needs", "items still need")} your check`] : []),
417
398
  ];
418
399
  const ready = parts.length === 0;
419
400
  const joined = parts.length <= 1 ? parts.join("") : `${parts.slice(0, -1).join(", ")} and ${parts.at(-1)}`;
420
- const effect = facts.action === "accept" ? "Records that you accept it. You mark it complete next."
421
- : facts.publishing === "pull-request" ? "Marks it complete. No pull request opens; you can open one from the task after."
422
- : facts.publishing === "off" ? "Marks it complete. The branch stays; publishing isn't set up."
423
- : "Marks it complete. Nothing is published.";
424
- return { label: ready ? "Accept" : "Accept without checks", ready, why: ready ? null : `${joined.charAt(0).toUpperCase()}${joined.slice(1)}.`, effect };
401
+ const effect = facts.action === "accept" ? "Records that you accept it. The task stays open."
402
+ : facts.publishing === "pull-request" ? "Finishes the task. No pull request opens; you can open one from the task after."
403
+ : facts.publishing === "off" ? "Finishes the task. The branch stays; publishing isn't set up."
404
+ : "Finishes the task. Nothing is published.";
405
+ return { label: ready ? "Accept and finish" : "Accept without checks", ready, why: ready ? null : `${joined.charAt(0).toUpperCase()}${joined.slice(1)}.`, effect };
425
406
  }
426
407
  export function resultHeadlineOf(status) {
427
408
  const read = RESULT_NEEDS_YOU.has(status.token) ? { stage: "needs-you", need: "other" }
@@ -549,6 +530,51 @@ export function learningHtml(view, csrf, canManage, source) {
549
530
  const cards = lessons.map(l => `<article class="card" data-lesson="${l.id}"><p><strong>${e(l.payload.observation)}</strong></p><p>${e(l.payload.action)}</p><p class="meta">${l.payload.kind === 'system' ? 'System suggestion · No change applied' : e(l.status === 'adopted' ? 'Adopted advice' : l.status === 'disabled' ? 'Disabled' : 'Proposed lesson')}</p><details><summary>Source and use</summary><p><a href="/r/${l.source}">Result #${l.source}</a> · <a href="/r/${l.reviewer}">Review #${l.reviewer}</a></p><p>${e(l.payload.paths.join(', '))} · ${e(l.payload.phases.join(', '))} · ${e({ darwin: "macOS", win32: "Windows", linux: "Linux" }[l.payload.platform] ?? l.payload.platform)}</p><ul>${evidence(l.payload.evidence, l.source)}</ul>${l.payload.kind !== 'project' ? '' : `<p class="meta">${l.status === 'proposed' ? 'Save permits advisory reuse when enabled and applicable. It does not prove a remedy works.' : 'Disabling affects future runs. Active snapshots, code and approvals stay unchanged.'}</p>${l.status === 'proposed' ? act('adopt', 'Save lesson', l) : ''}`}</details>${l.payload.kind === 'project' && l.status === 'adopted' ? act('disable', 'Disable lesson', l) : ''}</article>`).join('');
550
531
  if (source !== undefined)
551
532
  return lessons.length ? `<details class="learning result-learning"><summary>Learned from this task</summary>${cards}<a href="/settings/learning?repo=${encodeURIComponent(view.repo)}">Learning settings</a></details>` : '';
552
- const history = view.events.map(ev => `<article class="card" data-learning-event="${e(ev.action)}"><p><strong>${e({ assessment: { propose: 'Learning suggested', none: 'No lesson needed', unassessed: 'Learning not assessed', invalid: 'Learning assessment invalid' }[ev.after] ?? 'Learning not assessed', proposal: 'Suggestion recorded', adopt: 'Lesson adopted', disable: 'Lesson disabled', reset: 'Learning reset', enable: 'Reuse enabled', pause: 'Reuse paused', reuse: 'Run context saved', failure: 'Learning issue', capture: 'Capture finished' }[ev.action] ?? ev.action)}</strong>${ev.action === "reuse" ? ` · ${e(ev.after)}` : ""}</p><p class="meta"><time datetime="${e(ev.at)}">${e(ev.at.replace("T", " ").replace(/\.\d+Z$/, " UTC"))}</time> · ${e(ev.actor)}${ev.run === null ? '' : ` · <a href="/r/${ev.run}">Run #${ev.run}</a>${ev.outcome === null ? "" : ` · ${e(ev.outcome)}`}`}</p><details><summary>Details</summary><p>${e(ev.before)} → ${e(ev.after)}</p><p>${e(ev.reason)}</p>${ev.lesson === null ? '' : `<p>Lesson #${ev.lesson}</p>`}<ul>${evidence(ev.evidence, ev.run)}</ul>${ev.snapshot === null ? "" : `<details><summary>Exact context</summary><pre>${e(ev.snapshot)}</pre></details>`}</details></article>`).join('');
533
+ const history = view.events.map(ev => `<article class="card" data-learning-event="${e(ev.action)}"><p><strong>${e({ assessment: { propose: 'Learning suggested', none: 'No lesson needed', unassessed: 'Learning not assessed', invalid: 'Learning assessment invalid' }[ev.after] ?? 'Learning not assessed', proposal: 'Suggestion recorded', adopt: 'Lesson adopted', disable: 'Lesson disabled', reset: 'Learning reset', enable: 'Reuse enabled', pause: 'Reuse paused', reuse: 'Run context saved', failure: 'Learning issue', capture: 'Capture finished' }[ev.action] ?? ev.action)}</strong>${ev.action === "reuse" ? ` · ${e(ev.after)}` : ""}</p><p class="meta">${whenUtc(ev.at)} · ${e(ev.actor)}${ev.run === null ? '' : ` · <a href="/r/${ev.run}">Run #${ev.run}</a>${ev.outcome === null ? "" : ` · ${e(ev.outcome)}`}`}</p><details><summary>Details</summary><p>${e(ev.before)} → ${e(ev.after)}</p><p>${e(ev.reason)}</p>${ev.lesson === null ? '' : `<p>Lesson #${ev.lesson}</p>`}<ul>${evidence(ev.evidence, ev.run)}</ul>${ev.snapshot === null ? "" : `<details><summary>Exact context</summary><pre>${e(ev.snapshot)}</pre></details>`}</details></article>`).join('');
553
534
  return `<section class="learning">${view.damaged ? '<p class="problem" role="alert">Some lessons no longer verify and are excluded from reuse.</p>' : ''}<p>${view.enabled ? 'Use adopted lessons: on' : 'Use adopted lessons: off'}</p>${act(view.enabled ? 'pause' : 'enable', view.enabled ? 'Pause reuse' : 'Enable reuse')}${cards || '<p>No lessons yet. Reviews may suggest useful advice here.</p>'}<details><summary>Reset learning</summary><p>Disable all adopted lessons and future reuse. Keep history, code, approvals and active run snapshots.</p>${act('reset', 'Reset learning')}</details><h2>Changes</h2>${history || '<p>No learning changes yet.</p>'}${view.next === null ? '' : `<a class="button-link" href="/settings/learning?repo=${encodeURIComponent(view.repo)}&before=${view.next}">Older changes</a>`}</section>`;
554
535
  }
536
+ /** The first signed requirement a finished attempt missed (failed or unanswered; a person's own check is not a miss),
537
+ * with the evidence line behind it in plain words. Null when every requirement held. */
538
+ export function missedRequirementOf(matrix) {
539
+ const missed = matrix.find(row => row.state === "failed") ?? matrix.find(row => row.state === "missing") ?? null;
540
+ if (missed === null)
541
+ return null;
542
+ const detail = missed.detail.find(one => !/is waiting for the final check/.test(one)) ?? missed.detail[0] ?? null;
543
+ return { id: missed.id, statement: missed.statement, evidence: detail === null ? (missed.state === "missing" ? "The agent's report doesn't answer it." : null) : requirementEvidenceWords(missed.id, detail) };
544
+ }
545
+ /** A matrix row's recorded detail as a sentence a person reads: the agent's own note when one admits the miss. */
546
+ function requirementEvidenceWords(id, detail) {
547
+ const caveat = CONTRADICTED_BY_CAVEAT.exec(detail);
548
+ // The note often names its requirement first ("c1: …"); the line already says which requirement it is.
549
+ if (caveat !== null && caveat[1] === id)
550
+ return sentenceOf(`The agent's own note says: ${caveat[2].trim().replace(new RegExp(`^${id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*[:—-]\\s*`), "")}`);
551
+ const lead = [`criterion "${id}"'s `, `criterion "${id}" `].find(prefix => detail.startsWith(prefix));
552
+ return sentenceOf(plainReasonWords(lead === undefined ? detail : detail.slice(lead.length)));
553
+ }
554
+ const stepWords = (text) => {
555
+ const plain = text?.trim().replace(/\s+/g, " ").replace(/[.\s]+$/, "") ?? "";
556
+ return plain === "" ? null : plain;
557
+ };
558
+ /** A live build's progress: the step in progress (else the first one not done) as "Step N of M: <step>"; a blocked
559
+ * step replaces it as "Stuck on step N of M: <why>" (its note, else the step itself). Null with no plan steps. */
560
+ export function buildProgressOf(steps) {
561
+ if (steps == null || steps.length === 0)
562
+ return null;
563
+ const total = steps.length;
564
+ const blocked = steps.findIndex(one => one.state === "blocked");
565
+ if (blocked >= 0) {
566
+ const why = stepWords(steps[blocked].note) ?? stepWords(steps[blocked].description);
567
+ const line = `Stuck on step ${blocked + 1} of ${total}${why === null ? "" : `: ${why}`}.`;
568
+ return { step: blocked + 1, total, line, stuck: { step: blocked + 1, why, line } };
569
+ }
570
+ const current = steps.findIndex(one => one.state === "current");
571
+ const at = current >= 0 ? current : steps.findIndex(one => one.state !== "completed");
572
+ if (at < 0)
573
+ return { step: total, total, line: `All ${total} steps done. Finishing up.`, stuck: null };
574
+ const words = stepWords(steps[at].description);
575
+ return { step: at + 1, total, line: `Step ${at + 1} of ${total}${words === null ? "" : `: ${words}`}.`, stuck: null };
576
+ }
577
+ /** Earlier attempts that stopped before the one now running, in one quiet line. */
578
+ export function earlierAttemptsWords(count) {
579
+ return `${count} earlier attempt${count === 1 ? "" : "s"} stopped`;
580
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolroll",
3
- "version": "0.9.14",
3
+ "version": "0.9.16",
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"