toolroll 0.9.12 → 0.9.14
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 +14 -0
- package/dist/assignment-ui.js +3 -2
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +25 -25
- package/dist/browser-crew.js +3 -2
- package/dist/browser-workspace.d.ts +46 -0
- package/dist/dispatch.js +7 -2
- package/dist/needs-you.d.ts +24 -0
- package/dist/needs-you.js +100 -0
- package/dist/result-acts.d.ts +46 -0
- package/dist/result-acts.js +38 -0
- package/dist/result-review.js +1 -1
- package/dist/serve.d.ts +1 -0
- package/dist/serve.js +202 -75
- package/dist/task-status.d.ts +4 -0
- package/dist/task-status.js +8 -2
- package/dist/telegram-progress.js +2 -2
- package/dist/test-temp.js +1 -1
- package/dist/work-index.js +6 -3
- package/dist/workspace-ui.d.ts +48 -0
- package/dist/workspace-ui.js +95 -3
- package/package.json +1 -1
package/dist/task-status.js
CHANGED
|
@@ -114,7 +114,9 @@ function detailsOf(headline, facts) {
|
|
|
114
114
|
row("pull-request", "Pull request", `${name} open`, "none", { href: github });
|
|
115
115
|
}
|
|
116
116
|
const req = facts.requirements;
|
|
117
|
-
if (req != null && req.total > 0)
|
|
117
|
+
if (req != null && req.total > 0 && req.unverified === true)
|
|
118
|
+
row("requirements", "Requirements", "Unverified", "none");
|
|
119
|
+
else if (req != null && req.total > 0) {
|
|
118
120
|
const unmet = req.total - req.met - req.yours;
|
|
119
121
|
const text = `${req.met} of ${req.total} met${req.yours > 0 ? ` · You check ${req.yours}` : ""}`;
|
|
120
122
|
row("requirements", "Requirements", text, unmet > 0 ? problem(headline) : req.met === req.total ? "ok" : "none", unmet > 0 && headline !== "Failed" ? { action: { label: "See which", href: facts.links?.checks ?? facts.links?.result ?? null } } : {});
|
|
@@ -264,6 +266,10 @@ export function requirementsOf(matrix) {
|
|
|
264
266
|
const met = (row) => row.state === "pass" || (row.assessment?.evidenceState === "pass" && (row.review ?? null) === null);
|
|
265
267
|
return { met: matrix.filter(met).length, total: matrix.length, yours: matrix.filter(row => !met(row) && row.state === "manual-review").length };
|
|
266
268
|
}
|
|
269
|
+
/** A refuted report verifies none of its requirements, whatever it marked met (a person's acceptance doesn't change that). */
|
|
270
|
+
export function unverifiedWhenRefuted(requirements, verdict) {
|
|
271
|
+
return requirements === null || verdict !== "refuted" ? requirements : { ...requirements, unverified: true };
|
|
272
|
+
}
|
|
267
273
|
/** Saved evidence health from the receipt's artifact list and its caveats. */
|
|
268
274
|
export function evidenceOf(receipt, problems = receipt?.caveats ?? []) {
|
|
269
275
|
if (receipt === null)
|
|
@@ -297,7 +303,7 @@ export function assignmentStatusFacts(assignment, options = {}) {
|
|
|
297
303
|
stage, ...(need === undefined ? {} : { need }), ...(wait === undefined ? {} : { wait }), ...(build === null ? {} : { needContext: { build } }), reason, report,
|
|
298
304
|
checks,
|
|
299
305
|
pullRequest: withResult ? pullRequest : null,
|
|
300
|
-
requirements: withResult ? requirementsOf(receipt?.proof?.matrix) : null,
|
|
306
|
+
requirements: withResult ? unverifiedWhenRefuted(requirementsOf(receipt?.proof?.matrix), receipt?.proof?.verdict) : null,
|
|
301
307
|
evidence: withResult ? options.evidence ?? evidenceOf(receipt, [...(receipt?.caveats ?? []), ...assignment.attention]) : null,
|
|
302
308
|
completedBy: assignment.completion === null ? null : assignment.completion.lead === true ? "the lead" : assignment.completion.actor.replace(/^(?:operator|coordinator|lead):/, ""),
|
|
303
309
|
action: options.action ?? null,
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { assignmentOf } from "./assignment.js";
|
|
8
8
|
import { chatResultHref, chatControlHref } from "./chat-controls.js";
|
|
9
9
|
import { phoneText, projectLabel } from "./telegram-status.js";
|
|
10
|
-
import { assignmentStatusFacts, headlineEmoji, pullRequestFactOf, requirementsOf, statusDetailLines, taskStatusOf } from "./task-status.js";
|
|
10
|
+
import { assignmentStatusFacts, headlineEmoji, pullRequestFactOf, requirementsOf, statusDetailLines, unverifiedWhenRefuted, taskStatusOf } from "./task-status.js";
|
|
11
11
|
import { leadClaimOf } from "./lead-voice.js";
|
|
12
12
|
import { chatTitle } from "./chat-voice.js";
|
|
13
13
|
import { manualReviewOnly } from "./proof.js";
|
|
@@ -49,7 +49,7 @@ export function telegramProgressCard(store, run, taskId, project, now = new Date
|
|
|
49
49
|
: exit !== null ? { status: "failed", exitCode: exit, head: run.headRevision }
|
|
50
50
|
: machine === "verified" ? { status: "passed", exitCode: null, head: run.headRevision }
|
|
51
51
|
: machine === "attested" ? { status: "not-run", exitCode: null, head: null } : null;
|
|
52
|
-
facts = { stage: "finished", report: run.role === "scout", checks, requirements: requirementsOf(proof?.matrix),
|
|
52
|
+
facts = { stage: "finished", report: run.role === "scout", checks, requirements: unverifiedWhenRefuted(requirementsOf(proof?.matrix), proof?.verdict),
|
|
53
53
|
...(pullRequest === undefined ? {} : { pullRequest }) };
|
|
54
54
|
}
|
|
55
55
|
else if (accessBlocked) {
|
package/dist/test-temp.js
CHANGED
|
@@ -18,7 +18,7 @@ export const TEST_TEMP_PREFIXES = [
|
|
|
18
18
|
"flow-goal-", "flow-scratch", "flows-chat-", "invoke-attest-", "knowledge-test-", "learning-", "peek-", "phone-origin-",
|
|
19
19
|
"proposal-repo-", "race-", "refresh-migration-", "refresh-refusal-", "release-check-", "shared-chat-", "skills-test-",
|
|
20
20
|
"task-status-surfaces-", "template-repo-", "template-test-", "v14-migrate-", "v17-migrate-", "watch-anchor-",
|
|
21
|
-
"needs-you-", "deps-lock-", "deps-state-", "upgrade-fresh-", "upgrade-path-", "deploy-restore-", "result-review-",
|
|
21
|
+
"needs-you-", "deps-lock-", "deps-state-", "upgrade-fresh-", "upgrade-path-", "deploy-restore-", "result-review-", "one-result-page-",
|
|
22
22
|
// Playwright's browser profiles and artifacts.
|
|
23
23
|
"playwright_chromiumdev_profile-", "playwright_firefoxdev_profile-", "playwright-artifacts-",
|
|
24
24
|
];
|
package/dist/work-index.js
CHANGED
|
@@ -11,7 +11,7 @@ import { plainReasonOf, replacedWords, stageOfCode, taskStatusOf, workToneOf } f
|
|
|
11
11
|
import { isCheckLevel } from './check-levels.js';
|
|
12
12
|
import { MARKER } from './worktree.js';
|
|
13
13
|
import { withFollowUps } from './result-follow-ups.js';
|
|
14
|
-
import { ASKS, NEED_ASK, NEEDS, processNeedOf } from './needs-you.js';
|
|
14
|
+
import { ASKS, NEED_ASK, NEEDS, failedAttemptSentence, processNeedOf } from './needs-you.js';
|
|
15
15
|
import { LEAD_CLAIM_SQL, LEAD_IDLE_MS } from './lead-voice.js';
|
|
16
16
|
export const WORK_INDEX_PAGE_LIMIT = 40;
|
|
17
17
|
export const WORK_INDEX_MAX_LIMIT = 100;
|
|
@@ -369,7 +369,7 @@ export function workIndexPage(store, now, access, options = {}) {
|
|
|
369
369
|
'running_count',running_count,'completed_count',completed_count,'queued',queued,'done_recently',done_recently)) FROM project_totals) row_json FROM totals
|
|
370
370
|
UNION ALL SELECT 1,0,0,0,0,0,0,0,0,0,json_object('root_ref',root_ref,'root_id',root_id,'id',id,'root_title',root_title,'repo',repo,
|
|
371
371
|
'state',state,'created_at',created_at,'family_updated',family_updated,'version_count',version_count,'earlier_active',earlier_active,
|
|
372
|
-
'earlier_id',earlier_id,'broken',broken,'live_run',live_run,'result_id',page_result_id,'result_outcome',(SELECT outcome FROM run WHERE id=page_result_id),
|
|
372
|
+
'earlier_id',earlier_id,'broken',broken,'live_run',live_run,'result_id',page_result_id,'result_outcome',(SELECT outcome FROM run WHERE id=page_result_id),'attempt_reason',(SELECT substr(reason,1,1000) FROM run WHERE task_ref IN (SELECT ref_id FROM members WHERE root_ref=page.root_ref) AND (finished_at IS NOT NULL OR outcome IS NOT NULL) AND role NOT IN ('planner','reviewer') ORDER BY id DESC LIMIT 1),
|
|
373
373
|
'publication_url',(SELECT pr_url FROM publication WHERE run=page_result_id),
|
|
374
374
|
'result_role',(SELECT role FROM run WHERE id=page_result_id),'live_role',(SELECT role FROM run WHERE id=page.live_run),
|
|
375
375
|
'check_status',(SELECT status FROM run_check WHERE run=page_result_id),
|
|
@@ -462,7 +462,7 @@ function itemOf(row, principal, probe) {
|
|
|
462
462
|
'decision-queue': ['Needs your decision', 'The decision queue is full.', 'inspect-decisions', 'Review open questions'],
|
|
463
463
|
'result-needs-attention': ['Needs your decision', 'Inspect the saved result and resolve its remaining execution or scope issue.', 'inspect-run', 'Inspect run'],
|
|
464
464
|
'process-needs-attention': ['Needs your decision', 'A process exit is not recorded. Open the attempt to check whether its work has stopped.', 'inspect-run', 'Inspect run'],
|
|
465
|
-
failed: ['Needs your decision', 'The last attempt stopped
|
|
465
|
+
failed: ['Needs your decision', 'The last attempt stopped.', 'retry-task', 'Review and retry'],
|
|
466
466
|
'vanished-run': ['Needs your decision', 'An unfinished attempt has no current live claim.', 'reconcile-run', 'Check the unfinished attempt'],
|
|
467
467
|
stopping: ['Needs your decision', 'Stopping. The work so far is kept.', 'inspect-stop', 'View stop details'],
|
|
468
468
|
stopped: ['Needs your decision', 'Paused by a person. The work so far is kept.', 'resume-run', 'Review pause'],
|
|
@@ -502,6 +502,9 @@ function itemOf(row, principal, probe) {
|
|
|
502
502
|
}
|
|
503
503
|
if (code === 'terminal-dependency' && s(row, 'dependency_id') !== null)
|
|
504
504
|
detail = `${s(row, 'dependency_id')} ${s(row, 'dependency_state') === 'cancelled' ? 'was cancelled' : 'failed'} before it finished.`;
|
|
505
|
+
// A failed task says the latest finished attempt across its whole family, whatever its outcome: its recorded reason, or that none was recorded.
|
|
506
|
+
if (code === 'failed')
|
|
507
|
+
detail = failedAttemptSentence(s(row, 'attempt_reason'));
|
|
505
508
|
if (code === 'result-needs-attention' && n(row, 'custody_unresolved'))
|
|
506
509
|
detail = 'A process exit is not recorded. Open the result to check whether its work has stopped.';
|
|
507
510
|
if (code === 'result-needs-attention' && ['built', 'no-change'].includes(s(row, 'result_outcome') ?? '')) {
|
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 } 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";
|
|
@@ -190,6 +192,52 @@ export declare function dispatchActionLabel(dispatch: DispatchDiagnosis | null):
|
|
|
190
192
|
/** A finished result's display status in the shared headline's words: a
|
|
191
193
|
* failed project check is Failed, a result missing its record needs a
|
|
192
194
|
* person, every other saved result is Ready for review. Token and action stay. */
|
|
195
|
+
/** Why a result can't be accepted as it stands, in one plain line, or null
|
|
196
|
+
* when nothing refutes it (a person's acceptance settles it). The result page
|
|
197
|
+
* shows it before Request changes, its one ink act. */
|
|
198
|
+
export declare function cantAcceptYetOf(verdict: ProofVerdict | null, reasons: readonly string[], accepted: boolean): string | null;
|
|
199
|
+
/** 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;
|
|
193
241
|
/** The result page's own sentence when the decision is on it. */
|
|
194
242
|
export declare const RESULT_DECISION_SENTENCE = "Review the change, then accept it or ask for changes.";
|
|
195
243
|
/** What the result page's Accept says, from facts it already shows: the
|
package/dist/workspace-ui.js
CHANGED
|
@@ -16,7 +16,9 @@
|
|
|
16
16
|
* observed" name observed publication records, and deployment is never
|
|
17
17
|
* claimed without one.
|
|
18
18
|
*/
|
|
19
|
-
import {
|
|
19
|
+
import { ACCEPT_NEEDS_REASON } from "./result-acts.js";
|
|
20
|
+
export { ACCEPT_NEEDS_REASON };
|
|
21
|
+
import { GOAL_ASSESSMENT_PENDING, manualReviewOnly, plainReasonWords } from "./proof.js";
|
|
20
22
|
import { 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." },
|
|
@@ -189,9 +191,9 @@ function storedResultStatusOf(result, publication) {
|
|
|
189
191
|
label: "Result saved, but its record does not match",
|
|
190
192
|
// A structural refutation is settled before the approved check is
|
|
191
193
|
// weighed, so it says nothing about whether that check passed.
|
|
192
|
-
detail: withPublication("What the agent reported doesn't match the changes it saved.
|
|
194
|
+
detail: withPublication("What the agent reported doesn't match the changes it saved."),
|
|
193
195
|
tone: "problem",
|
|
194
|
-
action: { label: "Open the
|
|
196
|
+
action: { label: "Open the result", kind: "open-review" },
|
|
195
197
|
};
|
|
196
198
|
}
|
|
197
199
|
if (result.verdict === "short" && humanReview) {
|
|
@@ -312,6 +314,96 @@ const VERIFIED_RESULT = new Set(["ready-to-review", "pr-opened", "merge-observed
|
|
|
312
314
|
/** A finished result's display status in the shared headline's words: a
|
|
313
315
|
* failed project check is Failed, a result missing its record needs a
|
|
314
316
|
* person, every other saved result is Ready for review. Token and action stay. */
|
|
317
|
+
/** Why a result can't be accepted as it stands, in one plain line, or null
|
|
318
|
+
* when nothing refutes it (a person's acceptance settles it). The result page
|
|
319
|
+
* shows it before Request changes, its one ink act. */
|
|
320
|
+
export function cantAcceptYetOf(verdict, reasons, accepted) {
|
|
321
|
+
if (accepted || verdict !== "refuted")
|
|
322
|
+
return null;
|
|
323
|
+
// A mismatch is the status card's own headline; here only what accepting takes.
|
|
324
|
+
return evidenceProblemOf(verdict, reasons) === "checks-failed"
|
|
325
|
+
? "Can't accept yet: the project's check failed on these changes."
|
|
326
|
+
: ACCEPT_NEEDS_REASON;
|
|
327
|
+
}
|
|
328
|
+
/** 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);
|
|
406
|
+
}
|
|
315
407
|
/** The result page's own sentence when the decision is on it. */
|
|
316
408
|
export const RESULT_DECISION_SENTENCE = "Review the change, then accept it or ask for changes.";
|
|
317
409
|
export function acceptWordsOf(facts) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toolroll",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.14",
|
|
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"
|