toolroll 0.9.12 → 0.9.13
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 +6 -0
- package/dist/assignment-ui.js +1 -1
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +24 -24
- package/dist/browser-crew.js +1 -1
- package/dist/browser-workspace.d.ts +18 -0
- package/dist/result-acts.d.ts +44 -0
- package/dist/result-acts.js +36 -0
- package/dist/result-review.js +1 -1
- package/dist/serve.d.ts +1 -0
- package/dist/serve.js +65 -18
- package/dist/test-temp.js +1 -1
- package/dist/workspace-ui.d.ts +4 -0
- package/dist/workspace-ui.js +12 -2
- package/package.json +1 -1
package/dist/browser-crew.js
CHANGED
|
@@ -19,7 +19,7 @@ export function browserWorkActionHref(item) {
|
|
|
19
19
|
if (action.code === 'open-pr' && /^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/pull\/\d+$/.test(item.publicationUrl ?? ''))
|
|
20
20
|
return item.publicationUrl;
|
|
21
21
|
if (runId !== null && (action.code === 'open-pr' || action.code === 'retry-review')) {
|
|
22
|
-
return `/review?result=${encodeURIComponent(taskId)}&run=${runId}
|
|
22
|
+
return `/review?result=${encodeURIComponent(taskId)}&run=${runId}`;
|
|
23
23
|
}
|
|
24
24
|
if (runId !== null && (action.code === 'inspect-run' || action.code === 'reconcile-run'))
|
|
25
25
|
return `/r/${runId}`;
|
|
@@ -11,6 +11,7 @@ import type { Ask } 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
15
|
import type { FirstRunStep, FirstTaskSuggestion, JourneyStep } from './first-run.js';
|
|
15
16
|
export type BrowserProject = {
|
|
16
17
|
name: string;
|
|
@@ -586,6 +587,23 @@ export type BrowserResultView = {
|
|
|
586
587
|
problem: boolean;
|
|
587
588
|
logHref: string | null;
|
|
588
589
|
} | null;
|
|
590
|
+
/** 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). */
|
|
591
|
+
acts: ResultActs;
|
|
592
|
+
/** Run checks on this result's commit: the project's check, when it didn't run. */
|
|
593
|
+
runChecks: {
|
|
594
|
+
action: string;
|
|
595
|
+
level: "quick" | "full";
|
|
596
|
+
returnTo: string;
|
|
597
|
+
} | null;
|
|
598
|
+
/** The raw run record, under Details: its facts and the full record. */
|
|
599
|
+
record: {
|
|
600
|
+
build: number;
|
|
601
|
+
href: string;
|
|
602
|
+
facts: {
|
|
603
|
+
label: string;
|
|
604
|
+
value: string;
|
|
605
|
+
}[];
|
|
606
|
+
} | null;
|
|
589
607
|
/** The signed scope; null when none was filed. */
|
|
590
608
|
intent: {
|
|
591
609
|
approval: string;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** The result page's acts (2026-10-02), chosen in one place: exactly one ink
|
|
2
|
+
* act, the one that resolves the result's state, and never navigation; at
|
|
3
|
+
* most one outline act beside it. A result that can't be accepted says why
|
|
4
|
+
* in one line before them.
|
|
5
|
+
*
|
|
6
|
+
* everything met → Accept
|
|
7
|
+
* checks queued or running → Checks running (disabled), then Accept without checks
|
|
8
|
+
* checks didn't run, a check exists → Run checks, then Accept without checks
|
|
9
|
+
* report and changes disagree,
|
|
10
|
+
* or the proof is refuted → Request changes, then Accept (as allowed);
|
|
11
|
+
* never Accept as ink, even with no form here
|
|
12
|
+
* a Needs you form resolves it → that form (Build again, Confirm it stopped)
|
|
13
|
+
* saved notes or a failing PR → Revise / Draft a repair task
|
|
14
|
+
*
|
|
15
|
+
* Accepting means what it always meant: the same forms post to the same
|
|
16
|
+
* endpoints. This only decides which one is ink. */
|
|
17
|
+
export type ResultActKind = "accept" | "checks-running" | "run-checks" | "request-changes" | "rebuild" | "confirm-stopped" | "revise" | "draft-repair";
|
|
18
|
+
export type ResultActFacts = {
|
|
19
|
+
/** An Accept this page can post (Mark complete, or the person's acceptance); `ready`: every requirement met and checks passed. */
|
|
20
|
+
accept: {
|
|
21
|
+
ready: boolean;
|
|
22
|
+
} | null;
|
|
23
|
+
/** The checks didn't run, the project has one, and this person may run it. */
|
|
24
|
+
runChecks: boolean;
|
|
25
|
+
/** Checks on this result are queued or running. */
|
|
26
|
+
checksRunning: boolean;
|
|
27
|
+
/** Why it can't be accepted yet (cantAcceptYetOf), or null. */
|
|
28
|
+
blocked: string | null;
|
|
29
|
+
/** The feedback form is on this page. */
|
|
30
|
+
canRequest: boolean;
|
|
31
|
+
/** A Needs you form that resolves it here. */
|
|
32
|
+
need: "rebuild" | "confirm-stopped" | null;
|
|
33
|
+
/** Saved notes ready to become a revision, or a failing pull request's repair. */
|
|
34
|
+
next: "revise" | "draft-repair" | null;
|
|
35
|
+
};
|
|
36
|
+
export type ResultActs = {
|
|
37
|
+
/** The one ink act, or null when nothing waits on the person. */
|
|
38
|
+
primary: ResultActKind | null;
|
|
39
|
+
/** The one outline act beside it. */
|
|
40
|
+
secondary: ResultActKind | null;
|
|
41
|
+
/** The one line before the acts: why it can't be accepted yet. */
|
|
42
|
+
line: string | null;
|
|
43
|
+
};
|
|
44
|
+
export declare function resultActsOf(facts: ResultActFacts): ResultActs;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** The result page's acts (2026-10-02), chosen in one place: exactly one ink
|
|
2
|
+
* act, the one that resolves the result's state, and never navigation; at
|
|
3
|
+
* most one outline act beside it. A result that can't be accepted says why
|
|
4
|
+
* in one line before them.
|
|
5
|
+
*
|
|
6
|
+
* everything met → Accept
|
|
7
|
+
* checks queued or running → Checks running (disabled), then Accept without checks
|
|
8
|
+
* checks didn't run, a check exists → Run checks, then Accept without checks
|
|
9
|
+
* report and changes disagree,
|
|
10
|
+
* or the proof is refuted → Request changes, then Accept (as allowed);
|
|
11
|
+
* never Accept as ink, even with no form here
|
|
12
|
+
* a Needs you form resolves it → that form (Build again, Confirm it stopped)
|
|
13
|
+
* saved notes or a failing PR → Revise / Draft a repair task
|
|
14
|
+
*
|
|
15
|
+
* Accepting means what it always meant: the same forms post to the same
|
|
16
|
+
* endpoints. This only decides which one is ink. */
|
|
17
|
+
export function resultActsOf(facts) {
|
|
18
|
+
const request = facts.canRequest ? "request-changes" : null;
|
|
19
|
+
const accept = facts.accept === null ? null : "accept";
|
|
20
|
+
if (facts.need !== null)
|
|
21
|
+
return { primary: facts.need, secondary: request, line: null };
|
|
22
|
+
if (facts.next !== null)
|
|
23
|
+
return { primary: facts.next, secondary: accept ?? request, line: null };
|
|
24
|
+
// Can't be accepted yet: Accept is never the ink act, even when the feedback form isn't here.
|
|
25
|
+
if (facts.blocked !== null)
|
|
26
|
+
return { primary: request, secondary: accept, line: facts.blocked };
|
|
27
|
+
if (facts.checksRunning)
|
|
28
|
+
return { primary: "checks-running", secondary: accept ?? request, line: null };
|
|
29
|
+
if (facts.accept === null)
|
|
30
|
+
return { primary: null, secondary: request, line: null };
|
|
31
|
+
if (facts.accept.ready)
|
|
32
|
+
return { primary: "accept", secondary: request, line: null };
|
|
33
|
+
if (facts.runChecks)
|
|
34
|
+
return { primary: "run-checks", secondary: "accept", line: null };
|
|
35
|
+
return { primary: "accept", secondary: request, line: null };
|
|
36
|
+
}
|
package/dist/result-review.js
CHANGED
|
@@ -153,7 +153,7 @@ export function resultFactsFromHtml(html) {
|
|
|
153
153
|
* lands on the run page. Matched exactly — never a bare open redirect. */
|
|
154
154
|
export function resultReturnTarget(raw, runId) {
|
|
155
155
|
if (raw !== null && raw !== undefined) {
|
|
156
|
-
const review = /^\/review\?result=[A-Za-z0-9._~%-]{1,200}(?:&run=([0-9]{1,15}))?$/.exec(raw);
|
|
156
|
+
const review = /^\/review\?result=[A-Za-z0-9._~%-]{1,200}(?:&run=([0-9]{1,15}))?(?:&tab=(?:summary|changes|checks))?$/.exec(raw);
|
|
157
157
|
if (review !== null && (review[1] === undefined || Number(review[1]) === runId))
|
|
158
158
|
return raw;
|
|
159
159
|
const chat = /^\/chat\?task=([A-Za-z0-9._~%-]{1,200})&result=([0-9]{1,15})(?:&conversation=[A-Za-z0-9-]{1,128})?$/.exec(raw);
|
package/dist/serve.d.ts
CHANGED
|
@@ -558,6 +558,7 @@ export declare function needActionOf(assignment: AssignmentSnapshot | null, stat
|
|
|
558
558
|
run: number;
|
|
559
559
|
action: string;
|
|
560
560
|
rebuildable?: boolean;
|
|
561
|
+
refuted?: boolean;
|
|
561
562
|
}): BrowserNeedAction | null;
|
|
562
563
|
/** The facts region alone, for the open-run poll (A4). A finished run's
|
|
563
564
|
* fragment says so instead of quietly growing forms (finding 5), and a run
|
package/dist/serve.js
CHANGED
|
@@ -56,6 +56,7 @@ import { knowledgeHtml, knowledgeContextHtml, KNOWLEDGE_CSS, decisionsHtml, memo
|
|
|
56
56
|
import { listDecisions, recordDecision, retireDecision, searchMemory } from "./project-memory.js";
|
|
57
57
|
import { decideProposal, listProposals, memoryStatus } from "./memory-pass.js";
|
|
58
58
|
import { learningHtml } from "./workspace-ui.js";
|
|
59
|
+
import { resultActsOf } from "./result-acts.js";
|
|
59
60
|
import { createSessionEndpoint } from './session-server.js';
|
|
60
61
|
import { handleTeamHttp } from './team-http.js';
|
|
61
62
|
import { teamWorkspaceHtml } from './team-ui.js';
|
|
@@ -163,7 +164,7 @@ import { starterRecipes, savedRecipes, findRecipe, importRecipe, exportRecipe, c
|
|
|
163
164
|
import { recipeFromForm, recipeLibraryHtml, recipeEditorHtml, workflowPreviewHtml, recipeScript, RECIPE_CSS, recipeDefinitionPreviewHtml, recipeRunHtml, recipeAnswersFromForm } from "./recipe-ui.js";
|
|
164
165
|
import { EVIDENCE_CAPS, readVerifiedArtifact, readVerifiedReport, readVerifiedProofForRun, storeEvidence, writeEvidenceFile, scanForSecrets } from "./evidence.js";
|
|
165
166
|
import { GOAL_ASSESSMENT_PENDING, reviewConflict, manualReviewOnly, manualReviewCriterionOf, personCheckWords, plainReasonWords, dispatchStatusToken, passFraction, semanticCoverage, coverageWords, coverageStateWords } from "./proof.js";
|
|
166
|
-
import { WORK_VIEWS, REVIEW_TOKENS, RESULT_DECISION_SENTENCE, acceptWordsOf, parseWorkView, resultStatusOf, resultHeadlineOf, receiptHeadingOf, receiptPublicationWords, reviewFactsOf, workStatusOf, primaryDestinationOf, needsPerson, dispatchActionLabel, } from "./workspace-ui.js";
|
|
167
|
+
import { WORK_VIEWS, REVIEW_TOKENS, RESULT_DECISION_SENTENCE, acceptWordsOf, cantAcceptYetOf, parseWorkView, resultStatusOf, resultHeadlineOf, receiptHeadingOf, receiptPublicationWords, reviewFactsOf, workStatusOf, primaryDestinationOf, needsPerson, dispatchActionLabel, } from "./workspace-ui.js";
|
|
167
168
|
import { PRICED_BUILD_MODELS } from "./pricing.js";
|
|
168
169
|
import { RESULT_TABS, RESULT_REVIEW_SCRIPT, isRevisionFeedback, evidenceResultStatusOf, evidenceShortenedWords, parseResultTab, resultLeadOf, evidenceProblemDetailsOf, evidenceHealthOf, resultFactsAttributes, resultFactsAttributeMap, resultReturnTarget, commentSourceKey, REQUEST_TOKEN, revisionBatchOf, parseRevisionBatch, revisionSourceOf, } from "./result-review.js";
|
|
169
170
|
import { parseReport } from "./scout-report.js";
|
|
@@ -1761,7 +1762,9 @@ export function createDecisionServer(options) {
|
|
|
1761
1762
|
}
|
|
1762
1763
|
// Exact result links carry their own read context. Check the stored
|
|
1763
1764
|
// placement against both account and instance access before using it;
|
|
1764
|
-
// viewing a result never changes the session's selected project
|
|
1765
|
+
// viewing a result never changes the session's selected project, and
|
|
1766
|
+
// the page's project switch keeps showing the person's own choice.
|
|
1767
|
+
const chosenProject = project;
|
|
1765
1768
|
if (url.pathname === "/review" && url.searchParams.has("result")) {
|
|
1766
1769
|
const wanted = url.searchParams.get("result") ?? "";
|
|
1767
1770
|
const ref = wanted.length > 0 && wanted.length <= 64 && !hasForbiddenControls(wanted) ? store.lookupRef(wanted) : null;
|
|
@@ -2390,11 +2393,14 @@ export function createDecisionServer(options) {
|
|
|
2390
2393
|
const selectedRow = wantedId === null ? ranked[0] ?? null : chosen;
|
|
2391
2394
|
const csrf = who.via === "cookie" ? who.session.csrf : "";
|
|
2392
2395
|
const expectedRun = url.searchParams.get("run");
|
|
2396
|
+
// An exact link to a result this person can't see (or that never existed) reads the same either way.
|
|
2397
|
+
if (expectedRun !== null && wantedId !== null && chosen === null)
|
|
2398
|
+
return refuse(response, who, 404, "No such result in your projects.", "/work");
|
|
2393
2399
|
if (expectedRun !== null && (!/^[1-9]\d*$/.test(expectedRun) || selectedRow?.runId !== Number(expectedRun))) {
|
|
2394
2400
|
return sendScreen(response, 409, screen("Result changed", '<h1>Result changed</h1><p>This acceptance link no longer matches the current result. Review the current task before accepting.</p><p class="refusal-back"><a class="button-link" href="/review">Review results</a></p>', { chrome: chromeFor(project, "runs") }));
|
|
2395
2401
|
}
|
|
2396
2402
|
const selected = selectedRow === null ? null : reviewCockpitViewOf(selectedRow, who, now);
|
|
2397
|
-
const reviewPage = reviewCockpitPage(chromeFor(project, "runs"), {
|
|
2403
|
+
const reviewPage = reviewCockpitPage(chromeFor(wantedId === null ? project : chosenProject, "runs"), {
|
|
2398
2404
|
queue: ranked,
|
|
2399
2405
|
queueCap: REVIEW_QUEUE_CAP,
|
|
2400
2406
|
selected,
|
|
@@ -3020,6 +3026,14 @@ export function createDecisionServer(options) {
|
|
|
3020
3026
|
}
|
|
3021
3027
|
return respond(response, 200, "application/json", JSON.stringify({ text: window.text, nextOffset: window.nextOffset, final: !live && window.eof }));
|
|
3022
3028
|
}
|
|
3029
|
+
// One result page (2026-10-02): a builder's finished result opens on
|
|
3030
|
+
// its task's result page, titled with the task. The raw run record
|
|
3031
|
+
// waits there under Details, and here with ?record=1.
|
|
3032
|
+
if (url.searchParams.get("record") === null && !running && found.role === "builder" && (found.outcome === "built" || found.outcome === "no-change") &&
|
|
3033
|
+
completedRowFor(taskId, null)?.runId === found.id) {
|
|
3034
|
+
const tab = parseResultTab(url.searchParams.get("tab"));
|
|
3035
|
+
return redirect(response, `${reviewHref(taskId, found.id)}${tab === "summary" ? "" : `&tab=${tab}`}`);
|
|
3036
|
+
}
|
|
3023
3037
|
// Pollers exist only for a LIVE run — an orphaned null-outcome run
|
|
3024
3038
|
// would otherwise be refetched forever (finding 15). The nonce is
|
|
3025
3039
|
// sendScreen's business now.
|
|
@@ -10975,7 +10989,7 @@ export function createDecisionServer(options) {
|
|
|
10975
10989
|
const completed = checkAssignmentAsOperator(store, taskId, digest, principal, now, evidenceRoot);
|
|
10976
10990
|
if (!completed.ok)
|
|
10977
10991
|
return taskScreen(response, who, taskId, completed.message, 409);
|
|
10978
|
-
return redirect(response, reviewHref(taskId, Number(namedRun)
|
|
10992
|
+
return redirect(response, reviewHref(taskId, Number(namedRun)));
|
|
10979
10993
|
}
|
|
10980
10994
|
case "retry-review": {
|
|
10981
10995
|
return taskScreen(response, who, taskId, "Separate agent reviews have retired. Open the saved result to mark it complete or request a revision.", 410);
|
|
@@ -12708,7 +12722,7 @@ ${THEME_MAPPING}
|
|
|
12708
12722
|
--font-mono: "Geist Mono", ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
|
12709
12723
|
/* Phone spacing steps (760px and narrower): side gutter, gap under the
|
|
12710
12724
|
header, gap between page blocks, between rows, and inside a card. */
|
|
12711
|
-
--so-phone-gutter:
|
|
12725
|
+
--so-phone-gutter: 16px; --so-phone-gap: 12px; --so-phone-block: 14px; --so-phone-row: 8px; --so-phone-card: 12px;
|
|
12712
12726
|
}
|
|
12713
12727
|
@media (prefers-color-scheme: dark) {
|
|
12714
12728
|
:root:not([data-theme="light"]) {
|
|
@@ -18175,9 +18189,9 @@ function statusActionHref(status, taskId, runId, prUrl, repo) {
|
|
|
18175
18189
|
return null;
|
|
18176
18190
|
switch (status.action.kind) {
|
|
18177
18191
|
case "open-result": return runId === null ? taskHref(taskId) : `/r/${runId}`;
|
|
18178
|
-
case "open-review": return repo === undefined ? reviewHref(taskId) : reviewHref(taskId, runId
|
|
18192
|
+
case "open-review": return repo === undefined ? reviewHref(taskId) : reviewHref(taskId, runId);
|
|
18179
18193
|
case "open-run": return runId === null ? taskHref(taskId) : `/r/${runId}`;
|
|
18180
|
-
case "open-pr": return safePrUrl(prUrl) ?? (repo === undefined ? reviewHref(taskId) : reviewHref(taskId, runId
|
|
18194
|
+
case "open-pr": return safePrUrl(prUrl) ?? (repo === undefined ? reviewHref(taskId) : reviewHref(taskId, runId));
|
|
18181
18195
|
case "open-task":
|
|
18182
18196
|
default: return taskHref(taskId);
|
|
18183
18197
|
}
|
|
@@ -18211,7 +18225,7 @@ function workPage(chrome, data) {
|
|
|
18211
18225
|
const rowHtml = (row) => {
|
|
18212
18226
|
const target = row.primaryAction?.target;
|
|
18213
18227
|
const actionHref = row.primaryAction?.code === 'open-result' && target?.runId != null
|
|
18214
|
-
? `/review?result=${encodeURIComponent(target.taskId)}&run=${target.runId}
|
|
18228
|
+
? `/review?result=${encodeURIComponent(target.taskId)}&run=${target.runId}`
|
|
18215
18229
|
: browserWorkActionHref(row);
|
|
18216
18230
|
const action = actionHref === null || row.primaryAction === null ? '' : `<a class="work-action" data-primary-action href="${escape(actionHref)}">${escape(row.primaryAction.label)} →</a>`;
|
|
18217
18231
|
const project = data.multiProject || row.repo === null ? `<span class="project-label">${row.repo === null ? 'Unplaced' : escape(projectName(row.repo))}</span>` : '';
|
|
@@ -18245,7 +18259,7 @@ function workPage(chrome, data) {
|
|
|
18245
18259
|
rows: data.work.items.map(row => {
|
|
18246
18260
|
const target = row.primaryAction?.target;
|
|
18247
18261
|
const actionHref = row.primaryAction?.code === 'open-result' && target?.runId != null
|
|
18248
|
-
? `/review?result=${encodeURIComponent(target.taskId)}&run=${target.runId}
|
|
18262
|
+
? `/review?result=${encodeURIComponent(target.taskId)}&run=${target.runId}`
|
|
18249
18263
|
: browserWorkActionHref(row);
|
|
18250
18264
|
const needsYouDetail = (row.status.views.includes('needs-you') || row.status.label === 'Failed') && !['write-scope', 'approve-scope'].includes(row.primaryAction?.code ?? '') && row.status.label !== 'Ready for review' && row.status.detail !== row.status.label && row.status.detail !== row.familyProblem;
|
|
18251
18265
|
return {
|
|
@@ -21141,7 +21155,9 @@ export function orderChangedFiles(files, proofCitesPaths) {
|
|
|
21141
21155
|
? churn(b) - churn(a)
|
|
21142
21156
|
: a.path.localeCompare(b.path));
|
|
21143
21157
|
}
|
|
21144
|
-
|
|
21158
|
+
/** The one result page's address: the task and its run, never a project path
|
|
21159
|
+
* (the page finds the result's project itself and keeps the person's own). */
|
|
21160
|
+
const reviewHref = (taskId, runId = null) => `/review?result=${encodeURIComponent(taskId)}${runId === null ? "" : `&run=${runId}`}`;
|
|
21145
21161
|
/** The receipt's own proof-state word, so the cockpit and the task page
|
|
21146
21162
|
* never disagree on the state's name or its precedence: the stored
|
|
21147
21163
|
* verdict decides, whatever the run's outcome — a no-change run with a
|
|
@@ -21266,7 +21282,7 @@ function reviewCockpitPage(chrome, data) {
|
|
|
21266
21282
|
const why = status === null
|
|
21267
21283
|
? reasons.length === 0 ? "" : `<span class="cockpit-why">${escape(reasons[0])}${reasons.length > 1 ? ` · +${reasons.length - 1} more` : ""}</span>`
|
|
21268
21284
|
: `<span class="cockpit-why">${statusLineHtml(status)}</span>`;
|
|
21269
|
-
return (`<li data-review-priority="${row.priority.band}"><a class="cockpit-row${current ? " current" : ""}" href="${escape(reviewHref(row.taskId, row.runId
|
|
21285
|
+
return (`<li data-review-priority="${row.priority.band}"><a class="cockpit-row${current ? " current" : ""}" href="${escape(reviewHref(row.taskId, row.runId))}"${current ? ` aria-current="page"` : ""}>` +
|
|
21270
21286
|
`<span class="cockpit-row-head"><strong>${escape(row.title)}</strong></span>` +
|
|
21271
21287
|
`<span class="cockpit-row-meta">${whenTime(row.completedAt)}${row.runId === null ? " · no build" : row.outcome === "no-change" ? " · no change" : ""}${row.prNumber === null ? "" : ` · PR #${row.prNumber}`}</span>` +
|
|
21272
21288
|
(row.historyProblem ? `<span class="cockpit-why" data-history-problem>History unavailable</span>` : why) +
|
|
@@ -21295,7 +21311,7 @@ function reviewCockpitPage(chrome, data) {
|
|
|
21295
21311
|
const status = row.assignment == null ? null : assignmentPresentationOf(row.assignment).status;
|
|
21296
21312
|
const reasons = row.priority.reasons;
|
|
21297
21313
|
return {
|
|
21298
|
-
title: row.title, href: reviewHref(row.taskId, row.runId
|
|
21314
|
+
title: row.title, href: reviewHref(row.taskId, row.runId), at: row.completedAt,
|
|
21299
21315
|
status: status === null ? null : { label: status.label, tone: status.tone },
|
|
21300
21316
|
notes: [...(row.historyProblem ? ["History unavailable"] : status === null && reasons.length > 0 ? [reasons[0]] : []), ...(status !== null && row.ciFailing ? ["CI is failing"] : [])],
|
|
21301
21317
|
current: selected !== null && selected.taskId === row.taskId,
|
|
@@ -21377,6 +21393,15 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
|
|
|
21377
21393
|
problem: view.detail === null ? view.historyProblem : null,
|
|
21378
21394
|
next: reviewNextActionOf(view, csrf), complete: null, decision: null, checks: null, intent: intentView, noRun: null, panel: null, contest: "",
|
|
21379
21395
|
notes: view.notes.map(one => ({ author: one.author, at: one.createdAt, note: one.note })),
|
|
21396
|
+
acts: { primary: null, secondary: null, line: null }, runChecks: null,
|
|
21397
|
+
// The raw run record lives under Details now (2026-10-02): /r/<id> for this result redirects here.
|
|
21398
|
+
record: run === null ? null : { build: run.id, href: `/r/${run.id}?record=1`, facts: [
|
|
21399
|
+
{ label: "Agent", value: [run.provider, run.model].filter(Boolean).join(" · ") },
|
|
21400
|
+
{ label: "Worker", value: run.runner },
|
|
21401
|
+
...(run.branch === null ? [] : [{ label: "Branch", value: run.branch }]),
|
|
21402
|
+
...(run.finishedAt === null ? [] : [{ label: "Finished", value: when(run.finishedAt) }]),
|
|
21403
|
+
...(run.ranMinutes === null ? [] : [{ label: "Ran", value: `${run.ranMinutes} min` }]),
|
|
21404
|
+
] },
|
|
21380
21405
|
};
|
|
21381
21406
|
if (run === null || view.detail === null) {
|
|
21382
21407
|
parts.push(`<section class="card cockpit-section" id="verification" data-cockpit-section="proof"><h3>verification</h3>` +
|
|
@@ -21419,7 +21444,8 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
|
|
|
21419
21444
|
// The one decision, after the evidence: Accept only when every requirement is met and the checks passed.
|
|
21420
21445
|
const acceptsHere = complete !== null || panel.panel.need?.accept != null;
|
|
21421
21446
|
const matrix = proof === null || proof.proofProblem !== null ? [] : proof.matrix;
|
|
21422
|
-
const
|
|
21447
|
+
const blocked = cantAcceptYetOf(proof?.verdict ?? null, proof?.reasons ?? [], accepted);
|
|
21448
|
+
const words = !acceptsHere ? null : { sentence: RESULT_DECISION_SENTENCE, ...acceptWordsOf({
|
|
21423
21449
|
checks: checks === undefined ? null : checks.running != null ? "running" : checks.level === "off" && checks.status !== "passed" ? "off" : checks.status,
|
|
21424
21450
|
unmet: matrix.filter(row => row.state !== "pass" && row.state !== "manual-review").length,
|
|
21425
21451
|
yours: complete === null || accepted ? 0 : panel.panel.youCheck?.lines.length ?? 0,
|
|
@@ -21427,6 +21453,26 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
|
|
|
21427
21453
|
publishing: view.detail?.publishing ?? "other",
|
|
21428
21454
|
proof: proof !== null && proof.proof !== null && proof.proofProblem === null,
|
|
21429
21455
|
}) };
|
|
21456
|
+
// Refuted: plain Accept stays as allowed, in outline, and the one line before the acts (acts.line) says why.
|
|
21457
|
+
const decision = words === null || blocked === null ? words : { ...words, label: "Accept", ready: false };
|
|
21458
|
+
// Run checks: no check ran on this result (or its saved one can't be read), the project has one, and an approver may run it.
|
|
21459
|
+
const followUps = view.detail.followUps ?? null;
|
|
21460
|
+
const checksRunning = checks?.running != null || (followUps?.checks.some(one => one.state === "waiting" || one.state === "running") ?? false);
|
|
21461
|
+
const runChecks = canRetryReview && csrf !== "" && followUps !== null && (followUps.full || followUps.quick) && !checksRunning &&
|
|
21462
|
+
(checks === undefined || checks.status === "not-run" || checks.status === "unavailable")
|
|
21463
|
+
// Back on this result's Checks tab, where #follow-ups shows the run it started.
|
|
21464
|
+
? { action: `/r/${run.id}/checks`, level: followUps.full ? "full" : "quick", returnTo: `${here}&run=${run.id}&tab=checks` } : null;
|
|
21465
|
+
const need = panel.panel.need;
|
|
21466
|
+
const nextKind = selected.next?.kind;
|
|
21467
|
+
const acts = resultActsOf({
|
|
21468
|
+
accept: decision === null ? null : { ready: decision.ready },
|
|
21469
|
+
runChecks: runChecks !== null,
|
|
21470
|
+
checksRunning,
|
|
21471
|
+
blocked,
|
|
21472
|
+
canRequest: panel.panel.canRequest && panel.panel.request !== null,
|
|
21473
|
+
need: need === null || need.accept != null ? null : need.rebuild != null ? "rebuild" : need.confirm !== null ? "confirm-stopped" : null,
|
|
21474
|
+
next: nextKind === "revise" || nextKind === "draft-repair" ? nextKind : null,
|
|
21475
|
+
});
|
|
21430
21476
|
if (view.notes.length > 0) {
|
|
21431
21477
|
parts.push(`<section class="card cockpit-section" data-cockpit-section="notes"><h3>operator notes</h3>` +
|
|
21432
21478
|
view.notes.map(one => `<p class="row"><span class="meta">${escape(one.author)} · ${whenTime(one.createdAt)}</span> ${escape(one.note)}</p>`).join("\n") +
|
|
@@ -21435,7 +21481,7 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
|
|
|
21435
21481
|
return {
|
|
21436
21482
|
html: `<section class="cockpit-detail">${parts.join("\n")}</section>`,
|
|
21437
21483
|
selected: {
|
|
21438
|
-
...selected, complete, decision, panel: panel.panel, contest,
|
|
21484
|
+
...selected, complete, decision, panel: panel.panel, contest, acts, runChecks,
|
|
21439
21485
|
checks: checks === undefined ? null : { detail: checks.detail, problem: checks.status === "failed" || checks.status === "unavailable", logHref: checks.logArtifactId === null ? null : `/r/${run.id}/evidence/${checks.logArtifactId}` },
|
|
21440
21486
|
},
|
|
21441
21487
|
};
|
|
@@ -22344,7 +22390,7 @@ function resultPanelParts(detail, o) {
|
|
|
22344
22390
|
// Needs you: the action that resolves it comes first; Request changes stays beside it, never alone.
|
|
22345
22391
|
// On the result itself, "Review result" would link here: accepting it is what resolves it.
|
|
22346
22392
|
const need = needActionOf(current, taskStatus, o.csrf, o.returnTo, {
|
|
22347
|
-
accepted: proof?.accepted != null, humanReview, run: run.id, action: `${taskHref(detail.taskId)}/accept-proof`,
|
|
22393
|
+
accepted: proof?.accepted != null, humanReview, run: run.id, action: `${taskHref(detail.taskId)}/accept-proof`, refuted: proof?.verdict === "refuted",
|
|
22348
22394
|
// What Store.finalResultReason refuses: a verified, attested, accepted or published result.
|
|
22349
22395
|
rebuildable: proof?.accepted == null && proof?.verdict !== "verified" && proof?.verdict !== "attested" && !(detail.publication !== null && ["pushed", "opened"].includes(detail.publication.state))
|
|
22350
22396
|
});
|
|
@@ -22762,7 +22808,7 @@ export function needActionOf(assignment, status, csrf, returnTo, result) {
|
|
|
22762
22808
|
if (action?.code === "retry-task" && status.need.key === "rebuild" && csrf !== "")
|
|
22763
22809
|
return { label: status.need.action.label, ...rebuild };
|
|
22764
22810
|
if (result !== undefined) {
|
|
22765
|
-
if (status.need.key === "review-result" && !result.accepted && csrf !== "")
|
|
22811
|
+
if ((status.need.key === "review-result" || result.refuted === true) && !result.accepted && csrf !== "")
|
|
22766
22812
|
return { label: result.humanReview ? "Accept result" : "Accept with exception", href: null, confirm: null,
|
|
22767
22813
|
accept: { action: result.action, run: result.run, returnTo, note: result.humanReview ? null : "Why is this safe to accept?" } };
|
|
22768
22814
|
// The task page sends the person here: never back to it. What this page can do leads — Accept
|
|
@@ -22772,8 +22818,9 @@ export function needActionOf(assignment, status, csrf, returnTo, result) {
|
|
|
22772
22818
|
return { label: "Accept result", href: null, confirm: null, accept: { action: result.action, run: result.run, returnTo, note: null } };
|
|
22773
22819
|
if (here && csrf !== "" && result.rebuildable === true)
|
|
22774
22820
|
return { label: NEEDS.rebuild.action.label, ...rebuild };
|
|
22821
|
+
// Nothing here resolves it: no act at all, never a link back to the task (which sends the person here).
|
|
22775
22822
|
if (status.need.key === "review-result" || here)
|
|
22776
|
-
return
|
|
22823
|
+
return null;
|
|
22777
22824
|
}
|
|
22778
22825
|
const confirm = action?.code === "confirm-stopped" && action.target.runId !== null && csrf !== ""
|
|
22779
22826
|
? { action: `${taskHref(assignment.rootId)}/confirm-stopped`, run: action.target.runId, returnTo, checked: needsCheck(assignment) } : null;
|
|
@@ -22802,7 +22849,7 @@ function resultNeedAction(need, o, canRequest) {
|
|
|
22802
22849
|
function conciseOutcomeOf(conclusion) {
|
|
22803
22850
|
const flat = oneLineOf(conclusion, 2_000);
|
|
22804
22851
|
const sentence = /^[\s\S]*?[.!?](?=\s|$)/.exec(flat)?.[0] ?? flat;
|
|
22805
|
-
const bounded = sentence.length <=
|
|
22852
|
+
const bounded = sentence.length <= 140 ? sentence : `${sentence.slice(0, 140).replace(/\s+\S*$/, "")}…`;
|
|
22806
22853
|
return bounded.trim();
|
|
22807
22854
|
}
|
|
22808
22855
|
/** Exactly one primary act for the result (repair 2026-09-14), chosen from
|
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/workspace-ui.d.ts
CHANGED
|
@@ -190,6 +190,10 @@ export declare function dispatchActionLabel(dispatch: DispatchDiagnosis | null):
|
|
|
190
190
|
/** A finished result's display status in the shared headline's words: a
|
|
191
191
|
* failed project check is Failed, a result missing its record needs a
|
|
192
192
|
* person, every other saved result is Ready for review. Token and action stay. */
|
|
193
|
+
/** Why a result can't be accepted as it stands, in one plain line, or null
|
|
194
|
+
* when nothing refutes it (a person's acceptance settles it). The result page
|
|
195
|
+
* shows it before Request changes, its one ink act. */
|
|
196
|
+
export declare function cantAcceptYetOf(verdict: ProofVerdict | null, reasons: readonly string[], accepted: boolean): string | null;
|
|
193
197
|
/** The result page's own sentence when the decision is on it. */
|
|
194
198
|
export declare const RESULT_DECISION_SENTENCE = "Review the change, then accept it or ask for changes.";
|
|
195
199
|
/** What the result page's Accept says, from facts it already shows: the
|
package/dist/workspace-ui.js
CHANGED
|
@@ -189,9 +189,9 @@ function storedResultStatusOf(result, publication) {
|
|
|
189
189
|
label: "Result saved, but its record does not match",
|
|
190
190
|
// A structural refutation is settled before the approved check is
|
|
191
191
|
// weighed, so it says nothing about whether that check passed.
|
|
192
|
-
detail: withPublication("What the agent reported doesn't match the changes it saved.
|
|
192
|
+
detail: withPublication("What the agent reported doesn't match the changes it saved."),
|
|
193
193
|
tone: "problem",
|
|
194
|
-
action: { label: "Open the
|
|
194
|
+
action: { label: "Open the result", kind: "open-review" },
|
|
195
195
|
};
|
|
196
196
|
}
|
|
197
197
|
if (result.verdict === "short" && humanReview) {
|
|
@@ -312,6 +312,16 @@ const VERIFIED_RESULT = new Set(["ready-to-review", "pr-opened", "merge-observed
|
|
|
312
312
|
/** A finished result's display status in the shared headline's words: a
|
|
313
313
|
* failed project check is Failed, a result missing its record needs a
|
|
314
314
|
* person, every other saved result is Ready for review. Token and action stay. */
|
|
315
|
+
/** Why a result can't be accepted as it stands, in one plain line, or null
|
|
316
|
+
* when nothing refutes it (a person's acceptance settles it). The result page
|
|
317
|
+
* shows it before Request changes, its one ink act. */
|
|
318
|
+
export function cantAcceptYetOf(verdict, reasons, accepted) {
|
|
319
|
+
if (accepted || verdict !== "refuted")
|
|
320
|
+
return null;
|
|
321
|
+
return evidenceProblemOf(verdict, reasons) === "checks-failed"
|
|
322
|
+
? "Can't accept yet: the project's check failed on these changes."
|
|
323
|
+
: "Can't accept yet: what the agent reported doesn't match the changes it saved.";
|
|
324
|
+
}
|
|
315
325
|
/** The result page's own sentence when the decision is on it. */
|
|
316
326
|
export const RESULT_DECISION_SENTENCE = "Review the change, then accept it or ask for changes.";
|
|
317
327
|
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.13",
|
|
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"
|