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
@@ -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
+ }
package/dist/operate.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { UNSENT_REPLY_MS } from "./telegram-settings.js";
2
+ import { leadNameOf } from "./lead-identity.js";
2
3
  import { maybeTriggerRepair } from "./dispose.js";
3
4
  import { CHECK_LEVEL_HINTS, CHECK_LEVEL_WORDS, isCheckLevel, liveQuickCommand, projectCheckLevel, quickVerifyKey, setProjectCheckLevel, setTaskCheckLevel, suggestQuickCommand } from "./check-levels.js";
4
5
  import { fileAddTestsTask, followUpChecksOf, requestFollowUpChecks, runFollowUpCheck, runWaitingChecks } from "./result-follow-ups.js";
@@ -10698,7 +10699,7 @@ function leadClaimCommand(positional, flags, context) {
10698
10699
  const claimed = leadClaim(store, actor, id, context.clock());
10699
10700
  if (!claimed.ok)
10700
10701
  return fail(write, json, command, claimed.reason, claimed.message, claimed.reason === "usage" ? EXIT.usage : EXIT.refused);
10701
- return succeed(write, json, command, { task: id, root: claimed.root, lead: claimed.claim }, () => [`${id} is yours: it reads "Your lead is on it" until you complete it, hand it on with task ask, or go two hours without acting on it.`]);
10702
+ return succeed(write, json, command, { task: id, root: claimed.root, lead: claimed.claim }, () => [`${id} is yours: it reads "${leadNameOf(store, actor.account)} is on it" until you complete it, hand it on with task ask, or go two hours without acting on it.`]);
10702
10703
  }
