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.
@@ -4,7 +4,8 @@ import type { WorkIndexItem, WorkIndexPage } from './work-index.js';
4
4
  /** The index is navigation only. Controls retain their exact owning task, run,
5
5
  * decision and approval ceremony; opening a result keeps its saved execution. */
6
6
  export declare function browserWorkActionHref(item: WorkIndexItem): string | null;
7
- /** Render an already-admitted page without repeating its database query. */
7
+ /** Render an already-admitted page without repeating its database query. Every task reads its one headline, the
8
+ * same words as the Tasks list, task page and result page. */
8
9
  export declare function browserCrewFromIndex(page: WorkIndexPage, conversationId?: string): {
9
10
  crew: BrowserCrewItem[];
10
11
  crewTruncated: boolean;
@@ -32,7 +32,8 @@ export function browserWorkActionHref(item) {
32
32
  : action.code === 'start-worker' || action.code === 'repair-dependency' ? '#run-status' : '';
33
33
  return `/t/${encodeURIComponent(item.rootId)}?version=${encodeURIComponent(taskId)}` + anchor;
34
34
  }
35
- /** Render an already-admitted page without repeating its database query. */
35
+ /** Render an already-admitted page without repeating its database query. Every task reads its one headline, the
36
+ * same words as the Tasks list, task page and result page. */
36
37
  export function browserCrewFromIndex(page, conversationId) {
37
38
  const link = (href) => conversationId && href.startsWith('/chat?') ? href + '&conversation=' + encodeURIComponent(conversationId) : href;
38
39
  const rows = page.items.map(summary => {
@@ -7,11 +7,11 @@ export { browserCrewFromIndex, browserWorkActionHref } from './browser-crew.js';
7
7
  import type { Store } from './store.js';
8
8
  import type { WorkSummaryAccess } from './work-summary.js';
9
9
  import type { StatusTone } from './workspace-ui.js';
10
- import type { Ask } from './needs-you.js';
10
+ import type { Ask, AskChip } from './needs-you.js';
11
11
  import type { WorkIndexGroup } from './work-index.js';
12
12
  import type { AssignmentCard } from './assignment-ui.js';
13
13
  import type { TaskStatus } from './task-status.js';
14
- import type { ResultActs } from './result-acts.js';
14
+ import type { AcceptLabel, ResultActFacts, ResultActs } from './result-acts.js';
15
15
  import type { FirstRunStep, FirstTaskSuggestion, JourneyStep } from './first-run.js';
16
16
  export type BrowserProject = {
17
17
  name: string;
@@ -125,8 +125,10 @@ export type BrowserTasksView = {
125
125
  tone: StatusTone;
126
126
  token: string;
127
127
  };
128
- /** What a Needs you row asks (its chip); null for every other row. */
128
+ /** What a Needs you row asks: its group. */
129
129
  ask: Ask | null;
130
+ /** The row's chip under its group: the specific ask (Plan, Result, Mismatch, Plan changed, Failed, Builder offline), or none. */
131
+ chip: AskChip | null;
130
132
  group: BrowserTaskGroup;
131
133
  action: BrowserLink | null;
132
134
  detail: string | null;
@@ -367,17 +369,41 @@ export type BrowserTaskView = {
367
369
  rebuild?: {
368
370
  action: string;
369
371
  } | null;
370
- /** A failed task's recorded reason in one line (the run's reason, the failing check's last error line, or that none was
371
- * recorded) and where its log lines are; and Retry itself, with an optional note. */
372
- failure?: {
372
+ /** What a failed task missed, in one line (the requirement it missed, the failing check's error line, or the stop
373
+ * reason), the evidence line behind it, one suggestion of what to change, and where to see it (its build's result
374
+ * page, or that exact check-log line); and Retry itself, its note starting with the suggestion. */
375
+ failure?: BrowserFailure | null;
376
+ retry?: {
377
+ action: string;
378
+ note?: string;
379
+ } | null;
380
+ /** Run checks in place, on a status row that offers it: posts and comes back to this page. */
381
+ runChecks?: BrowserRunChecks | null;
382
+ /** A live build: the step it is on (or stuck on, with the act that helps), and the earlier attempts that stopped
383
+ * before it, folded into one quiet line. */
384
+ progress?: {
373
385
  line: string;
374
- link: {
375
- label: string;
376
- href: string;
386
+ stuck: {
387
+ step: number;
388
+ why: string | null;
389
+ line: string;
390
+ action: BrowserLink | null;
377
391
  } | null;
378
392
  } | null;
379
- retry?: {
393
+ /** Stop, on the Building card: the exact live build's stop form (posts its run id). */
394
+ stop?: {
380
395
  action: string;
396
+ run: number;
397
+ } | null;
398
+ /** The Building card's link to the live build's own record ("Build #N record", /r/<id>). */
399
+ record?: BrowserLink | null;
400
+ earlier?: {
401
+ summary: string;
402
+ attempts: {
403
+ label: string;
404
+ href: string;
405
+ text: string | null;
406
+ }[];
381
407
  } | null;
382
408
  /** Server cards that may need a person now (stop/resume, scope prompt, plan, live attempt). */
383
409
  lead: {
@@ -470,6 +496,12 @@ export type BrowserResultPanel = {
470
496
  returnTo: string;
471
497
  } | null;
472
498
  } | null;
499
+ /** The Requirements row's counts, so it updates as the person answers their checks (null when it doesn't count them). */
500
+ requirements?: {
501
+ met: number;
502
+ total: number;
503
+ yours: number;
504
+ } | null;
473
505
  /** Storage limits on saved output (shortened logs or diffs): shown on request. */
474
506
  limits: string[];
475
507
  tabs: {
@@ -518,13 +550,19 @@ export type BrowserCheckItem = {
518
550
  }[];
519
551
  };
520
552
  /** The result's one decision: what Accept is called, why it isn't plain
521
- * Accept, and what pressing it does. */
553
+ * Accept and finish, and what pressing it does. `base`: the same words before
554
+ * the person's own checks, which the page adds as they answer them. */
522
555
  export type BrowserResultDecision = {
523
- label: "Accept" | "Accept without checks";
556
+ label: AcceptLabel;
524
557
  ready: boolean;
525
558
  why: string | null;
526
559
  effect: string;
527
560
  sentence: string;
561
+ base?: {
562
+ label: AcceptLabel;
563
+ ready: boolean;
564
+ why: string | null;
565
+ };
528
566
  };
529
567
  /** A Needs you action: a link, (Confirm it stopped) a form behind the password, (Build again) one
530
568
  * button, or (on the result itself) Accept, which records the person's acceptance; `note` asks why
@@ -587,12 +625,17 @@ export type BrowserResultView = {
587
625
  detail: string;
588
626
  control: string;
589
627
  } | null;
628
+ /** Accept and finish: the exact receipt read; `accept` when the one request also records the person's acceptance
629
+ * (`note`: the words over the reason field it asks for, or null for none). */
590
630
  complete: {
591
631
  action: string;
592
632
  receipt: string;
593
633
  run: number;
634
+ accept?: {
635
+ note: string | null;
636
+ } | null;
594
637
  } | null;
595
- /** Words for the result's Accept: Mark complete when offered, else the Needs you acceptance. */
638
+ /** Words for the result's Accept: Accept and finish when offered, else the Needs you acceptance. */
596
639
  decision: BrowserResultDecision | null;
597
640
  checks: {
598
641
  detail: string;
@@ -617,12 +660,24 @@ export type BrowserResultView = {
617
660
  } | null;
618
661
  /** The one ink act that resolves the result, the one outline act beside it, and why it can't be accepted yet (result-acts.ts). */
619
662
  acts: ResultActs;
663
+ /** The facts the acts were chosen from, so the page chooses again as the person answers their checks. */
664
+ actFacts?: ResultActFacts;
620
665
  /** Run checks on this result's commit: the project's check, when it didn't run. */
621
- runChecks: {
622
- action: string;
623
- level: "quick" | "full";
624
- returnTo: string;
625
- } | null;
666
+ runChecks: BrowserRunChecks | null;
667
+ /** A failed build's result: what it missed and the suggestion Retry's note starts with; Retry itself when the task
668
+ * can be retried from here. Null for every other result. */
669
+ failure?: (BrowserFailure & {
670
+ retry: {
671
+ action: string;
672
+ note: string;
673
+ } | null;
674
+ /** A failed task's delivered result, accepted only in outline with a reason (the task's accept-proof). */
675
+ acceptAnyway?: {
676
+ action: string;
677
+ run: number;
678
+ returnTo: string;
679
+ } | null;
680
+ }) | null;
626
681
  /** The raw run record, under Details: its facts and the full record. */
627
682
  record: {
628
683
  build: number;
@@ -635,6 +690,7 @@ export type BrowserResultView = {
635
690
  /** The signed scope; null when none was filed. */
636
691
  intent: {
637
692
  approval: string;
693
+ approvedAt: string | null;
638
694
  html: string;
639
695
  } | null;
640
696
  noRun: string | null;
@@ -647,6 +703,20 @@ export type BrowserResultView = {
647
703
  }[];
648
704
  } | null;
649
705
  };
706
+ export type BrowserFailure = {
707
+ line: string;
708
+ evidence: string | null;
709
+ suggestion: string;
710
+ link: {
711
+ label: string;
712
+ href: string;
713
+ } | null;
714
+ };
715
+ export type BrowserRunChecks = {
716
+ action: string;
717
+ level: "quick" | "full";
718
+ returnTo: string;
719
+ };
650
720
  /** One zone on a flow's canvas: its step, where it leads, and where it sits. */
651
721
  export type BrowserFlowStage = {
652
722
  id: string;
@@ -202,7 +202,7 @@ export declare const CHAT_ACTIONS: {
202
202
  readonly password: true;
203
203
  };
204
204
  readonly result_accept: {
205
- readonly label: "Mark complete";
205
+ readonly label: "Accept and finish";
206
206
  readonly protected: true;
207
207
  readonly password: false;
208
208
  };
@@ -12,7 +12,7 @@ import { approve, authenticateApprover, describeScope, scopeAuthorityOf, } from
12
12
  import { readVerifiedArtifact, scanForSecrets } from "./evidence.js";
13
13
  import { readAuthModeStrict } from "./keys.js";
14
14
  import { parseProof } from "./proof.js";
15
- import { assignmentOf, checkAssignmentAsOperator } from "./assignment.js";
15
+ import { acceptAndCompleteAsOperator, assignmentOf } from "./assignment.js";
16
16
  import { getDecision, recordDecision, retireDecision } from "./project-memory.js";
17
17
  import { resumeTaskStop, taskControlOf } from "./task-control.js";
18
18
  import { addToolTo, catalogTool, projectToolsOf, removeToolFrom, toolCommandLine, validateToolSpec } from "./project-tools.js";
@@ -108,7 +108,7 @@ export const CHAT_ACTIONS = {
108
108
  decision_record: { label: "Record decision", protected: false, password: false },
109
109
  decision_retire: { label: "Retire decision", protected: false, password: false },
110
110
  scope_approve: { label: "Approve work", protected: true, password: true },
111
- result_accept: { label: "Mark complete", protected: true, password: false },
111
+ result_accept: { label: "Accept and finish", protected: true, password: false },
112
112
  task_cancel: { label: "Cancel task", protected: true, password: false },
113
113
  task_resume: { label: "Resume task", protected: true, password: true },
114
114
  };
@@ -1098,7 +1098,9 @@ export function executeSharedAction(store, who, id, payload, now, options) {
1098
1098
  throw Error("The comparison terms changed. Nothing was approved.");
1099
1099
  }
1100
1100
  else if (payload.operation === "result_accept") {
1101
- const completed = checkAssignmentAsOperator(store, task, String(payload.state["receipt"] ?? ""), who, now, options.root);
1101
+ // Accept and finish, as on the result page: whether a check only the person makes is accepted
1102
+ // too is decided inside the same transaction, against the receipt as it stands.
1103
+ const completed = acceptAndCompleteAsOperator(store, task, { runId: null, receiptDigest: String(payload.state["receipt"] ?? ""), note: null }, who, now, options.root);
1102
1104
  if (!completed.ok)
1103
1105
  throw Error(completed.message);
1104
1106
  }
@@ -1125,7 +1127,7 @@ export function executeSharedAction(store, who, id, payload, now, options) {
1125
1127
  const said = payload.operation === "skill_test"
1126
1128
  ? "Skill test created. Existing approval rules apply."
1127
1129
  : payload.operation === "result_accept"
1128
- ? "Marked complete. The recorded checks are unchanged."
1130
+ ? "Accepted and finished. The recorded checks are unchanged."
1129
1131
  : payload.operation === "scope_approve"
1130
1132
  ? "The exact work is approved."
1131
1133
  : payload.operation === "task_cancel"
@@ -76,7 +76,7 @@ export type ChatContent = {
76
76
  label: string;
77
77
  path: string;
78
78
  };
79
- /** More link buttons after `link`: [Look first] beside [Merge] or [Mark complete]. */
79
+ /** More link buttons after `link`: [Look first] beside [Merge] or [Accept and finish]. */
80
80
  also?: Array<{
81
81
  label: string;
82
82
  path: string;
@@ -24,7 +24,7 @@ export type FinishedFact = {
24
24
  checks: "passed" | "failed" | "not-run" | "off" | null;
25
25
  report: boolean;
26
26
  completedBy: string | null;
27
- /** An open pull request carries this result: the ask is "Merge it?" rather than "Mark it complete?". */
27
+ /** An open pull request carries this result: the ask is "Merge it?" rather than "Accept and finish it?". */
28
28
  pullRequest?: boolean;
29
29
  /** The lead did this (built it, or marked it complete), so the lead says "I". */
30
30
  lead?: boolean;
@@ -40,10 +40,10 @@ export type FinishedFact = {
40
40
  /** The two buttons under a result ready for a person: the real next step, then a look first. */
41
41
  export declare const READY_ACTIONS: {
42
42
  readonly merge: "Merge";
43
- readonly complete: "Mark complete";
43
+ readonly complete: "Accept and finish";
44
44
  readonly look: "Look first";
45
45
  };
46
- /** Whether this line asks for the result's next step, so it carries [Merge] or [Mark complete] and [Look first]. */
46
+ /** Whether this line asks for the result's next step, so it carries [Merge] or [Accept and finish] and [Look first]. */
47
47
  export declare function asksToFinish(fact: FinishedFact): boolean;
48
48
  /** One update, said plainly: what happened, then what happens next. The lead says "I" for what it did. */
49
49
  export declare function finishedLine(fact: FinishedFact): string;
@@ -42,8 +42,8 @@ export function inSentence(summary) {
42
42
  return /^\p{Lu}\p{Ll}/u.test(summary) ? summary[0].toLowerCase() + summary.slice(1) : summary;
43
43
  }
44
44
  /** The two buttons under a result ready for a person: the real next step, then a look first. */
45
- export const READY_ACTIONS = { merge: "Merge", complete: "Mark complete", look: "Look first" };
46
- /** Whether this line asks for the result's next step, so it carries [Merge] or [Mark complete] and [Look first]. */
45
+ export const READY_ACTIONS = { merge: "Merge", complete: "Accept and finish", look: "Look first" };
46
+ /** Whether this line asks for the result's next step, so it carries [Merge] or [Accept and finish] and [Look first]. */
47
47
  export function asksToFinish(fact) {
48
48
  return fact.update == null && fact.headline === "Ready for review" && !fact.report;
49
49
  }
@@ -61,7 +61,7 @@ export function finishedLine(fact) {
61
61
  if (fact.report)
62
62
  return fact.lead === true ? `I wrote up ${lower}. The report is ready to read.` : `${name}: the report is ready to read.`;
63
63
  if (fact.checks === "passed")
64
- return `${ready}. Your tests passed. ${fact.pullRequest === true ? "Merge it?" : "Mark it complete?"}`;
64
+ return `${ready}. Your tests passed. ${fact.pullRequest === true ? "Merge it?" : "Accept and finish it?"}`;
65
65
  if (fact.checks === "off")
66
66
  return `${ready}. Checks are off for this project, so look it over first.`;
67
67
  return `${ready}, but no tests ran. Look it over first.`;
package/dist/demo.js CHANGED
@@ -97,6 +97,57 @@ const DEMO_HANDOFF = {
97
97
  followUps: [],
98
98
  decisionsIncorporated: [],
99
99
  };
100
+ /** The failed demo build: it took the flag out of settlement and config, but
101
+ * not out of the admin override, and its own report says so. */
102
+ const DEMO_FAILED_PATCH = `diff --git a/src/payout.ts b/src/payout.ts
103
+ index 7c1d2e3..5a1f0c7 100644
104
+ --- a/src/payout.ts
105
+ +++ b/src/payout.ts
106
+ @@ -18,9 +18,5 @@ export function settleAll(rows: PayoutRow[]): number {
107
+ - if (config.LEGACY_PAYOUT) {
108
+ - return rows.reduce((sum, row) => sum + legacySettle(row), 0);
109
+ - }
110
+ -
111
+ - return rows.reduce((sum, row) => sum + settle(row.cents, row.rate), 0);
112
+ + return rows.reduce((sum, row) => sum + settle(row.cents, row.rate), 0);
113
+ }
114
+ diff --git a/src/config.ts b/src/config.ts
115
+ index 2b3c4d5..8e9f0a1 100644
116
+ --- a/src/config.ts
117
+ +++ b/src/config.ts
118
+ @@ -4,8 +4,6 @@ export const config = {
119
+ PAYOUT_BATCH_SIZE: 500,
120
+ - // Retired once every merchant is on the new settlement path.
121
+ - LEGACY_PAYOUT: process.env.LEGACY_PAYOUT === "1",
122
+ PAYOUT_CURRENCY: "USD",
123
+ };
124
+ `;
125
+ const DEMO_FAILED_HANDOFF = {
126
+ schema: 1,
127
+ outcome: "built",
128
+ committed: true,
129
+ conclusion: "Removed LEGACY_PAYOUT from settlement and config. The admin override still reads it, so one reference remains.",
130
+ changes: ["Dropped the legacy settlement branch in src/payout.ts.", "Removed the LEGACY_PAYOUT flag from src/config.ts."],
131
+ verification: ["Searched the codebase for LEGACY_PAYOUT after the change."],
132
+ followUps: [],
133
+ decisionsIncorporated: [],
134
+ };
135
+ const DEMO_FAILED_PROOF = {
136
+ version: 1,
137
+ criteria: [
138
+ {
139
+ id: "c1",
140
+ statement: "No reference to LEGACY_PAYOUT remains in the codebase.",
141
+ verdict: "met",
142
+ how: "Removed the flag and the settlement branch behind it.",
143
+ evidence: [{ kind: "changed-path", ref: "src/payout.ts" }, { kind: "changed-path", ref: "src/config.ts" }],
144
+ },
145
+ ],
146
+ checks: [],
147
+ changed: ["src/payout.ts", "src/config.ts"],
148
+ caveats: ["c1: src/admin/overrides.ts still reads LEGACY_PAYOUT for the per-merchant override toggle, so one reference remains."],
149
+ screenshots: [],
150
+ };
100
151
  /** The evidence bundle's own manifest (Acceptance Contract v2 demo): the
101
152
  * seeded sandbox answers its OWN signed rubric by exact id, with typed
102
153
  * evidence references — the same shape a real builder writes — so a
@@ -472,14 +523,15 @@ export function seedDemo(store, repos, evidenceRoot, now) {
472
523
  store.finishRun(buildingPriorRun, { outcome: "refused", reason: "plan-revised", now: hoursAgo(1.6) });
473
524
  // The board's "building" lane keys off a live claim — take one through
474
525
  // the real claim machinery so the card wears worker and lease honestly.
475
- acquire(store, buildingRef, "night-shift-1", {
526
+ const liveClaim = acquire(store, buildingRef, "night-shift-1", {
476
527
  now: hoursAgo(0.4),
477
528
  token: nightShift.token,
478
529
  ttlMs: 4 * 3_600_000,
479
530
  });
480
531
  const liveRun = store.startRun({
481
532
  taskRef: buildingRef,
482
- leaseId: "demo-lease-live",
533
+ // The claim's own lease, so the build reads as live: the run the task's current claim owns.
534
+ leaseId: liveClaim.ok ? liveClaim.claim.leaseId : "demo-lease-live",
483
535
  runner: "night-shift-1",
484
536
  branch: `toolroll/${building}`,
485
537
  worktree: join(repos.api, ".demo-worktree-2"),
@@ -603,6 +655,34 @@ export function seedDemo(store, repos, evidenceRoot, now) {
603
655
  now: hoursAgo(6),
604
656
  ...presentedRoute(store, store.refFor("built-in", failed).id, "builder"),
605
657
  });
658
+ // What the failed build left: its saved changes, its report and the
659
+ // machine's reading of them. It removed the flag from settlement but left
660
+ // the admin override reading it, and said so in its own note — so the
661
+ // signed requirement is missed, and the task card can name it.
662
+ store.stampRun(failedRun, { baseRevision: "4b825dc642cb6eb9a060e54bf8d69288fbee4904", scopeDigest: failedProposed.digest });
663
+ store.recordOutcomeFacts(failedRun, { headRevision: "5a1f0c7e2b9d4e6f8a0b1c2d3e4f5a6b7c8d9e0f" });
664
+ storeEvidence(store, evidenceRoot, failedRun, "terminal-diff", "terminal-diff.patch", Buffer.from(DEMO_FAILED_PATCH, "utf8"), "git diff --no-ext-diff --no-textconv --no-color 4b825dc6..HEAD (exit 0) [demo: synthetic]", hoursAgo(5.6));
665
+ const failedStat = {
666
+ schema: 1, base: "4b825dc642cb6eb9a060e54bf8d69288fbee4904", head: "5a1f0c7e2b9d4e6f8a0b1c2d3e4f5a6b7c8d9e0f",
667
+ fileCount: 2, additions: 1, deletions: 7, binaryCount: 0,
668
+ files: [{ path: "src/payout.ts", additions: 1, deletions: 5 }, { path: "src/config.ts", additions: 0, deletions: 2 }],
669
+ filesTruncated: false,
670
+ };
671
+ storeEvidence(store, evidenceRoot, failedRun, "diff-stat", "diff-stat.json", budgetedStatJson(failedStat), "parsed from git diff --numstat -z [demo: synthetic]", hoursAgo(5.6));
672
+ storeEvidence(store, evidenceRoot, failedRun, "handoff", "handoff.json", Buffer.from(JSON.stringify(DEMO_FAILED_HANDOFF, null, 2), "utf8"), "composed at completion [demo: synthetic]", hoursAgo(5.6));
673
+ storeEvidence(store, evidenceRoot, failedRun, "proof", "proof.json", Buffer.from(JSON.stringify(DEMO_FAILED_PROOF, null, 2), "utf8"), "agent-authored proof (validated, re-serialized) [demo: synthetic]", hoursAgo(5.6));
674
+ const failedAdjudicated = adjudicate({
675
+ proofArtifactPresent: true,
676
+ proofParse: parseProof(JSON.stringify(DEMO_FAILED_PROOF)),
677
+ handoffPresent: true,
678
+ terminalDiffPresent: true,
679
+ terminalDiffCaptureStatus: "ok",
680
+ diffStat: { captured: true, truncated: false, paths: new Set(failedStat.files.map(one => one.path)) },
681
+ verifyCommand: { configured: false },
682
+ screenshots: [],
683
+ approvedCriteria: failedProposed.acceptance,
684
+ });
685
+ store.saveProofVerdict(failedRun, failedAdjudicated.verdict, failedAdjudicated.reasons, hoursAgo(5.5), failedAdjudicated.matrix);
606
686
  store.finishRun(failedRun, {
607
687
  outcome: "failed",
608
688
  reason: "acceptance",
@@ -0,0 +1,6 @@
1
+ /** One disclosure glyph everywhere: the chevron. It points right while a fold is shut and turns down when it opens,
2
+ * on the server pages, the enhanced folds (workspace-motion.ts) and the React views (lucide's ChevronRight). Every
3
+ * other marker (the native triangle, ▸/▾, +/−, ⌄ and the old arrows) is turned off here, last, so no page can bring
4
+ * a second glyph back. A summary that carries its own icon keeps it. Drawn with two borders: the page's content
5
+ * policy allows no image, not even a data: one. */
6
+ export declare const DISCLOSURE_CSS: string;
@@ -0,0 +1,13 @@
1
+ /** One disclosure glyph everywhere: the chevron. It points right while a fold is shut and turns down when it opens,
2
+ * on the server pages, the enhanced folds (workspace-motion.ts) and the React views (lucide's ChevronRight). Every
3
+ * other marker (the native triangle, ▸/▾, +/−, ⌄ and the old arrows) is turned off here, last, so no page can bring
4
+ * a second glyph back. A summary that carries its own icon keeps it. Drawn with two borders: the page's content
5
+ * policy allows no image, not even a data: one. */
6
+ export const DISCLOSURE_CSS = `details>summary{list-style:none}details>summary::-webkit-details-marker{display:none}` +
7
+ `details>summary::after{content:none!important}` +
8
+ `details>summary:not(:has(>svg))::before{content:""!important;display:inline-block;flex:none;box-sizing:border-box;width:7px!important;height:7px!important;` +
9
+ `margin:0 .55rem 0 .2rem!important;padding:0!important;position:static!important;vertical-align:.1em;font-size:0;background:none!important;` +
10
+ `border:0 solid currentColor!important;border-right-width:1.5px!important;border-bottom-width:1.5px!important;opacity:.65;` +
11
+ `transform:rotate(-45deg)!important;transition:transform .15s ease}` +
12
+ `details[open]>summary:not(:has(>svg))::before{transform:rotate(45deg)!important}` +
13
+ `@media (prefers-reduced-motion:reduce){details>summary::before{transition:none!important}}`;
@@ -90,14 +90,14 @@ export function taskWaitSnapshot(store, taskId, now, watchedRun) {
90
90
  if (outcome !== "built" && outcome !== "no-change")
91
91
  return answer("Failed", "Inspect attempt", true, 1, "failed");
92
92
  if (state !== "done")
93
- return answer("Ready", "Review result", true, 0, "ready");
93
+ return answer("Ready", "Open result", true, 0, "ready");
94
94
  }
95
95
  // A completed family is Complete from any of its tasks.
96
96
  const completed = state === "done" && familyCompleted(store, taskId);
97
97
  if (completed)
98
98
  return answer("Complete", "No action needed", true, 0, "complete");
99
99
  if (state === "done")
100
- return answer("Ready", "Review result", true, 0, "ready");
100
+ return answer("Ready", "Open result", true, 0, "ready");
101
101
  if (state === "failed" || state === "cancelled")
102
102
  return answer("Failed", state === "cancelled" ? "Review cancellation" : "Inspect failure", true, 1, "failed");
103
103
  const decision = store.handle.prepare(`SELECT decision.id FROM run INDEXED BY lead_status_task_run
@@ -35,6 +35,30 @@ export declare const NEEDS: Readonly<Record<NeedKey, Need>>;
35
35
  export type Ask = "decide" | "review" | "unblock";
36
36
  export declare const ASKS: readonly Ask[];
37
37
  export declare const ASK_LABEL: Readonly<Record<Ask, string>>;
38
+ /** A waiting row's chip: the specific thing asked, never its group's own word (no "Decide" chip under Decide).
39
+ * Null: the group heading and the row's sentence already say it, so the row wears no chip. */
40
+ export type AskChip = "Plan" | "Result" | "Mismatch" | "Plan changed" | "Failed" | "Builder offline";
41
+ /** `mismatch`: the saved report doesn't match its changes; `planChanged`: the plan changed after it was built. */
42
+ export declare function askChipOf(read: {
43
+ headline: string;
44
+ need?: NeedKey | null;
45
+ mismatch?: boolean;
46
+ planChanged?: boolean;
47
+ ask?: Ask | null;
48
+ }): AskChip | null;
49
+ /** Why a saved result can't simply be accepted yet, in its own words (each Review row says its own reason,
50
+ * never a boilerplate line). The facts are the ones the Tasks list and the task page both read. */
51
+ export type ResultHoldUp = {
52
+ hold: string | null;
53
+ planChanged: boolean;
54
+ unapproved: boolean;
55
+ running: boolean;
56
+ unfinished: boolean;
57
+ noCommit: boolean;
58
+ question: boolean;
59
+ report: boolean;
60
+ };
61
+ export declare function resultHoldUpSentence(facts: Partial<ResultHoldUp>): string;
38
62
  export declare const NEED_ASK: Readonly<Record<NeedKey, Ask>>;
39
63
  export declare const WAITS: Readonly<Record<WaitKey, (context: NeedContext) => string>>;
40
64
  /** Words that name the machine's internals; no Needs you or Waiting sentence carries them. */
@@ -83,4 +107,28 @@ export declare function isInternalErrorReason(reason: string | null | undefined)
83
107
  /** A failed attempt in one plain line: its recorded reason in words (first line only, about 140 characters at most),
84
108
  * that it stopped with an internal error when the reason is machine output, or that none was recorded. */
85
109
  export declare function failedAttemptSentence(reason: string | null | undefined): string;
110
+ export declare const GENERAL_SUGGESTION = "Say what to do differently this time.";
111
+ export declare function stopSuggestionOf(reason: string | null | undefined): string;
112
+ /** The suggestion for a requirement the result missed: make sure of it before handing off. */
113
+ export declare function missedRequirementSuggestion(statement: string): string;
114
+ /** The suggestion for a check that failed on the result: make it pass, starting with the line it ended on. */
115
+ export declare function failingCheckSuggestion(line: string): string;
116
+ /** What a failed attempt missed, in plain words: what failed (the missed requirement, the failing check's line or the
117
+ * stop reason), the evidence line behind it, and one suggestion of what to change (Retry's note starts with it). */
118
+ export type FailureExplanation = {
119
+ kind: "requirement" | "check" | "reason";
120
+ line: string;
121
+ evidence: string | null;
122
+ suggestion: string;
123
+ link: {
124
+ label: string;
125
+ href: string;
126
+ } | null;
127
+ };
128
+ /** The line for a missed requirement: “Missed: <statement>”, cut to one line. */
129
+ export declare function missedRequirementLine(statement: string): string;
130
+ /** The longest note Retry's field takes. */
131
+ export declare const RETRY_NOTE_LIMIT = 500;
132
+ /** Retry's note, prefilled with the suggestion: cut to what the field takes, at a word, so the form always posts. */
133
+ export declare function retryNoteOf(suggestion: string): string;
86
134
  export {};
package/dist/needs-you.js CHANGED
@@ -28,7 +28,7 @@ export const NEEDS = {
28
28
  "connect-builder": { sentence: () => "No builder works on this project yet. Connect one so the task can start.", action: { code: "start-worker", label: "Connect a builder" } },
29
29
  "start-builder": { sentence: () => "The builder for this project is offline. Start it and the task begins on its own.", action: { code: "start-worker", label: "Start the builder" } },
30
30
  vanished: { sentence: context => `${Build(context)} stopped without finishing. Check it, then retry.`, action: { code: "reconcile-run", label: "Check the build" } },
31
- "review-result": { sentence: () => "Check the result, then accept it or ask for changes.", action: { code: "open-result", label: "Review result" } },
31
+ "review-result": { sentence: () => "Check the result, then accept it or ask for changes.", action: { code: "open-result", label: "Open result" } },
32
32
  // Accepting it would not help: it was built to terms that are no longer the plan.
33
33
  rebuild: { sentence: context => `${Build(context)} was made to an earlier plan. Build it again to the current plan.`, action: { code: "retry-task", label: "Build again" } },
34
34
  "earlier-version": { sentence: () => "An earlier version of this task still needs you. Finish it first.", action: { code: "inspect-task", label: "Open earlier version" } },
@@ -36,6 +36,41 @@ export const NEEDS = {
36
36
  };
37
37
  export const ASKS = ["decide", "review", "unblock"];
38
38
  export const ASK_LABEL = { decide: "Decide", review: "Review", unblock: "Unblock" };
39
+ /** `mismatch`: the saved report doesn't match its changes; `planChanged`: the plan changed after it was built. */
40
+ export function askChipOf(read) {
41
+ if (read.headline === "Failed")
42
+ return "Failed";
43
+ if (read.planChanged === true || read.need === "rebuild")
44
+ return "Plan changed";
45
+ if (read.mismatch === true)
46
+ return "Mismatch";
47
+ if (read.headline === "Ready for review" || read.need === "review-result" || read.ask === "review")
48
+ return "Result";
49
+ if (read.need === "approval")
50
+ return "Plan";
51
+ if (read.need === "start-builder" || read.need === "connect-builder")
52
+ return "Builder offline";
53
+ return null;
54
+ }
55
+ export function resultHoldUpSentence(facts) {
56
+ if (facts.hold != null && facts.hold.trim() !== "" && !hasInternalWords(facts.hold))
57
+ return `On hold: ${facts.hold.trim().replace(/[.\s]*$/, "")}. Release the hold to accept it.`;
58
+ if (facts.hold != null)
59
+ return "A hold is on this result. Release it to accept the result.";
60
+ if (facts.planChanged)
61
+ return "The plan changed after this was built. Build it again to the current plan.";
62
+ if (facts.unapproved)
63
+ return "The current plan isn't approved. Approve it, then review the result.";
64
+ if (facts.question)
65
+ return "A question is still open. Answer it, then review the result.";
66
+ if (facts.running)
67
+ return "A build is still running on this task.";
68
+ if (facts.unfinished)
69
+ return "An earlier attempt never finished. Check it, then review the result.";
70
+ if (facts.noCommit)
71
+ return facts.report ? "The report is missing or incomplete. Open it to see what was saved." : "The build saved no commit to review. Open the result to see what was kept.";
72
+ return "Open the result to see what it needs before you can accept it.";
73
+ }
39
74
  export const NEED_ASK = {
40
75
  approval: "decide", card: "decide", answer: "decide", questions: "decide", "choose-project": "decide", "define-task": "decide",
41
76
  "fix-request": "decide", "choose-agent": "decide",
@@ -138,6 +173,7 @@ export const RUN_REASON_WORDS = {
138
173
  "reviewer-error": "the review failed",
139
174
  "malformed-report": "the scout's report was malformed",
140
175
  "attempts-exhausted": "it failed too many times in a row",
176
+ "plan-revised": "the plan changed, so a fresh attempt took over",
141
177
  };
142
178
  /** A run's recorded reason in words. A recorded code is never shown as it is
143
179
  * stored: one with no words of its own reads as an unexpected stop; a reason
@@ -190,3 +226,59 @@ export function failedAttemptSentence(reason) {
190
226
  : `${words.slice(0, REASON_LINE_LIMIT - 1).replace(/\s+\S*$/, "").replace(/[,;:\s]+$/, "")}…`;
191
227
  return `${line.charAt(0).toUpperCase()}${line.slice(1)}${line.endsWith("…") ? "" : "."}`;
192
228
  }
229
+ /** What to change before the next attempt, by a stopped run's recorded reason: one plain suggestion, the note Retry
230
+ * starts with. A reason with no suggestion of its own gets the general one. */
231
+ const STOP_SUGGESTIONS = {
232
+ acceptance: "Before handing off, check each signed requirement against the changes.",
233
+ timeout: "Split the work into smaller steps, or name the one part to finish first.",
234
+ "no-op": "Say exactly which files or behaviour should change.",
235
+ agent: "Say what to do differently this time, or name the part to try first.",
236
+ "agent-reported": "Answer what the agent said it was missing, or narrow the task.",
237
+ "provider-init": "Check the agent is signed in and installed, then retry.",
238
+ "spawn-failed": "Check the agent is signed in and installed, then retry.",
239
+ "auth-expired": "Sign the agent in again, then retry.",
240
+ "provider-protocol": "Retry as it is; if it stops the same way, try another agent.",
241
+ setup: "Fix the project's setup step, then retry.",
242
+ git: "Make sure the project's branch is clean and reachable, then retry.",
243
+ "commit-failure": "Make sure the project's commit hooks pass, then retry.",
244
+ "moved-head": "Retry: the next attempt starts from where the branch is now.",
245
+ "moved-branch": "Retry: the next attempt starts from where the branch is now.",
246
+ "retryable-infra": "Retry as it is.",
247
+ interrupted: "Retry as it is.",
248
+ orphaned: "Retry as it is.",
249
+ capability: "Add what the project is missing, then retry.",
250
+ "attempts-exhausted": "Say what to do differently this time, or narrow the task.",
251
+ };
252
+ export const GENERAL_SUGGESTION = "Say what to do differently this time.";
253
+ export function stopSuggestionOf(reason) {
254
+ const code = reason?.trim() ?? "";
255
+ return STOP_SUGGESTIONS[code] ?? GENERAL_SUGGESTION;
256
+ }
257
+ /** A signed requirement in the middle of a sentence: its first letter lowered unless it starts a name or an acronym. */
258
+ function inSentence(statement) {
259
+ const plain = statement.trim().replace(/[.\s]+$/, "");
260
+ return /^[A-Z][a-z]/.test(plain) ? `${plain.charAt(0).toLowerCase()}${plain.slice(1)}` : plain;
261
+ }
262
+ /** The suggestion for a requirement the result missed: make sure of it before handing off. */
263
+ export function missedRequirementSuggestion(statement) {
264
+ return `Before handing off, make sure ${inSentence(statement)}.`;
265
+ }
266
+ /** The suggestion for a check that failed on the result: make it pass, starting with the line it ended on. */
267
+ export function failingCheckSuggestion(line) {
268
+ return `Make the check pass. It ended on: ${line.replace(/[.\s]+$/, "")}.`;
269
+ }
270
+ /** The line for a missed requirement: “Missed: <statement>”, cut to one line. */
271
+ export function missedRequirementLine(statement) {
272
+ const plain = statement.trim().replace(/\s+/g, " ").replace(/[.\s]+$/, "");
273
+ const cut = plain.length <= REASON_LINE_LIMIT - 10 ? plain : `${plain.slice(0, REASON_LINE_LIMIT - 11).replace(/\s+\S*$/, "")}…`;
274
+ return `Missed a requirement: ${cut}${cut.endsWith("…") ? "" : "."}`;
275
+ }
276
+ /** The longest note Retry's field takes. */
277
+ export const RETRY_NOTE_LIMIT = 500;
278
+ /** Retry's note, prefilled with the suggestion: cut to what the field takes, at a word, so the form always posts. */
279
+ export function retryNoteOf(suggestion) {
280
+ const plain = suggestion.trim();
281
+ if (plain.length <= RETRY_NOTE_LIMIT)
282
+ return plain;
283
+ return `${plain.slice(0, RETRY_NOTE_LIMIT - 1).replace(/\s+\S*$/, "").replace(/[,;:\s]+$/, "")}…`;
284
+ }