toolroll 0.9.14 → 0.9.15

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.
@@ -19,7 +19,7 @@
19
19
  import { ACCEPT_NEEDS_REASON } from "./result-acts.js";
20
20
  export { ACCEPT_NEEDS_REASON };
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
23
  export const WORK_VIEWS = [
24
24
  { 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
25
  { 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,7 +67,7 @@ export function reviewStatusOf(review) {
67
67
  // it would unreviewed, marked "not reviewed" by the result itself.
68
68
  if (review === null || review.automaticPending !== true || (review.state !== "queued" && review.state !== "running"))
69
69
  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" } };
70
+ 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" } };
71
71
  }
72
72
  const FAILED_CHECK = /^the repository's approved verification command exited (-?[0-9]+)/;
73
73
  export function evidenceProblemOf(verdict, reasons) {
@@ -193,11 +193,11 @@ function storedResultStatusOf(result, publication) {
193
193
  // weighed, so it says nothing about whether that check passed.
194
194
  detail: withPublication("What the agent reported doesn't match the changes it saved."),
195
195
  tone: "problem",
196
- action: { label: "Open the result", kind: "open-review" },
196
+ action: { label: OPEN_RESULT, kind: "open-review" },
197
197
  };
198
198
  }
199
199
  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" } };
200
+ 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
201
  }
202
202
  if (result.verdict === "short") {
203
203
  return {
@@ -214,7 +214,7 @@ function storedResultStatusOf(result, publication) {
214
214
  // presence facts here.
215
215
  return result.recordComplete === false && result.verdict === null
216
216
  ? { 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" } };
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_RESULT, kind: "open-result" } };
218
218
  }
219
219
  if (result.verdict === null) {
220
220
  return {
@@ -231,7 +231,7 @@ function storedResultStatusOf(result, publication) {
231
231
  label: "Result saved — checks reported by the agent",
232
232
  detail: withPublication("No independent project check ran; the checks listed are the agent's own report."),
233
233
  tone: "neutral",
234
- action: { label: "Open the result", kind: "open-result" },
234
+ action: { label: OPEN_RESULT, kind: "open-result" },
235
235
  };
236
236
  }
237
237
  if (published !== null && (published.token === "merge-observed" || published.token === "pr-opened" || published.token === "pr-closed")) {
@@ -240,7 +240,7 @@ function storedResultStatusOf(result, publication) {
240
240
  label: published.label,
241
241
  detail: `The approved check passed against this result. ${published.detail}`,
242
242
  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" },
243
+ action: published.token === "merge-observed" ? { label: OPEN_RESULT, kind: "open-result" } : { label: "Open the pull request", kind: "open-pr" },
244
244
  };
245
245
  }
246
246
  return {
@@ -248,7 +248,7 @@ function storedResultStatusOf(result, publication) {
248
248
  label: "Ready",
249
249
  detail: withPublication("The approved check passed against this result."),
250
250
  tone: "ready",
251
- action: { label: "Open the result", kind: "open-result" },
251
+ action: { label: OPEN_RESULT, kind: "open-result" },
252
252
  };
253
253
  }
254
254
  /** The completion receipt's heading: what the record actually supports.
@@ -288,7 +288,7 @@ export function needsPerson(dispatch) {
288
288
  /** Navigation labels describe the available help, not a mutation: opening
289
289
  * a hold or pause must never promise that the task has already resumed. */
290
290
  const DISPATCH_ACTION_LABELS = {
291
- "open-result": "Open the result",
291
+ "open-result": OPEN_RESULT,
292
292
  "retry-task": "Review and retry",
293
293
  "place-task": "Choose a project",
294
294
  "write-scope": "Define the task",
@@ -413,15 +413,14 @@ export function acceptWordsOf(facts) {
413
413
  ...(facts.checks === "passed" ? [] : [facts.checks === "failed" ? "checks failed" : facts.checks === "off" ? "checks are off for this project"
414
414
  : facts.checks === "running" ? "checks are still running" : facts.checks === "unavailable" ? "saved checks can't be read" : "checks didn't run"]),
415
415
  ...(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
416
  ];
418
417
  const ready = parts.length === 0;
419
418
  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 };
419
+ const effect = facts.action === "accept" ? "Records that you accept it. The task stays open."
420
+ : facts.publishing === "pull-request" ? "Finishes the task. No pull request opens; you can open one from the task after."
421
+ : facts.publishing === "off" ? "Finishes the task. The branch stays; publishing isn't set up."
422
+ : "Finishes the task. Nothing is published.";
423
+ return { label: ready ? "Accept and finish" : "Accept without checks", ready, why: ready ? null : `${joined.charAt(0).toUpperCase()}${joined.slice(1)}.`, effect };
425
424
  }
426
425
  export function resultHeadlineOf(status) {
427
426
  const read = RESULT_NEEDS_YOU.has(status.token) ? { stage: "needs-you", need: "other" }
@@ -552,3 +551,48 @@ export function learningHtml(view, csrf, canManage, source) {
552
551
  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('');
553
552
  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
553
  }
554
+ /** The first signed requirement a finished attempt missed (failed or unanswered; a person's own check is not a miss),
555
+ * with the evidence line behind it in plain words. Null when every requirement held. */
556
+ export function missedRequirementOf(matrix) {
557
+ const missed = matrix.find(row => row.state === "failed") ?? matrix.find(row => row.state === "missing") ?? null;
558
+ if (missed === null)
559
+ return null;
560
+ const detail = missed.detail.find(one => !/is waiting for the final check/.test(one)) ?? missed.detail[0] ?? null;
561
+ 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) };
562
+ }
563
+ /** A matrix row's recorded detail as a sentence a person reads: the agent's own note when one admits the miss. */
564
+ function requirementEvidenceWords(id, detail) {
565
+ const caveat = CONTRADICTED_BY_CAVEAT.exec(detail);
566
+ // The note often names its requirement first ("c1: …"); the line already says which requirement it is.
567
+ if (caveat !== null && caveat[1] === id)
568
+ return sentenceOf(`The agent's own note says: ${caveat[2].trim().replace(new RegExp(`^${id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*[:—-]\\s*`), "")}`);
569
+ const lead = [`criterion "${id}"'s `, `criterion "${id}" `].find(prefix => detail.startsWith(prefix));
570
+ return sentenceOf(plainReasonWords(lead === undefined ? detail : detail.slice(lead.length)));
571
+ }
572
+ const stepWords = (text) => {
573
+ const plain = text?.trim().replace(/\s+/g, " ").replace(/[.\s]+$/, "") ?? "";
574
+ return plain === "" ? null : plain;
575
+ };
576
+ /** A live build's progress: the step in progress (else the first one not done) as "Step N of M: <step>"; a blocked
577
+ * step replaces it as "Stuck on step N of M: <why>" (its note, else the step itself). Null with no plan steps. */
578
+ export function buildProgressOf(steps) {
579
+ if (steps == null || steps.length === 0)
580
+ return null;
581
+ const total = steps.length;
582
+ const blocked = steps.findIndex(one => one.state === "blocked");
583
+ if (blocked >= 0) {
584
+ const why = stepWords(steps[blocked].note) ?? stepWords(steps[blocked].description);
585
+ const line = `Stuck on step ${blocked + 1} of ${total}${why === null ? "" : `: ${why}`}.`;
586
+ return { step: blocked + 1, total, line, stuck: { step: blocked + 1, why, line } };
587
+ }
588
+ const current = steps.findIndex(one => one.state === "current");
589
+ const at = current >= 0 ? current : steps.findIndex(one => one.state !== "completed");
590
+ if (at < 0)
591
+ return { step: total, total, line: `All ${total} steps done. Finishing up.`, stuck: null };
592
+ const words = stepWords(steps[at].description);
593
+ return { step: at + 1, total, line: `Step ${at + 1} of ${total}${words === null ? "" : `: ${words}`}.`, stuck: null };
594
+ }
595
+ /** Earlier attempts that stopped before the one now running, in one quiet line. */
596
+ export function earlierAttemptsWords(count) {
597
+ return `${count} earlier attempt${count === 1 ? "" : "s"} stopped`;
598
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolroll",
3
- "version": "0.9.14",
3
+ "version": "0.9.15",
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"