10703
10704
  function blockTask(positional, flags, context) {
10704
10705
  const { store, write, json } = context;
@@ -218,6 +218,7 @@ function deleteRows(store, repo, d, now) {
218
218
  del(`${surface}_flow_prompt`, IN("card"), C);
219
219
  del(`${surface}_question_action`, IN("question"), questions);
220
220
  del(`${surface}_question_prompt`, IN("question"), questions);
221
+ del(`${surface}_ask_action`, IN("turn"), U);
221
222
  }
222
223
  del("telegram_action", IN("decision"), decisions);
223
224
  del("telegram_decision_message", IN("decision"), decisions);
@@ -242,6 +243,7 @@ function deleteRows(store, repo, d, now) {
242
243
  del("team_mate_session", IN("thread"), H);
243
244
  del("mate_turn_evidence", `${IN("turn")} OR ${IN("run")} OR ${IN("task_ref")} OR ${IN("artifact")}`, U, R, T, A);
244
245
  del("chat_turn", IN("mate_turn"), U);
246
+ del("mate_ask", IN("turn"), U);
245
247
  del("mate_turn", IN("id"), U);
246
248
  del("mate_proposal", IN("id"), proposals);
247
249
  del("mate_message", IN("thread"), H);
@@ -318,7 +320,7 @@ function deleteRows(store, repo, d, now) {
318
320
  for (const [table, column] of SETTINGS)
319
321
  del(table, `${column} = ?`, repo);
320
322
  del("project_mute", "repo = ?", repo);
321
- for (const table of ["knowledge_change", "project_skill_change", "memory_gap", "memory_proposal", "memory_rejection", "memory_session", "memory_search", "mode_rail", "side_spend", "watch_episode", "worktree", "coordinator_proposal"])
323
+ for (const table of ["knowledge_change", "project_skill_change", "memory_gap", "memory_proposal", "memory_rejection", "memory_session", "memory_search", "mode_rail", "side_spend", "watch_episode", "worktree", "coordinator_proposal", "lead_commitment"])
322
324
  del(table, "repo = ?", repo);
323
325
  // A builder watching the project keeps its lease until it stops; an ended one goes.
324
326
  del("watch_lease", "repo = ? AND expires_at <= ?", repo, now.toISOString());
@@ -3,7 +3,9 @@
3
3
  * most one outline act beside it. A result that can't be accepted says why
4
4
  * in one line before them.
5
5
  *
6
- * everything met → Accept
6
+ * everything met → Accept and finish
7
+ * a "You check this one" unanswered → the next check, then Accept without your check
8
+ * a check you marked Not right → Request changes, then Accept anyway
7
9
  * checks queued or running → Checks running (disabled), then Accept without checks
8
10
  * checks didn't run, a check exists → Run checks, then Accept without checks
9
11
  * report and changes disagree,
@@ -11,14 +13,33 @@
11
13
  * never Accept as ink, even with no form here
12
14
  * a Needs you form resolves it → that form (Build again, Confirm it stopped)
13
15
  * saved notes or a failing PR → Revise / Draft a repair task
16
+ * a failed build → Retry (its note starts with what to change), then Run checks when they
17
+ * didn't run; never Accept: a failed build is not a result to accept
18
+ * a failed task's delivered result → Retry, then Accept anyway (in outline, with a reason) where it may be
19
+ * accepted; Run checks stays in its Checks row
14
20
  *
15
- * Accepting means what it always meant: the same forms post to the same
16
- * endpoints. This only decides which one is ink. */
21
+ * Accepting finishes the task in one request (Accept and finish): the person's
22
+ * acceptance, when one is owed, and the completion together. This only decides
23
+ * which act is ink; the page recomputes it as the person answers each check. */
24
+ import type { ProofVerdict } from "./proof.js";
17
25
  /** The words over the reason field an Accept of a refuted result requires. */
18
26
  export declare const ACCEPT_NEEDS_REASON = "Accepting needs a reason";
19
- export type ResultActKind = "accept" | "checks-running" | "run-checks" | "request-changes" | "rebuild" | "confirm-stopped" | "revise" | "draft-repair";
27
+ /** What kind of evidence problem a verdict records: a check the machine
28
+ * actually ran and saw fail, evidence that contradicts the sealed record
29
+ * (a mismatched changed-path claim, an altered criterion, a caveat that
30
+ * contradicts a met verdict), or evidence that is simply absent. A generic
31
+ * refuted verdict is NOT a failed test claim — only the verify-command
32
+ * reasons say a check failed. */
33
+ export type EvidenceProblem = "checks-failed" | "mismatched" | "missing" | "none";
34
+ export declare const FAILED_CHECK: RegExp;
35
+ export declare function evidenceProblemOf(verdict: ProofVerdict | null, reasons: readonly string[]): EvidenceProblem;
36
+ /** Why a result can't be accepted as it stands, in one plain line, or null
37
+ * when nothing refutes it (a person's acceptance settles it). The result page
38
+ * shows it before Request changes, its one ink act. */
39
+ export declare function cantAcceptYetOf(verdict: ProofVerdict | null, reasons: readonly string[], accepted: boolean): string | null;
40
+ export type ResultActKind = "retry" | "accept" | "accept-anyway" | "next-check" | "checks-running" | "run-checks" | "request-changes" | "rebuild" | "confirm-stopped" | "revise" | "draft-repair";
20
41
  export type ResultActFacts = {
21
- /** An Accept this page can post (Mark complete, or the person's acceptance); `ready`: every requirement met and checks passed. */
42
+ /** An Accept this page can post (Accept and finish, or the person's acceptance alone); `ready`: every requirement met and checks passed. */
22
43
  accept: {
23
44
  ready: boolean;
24
45
  } | null;
@@ -34,6 +55,16 @@ export type ResultActFacts = {
34
55
  need: "rebuild" | "confirm-stopped" | null;
35
56
  /** Saved notes ready to become a revision, or a failing pull request's repair. */
36
57
  next: "revise" | "draft-repair" | null;
58
+ /** A failed build: `retry` when the task can be retried from here; `acceptAnyway` when it delivered a result that
59
+ * may still be accepted, with a reason. */
60
+ failed?: {
61
+ retry: boolean;
62
+ acceptAnyway?: boolean;
63
+ } | null;
64
+ /** "You check this one" items not answered yet on this page. */
65
+ unanswered?: number;
66
+ /** Items the person marked Not right. */
67
+ notRight?: number;
37
68
  };
38
69
  export type ResultActs = {
39
70
  /** The one ink act, or null when nothing waits on the person. */
@@ -44,3 +75,30 @@ export type ResultActs = {
44
75
  line: string | null;
45
76
  };
46
77
  export declare function resultActsOf(facts: ResultActFacts): ResultActs;
78
+ /** The words a result's Accept wears and the one line before it, given the
79
+ * server's words for everything but the person's own checks (`base`) and how
80
+ * those stand on this page: an unanswered check names itself; a check marked
81
+ * Not right says so; otherwise the base words stand. */
82
+ export type AcceptLabel = "Accept and finish" | "Accept without checks" | "Accept without your check" | "Accept anyway";
83
+ export declare function acceptWithChecksOf(base: {
84
+ label: AcceptLabel;
85
+ ready: boolean;
86
+ why: string | null;
87
+ }, checks: {
88
+ unanswered: readonly string[];
89
+ notRight: readonly string[];
90
+ }): {
91
+ label: AcceptLabel;
92
+ ready: boolean;
93
+ why: string | null;
94
+ };
95
+ /** The Requirements row as the person answers their checks: each Looks right is met, each answer leaves "You check". */
96
+ export declare function requirementsWordsOf(req: {
97
+ met: number;
98
+ total: number;
99
+ yours: number;
100
+ }, looked: number, notRight: number): {
101
+ text: string;
102
+ done: boolean;
103
+ short: boolean;
104
+ };
@@ -3,7 +3,9 @@
3
3
  * most one outline act beside it. A result that can't be accepted says why
4
4
  * in one line before them.
5
5
  *
6
- * everything met → Accept
6
+ * everything met → Accept and finish
7
+ * a "You check this one" unanswered → the next check, then Accept without your check
8
+ * a check you marked Not right → Request changes, then Accept anyway
7
9
  * checks queued or running → Checks running (disabled), then Accept without checks
8
10
  * checks didn't run, a check exists → Run checks, then Accept without checks
9
11
  * report and changes disagree,
@@ -11,14 +13,43 @@
11
13
  * never Accept as ink, even with no form here
12
14
  * a Needs you form resolves it → that form (Build again, Confirm it stopped)
13
15
  * saved notes or a failing PR → Revise / Draft a repair task
16
+ * a failed build → Retry (its note starts with what to change), then Run checks when they
17
+ * didn't run; never Accept: a failed build is not a result to accept
18
+ * a failed task's delivered result → Retry, then Accept anyway (in outline, with a reason) where it may be
19
+ * accepted; Run checks stays in its Checks row
14
20
  *
15
- * Accepting means what it always meant: the same forms post to the same
16
- * endpoints. This only decides which one is ink. */
21
+ * Accepting finishes the task in one request (Accept and finish): the person's
22
+ * acceptance, when one is owed, and the completion together. This only decides
23
+ * which act is ink; the page recomputes it as the person answers each check. */
17
24
  /** The words over the reason field an Accept of a refuted result requires. */
18
25
  export const ACCEPT_NEEDS_REASON = "Accepting needs a reason";
26
+ export const FAILED_CHECK = /^the repository's approved verification command exited (-?[0-9]+)/;
27
+ export function evidenceProblemOf(verdict, reasons) {
28
+ if (verdict === "verified" || verdict === "attested")
29
+ return "none";
30
+ if (verdict === "refuted")
31
+ return reasons.some(reason => FAILED_CHECK.test(reason)) ? "checks-failed" : "mismatched";
32
+ return "missing";
33
+ }
34
+ /** Why a result can't be accepted as it stands, in one plain line, or null
35
+ * when nothing refutes it (a person's acceptance settles it). The result page
36
+ * shows it before Request changes, its one ink act. */
37
+ export function cantAcceptYetOf(verdict, reasons, accepted) {
38
+ if (accepted || verdict !== "refuted")
39
+ return null;
40
+ // A mismatch is the status card's own headline; here only what accepting takes.
41
+ return evidenceProblemOf(verdict, reasons) === "checks-failed"
42
+ ? "Can't accept yet: the project's check failed on these changes."
43
+ : ACCEPT_NEEDS_REASON;
44
+ }
19
45
  export function resultActsOf(facts) {
20
46
  const request = facts.canRequest ? "request-changes" : null;
21
47
  const accept = facts.accept === null ? null : "accept";
48
+ if (facts.failed != null) {
49
+ const checks = facts.runChecks && !facts.checksRunning ? "run-checks" : facts.checksRunning ? "checks-running" : null;
50
+ const anyway = facts.failed.acceptAnyway === true ? "accept-anyway" : null;
51
+ return facts.failed.retry ? { primary: "retry", secondary: anyway ?? checks, line: null } : { primary: checks, secondary: anyway, line: null };
52
+ }
22
53
  if (facts.need !== null)
23
54
  return { primary: facts.need, secondary: request, line: null };
24
55
  if (facts.next !== null)
@@ -26,6 +57,11 @@ export function resultActsOf(facts) {
26
57
  // Can't be accepted yet: Accept is never the ink act, even when the feedback form isn't here.
27
58
  if (facts.blocked !== null)
28
59
  return { primary: request, secondary: accept, line: facts.blocked };
60
+ // The person's own checks come first: the next one is ink until each is answered.
61
+ if (accept !== null && (facts.unanswered ?? 0) > 0)
62
+ return { primary: "next-check", secondary: accept, line: null };
63
+ if (accept !== null && (facts.notRight ?? 0) > 0)
64
+ return { primary: request, secondary: accept, line: null };
29
65
  if (facts.checksRunning)
30
66
  return { primary: "checks-running", secondary: accept ?? request, line: null };
31
67
  if (facts.accept === null)
@@ -36,3 +72,17 @@ export function resultActsOf(facts) {
36
72
  return { primary: "run-checks", secondary: "accept", line: null };
37
73
  return { primary: "accept", secondary: request, line: null };
38
74
  }
75
+ export function acceptWithChecksOf(base, checks) {
76
+ const named = (all) => `“${all[0].replace(/[.!?]+$/, "")}”${all.length > 1 ? ` and ${all.length - 1} more` : ""}`;
77
+ if (checks.unanswered.length > 0)
78
+ return { label: "Accept without your check", ready: false, why: `Not checked yet: ${named(checks.unanswered)}.` };
79
+ if (checks.notRight.length > 0)
80
+ return { label: "Accept anyway", ready: false, why: `You marked ${named(checks.notRight)} not right.` };
81
+ return base;
82
+ }
83
+ /** The Requirements row as the person answers their checks: each Looks right is met, each answer leaves "You check". */
84
+ export function requirementsWordsOf(req, looked, notRight) {
85
+ const met = req.met + looked;
86
+ const yours = Math.max(0, req.yours - looked - notRight);
87
+ return { text: `${met} of ${req.total} met${yours > 0 ? ` · You check ${yours}` : ""}`, done: met === req.total, short: req.total - met - yours > 0 };
88
+ }
@@ -149,9 +149,10 @@ export declare const RESULT_FACT_KEYS: readonly ["run", "head", "base", "head-so
149
149
  * (place, lead, task, user) are not facts and are left out. */
150
150
  export declare function resultFactsFromHtml(html: string): Record<string, string>[];
151
151
  /** Where a result form may send its reader back: the review cockpit's
152
- * own deep link, the chat's result detail, or the run page. Anything else
153
- * lands on the run page. Matched exactly — never a bare open redirect. */
154
- export declare function resultReturnTarget(raw: string | null | undefined, runId: number): string;
152
+ * own deep link, the chat's result detail, the page of a task named in
153
+ * `tasks` (the run's own), or the run page. Anything else lands on the run
154
+ * page. Matched exactly — never a bare open redirect. */
155
+ export declare function resultReturnTarget(raw: string | null | undefined, runId: number, tasks?: readonly string[]): string;
155
156
  /** A form's own request token, minted per render — the same shape the
156
157
  * chat composer uses. A replayed submission carries the same token, so
157
158
  * the server can recognise it and mint nothing twice. */
@@ -149,9 +149,10 @@ export function resultFactsFromHtml(html) {
149
149
  return found;
150
150
  }
151
151
  /** Where a result form may send its reader back: the review cockpit's
152
- * own deep link, the chat's result detail, or the run page. Anything else
153
- * lands on the run page. Matched exactly — never a bare open redirect. */
154
- export function resultReturnTarget(raw, runId) {
152
+ * own deep link, the chat's result detail, the page of a task named in
153
+ * `tasks` (the run's own), or the run page. Anything else lands on the run
154
+ * page. Matched exactly — never a bare open redirect. */
155
+ export function resultReturnTarget(raw, runId, tasks = []) {
155
156
  if (raw !== null && raw !== undefined) {
156
157
  const review = /^\/review\?result=[A-Za-z0-9._~%-]{1,200}(?:&run=([0-9]{1,15}))?(?:&tab=(?:summary|changes|checks))?$/.exec(raw);
157
158
  if (review !== null && (review[1] === undefined || Number(review[1]) === runId))
@@ -159,6 +160,9 @@ export function resultReturnTarget(raw, runId) {
159
160
  const chat = /^\/chat\?task=([A-Za-z0-9._~%-]{1,200})&result=([0-9]{1,15})(?:&conversation=[A-Za-z0-9-]{1,128})?$/.exec(raw);
160
161
  if (chat !== null && Number(chat[2]) === runId)
161
162
  return raw;
163
+ // The run's own task page, which offered Run checks in place; never another task's.
164
+ if (tasks.some(task => raw === `/t/${encodeURIComponent(task)}`))
165
+ return raw;
162
166
  }
163
167
  return `/r/${runId}`;
164
168
  }
package/dist/retention.js CHANGED
@@ -263,11 +263,17 @@ export function sweepRetention(store, evidenceRoot, now, actor = "worker") {
263
263
  if (periods.chat !== null) {
264
264
  const team = db.prepare("DELETE FROM team_message WHERE message = ? AND status NOT IN ('queued', 'running', 'uncertain')");
265
265
  const message = db.prepare("DELETE FROM mate_message WHERE id = ? AND NOT EXISTS (SELECT 1 FROM team_message WHERE message = ?)");
266
+ const asked = db.prepare("SELECT turn FROM mate_message WHERE id = ? AND role = 'assistant' AND turn IS NOT NULL");
267
+ // The lead's question goes with the reply that asked it; its buttons then say it expired.
268
+ const ask = db.prepare("DELETE FROM mate_ask WHERE turn = ? AND NOT EXISTS (SELECT 1 FROM mate_message WHERE turn = ? AND role = 'assistant')");
266
269
  for (const one of plan.items.chat) {
267
270
  team.run(one.id);
271
+ const turn = asked.get(one.id)?.["turn"];
268
272
  if (Number(message.run(one.id, one.id).changes) === 1) {
269
273
  done.chat.count++;
270
274
  done.chat.bytes += one.bytes;
275
+ if (turn !== undefined && turn !== null)
276
+ ask.run(Number(turn), Number(turn));
271
277
  }
272
278
  }
273
279
  }
package/dist/serve.d.ts CHANGED
@@ -208,6 +208,9 @@ export declare function createDecisionServer(options: ServeOptions): DecisionSer
208
208
  * text, never to a 500. The scheme is a constant, never data.
209
209
  */
210
210
  export declare function editorFileHref(worktree: string, path: string, line?: number | null): string | null;
211
+ /** Earlier versions of a task still in flight, in plain words: queued
212
+ * while they only wait, running once one has started; null when none. */
213
+ export declare function earlierVersionsWords(active: number, running: number): string | null;
211
214
  /** The Agents view every console surface renders (v47): the SAME
212
215
  * projection the CLI prints — who plans, builds, and revises,
213
216
  * whether each was recommended, overridden, or pinned, its reasons, and