toolroll 0.9.11 → 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 +24 -0
- package/dist/assignment-ui.js +1 -1
- package/dist/assignment.d.ts +3 -0
- package/dist/assignment.js +3 -1
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +25 -23
- package/dist/browser-crew.js +1 -1
- package/dist/browser-workspace.d.ts +79 -0
- package/dist/chat-delivery.js +17 -0
- package/dist/chat-quiet.js +1 -0
- package/dist/chat-voice.d.ts +3 -1
- package/dist/chat-voice.js +13 -5
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +1 -0
- package/dist/coding-update.d.ts +13 -0
- package/dist/coding-update.js +68 -0
- package/dist/desktop-update.d.ts +7 -0
- package/dist/desktop-update.js +42 -1
- package/dist/flow-triggers.js +17 -1
- package/dist/guides.js +9 -0
- package/dist/lead-status.d.ts +10 -1
- package/dist/lead-status.js +13 -3
- package/dist/lead-voice.d.ts +81 -0
- package/dist/lead-voice.js +275 -0
- package/dist/needs-you.d.ts +7 -0
- package/dist/needs-you.js +9 -0
- package/dist/operate.js +80 -4
- 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 +6 -1
- package/dist/serve.js +500 -239
- package/dist/setup-guide.d.ts +2 -2
- package/dist/store.js +8 -0
- package/dist/surface.js +8 -1
- package/dist/task-status.d.ts +6 -0
- package/dist/task-status.js +19 -2
- package/dist/team-cli.js +1 -1
- package/dist/telegram-progress.js +7 -0
- package/dist/telegram.js +34 -3
- package/dist/test-temp.js +1 -1
- package/dist/toolroll-update.d.ts +3 -0
- package/dist/toolroll-update.js +13 -2
- package/dist/work-index.d.ts +9 -0
- package/dist/work-index.js +57 -20
- package/dist/work-summary.d.ts +4 -1
- package/dist/workspace-ui.d.ts +29 -0
- package/dist/workspace-ui.js +32 -3
- package/package.json +1 -1
package/dist/serve.js
CHANGED
|
@@ -56,12 +56,14 @@ 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';
|
|
62
63
|
import { createTeamRuntime } from './team-runtime.js';
|
|
63
64
|
import { prepareWorkspaceRevision, WorkspaceValidatorCache } from "./workspace-revision.js";
|
|
64
65
|
import { workIndexPage, workCountsByProject, WorkIndexCursorError, WORK_INDEX_MAX_LIMIT } from "./work-index.js";
|
|
66
|
+
import { leadActivity } from "./lead-voice.js";
|
|
65
67
|
import { openWorkDecisionOf } from "./work-summary.js";
|
|
66
68
|
import { assignmentOf, checkAssignmentAsOperator } from './assignment.js';
|
|
67
69
|
import { demoChatHtml, demoThreadHtml, DEMO_CHAT_SCRIPT } from "./demo-chat.js";
|
|
@@ -73,7 +75,7 @@ import { leadBriefHtml, LEAD_CONTEXT_CSS } from './lead-context.js';
|
|
|
73
75
|
import { assignmentCatchUp } from './assignment-brief.js';
|
|
74
76
|
import { assignmentActionHref, assignmentCardOf, assignmentStatusOf, assignmentSummaryHtml, assignmentWithEvidence, ASSIGNMENT_CSS } from './assignment-ui.js';
|
|
75
77
|
import { assignmentStageOf, pullRequestFactOf, stageOfCode, stageOfDispatch, statusDetailsHtml, statusIconSvg, statusWhyHtml, taskStatusOf, TASK_STATUS_CSS } from './task-status.js';
|
|
76
|
-
import { NEEDS } from './needs-you.js';
|
|
78
|
+
import { ASKS, ASK_LABEL, NEEDS } from './needs-you.js';
|
|
77
79
|
import { assignmentPresentationOf, historicalAssessmentReason, shortenedMaterialReason } from './assignment-presentation.js';
|
|
78
80
|
import { ledgerBody } from "./ledger-view.js";
|
|
79
81
|
import { EVIDENCE_PACK_CSS, evidencePack, evidencePackHtml, exportDay, ledgerExportChunks } from "./evidence-pack.js";
|
|
@@ -162,7 +164,7 @@ import { starterRecipes, savedRecipes, findRecipe, importRecipe, exportRecipe, c
|
|
|
162
164
|
import { recipeFromForm, recipeLibraryHtml, recipeEditorHtml, workflowPreviewHtml, recipeScript, RECIPE_CSS, recipeDefinitionPreviewHtml, recipeRunHtml, recipeAnswersFromForm } from "./recipe-ui.js";
|
|
163
165
|
import { EVIDENCE_CAPS, readVerifiedArtifact, readVerifiedReport, readVerifiedProofForRun, storeEvidence, writeEvidenceFile, scanForSecrets } from "./evidence.js";
|
|
164
166
|
import { GOAL_ASSESSMENT_PENDING, reviewConflict, manualReviewOnly, manualReviewCriterionOf, personCheckWords, plainReasonWords, dispatchStatusToken, passFraction, semanticCoverage, coverageWords, coverageStateWords } from "./proof.js";
|
|
165
|
-
import { WORK_VIEWS, REVIEW_TOKENS, 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";
|
|
166
168
|
import { PRICED_BUILD_MODELS } from "./pricing.js";
|
|
167
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";
|
|
168
170
|
import { parseReport } from "./scout-report.js";
|
|
@@ -408,6 +410,8 @@ class PersistentSessions extends Map {
|
|
|
408
410
|
}
|
|
409
411
|
hashOf(session) { const id = this.ids.get(session); return id === undefined ? null : PersistentSessions.hash(id); }
|
|
410
412
|
}
|
|
413
|
+
/** How long a stop waits for teammates and the lead's follow pass before closing anyway. */
|
|
414
|
+
export const SHUTDOWN_WAIT_MS = 5_000;
|
|
411
415
|
export function createDecisionServer(options) {
|
|
412
416
|
const { store, evidenceRoot } = options;
|
|
413
417
|
let coding = options.codingWorkspace ?? null;
|
|
@@ -1758,7 +1762,9 @@ export function createDecisionServer(options) {
|
|
|
1758
1762
|
}
|
|
1759
1763
|
// Exact result links carry their own read context. Check the stored
|
|
1760
1764
|
// placement against both account and instance access before using it;
|
|
1761
|
-
// 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;
|
|
1762
1768
|
if (url.pathname === "/review" && url.searchParams.has("result")) {
|
|
1763
1769
|
const wanted = url.searchParams.get("result") ?? "";
|
|
1764
1770
|
const ref = wanted.length > 0 && wanted.length <= 64 && !hasForbiddenControls(wanted) ? store.lookupRef(wanted) : null;
|
|
@@ -2026,7 +2032,7 @@ export function createDecisionServer(options) {
|
|
|
2026
2032
|
needsVerification: store
|
|
2027
2033
|
.listCompletedWorkScoped(project, 10, admission)
|
|
2028
2034
|
.filter(one => visible(one.repo) && (one.proofVerdict === "short" || one.proofVerdict === "refuted") && !one.proofAccepted &&
|
|
2029
|
-
assignmentOf(store, one.taskId, now,
|
|
2035
|
+
assignmentOf(store, one.taskId, now, workAccess(), evidenceRoot) === null)
|
|
2030
2036
|
.map(one => ({
|
|
2031
2037
|
taskId: one.taskId,
|
|
2032
2038
|
title: one.title,
|
|
@@ -2364,7 +2370,7 @@ export function createDecisionServer(options) {
|
|
|
2364
2370
|
// Admission binds BEFORE the SQL limit (the done page's own rule),
|
|
2365
2371
|
// and every row is re-proved against the ceiling before ranking.
|
|
2366
2372
|
const resultRow = (row) => {
|
|
2367
|
-
const assignment = assignmentOf(store, row.taskId, now,
|
|
2373
|
+
const assignment = assignmentOf(store, row.taskId, now, workAccess(), evidenceRoot);
|
|
2368
2374
|
return { ...row, proofReasons: row.runId === null ? [] : store.proofVerdictFor(row.runId)?.reasons ?? [], ciFailing: ciFailingFor(row.runId, row.prNumber),
|
|
2369
2375
|
assignment: assignment?.activeTaskId === row.taskId && (assignment.receipt?.runId ?? null) === row.runId ? assignment : null };
|
|
2370
2376
|
};
|
|
@@ -2387,11 +2393,14 @@ export function createDecisionServer(options) {
|
|
|
2387
2393
|
const selectedRow = wantedId === null ? ranked[0] ?? null : chosen;
|
|
2388
2394
|
const csrf = who.via === "cookie" ? who.session.csrf : "";
|
|
2389
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");
|
|
2390
2399
|
if (expectedRun !== null && (!/^[1-9]\d*$/.test(expectedRun) || selectedRow?.runId !== Number(expectedRun))) {
|
|
2391
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") }));
|
|
2392
2401
|
}
|
|
2393
2402
|
const selected = selectedRow === null ? null : reviewCockpitViewOf(selectedRow, who, now);
|
|
2394
|
-
const reviewPage = reviewCockpitPage(chromeFor(project, "runs"), {
|
|
2403
|
+
const reviewPage = reviewCockpitPage(chromeFor(wantedId === null ? project : chosenProject, "runs"), {
|
|
2395
2404
|
queue: ranked,
|
|
2396
2405
|
queueCap: REVIEW_QUEUE_CAP,
|
|
2397
2406
|
selected,
|
|
@@ -2859,7 +2868,7 @@ export function createDecisionServer(options) {
|
|
|
2859
2868
|
// v103: a task's evidence pack, as a printable page or JSON (the whole family, sealed ledger entries included).
|
|
2860
2869
|
const evidence = matchTaskPath(url.pathname, "/evidence$");
|
|
2861
2870
|
if (evidence !== null) {
|
|
2862
|
-
const access =
|
|
2871
|
+
const access = workAccess();
|
|
2863
2872
|
const family = store.taskFamilyOf(evidence.taskId, access.repos, access.includeUnplaced);
|
|
2864
2873
|
if (family === null || !visible(family.root.repo))
|
|
2865
2874
|
return refuse(response, who, 404, "no such task", "/tasks");
|
|
@@ -2883,7 +2892,7 @@ export function createDecisionServer(options) {
|
|
|
2883
2892
|
const to = new Date(Date.parse(last) + 86_400_000).toISOString();
|
|
2884
2893
|
if (to <= from || Date.parse(to) - Date.parse(from) > 366 * 86_400_000)
|
|
2885
2894
|
return refuse(response, who, 400, "Choose a range of a year or less, ending on or after its start.", "/ledger");
|
|
2886
|
-
const access =
|
|
2895
|
+
const access = workAccess();
|
|
2887
2896
|
store.recordAction({ at: now.toISOString(), actor: who.name, repo: null, taskId: null, runId: null, action: "ledger exported", outcome: "exported", source: "access",
|
|
2888
2897
|
detail: `${from.slice(0, 10)} to ${last.slice(0, 10)}` });
|
|
2889
2898
|
// A piece at a time (a page of entries, or one pack), yielding between them, and stopping if the reader's access
|
|
@@ -3017,6 +3026,14 @@ export function createDecisionServer(options) {
|
|
|
3017
3026
|
}
|
|
3018
3027
|
return respond(response, 200, "application/json", JSON.stringify({ text: window.text, nextOffset: window.nextOffset, final: !live && window.eof }));
|
|
3019
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
|
+
}
|
|
3020
3037
|
// Pollers exist only for a LIVE run — an orphaned null-outcome run
|
|
3021
3038
|
// would otherwise be refetched forever (finding 15). The nonce is
|
|
3022
3039
|
// sendScreen's business now.
|
|
@@ -3597,7 +3614,7 @@ export function createDecisionServer(options) {
|
|
|
3597
3614
|
if (repos.length > 0) {
|
|
3598
3615
|
try {
|
|
3599
3616
|
fleetSnapshot = store.chatSnapshot(repos, now);
|
|
3600
|
-
const summaries = workIndexPage(store, now, { principal: 'operator', repos, includeUnplaced: false }, { limit: 100 }).items;
|
|
3617
|
+
const summaries = workIndexPage(store, now, { principal: 'operator', repos, includeUnplaced: false, viewer: who.name }, { limit: 100 }).items;
|
|
3601
3618
|
fleetSnapshot.assignmentStates = Object.fromEntries(summaries.flatMap(value => [value.rootId, value.activeTaskId].map(id => [id, { state: value.assignmentState, label: value.status.label, detail: value.status.detail }])));
|
|
3602
3619
|
fleetSnapshot.attentionCount = needsYouBadge(null);
|
|
3603
3620
|
}
|
|
@@ -4891,8 +4908,6 @@ export function createDecisionServer(options) {
|
|
|
4891
4908
|
const pageHtml = s.body;
|
|
4892
4909
|
const extras = s.workspace ?? {};
|
|
4893
4910
|
const notices = [...(extras.notices ?? [])];
|
|
4894
|
-
if (s.chrome.demo)
|
|
4895
|
-
notices.unshift(DEMO_BANNER);
|
|
4896
4911
|
if (s.chrome.modeBanner)
|
|
4897
4912
|
notices.push(s.chrome.modeBanner.words);
|
|
4898
4913
|
if (s.chrome.updateWaiting)
|
|
@@ -4907,10 +4922,12 @@ export function createDecisionServer(options) {
|
|
|
4907
4922
|
notices.push('Crew updates are unavailable. Open Tasks to inspect saved work.');
|
|
4908
4923
|
}
|
|
4909
4924
|
// "Wake me only for these": the navigation carries the count of tasks
|
|
4910
|
-
// waiting on a person
|
|
4925
|
+
// waiting on a person behind its Tasks link — the open project's, or
|
|
4926
|
+
// everything this person may see — so it matches that page's Needs you tab.
|
|
4911
4927
|
let needsYou = 0;
|
|
4912
4928
|
try {
|
|
4913
|
-
needsYou = (requestFacts.workCounts ?? workCountsByProject(store, clock(), workAccess()))
|
|
4929
|
+
needsYou = (requestFacts.workCounts ?? workCountsByProject(store, clock(), workAccess()))
|
|
4930
|
+
.filter(one => s.chrome?.project == null || one.repo === s.chrome.project).reduce((sum, one) => sum + one.totals['needs-you'], 0);
|
|
4914
4931
|
}
|
|
4915
4932
|
catch {
|
|
4916
4933
|
needsYou = 0;
|
|
@@ -4947,6 +4964,7 @@ export function createDecisionServer(options) {
|
|
|
4947
4964
|
projects: browserProjectsOf(s.chrome.projects ?? []), ...crew,
|
|
4948
4965
|
conversation, ...(extras.team ? { team: extras.team } : {}), focus: extras.focus ?? null, result: extras.result ?? null,
|
|
4949
4966
|
catchUpHtml: extras.catchUpHtml ?? '', controlsHtml: extras.controlsHtml ?? '', notices, view: extras.view ?? null,
|
|
4967
|
+
...(s.chrome.demo ? { demo: { text: DEMO_BANNER, short: DEMO_BANNER_SHORT } } : {}),
|
|
4950
4968
|
pageHtml: extras.pageHtml === undefined ? (conversation === null ? pageHtml : null) : extras.pageHtml,
|
|
4951
4969
|
navigation: [...browserNavigationOf(currentPath, s.chrome.project, needsYou), { label: 'Workspace tools', href: '/menu', active: path.pathname === '/menu' }],
|
|
4952
4970
|
chats,
|
|
@@ -5013,7 +5031,7 @@ export function createDecisionServer(options) {
|
|
|
5013
5031
|
* phase, four counts, plan-window use (never dollars: subscriptions don't
|
|
5014
5032
|
* bill per run) and Catch up's items, each tagged with its tab. Reads only. */
|
|
5015
5033
|
function chatHomeOf(who, repos, now) {
|
|
5016
|
-
const access =
|
|
5034
|
+
const access = workAccess();
|
|
5017
5035
|
const admitted = (repo) => repo === null ? visible(null) : repos.includes(repo);
|
|
5018
5036
|
const agents = store.liveRuns(now).filter(run => admitted(run.repo)).map(run => {
|
|
5019
5037
|
const root = familyOf(run.taskId)?.root ?? null;
|
|
@@ -5042,8 +5060,13 @@ export function createDecisionServer(options) {
|
|
|
5042
5060
|
})(),
|
|
5043
5061
|
}));
|
|
5044
5062
|
const windows = who.via === "cookie" && store.isInstanceOperator(who.name) ? limitsView(store.providerLimits(), [], { project: projectName, teammate: id => `Teammate ${id}` }, now) : null;
|
|
5063
|
+
// One line: what this person's lead is doing now and when it last acted.
|
|
5064
|
+
const activity = leadActivity(store, who.name);
|
|
5065
|
+
const leadTask = activity?.taskId == null ? null : store.lookupRef(activity.taskId);
|
|
5066
|
+
const lead = activity === null ? null : { doing: activity.doing, at: activity.at,
|
|
5067
|
+
href: leadTask == null || !admitted(leadTask.repo) ? null : taskHref(familyOf(activity.taskId)?.root.id ?? activity.taskId) };
|
|
5045
5068
|
return {
|
|
5046
|
-
agents, counts, catchUp, allHref: "/work",
|
|
5069
|
+
agents, counts, catchUp, allHref: "/work", ...(lead === null ? {} : { lead }),
|
|
5047
5070
|
planUse: (windows?.tiles ?? []).map(one => ({ name: one.name, window: one.window, percent: one.percent, detail: one.detail, tone: one.tone })),
|
|
5048
5071
|
};
|
|
5049
5072
|
}
|
|
@@ -5141,7 +5164,8 @@ export function createDecisionServer(options) {
|
|
|
5141
5164
|
};
|
|
5142
5165
|
}
|
|
5143
5166
|
function workAccess() {
|
|
5144
|
-
|
|
5167
|
+
// The reader's own lead's claims read "Your lead is on it" (lead-voice.ts); nobody else's.
|
|
5168
|
+
return { principal: 'operator', repos: admissionList(), includeUnplaced: visible(null), viewer: requestContext.getStore()?.actor ?? null };
|
|
5145
5169
|
}
|
|
5146
5170
|
// All project counts are read together. No task artifacts or processes are
|
|
5147
5171
|
// inspected to paint a badge or a project switcher.
|
|
@@ -5272,7 +5296,7 @@ export function createDecisionServer(options) {
|
|
|
5272
5296
|
if (earlierLive.length > 0 && !status.views.includes("running"))
|
|
5273
5297
|
status.views = [...status.views, "running"];
|
|
5274
5298
|
const otherActive = new Set([...(task.family?.otherActive.map(one => one.id) ?? []), ...earlierLive]).size;
|
|
5275
|
-
const recordedAssignment = task.family === undefined ? null : assignmentOf(store, task.id, now,
|
|
5299
|
+
const recordedAssignment = task.family === undefined ? null : assignmentOf(store, task.id, now, workAccess(), evidenceRoot);
|
|
5276
5300
|
const assignment = freshAssignment(recordedAssignment, result ?? null);
|
|
5277
5301
|
const assignmentStatus = assignment === null ? status : assignmentStatusOf(assignment, status);
|
|
5278
5302
|
if (status.views.includes("running") && !assignmentStatus.views.includes("running"))
|
|
@@ -5498,7 +5522,7 @@ export function createDecisionServer(options) {
|
|
|
5498
5522
|
const planRevisions = ref === null ? null : revisionLedgerOf(ref.id);
|
|
5499
5523
|
const milestoneProgress = ref === null ? null : progressOf(ref.id, planRevisions?.current?.document ?? null);
|
|
5500
5524
|
const family = familyOf(taskId);
|
|
5501
|
-
const recordedAssignment = family?.current.id !== taskId ? null : assignmentOf(store, taskId, now,
|
|
5525
|
+
const recordedAssignment = family?.current.id !== taskId ? null : assignmentOf(store, taskId, now, workAccess(), evidenceRoot);
|
|
5502
5526
|
const assignment = freshAssignment(recordedAssignment, completion?.receipt ?? null);
|
|
5503
5527
|
const progressRun = runs.find(one => runIsLive(one))?.id ?? completion?.runId ?? null;
|
|
5504
5528
|
return {
|
|
@@ -5799,7 +5823,7 @@ export function createDecisionServer(options) {
|
|
|
5799
5823
|
title: family.root.title,
|
|
5800
5824
|
state: task.state,
|
|
5801
5825
|
status: view?.status ?? workRowOf({ ...task, repo: ref.repo }, now).status,
|
|
5802
|
-
assignment: view?.assignment ?? assignmentOf(store, task.id, now,
|
|
5826
|
+
assignment: view?.assignment ?? assignmentOf(store, task.id, now, workAccess(), evidenceRoot),
|
|
5803
5827
|
project: ref.repo === null ? null : projectName(ref.repo),
|
|
5804
5828
|
now,
|
|
5805
5829
|
dispatch: diagnoseTaskDispatch(store, taskId, now),
|
|
@@ -5827,6 +5851,7 @@ export function createDecisionServer(options) {
|
|
|
5827
5851
|
raceTerms: view.raceTerms ?? null,
|
|
5828
5852
|
revision: view.revision ?? null,
|
|
5829
5853
|
coordinator: view.coordinator ?? null,
|
|
5854
|
+
repairChain: view.repairChain ?? null,
|
|
5830
5855
|
},
|
|
5831
5856
|
decisions: (view?.decisions ?? store.decisionsForTask(ref.id))
|
|
5832
5857
|
.filter(one => one.state === "open" || one.state === "expired")
|
|
@@ -5864,7 +5889,7 @@ export function createDecisionServer(options) {
|
|
|
5864
5889
|
}
|
|
5865
5890
|
if (!publishing.on || who.via !== "cookie" || who.role !== "approver")
|
|
5866
5891
|
return null;
|
|
5867
|
-
const assignment = assignmentOf(store, taskId, clock(),
|
|
5892
|
+
const assignment = assignmentOf(store, taskId, clock(), workAccess(), evidenceRoot);
|
|
5868
5893
|
const receipt = assignment?.receipt ?? null;
|
|
5869
5894
|
if (assignment?.state !== "complete" || receipt === null || store.publicationForRun(receipt.runId) !== null || pullRequestBlocker(store, receipt.runId) !== null)
|
|
5870
5895
|
return null;
|
|
@@ -10964,7 +10989,7 @@ export function createDecisionServer(options) {
|
|
|
10964
10989
|
const completed = checkAssignmentAsOperator(store, taskId, digest, principal, now, evidenceRoot);
|
|
10965
10990
|
if (!completed.ok)
|
|
10966
10991
|
return taskScreen(response, who, taskId, completed.message, 409);
|
|
10967
|
-
return redirect(response, reviewHref(taskId, Number(namedRun)
|
|
10992
|
+
return redirect(response, reviewHref(taskId, Number(namedRun)));
|
|
10968
10993
|
}
|
|
10969
10994
|
case "retry-review": {
|
|
10970
10995
|
return taskScreen(response, who, taskId, "Separate agent reviews have retired. Open the saved result to mark it complete or request a revision.", 410);
|
|
@@ -11478,7 +11503,7 @@ export function createDecisionServer(options) {
|
|
|
11478
11503
|
run,
|
|
11479
11504
|
receipt,
|
|
11480
11505
|
assignment: (() => {
|
|
11481
|
-
const value = assignmentOf(store, taskId, now,
|
|
11506
|
+
const value = assignmentOf(store, taskId, now, workAccess(), evidenceRoot);
|
|
11482
11507
|
return value?.receipt?.runId === run.id && value.activeTaskId === taskId ? freshAssignment(value, receipt) : null;
|
|
11483
11508
|
})(),
|
|
11484
11509
|
handoff,
|
|
@@ -11486,6 +11511,7 @@ export function createDecisionServer(options) {
|
|
|
11486
11511
|
terminal,
|
|
11487
11512
|
publication,
|
|
11488
11513
|
pullRequestTo: pullRequestTargetOf(run.id),
|
|
11514
|
+
publishing: pullRequestTargetOf(run.id) !== null ? "pull-request" : publishingOf(store, store.refById(run.taskRef)?.repo ?? null).on ? "other" : "off",
|
|
11489
11515
|
ciFailing: publication !== null && publication.prNumber !== null && store.hasOpenCiEpisode(publication.githubRepo, publication.prNumber),
|
|
11490
11516
|
files,
|
|
11491
11517
|
outsideTouches: files.filter(one => one.outsideTouches).map(one => one.path),
|
|
@@ -11665,6 +11691,19 @@ export function createDecisionServer(options) {
|
|
|
11665
11691
|
});
|
|
11666
11692
|
});
|
|
11667
11693
|
// Keep native process custody until shutdown has verified agent/tool exit.
|
|
11694
|
+
// Coding custody does not depend on teammates or the lead's follow pass, so
|
|
11695
|
+
// it closes at once and never waits behind a model turn: a stop must release
|
|
11696
|
+
// the owner record well inside the service's exit window (Oct 2: deploys
|
|
11697
|
+
// over 0.9.11 found it still held by a killed process).
|
|
11698
|
+
let codingClosing = null;
|
|
11699
|
+
const closeCoding = () => {
|
|
11700
|
+
leadClosing = true;
|
|
11701
|
+
return codingClosing ??= Promise.resolve().then(() => coding?.close());
|
|
11702
|
+
};
|
|
11703
|
+
const bounded = (work) => new Promise(done => {
|
|
11704
|
+
const timer = setTimeout(done, SHUTDOWN_WAIT_MS);
|
|
11705
|
+
void Promise.resolve(work).catch(() => { }).finally(() => { clearTimeout(timer); done(); });
|
|
11706
|
+
});
|
|
11668
11707
|
const closeServer = server.close.bind(server);
|
|
11669
11708
|
server.close = ((callback) => {
|
|
11670
11709
|
leadClosing = true;
|
|
@@ -11675,7 +11714,7 @@ export function createDecisionServer(options) {
|
|
|
11675
11714
|
stream.end();
|
|
11676
11715
|
chatStreams.clear();
|
|
11677
11716
|
flowRooms.close();
|
|
11678
|
-
void (
|
|
11717
|
+
void Promise.all([closeCoding(), bounded(team.close()), bounded(leadMaintenance?.stop())]).then(() => closeServer(callback)).catch(error => {
|
|
11679
11718
|
if (callback)
|
|
11680
11719
|
callback(error instanceof Error ? error : Error('Coding session shutdown failed.'));
|
|
11681
11720
|
else
|
|
@@ -11683,7 +11722,7 @@ export function createDecisionServer(options) {
|
|
|
11683
11722
|
});
|
|
11684
11723
|
return server;
|
|
11685
11724
|
});
|
|
11686
|
-
return Object.assign(server, { mintSignInLink });
|
|
11725
|
+
return Object.assign(server, { mintSignInLink, closeCoding });
|
|
11687
11726
|
}
|
|
11688
11727
|
// ---- path plumbing ---------------------------------------------------------
|
|
11689
11728
|
/**
|
|
@@ -12103,6 +12142,199 @@ function acceptanceCeremonyHtml(criteria) {
|
|
|
12103
12142
|
.join("") +
|
|
12104
12143
|
`</ul>`);
|
|
12105
12144
|
}
|
|
12145
|
+
/** "claude" + "opus" → "Claude Opus"; "claude-opus-5-5" → "Claude Opus 5.5"; anything else keeps its exact id. */
|
|
12146
|
+
function agentNameWords(provider, model) {
|
|
12147
|
+
const name = providerName(provider);
|
|
12148
|
+
if (model === null || model === "" || model === "default")
|
|
12149
|
+
return name;
|
|
12150
|
+
const bare = model.startsWith(`${provider}-`) ? model.slice(provider.length + 1) : model;
|
|
12151
|
+
const title = (word) => `${word[0].toUpperCase()}${word.slice(1)}`;
|
|
12152
|
+
if (/^[a-z]+$/.test(bare))
|
|
12153
|
+
return `${name} ${title(bare)}`;
|
|
12154
|
+
const versioned = /^([a-z]+)-(\d+(?:-\d+)*)$/.exec(bare);
|
|
12155
|
+
if (versioned !== null)
|
|
12156
|
+
return `${name} ${title(versioned[1])} ${versioned[2].replace(/-/g, ".")}`;
|
|
12157
|
+
return `${name} ${model}`;
|
|
12158
|
+
}
|
|
12159
|
+
/** The permission a sealed profile grants, in two or three words. */
|
|
12160
|
+
function permissionWordsOf(profile) {
|
|
12161
|
+
if (profile === null || profile === undefined)
|
|
12162
|
+
return null;
|
|
12163
|
+
return profile.provider === "claude"
|
|
12164
|
+
? profile.permissionArgv === "bypassPermissions" ? "Full access" : "Auto permissions"
|
|
12165
|
+
: profile.provider === "gemini"
|
|
12166
|
+
? profile.approvalArgv === "yolo" ? "Full access" : "Auto permissions"
|
|
12167
|
+
: profile.sandboxMode === "danger-full-access" ? "Full access" : "Workspace sandbox";
|
|
12168
|
+
}
|
|
12169
|
+
/** One plain sentence for a revision (approval critique, Oct 2):
|
|
12170
|
+
* "Fixes what build #9 missed: <criterion>." The notes, paths and lineage
|
|
12171
|
+
* stay in the Details fold. */
|
|
12172
|
+
function revisionSentence(revision, acceptance) {
|
|
12173
|
+
const build = `build #${revision.sourceRun}`;
|
|
12174
|
+
if (revision.kind === "ci-repair")
|
|
12175
|
+
return `Fixes the checks that failed in ${build}.`;
|
|
12176
|
+
const clean = (text) => oneLineOf(text, 280).replace(/[.\s]+$/, "");
|
|
12177
|
+
if (revision.kind === "criterion-repair") {
|
|
12178
|
+
const ids = revision.comments.flatMap(one => (/Unmet: ([^.]+)\./.exec(one.note)?.[1] ?? "").split(",").map(id => id.trim()).filter(id => id !== ""));
|
|
12179
|
+
const missed = ids.map(id => acceptance.find(one => one.id === id)?.statement ?? null).filter((one) => one !== null);
|
|
12180
|
+
if (missed.length > 0)
|
|
12181
|
+
return `Fixes what ${build} missed: ${missed.map(clean).join("; ")}.`;
|
|
12182
|
+
}
|
|
12183
|
+
const notes = revision.comments.map(one => clean(one.note)).filter(one => one !== "");
|
|
12184
|
+
return notes.length === 0 ? `Revises ${build}.` : `Fixes what ${build} missed: ${notes.join("; ")}.`;
|
|
12185
|
+
}
|
|
12186
|
+
/** A planner's amendment, said plainly and kept in view: approving binds
|
|
12187
|
+
* the amended terms, so this is never folded away. */
|
|
12188
|
+
function approvalAmendmentHtml(view) {
|
|
12189
|
+
if (view === null || view === undefined || "problem" in view || !view.filed && view.revision !== true || view.changes.length === 0)
|
|
12190
|
+
return "";
|
|
12191
|
+
const line = (change) => {
|
|
12192
|
+
switch (change.field) {
|
|
12193
|
+
case "goal": return `Goal was: ${escape(change.before)}`;
|
|
12194
|
+
case "outOfScope": return change.after === null ? "No longer rules anything out" : change.before === null ? `Now rules out: ${escape(change.after)}` : `Won't touch was: ${escape(change.before)}`;
|
|
12195
|
+
case "touches": return `${change.kind === "added" ? "Adds the path" : "Drops the path"} <span class="mono">${escape(change.path)}</span>`;
|
|
12196
|
+
case "acceptance": return change.kind === "added" ? `Adds a check: ${escape(change.after?.statement ?? "")}` : change.kind === "removed" ? `Drops a check: ${escape(change.before?.statement ?? "")}` : `Rewords a check: ${escape(change.after?.statement ?? "")}`;
|
|
12197
|
+
}
|
|
12198
|
+
};
|
|
12199
|
+
const count = `${view.changes.length} change${view.changes.length === 1 ? "" : "s"}`;
|
|
12200
|
+
return (`<div class="approval-amendment" id="contract-amendment">` +
|
|
12201
|
+
`<p><strong>${view.revision === true ? `The plan makes ${count} from your notes` : `The plan makes ${count} to what you filed`}</strong></p>` +
|
|
12202
|
+
`<ul>${view.changes.map(one => `<li>${line(one)}</li>`).join("")}</ul>` +
|
|
12203
|
+
(view.amendment === null ? "" : `<p class="meta">Why: ${escape(view.amendment)}</p>`) +
|
|
12204
|
+
`</div>`);
|
|
12205
|
+
}
|
|
12206
|
+
/**
|
|
12207
|
+
* The approval sheet (approval critique, Oct 2): the plan open, in plain
|
|
12208
|
+
* rows — Goal, Changes, Won't touch, Done when — one line of who builds,
|
|
12209
|
+
* one Approve & start beside the password, and every remaining signed
|
|
12210
|
+
* term (lineage, routing, runtime limits, the seal) in one Details fold.
|
|
12211
|
+
* Presentation only: the same hidden fields, nonce, digest and password
|
|
12212
|
+
* post to the same route, and the task page and chat card share it.
|
|
12213
|
+
*/
|
|
12214
|
+
function approvalSheetHtml(input) {
|
|
12215
|
+
const { scope, route, raceTerms } = input;
|
|
12216
|
+
// A machine-drafted repair appends its brief to the signed goal; the row
|
|
12217
|
+
// shows the goal and one sentence says what the repair fixes. The whole
|
|
12218
|
+
// signed goal stays in Details.
|
|
12219
|
+
const repairText = / — (repair exactly the unmet criteria named below; a comment cannot widen the scope\. Unmet: ([^.]+)\.|Collect only the missing observations for ([^.]+)\. .*|Diagnose the saved failed project check and repair within the original scope\. .*)$/s.exec(scope.goal);
|
|
12220
|
+
const goalShown = repairText === null ? scope.goal : scope.goal.slice(0, repairText.index);
|
|
12221
|
+
const statementsOf = (ids) => ids.split(",").map(id => id.trim()).map(id => scope.acceptance.find(one => one.id === id)?.statement ?? id).map(one => one.replace(/[.\s]+$/, "")).join("; ");
|
|
12222
|
+
const repairBuild = input.revision?.sourceRun ?? input.repairChain?.sourceRun ?? null;
|
|
12223
|
+
const builtBy = repairBuild === null ? "the last build" : `build #${repairBuild}`;
|
|
12224
|
+
const sentence = input.revision !== null && (repairText === null || input.revision.comments.length > 0) ? revisionSentence(input.revision, scope.acceptance)
|
|
12225
|
+
: repairText === null ? null
|
|
12226
|
+
: repairText[2] !== undefined ? `Fixes what ${builtBy} missed: ${statementsOf(repairText[2])}.`
|
|
12227
|
+
: repairText[3] !== undefined ? `Collects the evidence ${builtBy} was missing for: ${statementsOf(repairText[3])}. The saved code stays the same.`
|
|
12228
|
+
: `Fixes the checks that failed in ${builtBy}.`;
|
|
12229
|
+
const plan = input.planDocument === null ? null : parseExecutionPlanDocument(input.planDocument);
|
|
12230
|
+
const milestones = plan !== null && plan.ok ? plan.document.milestones : [];
|
|
12231
|
+
const permission = permissionWordsOf(scope.profile);
|
|
12232
|
+
const money = (micro) => `$${(micro / 1_000_000).toFixed(2)}`;
|
|
12233
|
+
const row = (label, body) => `<div class="approval-row"><dt>${label}</dt><dd>${body}</dd></div>`;
|
|
12234
|
+
const changes = (milestones.length > 0 ? `<ol class="approval-steps">${milestones.map(one => `<li>${escape(one)}</li>`).join("")}</ol>` : "") +
|
|
12235
|
+
(scope.touches.length > 0
|
|
12236
|
+
? `<p>Only in these paths:</p>${approvalPathsHtml(scope.touches)}`
|
|
12237
|
+
: `<p${milestones.length > 0 ? ` class="meta"` : ""}>Any file in the project.</p>`);
|
|
12238
|
+
const doneWhen = scope.acceptance.length === 0
|
|
12239
|
+
? `<p>You decide when you review the result.</p>`
|
|
12240
|
+
: `<ul class="approval-done">${scope.acceptance.map(one => `<li>${escape(one.statement)}</li>`).join("")}</ul>`;
|
|
12241
|
+
// Who builds, in one line: the builder and planner by name, the cap, and full access when granted.
|
|
12242
|
+
const legs = route?.projection?.legs ?? [];
|
|
12243
|
+
const legOf = (phase) => legs.find(one => one.phase === phase);
|
|
12244
|
+
const who = [];
|
|
12245
|
+
const builder = legOf("build");
|
|
12246
|
+
if (raceTerms === null) {
|
|
12247
|
+
if (builder !== undefined)
|
|
12248
|
+
who.push(`Builder ${agentNameWords(builder.provider, builder.model)}`);
|
|
12249
|
+
else if (route?.legacy != null)
|
|
12250
|
+
who.push(`Builder ${agentNameWords(route.legacy.provider, route.legacy.model)}`);
|
|
12251
|
+
else if (scope.profile != null)
|
|
12252
|
+
who.push(`Builder ${agentNameWords(scope.profile.provider, scope.profile.model)}`);
|
|
12253
|
+
}
|
|
12254
|
+
const planner = legOf("plan");
|
|
12255
|
+
if (planner !== undefined)
|
|
12256
|
+
who.push(`Planner ${agentNameWords(planner.provider, planner.model)}`);
|
|
12257
|
+
if (scope.budgetMicrousd !== null)
|
|
12258
|
+
who.push(`Cap ${money(scope.budgetMicrousd)} per attempt`);
|
|
12259
|
+
if (permission === "Full access")
|
|
12260
|
+
who.push("Full access");
|
|
12261
|
+
const race = raceTerms === null
|
|
12262
|
+
? ""
|
|
12263
|
+
: `<p class="approval-race">${raceTerms.n} agents build this separately: ${raceTerms.agents.map(one => escape(agentNameWords(one.provider, one.model))).join(" and ")}. ` +
|
|
12264
|
+
(raceTerms.kind === "comparison"
|
|
12265
|
+
? "No dollar cap; you compare the results and pick one.</p>"
|
|
12266
|
+
: `Each may spend ${money(raceTerms.perAgentBudgetMicrousd)} plus a ${money(raceTerms.overrunReserveMicrousd)} reserve, ${money(raceTerms.totalBudgetMicrousd)} in all; you compare the results and pick one.</p>`);
|
|
12267
|
+
const after = input.deliverable === "report"
|
|
12268
|
+
? "An agent investigates without changing the repository. You'll hear when its report is ready."
|
|
12269
|
+
: raceTerms !== null
|
|
12270
|
+
? "Each agent starts in its own branch. You'll hear when the results are ready to compare."
|
|
12271
|
+
: "An agent starts in its own branch. You'll hear when it's ready to review.";
|
|
12272
|
+
const submit = raceTerms === null ? "Approve & start" : raceTerms.kind === "comparison" ? "Approve comparison" : "Approve tournament";
|
|
12273
|
+
// Everything else the digest binds, one tap away.
|
|
12274
|
+
const detail = (title, body, attrs = "") => body === "" ? "" : `<section class="approval-detail"${attrs}><h3>${title}</h3>${body}</section>`;
|
|
12275
|
+
const revisionDetail = input.revision === null ? "" : detail("Earlier build", `<div class="revision-card" data-revision-feedback>` +
|
|
12276
|
+
`<p><a href="${escape(input.revisionSourceHref)}"${input.surface === "chat" ? " data-revision-source" : ""}>${input.surface === "chat" ? `Original result: build #${input.revision.sourceRun} →` : `build #${input.revision.sourceRun}`}</a></p>` +
|
|
12277
|
+
(input.revision.comments.length === 0 ? "" : `<ul>${input.revision.comments.map(one => `<li>${one.path === null ? "" : `<span class="mono">${escape(one.path)}${one.line === null ? "" : `:${one.line}`}</span> · `}${escape(one.note)} <span class="meta">— ${escape(one.author)}</span></li>`).join("")}</ul>`) +
|
|
12278
|
+
revisionLineageHtml(input.revision.lineage) +
|
|
12279
|
+
`</div>`);
|
|
12280
|
+
const contract = input.planContract;
|
|
12281
|
+
const contractDetail = input.planDocument === null || approvalAmendmentHtml(contract) !== "" ? ""
|
|
12282
|
+
: contract === null || contract === undefined ? `<p class="meta">No record compares this plan with what was filed. Check the rows above against your request.</p>`
|
|
12283
|
+
: "problem" in contract ? `<p class="meta">${escape(contract.problem)} · <a href="/r/${contract.run}">run ${contract.run}</a></p>`
|
|
12284
|
+
: !contract.filed && contract.revision !== true ? `<p class="meta">Nothing was filed before planning, so every term is the planner's proposal.</p>`
|
|
12285
|
+
: `<p class="meta">${contract.revision === true ? "Same terms as the previous version." : "The plan keeps exactly what you filed."} <a href="/r/${contract.run}">Planning run ${contract.run}</a></p>`;
|
|
12286
|
+
const criteriaDetail = scope.acceptance.length === 0 ? ""
|
|
12287
|
+
: `<ul class="approval-signed-criteria">${scope.acceptance.map(one => `<li><code>${escape(one.id)}</code> ${escape(one.statement)} <span class="meta">needs ${one.evidence.map(escape).join(", ")}</span></li>`).join("")}</ul>`;
|
|
12288
|
+
const projection = route?.projection ?? null;
|
|
12289
|
+
const agentsDetail = route === null || route === undefined ? ""
|
|
12290
|
+
: projection === null
|
|
12291
|
+
? route.legacy === null ? "" : `<p>${escape(agentsSummaryWords(route))}</p>`
|
|
12292
|
+
: `<p>${escape(projection.summary)}</p>` +
|
|
12293
|
+
`<p class="meta">${escape(riskTitle(route.riskLevel))}: ${escape(riskConsequence(route.riskLevel))}. Uses ${escape(projection.postureWords)}.</p>` +
|
|
12294
|
+
(projection.demands.length === 0 ? "" : `<ul class="meta">${projection.demands.map(one => `<li>${escape(one)}</li>`).join("")}</ul>`) +
|
|
12295
|
+
`<dl class="approval-roles">${projection.legs.map(leg => `<div><dt>${escape(ROLE_NOUN[leg.phase])}</dt><dd><span class="mono">${escape(leg.provider)} · ${escape(leg.model)}</span> <span class="meta">${escape(chosenWords(leg))}</span>` +
|
|
12296
|
+
`<ul>${leg.reasons.map(reason => `<li>${escape(reason)}</li>`).join("")}${leg.problem === null ? "" : `<li><strong>${escape(leg.problem)}</strong></li>`}</ul></dd></div>`).join("")}</dl>` +
|
|
12297
|
+
`<p class="meta">These exact agents are part of what you approve. Changing any of them asks for a fresh approval.</p>`;
|
|
12298
|
+
const limits = profileWords(scope) +
|
|
12299
|
+
`<p class="meta">Checks level: ${escape(qualityModeTitle(scope.qualityMode ?? "default"))}${permission === null ? "" : ` · ${escape(permission)}`}</p>` +
|
|
12300
|
+
(scope.budgetMicrousd === null ? "" : `<p class="meta">Each build attempt has a ${money(scope.budgetMicrousd)} agent-reported usage cap. On a subscription this limits work; it is not an API charge.</p>`);
|
|
12301
|
+
const details = `<details class="approval-details"><summary>Details</summary>` +
|
|
12302
|
+
revisionDetail +
|
|
12303
|
+
(repairText === null ? "" : detail("Signed goal", `<p>${escape(scope.goal)}</p>`)) +
|
|
12304
|
+
detail("Plan record", contractDetail) +
|
|
12305
|
+
detail("Signed criteria", criteriaDetail) +
|
|
12306
|
+
detail("Why these agents", agentsDetail, ` id="${input.surface === "task" ? "approval-agents" : "chat-approval-agents"}"`) +
|
|
12307
|
+
detail("Runtime limits", limits) +
|
|
12308
|
+
(scope.candidate ? detail("Saved commit", `<p><code class="approval-commit">${escape(scope.candidate)}</code></p>`) : "") +
|
|
12309
|
+
detail("Seal", `<p class="meta">Approval binds to this exact wording. <span class="seal mono">signs ${shortDigest(scope.digest)}</span></p>`) +
|
|
12310
|
+
`</details>`;
|
|
12311
|
+
return (`<form method="post" action="${input.action}" class="approve-form approval-sheet"${input.surface === "task" ? ` id="approve"` : ""}${input.sticky ? " data-sticky" : ""}>` +
|
|
12312
|
+
`<input type="hidden" name="csrf" value="${escape(input.csrf)}">` +
|
|
12313
|
+
`<input type="hidden" name="nonce" value="${escape(input.nonce)}">` +
|
|
12314
|
+
`<input type="hidden" name="digest" value="${escape(input.digest)}">` +
|
|
12315
|
+
(input.returnTo === null ? "" : `<input type="hidden" name="return" value="${escape(input.returnTo)}">`) +
|
|
12316
|
+
`<input type="text" name="username" autocomplete="username" class="visually-hidden" tabindex="-1" aria-hidden="true">` +
|
|
12317
|
+
`<h2 class="approval-sheet-title">${input.deliverable === "report" ? "The investigation" : "The plan"}</h2>` +
|
|
12318
|
+
(sentence === null ? "" : `<p class="approval-revision">${escape(sentence)}</p>`) +
|
|
12319
|
+
(input.coordinator === null ? "" : `<p class="approval-note">An agent filed this: <span class="mono">${escape(input.coordinator.label)}</span>${input.coordinator.filedAgo === null ? "" : `, ${escape(input.coordinator.filedAgo)}`}. Nothing runs until you approve, and approving runs its request.</p>`) +
|
|
12320
|
+
(input.deliverable === "report" ? `<p class="approval-note">Read-only: it reports back and changes nothing in the repository.</p>` : "") +
|
|
12321
|
+
`<dl class="approval-rows">` +
|
|
12322
|
+
row("Goal", `<p class="approval-goal">${escape(goalShown)}</p>`) +
|
|
12323
|
+
row("Changes", changes) +
|
|
12324
|
+
row("Won’t touch", `<p>${scope.outOfScope === null ? "Nothing is ruled out." : escape(scope.outOfScope)}</p>`) +
|
|
12325
|
+
row("Done when", doneWhen) +
|
|
12326
|
+
`</dl>` +
|
|
12327
|
+
approvalAmendmentHtml(contract) +
|
|
12328
|
+
race +
|
|
12329
|
+
(who.length === 0 ? "" : `<p class="approval-who">${who.map(escape).join(" · ")}</p>`) +
|
|
12330
|
+
`<div class="approval-act" id="${input.surface === "task" ? "approval-confirm" : "chat-approval-confirm"}">` +
|
|
12331
|
+
`<label class="approval-password"><span class="visually-hidden">Your password</span><input type="password" name="token" autocomplete="current-password" placeholder="Your password"></label>` +
|
|
12332
|
+
`<button type="submit" data-primary-action>${submit}</button></div>` +
|
|
12333
|
+
`<p class="approval-after">${after}</p>` +
|
|
12334
|
+
`<p class="approval-secondary"><a class="approval-link" href="${escape(input.editHref)}">Edit plan</a><a class="approval-link" href="${escape(input.notNowHref)}">Not now</a></p>` +
|
|
12335
|
+
details +
|
|
12336
|
+
`</form>`);
|
|
12337
|
+
}
|
|
12106
12338
|
function consentDoorOf(scope, route) {
|
|
12107
12339
|
if (scope === null)
|
|
12108
12340
|
return { open: false, title: "There is no scope to approve yet", why: "write the scope first", road: "scope" };
|
|
@@ -12374,6 +12606,8 @@ function agentsStripHtml(view, taskId) {
|
|
|
12374
12606
|
}
|
|
12375
12607
|
/** The demo's one-line promise, on every page. */
|
|
12376
12608
|
const DEMO_BANNER = `Demo: a scripted lead and sample projects. Nothing calls a model, reaches outside or spends. For your own project, run ${START_COMMAND} in its folder.`;
|
|
12609
|
+
/** The demo notice on a phone: one line. */
|
|
12610
|
+
const DEMO_BANNER_SHORT = "Demo: sample projects. Nothing calls a model or spends.";
|
|
12377
12611
|
/** Every character that could open a tag or an attribute, dead at the sink. */
|
|
12378
12612
|
function escape(text) {
|
|
12379
12613
|
return text
|
|
@@ -12488,7 +12722,7 @@ ${THEME_MAPPING}
|
|
|
12488
12722
|
--font-mono: "Geist Mono", ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
|
12489
12723
|
/* Phone spacing steps (760px and narrower): side gutter, gap under the
|
|
12490
12724
|
header, gap between page blocks, between rows, and inside a card. */
|
|
12491
|
-
--so-phone-gutter:
|
|
12725
|
+
--so-phone-gutter: 16px; --so-phone-gap: 12px; --so-phone-block: 14px; --so-phone-row: 8px; --so-phone-card: 12px;
|
|
12492
12726
|
}
|
|
12493
12727
|
@media (prefers-color-scheme: dark) {
|
|
12494
12728
|
:root:not([data-theme="light"]) {
|
|
@@ -12864,7 +13098,7 @@ ${THEME_DARK}
|
|
|
12864
13098
|
/* Thumbnails, never a full-width poster: auto-fill leaves a lone
|
|
12865
13099
|
screenshot at thumbnail size (UI polish 2026-09-13). */
|
|
12866
13100
|
.receipt-visuals { display: grid; grid-template-columns: repeat(auto-fill, minmax(8rem, 14rem)); gap: .55rem; margin-top: .75rem; }
|
|
12867
|
-
.receipt-shot { display: grid; gap: .35rem; color: var(--muted-foreground); font-size: .
|
|
13101
|
+
.receipt-shot { display: grid; gap: .35rem; color: var(--muted-foreground); font-size: .75rem; text-decoration: none; }
|
|
12868
13102
|
.receipt-shot img { display: block; width: 100%; aspect-ratio: 16 / 10; object-fit: cover; border: 1px solid var(--glass-border); border-radius: calc(var(--radius) - 3px); background: var(--muted); }
|
|
12869
13103
|
@media (hover: hover) and (pointer: fine) { .receipt-shot:hover { color: var(--foreground); } }
|
|
12870
13104
|
.receipt-caveats, .receipt-coverage { margin-top: .8rem; padding: .7rem .8rem; border-left: 1px solid var(--border); border-radius: 0 calc(var(--radius) - 3px) calc(var(--radius) - 3px) 0; background: color-mix(in srgb, var(--muted) 62%, transparent); font-size: .78rem; }
|
|
@@ -12997,7 +13231,7 @@ ${THEME_DARK}
|
|
|
12997
13231
|
}
|
|
12998
13232
|
.diff-file[open] > summary::before { transform: rotate(45deg) translateY(-.1rem); }
|
|
12999
13233
|
.diff-file-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font: 500 .75rem/1.4 var(--font-mono); }
|
|
13000
|
-
.diff-file-counts { display: inline-flex; gap: .45rem; flex: none; margin-left: auto; font: 500 .
|
|
13234
|
+
.diff-file-counts { display: inline-flex; gap: .45rem; flex: none; margin-left: auto; font: 500 .75rem/1 var(--font-mono); }
|
|
13001
13235
|
.diff-file-counts b { color: var(--success); font-weight: 500; }
|
|
13002
13236
|
.diff-file-counts i { color: var(--destructive); font-style: normal; }
|
|
13003
13237
|
.diff-rename { margin: 0; padding: .4rem .85rem; border-top: 1px solid var(--glass-border); }
|
|
@@ -13005,12 +13239,12 @@ ${THEME_DARK}
|
|
|
13005
13239
|
.diff-hunk-head {
|
|
13006
13240
|
overflow-x: auto; padding: .42rem .85rem; border-top: 1px solid var(--glass-border); border-bottom: 1px solid var(--glass-border);
|
|
13007
13241
|
background: color-mix(in srgb, var(--running) 8%, var(--card)); color: color-mix(in srgb, var(--running) 72%, var(--foreground));
|
|
13008
|
-
font: 500 .
|
|
13242
|
+
font: 500 .75rem/1.4 var(--font-mono); white-space: pre;
|
|
13009
13243
|
}
|
|
13010
13244
|
.diff-lines { max-width: 100%; overflow-x: auto; background: color-mix(in srgb, var(--background) 48%, var(--card)); }
|
|
13011
13245
|
.diff-line {
|
|
13012
13246
|
display: grid; grid-template-columns: 2rem 3.2rem 3.2rem minmax(max-content, 1fr); align-items: stretch;
|
|
13013
|
-
min-width: max-content; min-height: 1.8rem; font: 400 .
|
|
13247
|
+
min-width: max-content; min-height: 1.8rem; font: 400 .75rem/1.55 var(--font-mono);
|
|
13014
13248
|
}
|
|
13015
13249
|
@media (hover: hover) and (pointer: fine) { .diff-line:hover { background: color-mix(in srgb, var(--foreground) 4%, transparent); } }
|
|
13016
13250
|
.diff-line code { display: flex; min-width: 0; padding: .3rem .75rem .3rem .6rem; color: inherit; white-space: pre; }
|
|
@@ -13548,7 +13782,9 @@ ${THEME_DARK}
|
|
|
13548
13782
|
.approval-title { display: grid; gap: .15rem; }
|
|
13549
13783
|
.approval-kicker { color: var(--muted-foreground); font: 500 .66rem/1.3 var(--font-mono); letter-spacing: .06em; text-transform: uppercase; }
|
|
13550
13784
|
.approval-title strong { font-size: 1.05rem; letter-spacing: -.02em; }
|
|
13551
|
-
.approval-label { margin: .9rem 0 .2rem; color: var(--muted-foreground); font: 500
|
|
13785
|
+
.approval-label { display: block; margin: .9rem 0 .2rem; color: var(--muted-foreground); font: 500 12px/1.4 var(--font-sans); }
|
|
13786
|
+
.approval-label::first-letter, .planner-plan .eyebrow::first-letter { text-transform: uppercase; }
|
|
13787
|
+
.planner-plan .eyebrow { font: 500 12px/1.4 var(--font-sans); letter-spacing: 0; text-transform: none; }
|
|
13552
13788
|
.approval-goal { margin: 0; font-size: 1rem; line-height: 1.55; white-space: pre-wrap; }
|
|
13553
13789
|
.approval-boundaries { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .6rem; margin-top: .75rem; }
|
|
13554
13790
|
.approval-boundary { padding: .7rem .8rem; border: 1px solid var(--glass-border); border-radius: calc(var(--radius) - 3px); background: color-mix(in srgb, var(--muted) 45%, transparent); }
|
|
@@ -13634,6 +13870,66 @@ ${THEME_DARK}
|
|
|
13634
13870
|
.approval-confirm { scroll-margin-top: 5rem; }
|
|
13635
13871
|
.approval-paths { margin: 0; padding: 0; list-style: none; display: grid; gap: .15rem; }
|
|
13636
13872
|
.approval-paths li { color: var(--muted-foreground); font-size: .8rem; overflow-wrap: anywhere; }
|
|
13873
|
+
/* The approval sheet (approval critique, Oct 2): the plan open in plain
|
|
13874
|
+
rows, who builds in one line, one Approve & start beside the password,
|
|
13875
|
+
and every other signed term in one Details fold at the end. Sentence
|
|
13876
|
+
case, Geist, 12px and up; prose capped at 75ch. */
|
|
13877
|
+
.approval-sheet { display: grid; gap: 16px; margin: 0; padding: 20px; border: 1px solid var(--border); border-radius: 10px; background: var(--card); }
|
|
13878
|
+
.approval-sheet.approve-form > :not(input) { margin: 0; }
|
|
13879
|
+
.approval-sheet-title { margin: 0; font-size: 15px; font-weight: 600; line-height: 1.4; letter-spacing: -.01em; }
|
|
13880
|
+
.approval-revision, .approval-note, .approval-race { max-width: 75ch; font-size: 14px; line-height: 1.6; overflow-wrap: anywhere; }
|
|
13881
|
+
.approval-note { color: var(--muted-foreground); }
|
|
13882
|
+
.approval-rows { display: grid; gap: 14px; margin: 0; }
|
|
13883
|
+
.approval-row { display: grid; grid-template-columns: 7.5rem minmax(0, 1fr); gap: 16px; }
|
|
13884
|
+
.approval-row dt { margin: 0; color: var(--muted-foreground); font-size: 13px; font-weight: 500; line-height: 1.6; }
|
|
13885
|
+
.approval-row dd { min-width: 0; max-width: 75ch; margin: 0; font-size: 14px; line-height: 1.6; overflow-wrap: anywhere; }
|
|
13886
|
+
.approval-row dd p, .approval-sheet .approval-goal { margin: 0; font-size: 14px; line-height: 1.6; }
|
|
13887
|
+
.approval-row dd :is(ol, ul) { display: grid; gap: 4px; margin: 0; padding-left: 1.25rem; }
|
|
13888
|
+
.approval-row dd ol + p { margin-top: 6px; }
|
|
13889
|
+
.approval-row dd .approval-paths { margin-top: 2px; padding-left: 0; }
|
|
13890
|
+
.approval-row dd .approval-paths li { font-size: 13px; }
|
|
13891
|
+
.approval-amendment { max-width: 75ch; padding: 12px 14px; border-radius: 8px; background: var(--muted); font-size: 14px; line-height: 1.55; overflow-wrap: anywhere; }
|
|
13892
|
+
.approval-amendment p { margin: 0; }
|
|
13893
|
+
.approval-amendment ul { display: grid; gap: 4px; margin: 6px 0; padding-left: 1.25rem; }
|
|
13894
|
+
.approval-who { max-width: 75ch; color: var(--muted-foreground); font-size: 13px; line-height: 1.5; }
|
|
13895
|
+
.approval-act { display: grid; grid-template-columns: minmax(0, 20rem) auto; justify-content: start; align-items: center; gap: 8px; }
|
|
13896
|
+
.approval-password { margin: 0; }
|
|
13897
|
+
.approval-act input[type=password] { margin: 0; font-family: var(--font-sans); }
|
|
13898
|
+
.approval-act button { margin: 0; min-height: 2.125rem; white-space: nowrap; }
|
|
13899
|
+
.approval-after { max-width: 75ch; color: var(--muted-foreground); font-size: 13px; line-height: 1.5; }
|
|
13900
|
+
.approval-secondary { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
13901
|
+
.approval-link { display: inline-flex; align-items: center; min-height: 32px; padding: 0 12px; border: 1px solid var(--border); border-radius: 8px; background: var(--card); color: var(--foreground); font-size: 13px; font-weight: 500; text-decoration: none; }
|
|
13902
|
+
.approval-link + .approval-link { border-color: transparent; background: none; color: var(--muted-foreground); }
|
|
13903
|
+
@media (hover: hover) and (pointer: fine) { .approval-link:hover { background: var(--muted); color: var(--foreground); } }
|
|
13904
|
+
details.approval-details { margin: 0; padding: 0; border: 0; border-top: 1px solid var(--border); border-radius: 0; background: none; }
|
|
13905
|
+
details.approval-details[open] { padding-bottom: 0; }
|
|
13906
|
+
.approval-details > summary { display: flex; align-items: center; min-height: 40px; padding: 4px 0 0; font-size: 13px; }
|
|
13907
|
+
.approval-detail { max-width: 75ch; margin: 12px 0 0; font-size: 13px; line-height: 1.55; overflow-wrap: anywhere; }
|
|
13908
|
+
.approval-detail h3 { margin: 0 0 4px; font-size: 13px; font-weight: 600; letter-spacing: 0; text-transform: none; }
|
|
13909
|
+
.approval-detail p, .approval-detail ul, .approval-detail dl { margin: 4px 0; }
|
|
13910
|
+
.approval-detail ul { padding-left: 1.25rem; }
|
|
13911
|
+
.approval-detail .revision-card { margin: 0; padding: 0; border: 0; background: none; }
|
|
13912
|
+
.approval-roles { display: grid; gap: 8px; }
|
|
13913
|
+
.approval-roles > div { display: grid; grid-template-columns: 6rem minmax(0, 1fr); gap: 12px; }
|
|
13914
|
+
.approval-roles dt { color: var(--muted-foreground); font-weight: 500; }
|
|
13915
|
+
.approval-roles dd { margin: 0; }
|
|
13916
|
+
.approval-roles dd ul { margin: 2px 0 0; color: var(--muted-foreground); }
|
|
13917
|
+
.chat-plan .approval-sheet { padding: 0; border: 0; background: none; }
|
|
13918
|
+
@media (max-width: 760px) {
|
|
13919
|
+
.approval-sheet { gap: 12px; padding: 14px; }
|
|
13920
|
+
.approval-row { grid-template-columns: 1fr; gap: 2px; }
|
|
13921
|
+
.approval-act { grid-template-columns: 1fr; }
|
|
13922
|
+
.approval-act input[type=password], .approval-act button { min-height: 44px; font-size: 16px; }
|
|
13923
|
+
.approval-act button { width: 100%; }
|
|
13924
|
+
/* The one act stays under the thumb: sticky at the bottom, clear of the home indicator. */
|
|
13925
|
+
.approval-sheet[data-sticky] .approval-act {
|
|
13926
|
+
position: sticky; bottom: 0; z-index: 20; margin: 0 -14px; padding: 10px 14px calc(10px + env(safe-area-inset-bottom, 0px));
|
|
13927
|
+
border-top: 1px solid var(--border); background: var(--card);
|
|
13928
|
+
}
|
|
13929
|
+
.approval-link { min-height: 44px; padding: 0 16px; }
|
|
13930
|
+
.approval-details > summary { min-height: 44px; }
|
|
13931
|
+
.approval-roles > div { grid-template-columns: 1fr; gap: 2px; }
|
|
13932
|
+
}
|
|
13637
13933
|
.run-facts-details { margin-top: 1.25rem; }
|
|
13638
13934
|
.run-facts-details > summary { display: flex; align-items: center; justify-content: space-between; gap: .75rem; }
|
|
13639
13935
|
.run-facts-details > summary .meta { font-size: .75rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
@@ -13867,7 +14163,7 @@ ${THEME_DARK}
|
|
|
13867
14163
|
.result-pin[open] > summary::before { content: "▾"; }
|
|
13868
14164
|
.result-pin > summary .meta { font-weight: 400; }
|
|
13869
14165
|
.result-pin .diff-comment-target { margin: .25rem 0 .5rem; }
|
|
13870
|
-
.
|
|
14166
|
+
.revision-card, .revision-lineage, .task-history, .diff-comment p { min-width: 0; overflow-wrap: anywhere; }
|
|
13871
14167
|
.result-feedback-history > summary, .task-history > summary, .task-history a { display: inline-flex; align-items: center; min-height: 44px; padding: .25rem .5rem; }
|
|
13872
14168
|
.task-history > summary::before { content: "▸"; margin-right: .35rem; }
|
|
13873
14169
|
.task-history[open] > summary::before { content: "▾"; }
|
|
@@ -14332,11 +14628,10 @@ ${THEME_DARK}
|
|
|
14332
14628
|
.check-progress[data-final="passed"] { color: var(--success); }
|
|
14333
14629
|
.check-progress[data-final="failed"] { color: var(--destructive); }
|
|
14334
14630
|
.check-progress[data-final="unknown"] { color: var(--warning); }
|
|
14335
|
-
.receipt-actions [data-primary-action]
|
|
14631
|
+
.receipt-actions [data-primary-action] { min-height: 44px; }
|
|
14336
14632
|
.task-status-reason { margin-top: .75rem; }
|
|
14337
14633
|
.task-status-reason summary, .task-status-details > summary { color: var(--muted-foreground); }
|
|
14338
14634
|
.task-status-details > summary { min-height: 44px; }
|
|
14339
|
-
.task-plan-review > summary { min-height: 44px; display: flex; align-items: center; cursor: pointer; font-weight: 650; }
|
|
14340
14635
|
.task-status-details, #task-control-details { margin: 1rem 0; }
|
|
14341
14636
|
.task-control-copy { min-width: 0; overflow-wrap: anywhere; }
|
|
14342
14637
|
[data-primary-action] { max-width: 100%; }
|
|
@@ -14351,16 +14646,9 @@ ${THEME_DARK}
|
|
|
14351
14646
|
.chat-action-card > summary strong { font-size: .92rem; }
|
|
14352
14647
|
.chat-action-card > summary small { color: var(--muted-foreground); font-size: .68rem; font-weight: 400; }
|
|
14353
14648
|
.chat-action-card[open] > summary { border-bottom: 1px solid var(--border); }
|
|
14354
|
-
.chat-approval-form { display: grid; gap: .85rem; padding: 1.05rem; }
|
|
14355
|
-
.chat-approval-section { display: grid; gap: .3rem; }
|
|
14356
|
-
.chat-approval-section > p, .chat-approval-section > pre { margin: 0; }
|
|
14357
|
-
.chat-approval-form .approval-boundaries { margin: 0; }
|
|
14358
14649
|
.chat-run-details { padding: .65rem .75rem; border: 1px solid var(--border); border-radius: calc(var(--radius) - 3px); background: color-mix(in srgb,var(--muted) 45%,transparent); }
|
|
14359
14650
|
.chat-run-details > summary { color: var(--muted-foreground); cursor: pointer; font-size: .68rem; }
|
|
14360
14651
|
.chat-run-details > .meta { margin-bottom: 0; }
|
|
14361
|
-
.chat-approval-form .approval-confirm { align-items: end; margin-top: .15rem; }
|
|
14362
|
-
.chat-approval-form .approval-confirm label { flex: 1 1 18rem; }
|
|
14363
|
-
.chat-approval-form .approval-confirm button { min-height: 2.65rem; }
|
|
14364
14652
|
.chat-decisions { display: grid; gap: .7rem; }
|
|
14365
14653
|
.chat-section-head { margin: .15rem .15rem 0; }
|
|
14366
14654
|
.chat-section-head h2 { margin: .2rem 0 0; font-size: 1rem; }
|
|
@@ -14557,18 +14845,9 @@ ${THEME_DARK}
|
|
|
14557
14845
|
/* The concise plan (package 2): a title, the outcome, one line of
|
|
14558
14846
|
counts and limits, then one Review plan disclosure over the unchanged
|
|
14559
14847
|
exact-terms form. */
|
|
14560
|
-
.chat-plan-head h2 { margin: 0; font-size: 1rem; letter-spacing: -.025em; }
|
|
14561
|
-
.chat-plan-outcome { margin: .65rem 0 .55rem; font-size: .88rem; line-height: 1.5; overflow-wrap: anywhere; }
|
|
14562
|
-
.chat-plan-facts { margin: 0; color: var(--muted-foreground); font-size: .74rem; line-height: 1.5; overflow-wrap: anywhere; }
|
|
14563
|
-
.chat-plan > details.chat-approval { margin: .85rem 0 0; padding: .85rem 0 0; border: 0; border-top: 1px solid var(--border); border-radius: 0; background: transparent; }
|
|
14564
|
-
.chat-plan > details.chat-approval > summary { display: flex; align-items: center; gap: .75rem; cursor: pointer; list-style: none; }
|
|
14565
|
-
.chat-plan > details.chat-approval > summary::-webkit-details-marker { display: none; }
|
|
14566
|
-
.chat-plan > details.chat-approval > summary .button-link { flex: 0 0 auto; white-space: nowrap; min-height: 44px; }
|
|
14567
|
-
.chat-plan > details.chat-approval > .chat-approval-form { padding: .85rem 0 0; }
|
|
14568
|
-
.chat-plan .chat-approval-lead { margin: 0 0 .85rem; }
|
|
14569
14848
|
.chat-approval-stale { margin: .75rem 0 0; }
|
|
14570
14849
|
.chat-approval-stale a { font-weight: 600; }
|
|
14571
|
-
form.approve-form[data-stale="1"] .approval-confirm { opacity: .55; }
|
|
14850
|
+
form.approve-form[data-stale="1"] .approval-confirm, form.approve-form[data-stale="1"] .approval-act { opacity: .55; }
|
|
14572
14851
|
.proposal-filed { display: flex; align-items: center; flex-wrap: wrap; gap: .6rem; margin: .5rem 0 0; }
|
|
14573
14852
|
.chat-thinking { display: flex; align-items: center; gap: .75rem; padding: .75rem .85rem; }
|
|
14574
14853
|
.chat-thinking p { flex: 1; margin: 0; }
|
|
@@ -14695,10 +14974,6 @@ ${THEME_DARK}
|
|
|
14695
14974
|
.chat-action-card > summary { align-items: flex-start; padding: .85rem; }
|
|
14696
14975
|
.chat-action-card > summary .button-link { min-height: 2.1rem; padding-inline: .6rem; font-size: .65rem; white-space: nowrap; }
|
|
14697
14976
|
.chat-action-card > summary small { max-width: 14rem; }
|
|
14698
|
-
.chat-approval-form { padding: .85rem; }
|
|
14699
|
-
.chat-approval-form .approval-boundaries { grid-template-columns: 1fr; }
|
|
14700
|
-
.chat-approval-form .approval-confirm { display: grid; }
|
|
14701
|
-
.chat-approval-form .approval-confirm button { width: 100%; }
|
|
14702
14977
|
.chat-budget { gap: .3rem; margin: .65rem 0 .9rem; }
|
|
14703
14978
|
.chat-budget > span { padding: .25rem .48rem; }
|
|
14704
14979
|
.chat-budget > span:first-child { max-width: 100%; overflow: hidden; text-overflow: ellipsis; }
|
|
@@ -15012,6 +15287,15 @@ button { min-height: 44px; }
|
|
|
15012
15287
|
chips flow after it, never above the name. */
|
|
15013
15288
|
.queue-card p.row > a:first-of-type { flex: 1 1 12rem; min-width: 0; }
|
|
15014
15289
|
}
|
|
15290
|
+
/* A phone reads a long changed line wrapped, never scrolled sideways: the
|
|
15291
|
+
line keeps its numbers and marker, the code column takes what is left. */
|
|
15292
|
+
@media (max-width: 760px) {
|
|
15293
|
+
.diff-lines { overflow-x: visible; }
|
|
15294
|
+
.diff-line, .diff-review .diff-line { min-width: 0; grid-template-columns: 2.75rem 2.65rem 2.65rem minmax(0, 1fr); }
|
|
15295
|
+
.diff-review[data-mode="view"] .diff-line { grid-template-columns: 0 2.65rem 2.65rem minmax(0, 1fr); }
|
|
15296
|
+
.diff-line code { white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
15297
|
+
.diff-hunk-head { overflow-x: visible; white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
15298
|
+
}
|
|
15015
15299
|
@media (max-width: 30rem) {
|
|
15016
15300
|
.dependency-repair-actions .dependency-repair-replace { grid-template-columns: minmax(0, 1fr); }
|
|
15017
15301
|
.dependency-repair-actions .dependency-repair-replace > button[type=submit] { width: 100%; }
|
|
@@ -15155,6 +15439,7 @@ const INBOX_TABS_CSS = '.inbox-tabs{display:inline-flex;gap:2px;max-width:100%;o
|
|
|
15155
15439
|
'.inbox-tabs a[aria-current="page"]{background:var(--so-paper);color:var(--so-ink);box-shadow:var(--so-pill-shadow)}' +
|
|
15156
15440
|
'.inbox-tab-count{min-width:18px;padding:0 5px;border-radius:9999px;font:500 11px/18px var(--so-mono,ui-monospace,monospace);font-variant-numeric:tabular-nums;text-align:center}' +
|
|
15157
15441
|
'.inbox-tab-count--needs{background:var(--so-signal);color:var(--so-on-signal)}' +
|
|
15442
|
+
'.inbox-ask{margin:18px 0 0}.inbox-ask>h2{display:flex;align-items:baseline;gap:8px;margin:0 0 4px;font-size:15px;font-weight:600}.inbox-ask>h2 .count{font:500 12px var(--so-mono,ui-monospace,monospace);font-variant-numeric:tabular-nums;color:var(--so-muted)}.inbox-ask h3{font-size:13px;font-weight:600;margin:12px 0 4px}' +
|
|
15158
15443
|
'.inbox-unread{display:none;position:absolute;top:4px;right:3px;width:6px;height:6px;border-radius:50%;background:var(--so-signal)}' +
|
|
15159
15444
|
'@media (max-width:760px){.inbox-tabs{display:flex;width:100%}.inbox-tabs a{flex:1;justify-content:center;min-height:44px;padding:0 6px}.inbox-unread{display:block}}';
|
|
15160
15445
|
const WORKSPACE_STYLE = styleAsset(STYLE + INBOX_TABS_CSS + APPROVAL_RULES_CSS + SPEND_CSS + RETENTION_CSS + STORAGE_CSS + UPDATES_CSS + LIMITS_CSS + MONITORING_CSS + INTEGRATIONS_CSS + BACKUP_CSS + EXPORT_CSS + PROJECT_DELETE_CSS + POLICY_CSS + EVIDENCE_PACK_CSS + THEME_CONTROLS_CSS + CODING_CSS + CODING_SHIPPING_CSS + RECIPE_CSS + SKILLS_CSS + TOOLS_CSS + FLOWS_CSS + TEAMMATE_CSS + KITS_CSS + STARTERS_CSS + GALLERY_CSS + SSO_CSS + CREDENTIALS_CSS + KNOWLEDGE_CSS + MODELS_CSS + CHAT_POLISH_CSS + TRANSITIONS_CSS + WORKSPACE_MOTION_CSS + ASSIGNMENT_CSS + TASK_STATUS_CSS + LEAD_CONTEXT_CSS + PULL_REQUEST_SETTINGS_CSS + CHECK_SETTINGS_CSS + '.learning{min-width:0;overflow-wrap:anywhere}.learning .card{min-width:0}.learning code,.learning blockquote,.learning pre{white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word}.learning button,.learning summary,.learning .button-link{min-height:44px}.learning button{white-space:nowrap}.learning summary{padding:12px 0;cursor:pointer}.learning form{margin:12px 0}.learning select{max-width:100%}.learning blockquote{margin:8px 0}.learning ul{padding-left:20px}');
|
|
@@ -15880,13 +16165,17 @@ function inboxPage(chrome, data) {
|
|
|
15880
16165
|
const listRows = (rows) => rows.map(one => `<p class="row"><a href="${taskHref(one.taskId)}">${escape(one.title)}</a>${chip(one.repo)} <span class="meta">${escape(one.detail)}</span></p>`).join("\n");
|
|
15881
16166
|
const ready = (data.ready ?? []).length === 0 ? "" : `<h2>ready to review</h2>` + listRows(data.ready ?? []);
|
|
15882
16167
|
const running = (data.running ?? []).length === 0 ? "" : `<h2>running now</h2>` + listRows(data.running ?? []);
|
|
15883
|
-
const
|
|
15884
|
-
|
|
16168
|
+
const asks = { decide: data.decisions.length + data.approvals.length, unblock: data.requeueables.length + data.cancelledBlockers.length + data.gaps.length };
|
|
16169
|
+
const counts = { ...asks,
|
|
16170
|
+
"needs-you": asks.decide + asks.unblock,
|
|
15885
16171
|
ready: data.needsVerification.length + (data.ready ?? []).length,
|
|
15886
16172
|
running: (data.running ?? []).length,
|
|
15887
16173
|
all: 0,
|
|
15888
16174
|
};
|
|
15889
16175
|
counts.all = counts["needs-you"] + counts.ready + counts.running;
|
|
16176
|
+
// Each ask is a small heading with its count over its sections (their own headings one step down).
|
|
16177
|
+
const askGroup = (ask, count, sections) => count === 0 ? ""
|
|
16178
|
+
: `<section class="inbox-ask" data-ask="${ask}"><h2>${ASK_LABEL[ask]} <span class="count">${count}</span></h2>${sections.join("").replace(/<(\/?)h2>/g, "<$1h3>")}</section>`;
|
|
15890
16179
|
const tabs = data.tab === undefined ? "" : `<nav class="inbox-tabs" aria-label="Inbox views">` + INBOX_TABS.map(one => `<a href="/inbox?tab=${one.id}"${one.id === tab ? ` aria-current="page"` : ""} data-inbox-tab="${one.id}">${one.label}` +
|
|
15891
16180
|
`<span class="inbox-tab-count${one.id === "needs-you" && counts[one.id] > 0 ? " inbox-tab-count--needs" : ""}">${counts[one.id]}</span>` +
|
|
15892
16181
|
`${one.id !== tab && (data.unread ?? []).includes(one.id) ? `<span class="inbox-unread" aria-label="new"></span>` : ""}</a>`).join("") + `</nav>`;
|
|
@@ -15900,14 +16189,11 @@ function inboxPage(chrome, data) {
|
|
|
15900
16189
|
wizard,
|
|
15901
16190
|
empty || !shows("needs-you") ? "" : `<p><a class="new-task" style="display:inline-block" href="/next">clear the queue \u2192 one thing at a time</a></p>`,
|
|
15902
16191
|
empty && data.wizard === null && shows("needs-you") && counts.all === 0 ? `<div class="card"><p><strong>Nothing needs you.</strong></p><p class="meta">The queue is either working or waiting on its own timers. <a href="/board">Watch the board</a> or <a href="/activity">read the activity report</a>.</p></div>` : tabEmpty,
|
|
15903
|
-
|
|
15904
|
-
shows("needs-you") ? approvals : "",
|
|
15905
|
-
shows("
|
|
15906
|
-
shows("
|
|
15907
|
-
shows("ready") ? ready : "",
|
|
16192
|
+
// What waits on a person, grouped by what it asks: Decide, then Review, then Unblock.
|
|
16193
|
+
shows("needs-you") ? askGroup("decide", counts.decide, [approvals, decisions]) : "",
|
|
16194
|
+
shows("ready") ? askGroup("review", counts.ready, [needsVerification, ready]) : "",
|
|
16195
|
+
shows("needs-you") ? askGroup("unblock", counts.unblock, [requeueables, cancelled, gaps]) : "",
|
|
15908
16196
|
shows("running") ? running : "",
|
|
15909
|
-
shows("needs-you") ? cancelled : "",
|
|
15910
|
-
shows("needs-you") ? gaps : "",
|
|
15911
16197
|
data.rollup
|
|
15912
16198
|
? `<p class="meta">requirement gaps are checked one project at a time \u2014 open a project to see and fill its gaps · <a href="/projects">open a project</a></p>`
|
|
15913
16199
|
: "",
|
|
@@ -16530,80 +16816,31 @@ function taskChatApproval(focus, csrf) {
|
|
|
16530
16816
|
if (approval.nonce === "") {
|
|
16531
16817
|
return `<section class="card chat-action-card" id="task-chat-action"><span class="eyebrow">approval needs attention</span><h2>The agent setup isn’t ready yet</h2>${profileWords(scope)}<a class="button-link" href="${taskHref(focus.id)}#scope">Fix the agent setup →</a></section>`;
|
|
16532
16818
|
}
|
|
16533
|
-
|
|
16534
|
-
|
|
16535
|
-
? null
|
|
16536
|
-
: profile.provider === "claude"
|
|
16537
|
-
? profile.permissionArgv === "bypassPermissions" ? "Full access" : "Auto permissions"
|
|
16538
|
-
: profile.provider === "gemini"
|
|
16539
|
-
? profile.approvalArgv === "yolo" ? "Full access" : "Auto permissions"
|
|
16540
|
-
: profile.sandboxMode === "danger-full-access" ? "Full access" : "Workspace sandbox";
|
|
16541
|
-
const revision = approval.revision === null
|
|
16542
|
-
? ""
|
|
16543
|
-
: `<div class="chat-approval-section"><span class="approval-label">${approval.revision.kind === "ci-repair" ? "CI repair" : approval.revision.kind === "criterion-repair" ? "criterion repair" : "revision notes"}</span>` +
|
|
16544
|
-
(approval.revision.comments.length === 0 ? "" : `<ul class="recap">${approval.revision.comments.map(one => `<li>${one.path === null ? "" : `<span class="mono">${escape(one.path)}${one.line === null ? "" : `:${one.line}`}</span> · `}${escape(one.note)} <span class="meta">— ${escape(one.author)}</span></li>`).join("")}</ul>`) +
|
|
16545
|
-
// The link back (package 3): the original result stays on record
|
|
16546
|
-
// and one tap away from the revision it produced, in both directions.
|
|
16547
|
-
`<p class="meta"><a href="${escape(chatResultHref(focus.id, approval.revision.sourceRun))}" data-revision-source>Original result: build #${approval.revision.sourceRun} →</a></p>` +
|
|
16548
|
-
revisionLineageHtml(approval.revision.lineage) +
|
|
16549
|
-
`</div>`;
|
|
16550
|
-
const race = approval.raceTerms === null
|
|
16551
|
-
? ""
|
|
16552
|
-
: `<div class="chat-approval-section"><span class="approval-label">${approval.raceTerms.kind === "comparison" ? "comparison" : "tournament"}</span>` +
|
|
16553
|
-
`<p>${approval.raceTerms.agents.length} agents build independently: ${approval.raceTerms.agents.map(one => `<span class="mono">${escape(one.provider)} · ${escape(one.model)}</span>`).join(" vs ")}.</p>` +
|
|
16554
|
-
(approval.raceTerms.kind === "comparison"
|
|
16555
|
-
? `<p class="meta">No dollar caps; every result and its evidence is kept for you to compare.</p>`
|
|
16556
|
-
: `<p class="meta">$${(approval.raceTerms.perAgentBudgetMicrousd / 1_000_000).toFixed(2)} per agent plus $${(approval.raceTerms.overrunReserveMicrousd / 1_000_000).toFixed(2)} reserve; $${(approval.raceTerms.totalBudgetMicrousd / 1_000_000).toFixed(2)} total.</p>`) +
|
|
16557
|
-
`</div>`;
|
|
16558
|
-
// The concise plan (package 2, revised on the operator's screenshot
|
|
16559
|
-
// feedback and AGENTS.md): a short title, the outcome in one bounded
|
|
16560
|
-
// line, one compact line of counts and limits, and ONE Review plan
|
|
16561
|
-
// action — no eyebrow, no second sentence saying the same thing, no
|
|
16562
|
-
// footer. Every word is a signed fact the exact terms below restate in
|
|
16563
|
-
// full; the password and approval instructions live inside the
|
|
16564
|
-
// expanded review, where Approve & start is the only submit.
|
|
16565
|
-
const plan = approval.planDocument === null ? null : parseExecutionPlanDocument(approval.planDocument);
|
|
16566
|
-
const milestones = plan !== null && plan.ok ? plan.document.milestones : [];
|
|
16567
|
-
const count = (n, singular) => `${n} ${singular}${n === 1 ? "" : "s"}`;
|
|
16568
|
-
const facts = [
|
|
16569
|
-
milestones.length > 0 ? count(milestones.length, "step") : count(scope.touches.length, "path"),
|
|
16570
|
-
count(scope.acceptance.length, "check"),
|
|
16571
|
-
];
|
|
16572
|
-
if (approval.deliverable === "report")
|
|
16573
|
-
facts.push("Report only");
|
|
16574
|
-
if (scope.budgetMicrousd !== null)
|
|
16575
|
-
facts.push(`$${(scope.budgetMicrousd / 1_000_000).toFixed(2)} attempt cap`);
|
|
16576
|
-
if (permission !== null)
|
|
16577
|
-
facts.push(permission);
|
|
16578
|
-
if (approval.raceTerms !== null)
|
|
16579
|
-
facts.push(`${approval.raceTerms.agents.length}-agent ${approval.raceTerms.kind}`);
|
|
16819
|
+
// The same sheet as the task page (approval critique, Oct 2): the plan
|
|
16820
|
+
// open in plain rows, one Approve & start, the rest in Details.
|
|
16580
16821
|
return (`<section class="card chat-action-card chat-plan" id="task-chat-action" data-approval="${escape(approval.digest)}">` +
|
|
16581
|
-
|
|
16582
|
-
|
|
16583
|
-
|
|
16584
|
-
|
|
16585
|
-
|
|
16586
|
-
|
|
16587
|
-
|
|
16588
|
-
|
|
16589
|
-
|
|
16590
|
-
|
|
16591
|
-
|
|
16592
|
-
|
|
16593
|
-
|
|
16594
|
-
|
|
16595
|
-
|
|
16596
|
-
|
|
16597
|
-
|
|
16598
|
-
|
|
16599
|
-
|
|
16600
|
-
|
|
16601
|
-
|
|
16602
|
-
`</
|
|
16603
|
-
agentsCeremonyHtml(focus.route) +
|
|
16604
|
-
runtimeDetailsHtml(scope) +
|
|
16605
|
-
`<div class="approval-confirm"><label>Your password <span class="meta">— confirms this exact scope</span><input type="password" name="token" autocomplete="current-password" placeholder="Password"></label>` +
|
|
16606
|
-
`<button type="submit">Approve & start</button></div></form></details></section>`);
|
|
16822
|
+
approvalSheetHtml({
|
|
16823
|
+
surface: "chat",
|
|
16824
|
+
action: `${taskHref(focus.executionId)}/approve`,
|
|
16825
|
+
csrf,
|
|
16826
|
+
nonce: approval.nonce,
|
|
16827
|
+
digest: approval.digest,
|
|
16828
|
+
returnTo,
|
|
16829
|
+
scope,
|
|
16830
|
+
planDocument: approval.planDocument,
|
|
16831
|
+
planContract: approval.planDocument === null ? null : approval.planContract,
|
|
16832
|
+
revision: approval.revision,
|
|
16833
|
+
revisionSourceHref: approval.revision === null ? "" : chatResultHref(focus.id, approval.revision.sourceRun),
|
|
16834
|
+
repairChain: approval.repairChain,
|
|
16835
|
+
raceTerms: approval.raceTerms,
|
|
16836
|
+
route: focus.route,
|
|
16837
|
+
coordinator: approval.coordinator,
|
|
16838
|
+
deliverable: approval.deliverable,
|
|
16839
|
+
editHref: `${taskHref(focus.id)}#${approval.planDocument === null ? "scope" : "plan-edit"}`,
|
|
16840
|
+
notNowHref: "/chat",
|
|
16841
|
+
sticky: false,
|
|
16842
|
+
}) +
|
|
16843
|
+
`</section>`);
|
|
16607
16844
|
}
|
|
16608
16845
|
/** One live, server-derived journey from request to proof. The fragment is
|
|
16609
16846
|
* safe to refresh independently, so an in-progress message is never lost. */
|
|
@@ -17952,9 +18189,9 @@ function statusActionHref(status, taskId, runId, prUrl, repo) {
|
|
|
17952
18189
|
return null;
|
|
17953
18190
|
switch (status.action.kind) {
|
|
17954
18191
|
case "open-result": return runId === null ? taskHref(taskId) : `/r/${runId}`;
|
|
17955
|
-
case "open-review": return repo === undefined ? reviewHref(taskId) : reviewHref(taskId, runId
|
|
18192
|
+
case "open-review": return repo === undefined ? reviewHref(taskId) : reviewHref(taskId, runId);
|
|
17956
18193
|
case "open-run": return runId === null ? taskHref(taskId) : `/r/${runId}`;
|
|
17957
|
-
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));
|
|
17958
18195
|
case "open-task":
|
|
17959
18196
|
default: return taskHref(taskId);
|
|
17960
18197
|
}
|
|
@@ -17972,6 +18209,7 @@ function statusLineHtml(status, extra = "") {
|
|
|
17972
18209
|
function workDiagnosticsHtml(diagnostics) {
|
|
17973
18210
|
return (diagnostics ?? []).map(one => `<p class="work-detail" data-work-diagnostic="${escape(one.token)}"><strong>${escape(one.label)}</strong> · ${escape(one.detail)}</p>`).join("");
|
|
17974
18211
|
}
|
|
18212
|
+
const TASK_GROUP_LABEL = { ...ASK_LABEL, building: 'Building', rest: 'Recent' };
|
|
17975
18213
|
function workPage(chrome, data) {
|
|
17976
18214
|
const href = (view, cursor) => {
|
|
17977
18215
|
const query = new URLSearchParams();
|
|
@@ -17987,7 +18225,7 @@ function workPage(chrome, data) {
|
|
|
17987
18225
|
const rowHtml = (row) => {
|
|
17988
18226
|
const target = row.primaryAction?.target;
|
|
17989
18227
|
const actionHref = row.primaryAction?.code === 'open-result' && target?.runId != null
|
|
17990
|
-
? `/review?result=${encodeURIComponent(target.taskId)}&run=${target.runId}
|
|
18228
|
+
? `/review?result=${encodeURIComponent(target.taskId)}&run=${target.runId}`
|
|
17991
18229
|
: browserWorkActionHref(row);
|
|
17992
18230
|
const action = actionHref === null || row.primaryAction === null ? '' : `<a class="work-action" data-primary-action href="${escape(actionHref)}">${escape(row.primaryAction.label)} →</a>`;
|
|
17993
18231
|
const project = data.multiProject || row.repo === null ? `<span class="project-label">${row.repo === null ? 'Unplaced' : escape(projectName(row.repo))}</span>` : '';
|
|
@@ -18014,10 +18252,14 @@ function workPage(chrome, data) {
|
|
|
18014
18252
|
const view = {
|
|
18015
18253
|
kind: 'tasks',
|
|
18016
18254
|
tabs: WORK_VIEWS.map(one => ({ label: one.label, href: href(one.key), count: data.work.totals[one.key], active: one.key === data.view })),
|
|
18255
|
+
needsYou: data.work.totals['needs-you'],
|
|
18256
|
+
groups: data.view !== 'all' && data.view !== 'needs-you' ? null
|
|
18257
|
+
: [...ASKS, ...(data.view === 'all' ? ['building', 'rest'] : [])]
|
|
18258
|
+
.map(key => ({ key, label: TASK_GROUP_LABEL[key], count: data.work.groups[key] })).filter(one => one.count > 0),
|
|
18017
18259
|
rows: data.work.items.map(row => {
|
|
18018
18260
|
const target = row.primaryAction?.target;
|
|
18019
18261
|
const actionHref = row.primaryAction?.code === 'open-result' && target?.runId != null
|
|
18020
|
-
? `/review?result=${encodeURIComponent(target.taskId)}&run=${target.runId}
|
|
18262
|
+
? `/review?result=${encodeURIComponent(target.taskId)}&run=${target.runId}`
|
|
18021
18263
|
: browserWorkActionHref(row);
|
|
18022
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;
|
|
18023
18265
|
return {
|
|
@@ -18025,6 +18267,7 @@ function workPage(chrome, data) {
|
|
|
18025
18267
|
project: data.multiProject || row.repo === null ? (row.repo === null ? 'Unplaced' : projectName(row.repo)) : null,
|
|
18026
18268
|
age: relativeAge(row.updatedAt, data.now),
|
|
18027
18269
|
status: { label: row.status.label, tone: row.status.tone, token: row.status.token },
|
|
18270
|
+
ask: row.ask, group: row.ask ?? (row.status.rank === 1 ? 'building' : 'rest'),
|
|
18028
18271
|
action: actionHref === null || row.primaryAction === null ? null : { label: row.primaryAction.label, href: actionHref },
|
|
18029
18272
|
detail: needsYouDetail ? row.status.detail : null,
|
|
18030
18273
|
problem: row.familyProblem,
|
|
@@ -19846,6 +20089,10 @@ function taskBodyParts(data) {
|
|
|
19846
20089
|
: approval.approved
|
|
19847
20090
|
? "approved"
|
|
19848
20091
|
: "review before starting";
|
|
20092
|
+
// While the approval sheet is open it states any amendment; the plan
|
|
20093
|
+
// card does not repeat it (one #contract-amendment on the page).
|
|
20094
|
+
const sheetStatesContract = scope !== null && !approval.approved && data.plan === "drafted" && data.dispatch?.action !== "repair-dependency" &&
|
|
20095
|
+
!(data.revision != null && "problem" in data.revision) && scope.profileState !== "unresolved" && consentDoorOf(scope, data.route).open;
|
|
19849
20096
|
const planCard = data.planDocument === null
|
|
19850
20097
|
? data.plan === "requested"
|
|
19851
20098
|
? data.revision != null && !("problem" in data.revision)
|
|
@@ -19859,10 +20106,10 @@ function taskBodyParts(data) {
|
|
|
19859
20106
|
(approval.approved ? `<p class="meta">The agent can adapt this route when evidence changes; your approved outcome stays fixed.</p>` : "") +
|
|
19860
20107
|
`</div><span class="plan-lock">${planStanding}</span>${approval.approved ? `</summary><div class="planner-plan-body">` : `</div>`}` +
|
|
19861
20108
|
executionPlanHtml(displayedPlanDocument ?? data.planDocument) +
|
|
19862
|
-
(approval.approved ? "" : planContractHtml(data.planContract ?? null, "full")) +
|
|
20109
|
+
(approval.approved || sheetStatesContract ? "" : planContractHtml(data.planContract ?? null, "full")) +
|
|
19863
20110
|
(data.csrf === "" || data.planSha == null || approval.approved
|
|
19864
20111
|
? ""
|
|
19865
|
-
: `<details class="plan-editor"><summary>Edit plan</summary><form method="post" action="${taskHref(task.id)}/plan-edit">` +
|
|
20112
|
+
: `<details class="plan-editor" id="plan-edit"><summary>Edit plan</summary><form method="post" action="${taskHref(task.id)}/plan-edit">` +
|
|
19866
20113
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}"><input type="hidden" name="saw-plan" value="${escape(data.planSha)}">` +
|
|
19867
20114
|
`<label>plan details <span class="meta">Keep the five headings. Approval locks this version for the build.</span>` +
|
|
19868
20115
|
`<textarea name="plan-document" rows="14">${escape(data.planDocument)}</textarea></label>` +
|
|
@@ -19889,20 +20136,10 @@ function taskBodyParts(data) {
|
|
|
19889
20136
|
// The approval form restates every field the digest binds — an operator
|
|
19890
20137
|
// approves what is on this form, not what is elsewhere on the page — and
|
|
19891
20138
|
// requires the token typed again. The session got you here; only the
|
|
19892
|
-
// token agrees.
|
|
19893
|
-
//
|
|
19894
|
-
//
|
|
19895
|
-
//
|
|
19896
|
-
// shape. An unapprovable scope gets the problem and the edit road
|
|
19897
|
-
// instead of a password it cannot use.
|
|
19898
|
-
const approvalProfile = scope?.profile ?? null;
|
|
19899
|
-
const approvalPermission = approvalProfile === null
|
|
19900
|
-
? null
|
|
19901
|
-
: approvalProfile.provider === "claude"
|
|
19902
|
-
? approvalProfile.permissionArgv === "bypassPermissions" ? "Full access" : "Auto permissions"
|
|
19903
|
-
: approvalProfile.provider === "gemini"
|
|
19904
|
-
? approvalProfile.approvalArgv === "yolo" ? "Full access" : "Auto permissions"
|
|
19905
|
-
: approvalProfile.sandboxMode === "danger-full-access" ? "Full access" : "Workspace sandbox";
|
|
20139
|
+
// token agrees. The sheet shows the plan open in plain rows with one
|
|
20140
|
+
// Approve & start; the rest of the signed terms sit in its Details fold.
|
|
20141
|
+
// An unapprovable scope gets the problem and the edit road instead of a
|
|
20142
|
+
// password it cannot use.
|
|
19906
20143
|
const consentDoor = consentDoorOf(scope, data.route);
|
|
19907
20144
|
const revisionInApproval = scope !== null && !approval.approved && data.plan !== "requested" &&
|
|
19908
20145
|
scope.profileState !== "unresolved" && consentDoor.open && data.dispatch?.action !== "repair-dependency" &&
|
|
@@ -19917,79 +20154,27 @@ function taskBodyParts(data) {
|
|
|
19917
20154
|
`<p class="ceremony-road"><a class="button-link" href="#scope">edit the scope to fix it →</a></p></div>`
|
|
19918
20155
|
: !consentDoor.open
|
|
19919
20156
|
? consentClosedHtml(task.id, consentDoor, "task")
|
|
19920
|
-
:
|
|
19921
|
-
|
|
19922
|
-
|
|
19923
|
-
|
|
19924
|
-
|
|
19925
|
-
|
|
19926
|
-
|
|
19927
|
-
|
|
19928
|
-
|
|
19929
|
-
|
|
19930
|
-
|
|
19931
|
-
|
|
19932
|
-
|
|
19933
|
-
|
|
19934
|
-
data.
|
|
19935
|
-
|
|
19936
|
-
|
|
19937
|
-
|
|
19938
|
-
|
|
19939
|
-
|
|
19940
|
-
|
|
19941
|
-
? ""
|
|
19942
|
-
: `<p class="meta">filed by <span class="mono">${escape(data.coordinator.label)}</span>${data.coordinator.filedAgo === null ? "" : ` \u00b7 ${escape(data.coordinator.filedAgo)}`} — an agent asked for this; nothing plans, claims, or runs until you sign, and your signature runs THEIR request</p>`,
|
|
19943
|
-
`<p class="approval-label">goal</p><p class="approval-goal">${escape(scope.goal)}</p>`,
|
|
19944
|
-
scope.candidate ? `<p class="approval-label">Saved commit</p><p style="overflow-wrap:anywhere"><code>${escape(scope.candidate)}</code></p>` : "",
|
|
19945
|
-
revisionCard,
|
|
19946
|
-
`<div class="approval-boundaries">`,
|
|
19947
|
-
`<div class="approval-boundary"><p class="approval-label">not this</p><p>${scope.outOfScope === null ? "<em>no exclusions</em>" : escape(scope.outOfScope)}</p></div>`,
|
|
19948
|
-
`<div class="approval-boundary"><p class="approval-label">touches</p>${approvalPathsHtml(scope.touches)}</div>`,
|
|
19949
|
-
`</div>`,
|
|
19950
|
-
acceptanceCeremonyHtml(scope.acceptance),
|
|
19951
|
-
// The contract amendment INSIDE the ceremony (contract handoff,
|
|
19952
|
-
// task 1): what the yes accepts that the operator did not file.
|
|
19953
|
-
data.plan === "drafted" ? planContractHtml(data.planContract ?? null, "ceremony") : "",
|
|
19954
|
-
`</div>`,
|
|
19955
|
-
`<div class="approval-chips"><span class="approval-chip">quality · <strong>${escape(qualityModeTitle(scope.qualityMode ?? "default"))}</strong></span>` +
|
|
19956
|
-
(approvalPermission === null ? "" : `<span class="approval-chip">${escape(approvalPermission)}</span>`) +
|
|
19957
|
-
`</div>`,
|
|
19958
|
-
// The revision feedback above includes its lineage: the source,
|
|
19959
|
-
// carried terms, re-resolved terms, and grants that never inherit.
|
|
19960
|
-
// The AGENTS inside the ceremony (v47/v48): who plans, builds,
|
|
19961
|
-
// repairs, and reviews, and why — said where the yes is given, in
|
|
19962
|
-
// the same block chat and /next show. Availability is volatile and
|
|
19963
|
-
// stays outside these terms; the runtime limits the profile seals
|
|
19964
|
-
// are one tap away, never in the way.
|
|
19965
|
-
agentsCeremonyHtml(data.route),
|
|
19966
|
-
runtimeDetailsHtml(scope),
|
|
19967
|
-
scope.budgetMicrousd === null
|
|
19968
|
-
? ""
|
|
19969
|
-
: `<p class="meta">each build attempt has a $${(scope.budgetMicrousd / 1_000_000).toFixed(2)} agent-reported usage cap — on a subscription this is a work limiter, not an API charge</p>`,
|
|
19970
|
-
// One yes covers BOTH documents (finding 31): the race terms are
|
|
19971
|
-
// restated on the same card the password signs, or they are not
|
|
19972
|
-
// approved at all.
|
|
19973
|
-
data.raceTerms === null || data.raceTerms === undefined
|
|
19974
|
-
? ""
|
|
19975
|
-
: data.raceTerms.kind === "comparison"
|
|
19976
|
-
? `<p><strong>and this comparison:</strong></p>` +
|
|
19977
|
-
`<p class="recap" style="margin-top:0">${data.raceTerms.n} agents build this independently — ` +
|
|
19978
|
-
`${data.raceTerms.agents.map(agent => `${escape(agent.provider)} · ${escape(agent.model)}`).join(" vs ")}. ` +
|
|
19979
|
-
`No dollar caps exist on a comparison — each agent runs until it finishes or stops making progress; ` +
|
|
19980
|
-
`spend lands measured only where the harness reports dollars (` +
|
|
19981
|
-
`${data.raceTerms.agents.filter(agent => agent.provider === "claude").length} of ${data.raceTerms.n} lanes here). ` +
|
|
19982
|
-
`You will compare the results and pick one.</p>`
|
|
19983
|
-
: `<p><strong>and this tournament:</strong></p>` +
|
|
19984
|
-
`<p class="recap" style="margin-top:0">${data.raceTerms.n} agents build this independently — ` +
|
|
19985
|
-
`${data.raceTerms.agents.map(agent => `${escape(agent.provider)} · ${escape(agent.model)}`).join(" vs ")}. ` +
|
|
19986
|
-
`Each may spend $${(data.raceTerms.perAgentBudgetMicrousd / 1_000_000).toFixed(2)} plus a ` +
|
|
19987
|
-
`$${(data.raceTerms.overrunReserveMicrousd / 1_000_000).toFixed(2)} overrun reserve; the whole tournament is capped at ` +
|
|
19988
|
-
`$${(data.raceTerms.totalBudgetMicrousd / 1_000_000).toFixed(2)}. You will compare the results and pick one.</p>`,
|
|
19989
|
-
`<div class="approval-confirm" id="approval-confirm"><label>Your password <span class="meta">— confirms exactly the terms above</span><input type="password" name="token" autocomplete="current-password" placeholder="Password"></label>`,
|
|
19990
|
-
`<div class="sticky-actions"><button type="submit">${data.raceTerms === null || data.raceTerms === undefined ? "Approve & start" : data.raceTerms.kind === "comparison" ? "Approve comparison" : "Approve tournament"}</button></div></div>`,
|
|
19991
|
-
`</form>`,
|
|
19992
|
-
].join("\n");
|
|
20157
|
+
: approvalSheetHtml({
|
|
20158
|
+
surface: "task",
|
|
20159
|
+
action: `${taskHref(task.id)}/approve`,
|
|
20160
|
+
csrf: data.csrf,
|
|
20161
|
+
nonce: data.nonce,
|
|
20162
|
+
digest: data.approvalDigest ?? scope.digest,
|
|
20163
|
+
returnTo: null,
|
|
20164
|
+
scope,
|
|
20165
|
+
planDocument: data.plan === "drafted" ? data.planDocument : null,
|
|
20166
|
+
planContract: data.plan === "drafted" ? data.planContract ?? null : null,
|
|
20167
|
+
revision: data.revision == null || "problem" in data.revision ? null : data.revision,
|
|
20168
|
+
revisionSourceHref: data.revision == null || "problem" in data.revision ? "" : `/r/${data.revision.sourceRun}`,
|
|
20169
|
+
repairChain: data.repairChain ?? null,
|
|
20170
|
+
raceTerms: data.raceTerms ?? null,
|
|
20171
|
+
route: data.route,
|
|
20172
|
+
coordinator: data.coordinator ?? null,
|
|
20173
|
+
deliverable: data.deliverable ?? "branch",
|
|
20174
|
+
editHref: data.csrf !== "" && data.planSha != null && data.planDocument !== null ? "#plan-edit" : "#scope",
|
|
20175
|
+
notNowHref: "/work",
|
|
20176
|
+
sticky: data.dispatch?.action === "approve-scope",
|
|
20177
|
+
});
|
|
19993
20178
|
// The attended road (Phase 2E): beside the approval, never replacing it.
|
|
19994
20179
|
// The mint button leads to the CONFIRM screen where every term renders
|
|
19995
20180
|
// and the password signs; an open authorization shows its state, its
|
|
@@ -20514,7 +20699,7 @@ function taskBodyParts(data) {
|
|
|
20514
20699
|
`<p class="meta"><strong>Plan first</strong> drafts it from the repository, or <a href="#scope">write it yourself</a>.</p></div>`
|
|
20515
20700
|
: "";
|
|
20516
20701
|
const approvalHtml = dependencyChoiceNeeded || approveForm === "" ? "" : data.dispatch?.action === "approve-scope"
|
|
20517
|
-
? `<
|
|
20702
|
+
? `<section class="task-plan-review" aria-label="Approve the plan">${approveForm}</section>`
|
|
20518
20703
|
: `<details class="task-secondary-approval"><summary>Updated approval terms</summary>${approveForm}</details>`;
|
|
20519
20704
|
const optionsHtml = `${identity}${dispatchStatus}${dependencyChoiceNeeded ? "" : actsBar}`;
|
|
20520
20705
|
const optionsOpen = data.assignment?.primaryAction?.code === "unhold" || data.assignment?.primaryAction?.code === "retry-task";
|
|
@@ -20970,7 +21155,9 @@ export function orderChangedFiles(files, proofCitesPaths) {
|
|
|
20970
21155
|
? churn(b) - churn(a)
|
|
20971
21156
|
: a.path.localeCompare(b.path));
|
|
20972
21157
|
}
|
|
20973
|
-
|
|
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}`}`;
|
|
20974
21161
|
/** The receipt's own proof-state word, so the cockpit and the task page
|
|
20975
21162
|
* never disagree on the state's name or its precedence: the stored
|
|
20976
21163
|
* verdict decides, whatever the run's outcome — a no-change run with a
|
|
@@ -21095,7 +21282,7 @@ function reviewCockpitPage(chrome, data) {
|
|
|
21095
21282
|
const why = status === null
|
|
21096
21283
|
? reasons.length === 0 ? "" : `<span class="cockpit-why">${escape(reasons[0])}${reasons.length > 1 ? ` · +${reasons.length - 1} more` : ""}</span>`
|
|
21097
21284
|
: `<span class="cockpit-why">${statusLineHtml(status)}</span>`;
|
|
21098
|
-
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"` : ""}>` +
|
|
21099
21286
|
`<span class="cockpit-row-head"><strong>${escape(row.title)}</strong></span>` +
|
|
21100
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>` +
|
|
21101
21288
|
(row.historyProblem ? `<span class="cockpit-why" data-history-problem>History unavailable</span>` : why) +
|
|
@@ -21124,7 +21311,7 @@ function reviewCockpitPage(chrome, data) {
|
|
|
21124
21311
|
const status = row.assignment == null ? null : assignmentPresentationOf(row.assignment).status;
|
|
21125
21312
|
const reasons = row.priority.reasons;
|
|
21126
21313
|
return {
|
|
21127
|
-
title: row.title, href: reviewHref(row.taskId, row.runId
|
|
21314
|
+
title: row.title, href: reviewHref(row.taskId, row.runId), at: row.completedAt,
|
|
21128
21315
|
status: status === null ? null : { label: status.label, tone: status.tone },
|
|
21129
21316
|
notes: [...(row.historyProblem ? ["History unavailable"] : status === null && reasons.length > 0 ? [reasons[0]] : []), ...(status !== null && row.ciFailing ? ["CI is failing"] : [])],
|
|
21130
21317
|
current: selected !== null && selected.taskId === row.taskId,
|
|
@@ -21204,8 +21391,17 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
|
|
|
21204
21391
|
taskHref: taskHref(view.taskId), chatHref: taskChatHref(view.taskId),
|
|
21205
21392
|
status: { label: status.label, tone: status.tone, token: status.token },
|
|
21206
21393
|
problem: view.detail === null ? view.historyProblem : null,
|
|
21207
|
-
next: reviewNextActionOf(view, csrf), complete: null, checks: null, intent: intentView, noRun: null, panel: null, contest: "",
|
|
21394
|
+
next: reviewNextActionOf(view, csrf), complete: null, decision: null, checks: null, intent: intentView, noRun: null, panel: null, contest: "",
|
|
21208
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
|
+
] },
|
|
21209
21405
|
};
|
|
21210
21406
|
if (run === null || view.detail === null) {
|
|
21211
21407
|
parts.push(`<section class="card cockpit-section" id="verification" data-cockpit-section="proof"><h3>verification</h3>` +
|
|
@@ -21245,6 +21441,38 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
|
|
|
21245
21441
|
? { action: `${taskHref(view.taskId)}/complete`, receipt: assignment.receipt.digest, run: run.id } : null;
|
|
21246
21442
|
if (complete !== null)
|
|
21247
21443
|
parts.push(completionForm(view.taskId, run.id, complete.receipt, csrf, view.detail?.pullRequestTo ?? null));
|
|
21444
|
+
// The one decision, after the evidence: Accept only when every requirement is met and the checks passed.
|
|
21445
|
+
const acceptsHere = complete !== null || panel.panel.need?.accept != null;
|
|
21446
|
+
const matrix = proof === null || proof.proofProblem !== null ? [] : proof.matrix;
|
|
21447
|
+
const blocked = cantAcceptYetOf(proof?.verdict ?? null, proof?.reasons ?? [], accepted);
|
|
21448
|
+
const words = !acceptsHere ? null : { sentence: RESULT_DECISION_SENTENCE, ...acceptWordsOf({
|
|
21449
|
+
checks: checks === undefined ? null : checks.running != null ? "running" : checks.level === "off" && checks.status !== "passed" ? "off" : checks.status,
|
|
21450
|
+
unmet: matrix.filter(row => row.state !== "pass" && row.state !== "manual-review").length,
|
|
21451
|
+
yours: complete === null || accepted ? 0 : panel.panel.youCheck?.lines.length ?? 0,
|
|
21452
|
+
action: complete !== null ? "complete" : "accept",
|
|
21453
|
+
publishing: view.detail?.publishing ?? "other",
|
|
21454
|
+
proof: proof !== null && proof.proof !== null && proof.proofProblem === null,
|
|
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
|
+
});
|
|
21248
21476
|
if (view.notes.length > 0) {
|
|
21249
21477
|
parts.push(`<section class="card cockpit-section" data-cockpit-section="notes"><h3>operator notes</h3>` +
|
|
21250
21478
|
view.notes.map(one => `<p class="row"><span class="meta">${escape(one.author)} · ${whenTime(one.createdAt)}</span> ${escape(one.note)}</p>`).join("\n") +
|
|
@@ -21253,7 +21481,7 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
|
|
|
21253
21481
|
return {
|
|
21254
21482
|
html: `<section class="cockpit-detail">${parts.join("\n")}</section>`,
|
|
21255
21483
|
selected: {
|
|
21256
|
-
...selected, complete, panel: panel.panel, contest,
|
|
21484
|
+
...selected, complete, decision, panel: panel.panel, contest, acts, runChecks,
|
|
21257
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}` },
|
|
21258
21486
|
},
|
|
21259
21487
|
};
|
|
@@ -22162,7 +22390,7 @@ function resultPanelParts(detail, o) {
|
|
|
22162
22390
|
// Needs you: the action that resolves it comes first; Request changes stays beside it, never alone.
|
|
22163
22391
|
// On the result itself, "Review result" would link here: accepting it is what resolves it.
|
|
22164
22392
|
const need = needActionOf(current, taskStatus, o.csrf, o.returnTo, {
|
|
22165
|
-
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",
|
|
22166
22394
|
// What Store.finalResultReason refuses: a verified, attested, accepted or published result.
|
|
22167
22395
|
rebuildable: proof?.accepted == null && proof?.verdict !== "verified" && proof?.verdict !== "attested" && !(detail.publication !== null && ["pushed", "opened"].includes(detail.publication.state))
|
|
22168
22396
|
});
|
|
@@ -22229,6 +22457,7 @@ function resultPanelParts(detail, o) {
|
|
|
22229
22457
|
const acceptable = personChecks.length > 0 && humanReview && o.csrf !== "" && (current != null || detail.assignment == null);
|
|
22230
22458
|
const youCheck = personChecks.length === 0 ? null : {
|
|
22231
22459
|
lines: [...new Set(personChecks)],
|
|
22460
|
+
items: personCheckItems(proof, patchOk ? patch.text : null, shown, runId),
|
|
22232
22461
|
accept: acceptable && need?.accept == null ? { action: `${taskHref(detail.taskId)}/accept-proof`, run: run.id, returnTo: o.returnTo } : null,
|
|
22233
22462
|
};
|
|
22234
22463
|
const youCheckHtml = youCheck === null ? "" :
|
|
@@ -22537,6 +22766,37 @@ function resultPanelParts(detail, o) {
|
|
|
22537
22766
|
};
|
|
22538
22767
|
return { html, panel };
|
|
22539
22768
|
}
|
|
22769
|
+
/** Changed lines shown beside one "You check this one" item: at most this many per file, three files. */
|
|
22770
|
+
const CHECK_EXCERPT_LINES = 12;
|
|
22771
|
+
/** Each requirement only a person can confirm, with the evidence to judge
|
|
22772
|
+
* it by, inline: the changed lines in the files it cites (citing none, the
|
|
22773
|
+
* change's first file, labelled as not cited), the screenshots it cites
|
|
22774
|
+
* (none cited: every validated one), and the agent's own note. */
|
|
22775
|
+
function personCheckItems(proof, patchText, shots, runId) {
|
|
22776
|
+
const rows = proof.matrix.filter(row => row.state === "manual-review");
|
|
22777
|
+
if (rows.length === 0)
|
|
22778
|
+
return proof.reasons.some(reason => manualReviewCriterionOf(reason) !== null)
|
|
22779
|
+
? [{ id: "you-check", statement: "", words: personCheckWords(null), note: null, excerpts: [], shots: [] }] : [];
|
|
22780
|
+
const diff = patchText === null ? null : parseReviewDiff(patchText);
|
|
22781
|
+
const excerptOf = (file, cited) => {
|
|
22782
|
+
const changed = file.hunks.flatMap(hunk => hunk.lines).filter((line) => line.kind === "addition" || line.kind === "deletion");
|
|
22783
|
+
return { path: file.path, cited, lines: changed.slice(0, CHECK_EXCERPT_LINES).map(line => ({ kind: line.kind, line: line.newLine ?? line.oldLine, text: line.text })), more: Math.max(0, changed.length - CHECK_EXCERPT_LINES) };
|
|
22784
|
+
};
|
|
22785
|
+
return rows.map(row => {
|
|
22786
|
+
const refs = row.answered ?? [];
|
|
22787
|
+
const paths = refs.filter(one => one.kind === "changed-path").map(one => one.ref);
|
|
22788
|
+
const files = diff === null ? [] : paths.length > 0 ? diff.files.filter(file => paths.includes(file.path)).map(file => excerptOf(file, true))
|
|
22789
|
+
: diff.files.slice(0, 1).map(file => excerptOf(file, false));
|
|
22790
|
+
const cited = refs.filter(one => one.kind === "screenshot").map(one => one.ref);
|
|
22791
|
+
const pictures = cited.length > 0 ? shots.filter(shot => cited.includes(shot.path)) : shots;
|
|
22792
|
+
const notes = refs.filter(one => one.kind === "manual-review").map(one => one.ref.trim()).filter(one => one !== "");
|
|
22793
|
+
return {
|
|
22794
|
+
id: row.id, statement: row.statement, words: personCheckWords(row.statement), note: notes.length === 0 ? null : notes.join(" "),
|
|
22795
|
+
excerpts: files.filter(one => one.lines.length > 0).slice(0, 3),
|
|
22796
|
+
shots: pictures.slice(0, 4).map(shot => ({ src: `/r/${runId}/evidence/${shot.artifactId}`, href: `/r/${runId}/evidence/${shot.artifactId}`, caption: shot.caption })),
|
|
22797
|
+
};
|
|
22798
|
+
});
|
|
22799
|
+
}
|
|
22540
22800
|
/** A Needs you result's one action (needs-you.ts): a link to the act that resolves it, or Confirm it
|
|
22541
22801
|
* stopped behind the password. Null under every other headline. */
|
|
22542
22802
|
export function needActionOf(assignment, status, csrf, returnTo, result) {
|
|
@@ -22548,7 +22808,7 @@ export function needActionOf(assignment, status, csrf, returnTo, result) {
|
|
|
22548
22808
|
if (action?.code === "retry-task" && status.need.key === "rebuild" && csrf !== "")
|
|
22549
22809
|
return { label: status.need.action.label, ...rebuild };
|
|
22550
22810
|
if (result !== undefined) {
|
|
22551
|
-
if (status.need.key === "review-result" && !result.accepted && csrf !== "")
|
|
22811
|
+
if ((status.need.key === "review-result" || result.refuted === true) && !result.accepted && csrf !== "")
|
|
22552
22812
|
return { label: result.humanReview ? "Accept result" : "Accept with exception", href: null, confirm: null,
|
|
22553
22813
|
accept: { action: result.action, run: result.run, returnTo, note: result.humanReview ? null : "Why is this safe to accept?" } };
|
|
22554
22814
|
// The task page sends the person here: never back to it. What this page can do leads — Accept
|
|
@@ -22558,8 +22818,9 @@ export function needActionOf(assignment, status, csrf, returnTo, result) {
|
|
|
22558
22818
|
return { label: "Accept result", href: null, confirm: null, accept: { action: result.action, run: result.run, returnTo, note: null } };
|
|
22559
22819
|
if (here && csrf !== "" && result.rebuildable === true)
|
|
22560
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).
|
|
22561
22822
|
if (status.need.key === "review-result" || here)
|
|
22562
|
-
return
|
|
22823
|
+
return null;
|
|
22563
22824
|
}
|
|
22564
22825
|
const confirm = action?.code === "confirm-stopped" && action.target.runId !== null && csrf !== ""
|
|
22565
22826
|
? { action: `${taskHref(assignment.rootId)}/confirm-stopped`, run: action.target.runId, returnTo, checked: needsCheck(assignment) } : null;
|
|
@@ -22588,7 +22849,7 @@ function resultNeedAction(need, o, canRequest) {
|
|
|
22588
22849
|
function conciseOutcomeOf(conclusion) {
|
|
22589
22850
|
const flat = oneLineOf(conclusion, 2_000);
|
|
22590
22851
|
const sentence = /^[\s\S]*?[.!?](?=\s|$)/.exec(flat)?.[0] ?? flat;
|
|
22591
|
-
const bounded = sentence.length <=
|
|
22852
|
+
const bounded = sentence.length <= 140 ? sentence : `${sentence.slice(0, 140).replace(/\s+\S*$/, "")}…`;
|
|
22592
22853
|
return bounded.trim();
|
|
22593
22854
|
}
|
|
22594
22855
|
/** Exactly one primary act for the result (repair 2026-09-14), chosen from
|