toolroll 0.9.13 → 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.
- package/CHANGELOG.md +23 -0
- package/dist/assignment-ui.d.ts +1 -0
- package/dist/assignment-ui.js +8 -5
- package/dist/assignment.d.ts +32 -0
- package/dist/assignment.js +81 -7
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +38 -38
- package/dist/browser-crew.d.ts +2 -1
- package/dist/browser-crew.js +4 -2
- package/dist/browser-workspace.d.ts +109 -11
- package/dist/chat-actions.d.ts +1 -1
- package/dist/chat-actions.js +6 -4
- package/dist/chat-delivery-state.d.ts +1 -1
- package/dist/chat-voice.d.ts +3 -3
- package/dist/chat-voice.js +3 -3
- package/dist/demo.js +82 -2
- package/dist/disclosure.d.ts +6 -0
- package/dist/disclosure.js +13 -0
- package/dist/dispatch.js +7 -2
- package/dist/lead-status.js +2 -2
- package/dist/needs-you.d.ts +72 -0
- package/dist/needs-you.js +193 -1
- package/dist/result-acts.d.ts +51 -5
- package/dist/result-acts.js +36 -3
- package/dist/result-review.d.ts +4 -3
- package/dist/result-review.js +7 -3
- package/dist/serve.d.ts +3 -0
- package/dist/serve.js +1235 -711
- package/dist/slack-chat.js +1 -1
- package/dist/task-status.d.ts +24 -3
- package/dist/task-status.js +48 -17
- package/dist/telegram-progress.js +2 -2
- package/dist/test-temp.js +1 -1
- package/dist/transitions-recipes.d.ts +1 -1
- package/dist/transitions-recipes.js +5 -9
- package/dist/when-html.d.ts +19 -2
- package/dist/when-html.js +78 -11
- package/dist/work-index.d.ts +3 -1
- package/dist/work-index.js +18 -5
- package/dist/workspace-motion.js +3 -3
- package/dist/workspace-ui.d.ts +86 -6
- package/dist/workspace-ui.js +143 -17
- package/package.json +1 -1
package/dist/workspace-ui.d.ts
CHANGED
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
* claimed without one.
|
|
18
18
|
*/
|
|
19
19
|
import type { DispatchDiagnosis } from "./dispatch.js";
|
|
20
|
+
import { ACCEPT_NEEDS_REASON, type AcceptLabel } from "./result-acts.js";
|
|
21
|
+
export { ACCEPT_NEEDS_REASON };
|
|
20
22
|
import { type ProofVerdict } from "./proof.js";
|
|
21
23
|
import type { ReviewRetryState, TaskState } from "./store.js";
|
|
22
24
|
import type { TaskControlView } from "./task-control.js";
|
|
@@ -194,18 +196,60 @@ export declare function dispatchActionLabel(dispatch: DispatchDiagnosis | null):
|
|
|
194
196
|
* when nothing refutes it (a person's acceptance settles it). The result page
|
|
195
197
|
* shows it before Request changes, its one ink act. */
|
|
196
198
|
export declare function cantAcceptYetOf(verdict: ProofVerdict | null, reasons: readonly string[], accepted: boolean): string | null;
|
|
199
|
+
/** The result's headline while its report disagrees with its saved changes. */
|
|
200
|
+
export declare const MISMATCH_HEADLINE = "The report doesn't match the changes";
|
|
201
|
+
/** One way the report disagrees with the saved changes, in plain words: the
|
|
202
|
+
* file and lines it concerns when they are in the saved changes, or
|
|
203
|
+
* `inChanges: false` for a file the report names that the changes don't have.
|
|
204
|
+
* `reason` is the recorded words it says plainly. */
|
|
205
|
+
export type ReportMismatch = {
|
|
206
|
+
text: string;
|
|
207
|
+
path: string | null;
|
|
208
|
+
lines: {
|
|
209
|
+
from: number;
|
|
210
|
+
to: number;
|
|
211
|
+
} | null;
|
|
212
|
+
inChanges: boolean | null;
|
|
213
|
+
/** The report's own note (1-based) the disagreement is about, when it names one. */
|
|
214
|
+
note: number | null;
|
|
215
|
+
reason: string;
|
|
216
|
+
};
|
|
217
|
+
/** Each recorded way a refuted report disagrees with its saved changes, said
|
|
218
|
+
* plainly and tied to the changed lines it concerns. `changes` maps each
|
|
219
|
+
* changed file to the lines of its first change (null when it has no line
|
|
220
|
+
* changes, a binary file say). Presentation only: the verdict is unchanged. */
|
|
221
|
+
export declare function reportMismatchesOf(reasons: readonly string[], criteria: readonly {
|
|
222
|
+
id: string;
|
|
223
|
+
statement: string;
|
|
224
|
+
answered: readonly {
|
|
225
|
+
kind: string;
|
|
226
|
+
ref: string;
|
|
227
|
+
}[];
|
|
228
|
+
state?: string;
|
|
229
|
+
detail?: readonly string[];
|
|
230
|
+
}[], changes: ReadonlyMap<string, {
|
|
231
|
+
from: number;
|
|
232
|
+
to: number;
|
|
233
|
+
} | null>): ReportMismatch[];
|
|
234
|
+
/** The line a failing check ended on: the last line of its error output that
|
|
235
|
+
* names an error, else the last line it printed, with its 1-based number in
|
|
236
|
+
* the saved log. Null when the log printed nothing. */
|
|
237
|
+
export declare function lastErrorLineOf(log: string): {
|
|
238
|
+
line: number;
|
|
239
|
+
text: string;
|
|
240
|
+
} | null;
|
|
197
241
|
/** The result page's own sentence when the decision is on it. */
|
|
198
242
|
export declare const RESULT_DECISION_SENTENCE = "Review the change, then accept it or ask for changes.";
|
|
199
243
|
/** What the result page's Accept says, from facts it already shows: the
|
|
200
|
-
* recorded checks, the requirements not met, the
|
|
201
|
-
*
|
|
202
|
-
* checks passed; otherwise "Accept without checks" and one line naming
|
|
244
|
+
* recorded checks, the requirements not met, whether the saved proof reads,
|
|
245
|
+
* and what the button posts. "Accept and finish" only when everything is met
|
|
246
|
+
* and the checks passed; otherwise "Accept without checks" and one line naming
|
|
247
|
+
* what. The person's own checks are the page's to add (acceptWithChecksOf).
|
|
203
248
|
* `effect` says what pressing it does, in the action's own terms. */
|
|
204
249
|
export type AcceptFacts = {
|
|
205
250
|
checks: "passed" | "failed" | "not-run" | "off" | "running" | "unavailable" | null;
|
|
206
251
|
unmet: number;
|
|
207
|
-
|
|
208
|
-
/** `complete`: marks the exact result complete; `accept`: records a person's acceptance only. */
|
|
252
|
+
/** `complete`: accepts and finishes the exact result in one request; `accept`: records a person's acceptance only (nothing here can finish it). */
|
|
209
253
|
action: "complete" | "accept";
|
|
210
254
|
/** `pull-request`: one can be opened from the task after; `off`: publishing isn't set up; `other`: neither is said. Accept itself never publishes. */
|
|
211
255
|
publishing: "pull-request" | "off" | "other";
|
|
@@ -213,7 +257,7 @@ export type AcceptFacts = {
|
|
|
213
257
|
proof: boolean;
|
|
214
258
|
};
|
|
215
259
|
export type AcceptWords = {
|
|
216
|
-
label:
|
|
260
|
+
label: AcceptLabel;
|
|
217
261
|
ready: boolean;
|
|
218
262
|
why: string | null;
|
|
219
263
|
effect: string;
|
|
@@ -243,3 +287,39 @@ export type PrimaryDestination = "chat" | "work" | "projects" | "flows" | "setti
|
|
|
243
287
|
export declare function primaryDestinationOf(active: string): PrimaryDestination;
|
|
244
288
|
/** Learning stays in Settings and a useful, closed result disclosure. */
|
|
245
289
|
export declare function learningHtml(view: import('./project-learning.js').LearningView, csrf: string, canManage: boolean, source?: number): string;
|
|
290
|
+
/** The first signed requirement a finished attempt missed (failed or unanswered; a person's own check is not a miss),
|
|
291
|
+
* with the evidence line behind it in plain words. Null when every requirement held. */
|
|
292
|
+
export declare function missedRequirementOf(matrix: readonly {
|
|
293
|
+
id: string;
|
|
294
|
+
statement: string;
|
|
295
|
+
state: string;
|
|
296
|
+
detail: readonly string[];
|
|
297
|
+
}[]): {
|
|
298
|
+
id: string;
|
|
299
|
+
statement: string;
|
|
300
|
+
evidence: string | null;
|
|
301
|
+
} | null;
|
|
302
|
+
/** One milestone of a live build's plan, as the agent last reported it; its words are null where only the recorded
|
|
303
|
+
* progress was read (a list row reads no plan file). */
|
|
304
|
+
export type BuildStep = {
|
|
305
|
+
description: string | null;
|
|
306
|
+
state: "pending" | "current" | "completed" | "blocked";
|
|
307
|
+
note: string | null;
|
|
308
|
+
};
|
|
309
|
+
/** 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
|
|
310
|
+
* its place. `stuck` says which step and why; null while nothing is blocked. */
|
|
311
|
+
export type BuildProgress = {
|
|
312
|
+
step: number;
|
|
313
|
+
total: number;
|
|
314
|
+
line: string;
|
|
315
|
+
stuck: {
|
|
316
|
+
step: number;
|
|
317
|
+
why: string | null;
|
|
318
|
+
line: string;
|
|
319
|
+
} | null;
|
|
320
|
+
};
|
|
321
|
+
/** A live build's progress: the step in progress (else the first one not done) as "Step N of M: <step>"; a blocked
|
|
322
|
+
* step replaces it as "Stuck on step N of M: <why>" (its note, else the step itself). Null with no plan steps. */
|
|
323
|
+
export declare function buildProgressOf(steps: readonly BuildStep[] | null | undefined): BuildProgress | null;
|
|
324
|
+
/** Earlier attempts that stopped before the one now running, in one quiet line. */
|
|
325
|
+
export declare function earlierAttemptsWords(count: number): string;
|
package/dist/workspace-ui.js
CHANGED
|
@@ -16,8 +16,10 @@
|
|
|
16
16
|
* observed" name observed publication records, and deployment is never
|
|
17
17
|
* claimed without one.
|
|
18
18
|
*/
|
|
19
|
-
import {
|
|
20
|
-
|
|
19
|
+
import { ACCEPT_NEEDS_REASON } from "./result-acts.js";
|
|
20
|
+
export { ACCEPT_NEEDS_REASON };
|
|
21
|
+
import { GOAL_ASSESSMENT_PENDING, manualReviewOnly, plainReasonWords } from "./proof.js";
|
|
22
|
+
import { OPEN_RESULT, plainReasonOf, stageOfCode, taskStatusOf, workToneOf } from "./task-status.js";
|
|
21
23
|
export const WORK_VIEWS = [
|
|
22
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." },
|
|
23
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." },
|
|
@@ -65,7 +67,7 @@ export function reviewStatusOf(review) {
|
|
|
65
67
|
// it would unreviewed, marked "not reviewed" by the result itself.
|
|
66
68
|
if (review === null || review.automaticPending !== true || (review.state !== "queued" && review.state !== "running"))
|
|
67
69
|
return null;
|
|
68
|
-
return { token: "reviewing", label: "Reviewing", detail: "An automatic review is reading this result before it reaches you.", tone: "live", action: { label:
|
|
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" } };
|
|
69
71
|
}
|
|
70
72
|
const FAILED_CHECK = /^the repository's approved verification command exited (-?[0-9]+)/;
|
|
71
73
|
export function evidenceProblemOf(verdict, reasons) {
|
|
@@ -191,11 +193,11 @@ function storedResultStatusOf(result, publication) {
|
|
|
191
193
|
// weighed, so it says nothing about whether that check passed.
|
|
192
194
|
detail: withPublication("What the agent reported doesn't match the changes it saved."),
|
|
193
195
|
tone: "problem",
|
|
194
|
-
action: { label:
|
|
196
|
+
action: { label: OPEN_RESULT, kind: "open-review" },
|
|
195
197
|
};
|
|
196
198
|
}
|
|
197
199
|
if (result.verdict === "short" && humanReview) {
|
|
198
|
-
return { token: "verification-needed", label: "Ready to inspect", detail: withPublication("The remaining requirements need a person's inspection."), tone: "attention", action: { label:
|
|
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" } };
|
|
199
201
|
}
|
|
200
202
|
if (result.verdict === "short") {
|
|
201
203
|
return {
|
|
@@ -212,7 +214,7 @@ function storedResultStatusOf(result, publication) {
|
|
|
212
214
|
// presence facts here.
|
|
213
215
|
return result.recordComplete === false && result.verdict === null
|
|
214
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" } }
|
|
215
|
-
: { 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:
|
|
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" } };
|
|
216
218
|
}
|
|
217
219
|
if (result.verdict === null) {
|
|
218
220
|
return {
|
|
@@ -229,7 +231,7 @@ function storedResultStatusOf(result, publication) {
|
|
|
229
231
|
label: "Result saved — checks reported by the agent",
|
|
230
232
|
detail: withPublication("No independent project check ran; the checks listed are the agent's own report."),
|
|
231
233
|
tone: "neutral",
|
|
232
|
-
action: { label:
|
|
234
|
+
action: { label: OPEN_RESULT, kind: "open-result" },
|
|
233
235
|
};
|
|
234
236
|
}
|
|
235
237
|
if (published !== null && (published.token === "merge-observed" || published.token === "pr-opened" || published.token === "pr-closed")) {
|
|
@@ -238,7 +240,7 @@ function storedResultStatusOf(result, publication) {
|
|
|
238
240
|
label: published.label,
|
|
239
241
|
detail: `The approved check passed against this result. ${published.detail}`,
|
|
240
242
|
tone: published.token === "pr-closed" ? "muted" : "done",
|
|
241
|
-
action: published.token === "merge-observed" ? { label:
|
|
243
|
+
action: published.token === "merge-observed" ? { label: OPEN_RESULT, kind: "open-result" } : { label: "Open the pull request", kind: "open-pr" },
|
|
242
244
|
};
|
|
243
245
|
}
|
|
244
246
|
return {
|
|
@@ -246,7 +248,7 @@ function storedResultStatusOf(result, publication) {
|
|
|
246
248
|
label: "Ready",
|
|
247
249
|
detail: withPublication("The approved check passed against this result."),
|
|
248
250
|
tone: "ready",
|
|
249
|
-
action: { label:
|
|
251
|
+
action: { label: OPEN_RESULT, kind: "open-result" },
|
|
250
252
|
};
|
|
251
253
|
}
|
|
252
254
|
/** The completion receipt's heading: what the record actually supports.
|
|
@@ -286,7 +288,7 @@ export function needsPerson(dispatch) {
|
|
|
286
288
|
/** Navigation labels describe the available help, not a mutation: opening
|
|
287
289
|
* a hold or pause must never promise that the task has already resumed. */
|
|
288
290
|
const DISPATCH_ACTION_LABELS = {
|
|
289
|
-
"open-result":
|
|
291
|
+
"open-result": OPEN_RESULT,
|
|
290
292
|
"retry-task": "Review and retry",
|
|
291
293
|
"place-task": "Choose a project",
|
|
292
294
|
"write-scope": "Define the task",
|
|
@@ -318,9 +320,89 @@ const VERIFIED_RESULT = new Set(["ready-to-review", "pr-opened", "merge-observed
|
|
|
318
320
|
export function cantAcceptYetOf(verdict, reasons, accepted) {
|
|
319
321
|
if (accepted || verdict !== "refuted")
|
|
320
322
|
return null;
|
|
323
|
+
// A mismatch is the status card's own headline; here only what accepting takes.
|
|
321
324
|
return evidenceProblemOf(verdict, reasons) === "checks-failed"
|
|
322
325
|
? "Can't accept yet: the project's check failed on these changes."
|
|
323
|
-
:
|
|
326
|
+
: ACCEPT_NEEDS_REASON;
|
|
327
|
+
}
|
|
328
|
+
/** The result's headline while its report disagrees with its saved changes. */
|
|
329
|
+
export const MISMATCH_HEADLINE = "The report doesn't match the changes";
|
|
330
|
+
const OVERCLAIMED = /^claimed changed paths? not in the sealed diff: ([\s\S]+)$/;
|
|
331
|
+
const RESTATED = /^criterion "([^"]+)" was signed as "[\s\S]*" and the proof restates it as "([\s\S]*)"$/;
|
|
332
|
+
const CONTRADICTED_BY_CAVEAT = /^criterion "([^"]+)" is marked met, but caveat [0-9]+ admits an exception to it: ([\s\S]*)$/;
|
|
333
|
+
const NOTE_NUMBER = /^caveat ([0-9]+) /;
|
|
334
|
+
const CONTRADICTED_BY_REVIEW = /^(?:reviewer:)?[^\s"]+ contradicts criterion "([^"]+)": [\s\S]*$/i;
|
|
335
|
+
/** Each recorded way a refuted report disagrees with its saved changes, said
|
|
336
|
+
* plainly and tied to the changed lines it concerns. `changes` maps each
|
|
337
|
+
* changed file to the lines of its first change (null when it has no line
|
|
338
|
+
* changes, a binary file say). Presentation only: the verdict is unchanged. */
|
|
339
|
+
export function reportMismatchesOf(reasons, criteria, changes) {
|
|
340
|
+
const named = (id) => {
|
|
341
|
+
const statement = criteria.find(one => one.id === id)?.statement.trim() ?? "";
|
|
342
|
+
if (statement === "")
|
|
343
|
+
return `requirement ${id}`;
|
|
344
|
+
return `“${statement.length > 90 ? `${statement.slice(0, 90).replace(/\s+\S*$/, "")}…` : statement}”`;
|
|
345
|
+
};
|
|
346
|
+
// The first file the requirement cites that the saved changes have.
|
|
347
|
+
const place = (id) => {
|
|
348
|
+
const path = criteria.find(one => one.id === id)?.answered.find(ref => ref.kind === "changed-path" && changes.has(ref.ref))?.ref ?? null;
|
|
349
|
+
return path === null ? { path: null, lines: null, inChanges: null } : { path, lines: changes.get(path) ?? null, inChanges: true };
|
|
350
|
+
};
|
|
351
|
+
const rows = reasons.flatMap((reason) => {
|
|
352
|
+
const over = OVERCLAIMED.exec(reason);
|
|
353
|
+
if (over !== null)
|
|
354
|
+
return over[1].split(", ").map(path => ({ text: "The report says it changed", path, lines: null, inChanges: changes.has(path), note: null, reason }));
|
|
355
|
+
const restated = RESTATED.exec(reason);
|
|
356
|
+
if (restated !== null)
|
|
357
|
+
return [{ text: `The report rewords ${named(restated[1])} as “${restated[2]}”`, ...place(restated[1]), note: null, reason }];
|
|
358
|
+
const caveat = CONTRADICTED_BY_CAVEAT.exec(reason);
|
|
359
|
+
const note = /^criterion "[^"]+" is marked met, but caveat ([0-9]+) /.exec(reason)?.[1] ?? NOTE_NUMBER.exec(reason)?.[1] ?? null;
|
|
360
|
+
if (caveat !== null)
|
|
361
|
+
return [{ text: `The report marks ${named(caveat[1])} met, but its own note says: ${caveat[2]}`, ...place(caveat[1]), note: Number(note), reason }];
|
|
362
|
+
const review = CONTRADICTED_BY_REVIEW.exec(reason);
|
|
363
|
+
if (review !== null)
|
|
364
|
+
return [{ text: plainReasonWords(reason), ...place(review[1]), note: null, reason }];
|
|
365
|
+
const unsigned = UNSIGNED_NOTE.exec(reason);
|
|
366
|
+
if (unsigned !== null)
|
|
367
|
+
return [{ text: `The report's note ${unsigned[1]} is about ${unsigned[2]}, which isn't one of the signed requirements: ${unsigned[3]}`, path: null, lines: null, inChanges: null, note: Number(unsigned[1]), reason }];
|
|
368
|
+
if (FAILED_CHECK.test(reason))
|
|
369
|
+
return [];
|
|
370
|
+
return [{ text: sentenceOf(plainReasonWords(reason)), path: null, lines: null, inChanges: null, note: note === null ? null : Number(note), reason }];
|
|
371
|
+
});
|
|
372
|
+
// Evidence a requirement cites that doesn't hold, unless a reason above already says so.
|
|
373
|
+
const evidence = criteria.flatMap(one => one.state !== "failed" && one.state !== "missing" ? [] : (one.detail ?? [])
|
|
374
|
+
.filter(detail => !reasons.includes(detail) && !/is waiting for the final check|requires manual-review evidence|is marked met, but caveat|was signed as/.test(detail))
|
|
375
|
+
.map((detail) => {
|
|
376
|
+
const lead = [`criterion "${one.id}"'s `, `criterion "${one.id}" `].find(prefix => detail.startsWith(prefix));
|
|
377
|
+
const rest = lead === undefined ? detail : detail.slice(lead.length);
|
|
378
|
+
return { text: `${sentenceOf(named(one.id))}: ${rest}`, ...place(one.id), note: null, reason: detail };
|
|
379
|
+
}));
|
|
380
|
+
return [...rows, ...evidence];
|
|
381
|
+
}
|
|
382
|
+
const UNSIGNED_NOTE = /^caveat ([0-9]+) names ("[^"]*"(?:, "[^"]*")*), (?:a criterion the proof authored|which is no signed)[\s\S]*?: ([\s\S]*)$/;
|
|
383
|
+
/** A sentence starts with a capital letter. */
|
|
384
|
+
function sentenceOf(text) {
|
|
385
|
+
return text === "" ? text : `${text.charAt(0).toUpperCase()}${text.slice(1)}`;
|
|
386
|
+
}
|
|
387
|
+
/** The line a failing check ended on: the last line of its error output that
|
|
388
|
+
* names an error, else the last line it printed, with its 1-based number in
|
|
389
|
+
* the saved log. Null when the log printed nothing. */
|
|
390
|
+
export function lastErrorLineOf(log) {
|
|
391
|
+
const lines = log.split("\n");
|
|
392
|
+
const printed = (text) => text.trim() !== "" && !/^\$ /.test(text) && !/^\(exit -?[0-9]+\)$/.test(text.trim()) && !/^--- (stdout|stderr) ---$/.test(text.trim());
|
|
393
|
+
const stderr = lines.findIndex(text => text.trim() === "--- stderr ---");
|
|
394
|
+
const pick = (from, to) => {
|
|
395
|
+
let last = null;
|
|
396
|
+
for (let index = to - 1; index >= from; index -= 1) {
|
|
397
|
+
if (!printed(lines[index]))
|
|
398
|
+
continue;
|
|
399
|
+
if (/error|fail|✗|✕|×|assert|exception|panic|cannot|not found/i.test(lines[index]))
|
|
400
|
+
return { line: index + 1, text: lines[index].trim() };
|
|
401
|
+
last ??= index;
|
|
402
|
+
}
|
|
403
|
+
return last === null ? null : { line: last + 1, text: lines[last].trim() };
|
|
404
|
+
};
|
|
405
|
+
return (stderr >= 0 ? pick(stderr + 1, lines.length) : null) ?? pick(0, stderr >= 0 ? stderr : lines.length);
|
|
324
406
|
}
|
|
325
407
|
/** The result page's own sentence when the decision is on it. */
|
|
326
408
|
export const RESULT_DECISION_SENTENCE = "Review the change, then accept it or ask for changes.";
|
|
@@ -331,15 +413,14 @@ export function acceptWordsOf(facts) {
|
|
|
331
413
|
...(facts.checks === "passed" ? [] : [facts.checks === "failed" ? "checks failed" : facts.checks === "off" ? "checks are off for this project"
|
|
332
414
|
: facts.checks === "running" ? "checks are still running" : facts.checks === "unavailable" ? "saved checks can't be read" : "checks didn't run"]),
|
|
333
415
|
...(facts.unmet > 0 ? [`${plural(facts.unmet, "requirement isn't", "requirements aren't")} met`] : []),
|
|
334
|
-
...(facts.yours > 0 ? [`${plural(facts.yours, "item still needs", "items still need")} your check`] : []),
|
|
335
416
|
];
|
|
336
417
|
const ready = parts.length === 0;
|
|
337
418
|
const joined = parts.length <= 1 ? parts.join("") : `${parts.slice(0, -1).join(", ")} and ${parts.at(-1)}`;
|
|
338
|
-
const effect = facts.action === "accept" ? "Records that you accept it.
|
|
339
|
-
: facts.publishing === "pull-request" ? "
|
|
340
|
-
: facts.publishing === "off" ? "
|
|
341
|
-
: "
|
|
342
|
-
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 };
|
|
343
424
|
}
|
|
344
425
|
export function resultHeadlineOf(status) {
|
|
345
426
|
const read = RESULT_NEEDS_YOU.has(status.token) ? { stage: "needs-you", need: "other" }
|
|
@@ -470,3 +551,48 @@ export function learningHtml(view, csrf, canManage, source) {
|
|
|
470
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('');
|
|
471
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>`;
|
|
472
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.
|
|
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"
|