toolroll 0.9.13 → 0.9.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/dist/assignment-ui.d.ts +1 -0
- package/dist/assignment-ui.js +8 -5
- package/dist/assignment.d.ts +32 -0
- package/dist/assignment.js +81 -7
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +38 -38
- package/dist/browser-crew.d.ts +2 -1
- package/dist/browser-crew.js +4 -2
- package/dist/browser-workspace.d.ts +109 -11
- package/dist/chat-actions.d.ts +1 -1
- package/dist/chat-actions.js +6 -4
- package/dist/chat-delivery-state.d.ts +1 -1
- package/dist/chat-voice.d.ts +3 -3
- package/dist/chat-voice.js +3 -3
- package/dist/demo.js +82 -2
- package/dist/disclosure.d.ts +6 -0
- package/dist/disclosure.js +13 -0
- package/dist/dispatch.js +7 -2
- package/dist/lead-status.js +2 -2
- package/dist/needs-you.d.ts +72 -0
- package/dist/needs-you.js +193 -1
- package/dist/result-acts.d.ts +51 -5
- package/dist/result-acts.js +36 -3
- package/dist/result-review.d.ts +4 -3
- package/dist/result-review.js +7 -3
- package/dist/serve.d.ts +3 -0
- package/dist/serve.js +1235 -711
- package/dist/slack-chat.js +1 -1
- package/dist/task-status.d.ts +24 -3
- package/dist/task-status.js +48 -17
- package/dist/telegram-progress.js +2 -2
- package/dist/test-temp.js +1 -1
- package/dist/transitions-recipes.d.ts +1 -1
- package/dist/transitions-recipes.js +5 -9
- package/dist/when-html.d.ts +19 -2
- package/dist/when-html.js +78 -11
- package/dist/work-index.d.ts +3 -1
- package/dist/work-index.js +18 -5
- package/dist/workspace-motion.js +3 -3
- package/dist/workspace-ui.d.ts +86 -6
- package/dist/workspace-ui.js +143 -17
- package/package.json +1 -1
package/dist/serve.js
CHANGED
|
@@ -56,7 +56,7 @@ import { knowledgeHtml, knowledgeContextHtml, KNOWLEDGE_CSS, decisionsHtml, memo
|
|
|
56
56
|
import { listDecisions, recordDecision, retireDecision, searchMemory } from "./project-memory.js";
|
|
57
57
|
import { decideProposal, listProposals, memoryStatus } from "./memory-pass.js";
|
|
58
58
|
import { learningHtml } from "./workspace-ui.js";
|
|
59
|
-
import { resultActsOf } from "./result-acts.js";
|
|
59
|
+
import { acceptWithChecksOf, resultActsOf } from "./result-acts.js";
|
|
60
60
|
import { createSessionEndpoint } from './session-server.js';
|
|
61
61
|
import { handleTeamHttp } from './team-http.js';
|
|
62
62
|
import { teamWorkspaceHtml } from './team-ui.js';
|
|
@@ -65,7 +65,7 @@ import { prepareWorkspaceRevision, WorkspaceValidatorCache } from "./workspace-r
|
|
|
65
65
|
import { workIndexPage, workCountsByProject, WorkIndexCursorError, WORK_INDEX_MAX_LIMIT } from "./work-index.js";
|
|
66
66
|
import { leadActivity } from "./lead-voice.js";
|
|
67
67
|
import { openWorkDecisionOf } from "./work-summary.js";
|
|
68
|
-
import { assignmentOf, checkAssignmentAsOperator } from './assignment.js';
|
|
68
|
+
import { acceptAndCompleteAsOperator, assignmentOf, checkAssignmentAsOperator, personCheckPending } from './assignment.js';
|
|
69
69
|
import { demoChatHtml, demoThreadHtml, DEMO_CHAT_SCRIPT } from "./demo-chat.js";
|
|
70
70
|
import { pullRequestSettingsHtml, PULL_REQUEST_SETTINGS_CSS } from './pull-request-ui.js';
|
|
71
71
|
import { checkSettingsHtml, CHECK_SETTINGS_CSS } from './check-levels-ui.js';
|
|
@@ -74,8 +74,8 @@ import { checkPublishing, completeAndOpenPullRequest, mergeAsPerson, pullRequest
|
|
|
74
74
|
import { leadBriefHtml, LEAD_CONTEXT_CSS } from './lead-context.js';
|
|
75
75
|
import { assignmentCatchUp } from './assignment-brief.js';
|
|
76
76
|
import { assignmentActionHref, assignmentCardOf, assignmentStatusOf, assignmentSummaryHtml, assignmentWithEvidence, ASSIGNMENT_CSS } from './assignment-ui.js';
|
|
77
|
-
import { assignmentStageOf, pullRequestFactOf, stageOfCode, stageOfDispatch, statusDetailsHtml, statusIconSvg, statusWhyHtml, taskStatusOf, TASK_STATUS_CSS } from './task-status.js';
|
|
78
|
-
import { ASKS, ASK_LABEL, NEEDS } from './needs-you.js';
|
|
77
|
+
import { assignmentStageOf, demoChecksOf, requirementWordOf, STATUS_MORE, pullRequestFactOf, requirementsOf, stageOfCode, stageOfDispatch, statusDetailsHtml, statusIconSvg, statusWhyHtml, taskStatusOf, TASK_STATUS_CSS } from './task-status.js';
|
|
78
|
+
import { ASKS, ASK_LABEL, GENERAL_SUGGESTION, NEEDS, NO_REASON_RECORDED, failedAttemptSentence, retryNoteOf, failingCheckSuggestion, isInternalErrorReason, latestFinishedAttempt, missedRequirementLine, missedRequirementSuggestion, runReasonWords, stopSuggestionOf } from './needs-you.js';
|
|
79
79
|
import { assignmentPresentationOf, historicalAssessmentReason, shortenedMaterialReason } from './assignment-presentation.js';
|
|
80
80
|
import { ledgerBody } from "./ledger-view.js";
|
|
81
81
|
import { EVIDENCE_PACK_CSS, evidencePack, evidencePackHtml, exportDay, ledgerExportChunks } from "./evidence-pack.js";
|
|
@@ -86,6 +86,7 @@ import { createResultRevision, requestResultChanges } from "./result-actions.js"
|
|
|
86
86
|
import { CHAT_CONTROLS, chatControlHref, chatResultHref as sharedResultHref, isChatControl } from "./chat-controls.js";
|
|
87
87
|
import { CHAT_TASK_ACTIONS, isChatTaskAction } from "./chat-task-actions.js";
|
|
88
88
|
import { WORKSPACE_MOTION_CSS, WORKSPACE_MOTION_SCRIPT } from "./workspace-motion.js";
|
|
89
|
+
import { DISCLOSURE_CSS } from "./disclosure.js";
|
|
89
90
|
import { styleAsset } from "./style-asset.js";
|
|
90
91
|
import { MOBILE_VIEWPORT_SCRIPT } from "./mobile-viewport.js";
|
|
91
92
|
import { authorizePlanUnderMode, applyModeToNewFiling, planAutoPending } from "./plan-auto.js";
|
|
@@ -164,7 +165,7 @@ import { starterRecipes, savedRecipes, findRecipe, importRecipe, exportRecipe, c
|
|
|
164
165
|
import { recipeFromForm, recipeLibraryHtml, recipeEditorHtml, workflowPreviewHtml, recipeScript, RECIPE_CSS, recipeDefinitionPreviewHtml, recipeRunHtml, recipeAnswersFromForm } from "./recipe-ui.js";
|
|
165
166
|
import { EVIDENCE_CAPS, readVerifiedArtifact, readVerifiedReport, readVerifiedProofForRun, storeEvidence, writeEvidenceFile, scanForSecrets } from "./evidence.js";
|
|
166
167
|
import { GOAL_ASSESSMENT_PENDING, reviewConflict, manualReviewOnly, manualReviewCriterionOf, personCheckWords, plainReasonWords, dispatchStatusToken, passFraction, semanticCoverage, coverageWords, coverageStateWords } from "./proof.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";
|
|
168
|
+
import { WORK_VIEWS, REVIEW_TOKENS, RESULT_DECISION_SENTENCE, acceptWordsOf, buildProgressOf, cantAcceptYetOf, earlierAttemptsWords, evidenceProblemOf, lastErrorLineOf, missedRequirementOf, reportMismatchesOf, ACCEPT_NEEDS_REASON, MISMATCH_HEADLINE, parseWorkView, resultStatusOf, resultHeadlineOf, receiptHeadingOf, receiptPublicationWords, reviewFactsOf, workStatusOf, primaryDestinationOf, needsPerson, dispatchActionLabel, } from "./workspace-ui.js";
|
|
168
169
|
import { PRICED_BUILD_MODELS } from "./pricing.js";
|
|
169
170
|
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";
|
|
170
171
|
import { parseReport } from "./scout-report.js";
|
|
@@ -240,7 +241,7 @@ import { parseProjectConcurrency, PROJECT_CONCURRENCY_DEFAULT, projectConcurrenc
|
|
|
240
241
|
import { buildReviewOf, findingWords } from "./review-switch.js";
|
|
241
242
|
import { deleteProject, holdingsWords, projectHoldings, projectRunning } from "./project-delete.js";
|
|
242
243
|
import { POLICY_CSS, policyHtml } from "./policy-ui.js";
|
|
243
|
-
import { checkPolicy, parseList, policyParts } from "./policy.js";
|
|
244
|
+
import { checkPolicy, levelOfProfile, parseList, policyParts } from "./policy.js";
|
|
244
245
|
import { LIMITS_CSS, limitsHtml, limitsView } from "./limits-ui.js";
|
|
245
246
|
import { budgetHoldWords, budgetLabel, budgetStates, monthNamed, monthOf, spendItems, teammateNames as teammateNamesOf, usd as spendUsd } from "./spend.js";
|
|
246
247
|
import { targetOf } from "./monitoring.js";
|
|
@@ -2104,6 +2105,30 @@ export function createDecisionServer(options) {
|
|
|
2104
2105
|
return refuse(response, who, 400, 'This task page has expired. Open the first page.', '/work');
|
|
2105
2106
|
throw error;
|
|
2106
2107
|
}
|
|
2108
|
+
// A failed row says what its latest attempt missed, as its task page does (a stop reason already reads so), from
|
|
2109
|
+
// the database alone: a row reads no check log. A live build's row says which step it is on, or the step it is
|
|
2110
|
+
// stuck on, from that live attempt's own progress only, never a stopped attempt's; the step's words come from its
|
|
2111
|
+
// current plan (one saved file, for live rows only), and without it the row names the step by number.
|
|
2112
|
+
work = { ...work, items: work.items.map((item) => {
|
|
2113
|
+
if (item.liveRunId !== null) {
|
|
2114
|
+
const ref = store.lookupRef(item.activeTaskId);
|
|
2115
|
+
const recorded = ref === null ? null : store.latestCheckpointForRun(item.liveRunId);
|
|
2116
|
+
if (ref === null || recorded === null || recorded.taskRef !== ref.id)
|
|
2117
|
+
return item;
|
|
2118
|
+
const plan = store.currentPlanRevision(ref.id);
|
|
2119
|
+
const parsed = plan === null ? null : (() => { const doc = revisionDocOf(plan); return doc === null ? null : parseExecutionPlanDocument(doc.document); })();
|
|
2120
|
+
const words = new Map(parsed?.ok === true ? milestonesOf(parsed.document).map(one => [one.id, one.description]) : []);
|
|
2121
|
+
const steps = buildProgressOf(recorded.snapshot.milestones.map(one => ({ description: words.get(one.id) ?? null, state: one.state, note: one.note ?? null })));
|
|
2122
|
+
return steps === null ? item : { ...item, progress: steps.line };
|
|
2123
|
+
}
|
|
2124
|
+
if (item.state !== "failed")
|
|
2125
|
+
return item;
|
|
2126
|
+
const family = familyOf(item.activeTaskId);
|
|
2127
|
+
if (family === null || family.problem !== null)
|
|
2128
|
+
return item;
|
|
2129
|
+
const failure = failureOf(family.versions.flatMap(version => store.runsFor(version.refId)), null, false);
|
|
2130
|
+
return failure.kind === "reason" ? item : { ...item, status: { ...item.status, detail: failure.line } };
|
|
2131
|
+
}) };
|
|
2107
2132
|
const page = workPage(chromeFor(project, "work", undefined, rollup ? "all" : undefined), {
|
|
2108
2133
|
view,
|
|
2109
2134
|
...(url.searchParams.has('project') && project !== null ? { projectFilter: project } : {}),
|
|
@@ -2234,7 +2259,7 @@ export function createDecisionServer(options) {
|
|
|
2234
2259
|
if (selected !== null) {
|
|
2235
2260
|
const view = taskViewData(selected, who, null);
|
|
2236
2261
|
detail = view === null
|
|
2237
|
-
? `<p class="workbench-mobile-back"><a href="/workbench">← all work</a></p><div class="card"><p class="meta">
|
|
2262
|
+
? `<p class="workbench-mobile-back"><a href="/workbench">← all work</a></p><div class="card"><p class="meta">No such task — it may have been outside this console's view</p></div>`
|
|
2238
2263
|
: `<p class="workbench-mobile-back"><a href="/workbench">← all work</a></p>${taskBody({ ...view, degraded: "pane" })}`;
|
|
2239
2264
|
}
|
|
2240
2265
|
return sendScreen(response, 200, screen("portfolio", detail, {
|
|
@@ -2389,10 +2414,15 @@ export function createDecisionServer(options) {
|
|
|
2389
2414
|
// The ranked queue itself stays bounded and says so.
|
|
2390
2415
|
const beyond = wantedId === null || inQueue !== null ? null : completedRowFor(wantedId, project);
|
|
2391
2416
|
const beyondRow = beyond === null ? null : resultRow(beyond);
|
|
2392
|
-
const
|
|
2417
|
+
const completedChoice = inQueue ?? (beyondRow === null ? null : { ...beyondRow, priority: reviewPriorityOf(beyondRow) });
|
|
2418
|
+
const expectedRun = url.searchParams.get("run");
|
|
2419
|
+
const namedRun = expectedRun !== null && /^[1-9]\d*$/.test(expectedRun) ? Number(expectedRun) : null;
|
|
2420
|
+
// A build that failed (or stopped) has its own result page too: the run named, or a failed task's latest attempt.
|
|
2421
|
+
const attempt = wantedId === null || (completedChoice !== null && (namedRun === null || completedChoice.runId === namedRun)) ? null : attemptRowFor(wantedId, namedRun, project);
|
|
2422
|
+
const attemptRow = attempt === null ? null : resultRow(attempt);
|
|
2423
|
+
const chosen = attemptRow !== null ? { ...attemptRow, priority: reviewPriorityOf(attemptRow) } : completedChoice;
|
|
2393
2424
|
const selectedRow = wantedId === null ? ranked[0] ?? null : chosen;
|
|
2394
2425
|
const csrf = who.via === "cookie" ? who.session.csrf : "";
|
|
2395
|
-
const expectedRun = url.searchParams.get("run");
|
|
2396
2426
|
// An exact link to a result this person can't see (or that never existed) reads the same either way.
|
|
2397
2427
|
if (expectedRun !== null && wantedId !== null && chosen === null)
|
|
2398
2428
|
return refuse(response, who, 404, "No such result in your projects.", "/work");
|
|
@@ -2400,11 +2430,34 @@ export function createDecisionServer(options) {
|
|
|
2400
2430
|
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") }));
|
|
2401
2431
|
}
|
|
2402
2432
|
const selected = selectedRow === null ? null : reviewCockpitViewOf(selectedRow, who, now);
|
|
2433
|
+
// A build that delivered nothing reads Failed here, and so does a failed task's delivered result: what went wrong
|
|
2434
|
+
// is the card and Retry the ink act. That result may still be accepted, but only in outline, as Accept anyway, with
|
|
2435
|
+
// a reason.
|
|
2436
|
+
const delivered = selected?.run?.outcome === "built" || selected?.run?.outcome === "no-change";
|
|
2437
|
+
const failedTask = selected === null ? null : store.getTask(selected.taskId);
|
|
2438
|
+
if (selected !== null && attemptRow !== null && selected.run !== null && (!delivered || failedTask?.state === "failed")) {
|
|
2439
|
+
const attemptRun = store.getRun(selected.run.id);
|
|
2440
|
+
const runs = attemptRun === null ? [] : store.runsFor(attemptRun.taskRef);
|
|
2441
|
+
const latest = latestFinishedAttempt(runs);
|
|
2442
|
+
// Retry from here only when the task failed, nothing holds it, and this is its latest attempt or a result it delivered.
|
|
2443
|
+
const retry = failedTask?.state === "failed" && (latest?.id === selected.run.id || delivered) && csrf !== "" && who.role === "approver" && store.currentLiveLease(attemptRun.taskRef, now) === null
|
|
2444
|
+
? { action: `${taskHref(selected.taskId)}/requeue` } : null;
|
|
2445
|
+
// Accept anyway: the same acceptance this task's latest result takes, never for an older one or one already accepted.
|
|
2446
|
+
const family = delivered ? familyOf(selected.taskId) : null;
|
|
2447
|
+
const acceptAnyway = delivered && csrf !== "" && who.role === "approver" && family?.current.id === selected.taskId && family.problem === null &&
|
|
2448
|
+
runs.find(runIsTaskResult)?.id === selected.run.id && store.proofAcceptance(selected.run.id) === null
|
|
2449
|
+
? { action: `${taskHref(selected.taskId)}/accept-proof`, run: selected.run.id } : null;
|
|
2450
|
+
if (attemptRun !== null) {
|
|
2451
|
+
// A delivered result didn't fail itself: the task's failure is its latest attempt's.
|
|
2452
|
+
const failure = delivered ? failureOf(runs, null) : explainAttempt(attemptRun, null);
|
|
2453
|
+
selected.failure = { ...failure, retry: retry === null ? null : { ...retry, note: retryNoteOf(failure.suggestion) }, ...(acceptAnyway === null ? {} : { acceptAnyway }) };
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2403
2456
|
const reviewPage = reviewCockpitPage(chromeFor(wantedId === null ? project : chosenProject, "runs"), {
|
|
2404
2457
|
queue: ranked,
|
|
2405
2458
|
queueCap: REVIEW_QUEUE_CAP,
|
|
2406
2459
|
selected,
|
|
2407
|
-
beyondQueue: beyond !== null,
|
|
2460
|
+
beyondQueue: beyond !== null && attemptRow === null,
|
|
2408
2461
|
// A deep link to a result this console cannot show — not done,
|
|
2409
2462
|
// not admitted, or never existed — says so in one sentence and
|
|
2410
2463
|
// shows the top of the queue; the three cases read identically.
|
|
@@ -2547,7 +2600,7 @@ export function createDecisionServer(options) {
|
|
|
2547
2600
|
const csrf = who.via === "cookie" ? who.session.csrf : "";
|
|
2548
2601
|
const hasGrant = store.hasMergeCapableGrant(project, now);
|
|
2549
2602
|
const current = live === null || liveTerms === null
|
|
2550
|
-
? `<div class="card"><h2>
|
|
2603
|
+
? `<div class="card"><h2>Locked</h2><p class="meta">No mode is signed — every action asks for your password. That is the default.</p></div>`
|
|
2551
2604
|
: [
|
|
2552
2605
|
`<div class="card">`,
|
|
2553
2606
|
`<h2 style="margin-top:0">${escape(live.name)} <span class="meta">signed by ${personChip(live.signedBy)}</span></h2>`,
|
|
@@ -2557,7 +2610,7 @@ export function createDecisionServer(options) {
|
|
|
2557
2610
|
who.role === "approver"
|
|
2558
2611
|
? `<form method="post" action="/mode/revoke" class="row">` +
|
|
2559
2612
|
`<input type="hidden" name="csrf" value="${escape(csrf)}">` +
|
|
2560
|
-
`<button type="submit">
|
|
2613
|
+
`<button type="submit">End this mode now</button>` +
|
|
2561
2614
|
`<span class="meta">one click — every act it covered falls back to its own ceremony</span></form>`
|
|
2562
2615
|
: "",
|
|
2563
2616
|
`</div>`,
|
|
@@ -2569,31 +2622,31 @@ export function createDecisionServer(options) {
|
|
|
2569
2622
|
`<h2 style="margin-top:0">${live === null ? "sign a mode" : "replace it — a renewal is a new signature"}</h2>`,
|
|
2570
2623
|
`<form method="post" action="/mode/confirm">`,
|
|
2571
2624
|
`<input type="hidden" name="csrf" value="${escape(csrf)}">`,
|
|
2572
|
-
`<label>
|
|
2573
|
-
`<label>
|
|
2574
|
-
`<label>
|
|
2625
|
+
`<label>Preset<select name="name"><option value="standard">standard — approvals stay with me</option><option value="hands-off">hands-off — your filings auto-approve, full permissions</option></select></label>`,
|
|
2626
|
+
`<label>Days <span class="meta">(1\u2013${MODE_MAX_DAYS})</span><input type="number" name="days" value="1" min="1" max="${MODE_MAX_DAYS}"></label>`,
|
|
2627
|
+
`<label>Merges<select name="publication">` +
|
|
2575
2628
|
`<option value="notify">wait for me — even under a merge grant</option>` +
|
|
2576
2629
|
(hasGrant ? `<option value="automerge">merge themselves when CI is green on the exact commit</option>` : "") +
|
|
2577
2630
|
`</select></label>` +
|
|
2578
2631
|
(hasGrant ? "" : `<span class="meta">self-merging needs a merge-capable publication grant first</span>`),
|
|
2579
|
-
`<label>
|
|
2632
|
+
`<label>My filings<select name="auto-approve">` +
|
|
2580
2633
|
`<option value="">the preset's default (standard: wait for approval; hands-off: auto-approve)</option>` +
|
|
2581
2634
|
`<option value="1">approve the moment I file them</option>` +
|
|
2582
2635
|
`<option value="0">wait for their own approval</option>` +
|
|
2583
2636
|
`</select></label>`,
|
|
2584
|
-
`<label>
|
|
2637
|
+
`<label>Planner approval<select name="plan-auto">` +
|
|
2585
2638
|
`<option value="0">wait for my approval</option>` +
|
|
2586
2639
|
`<option value="1">auto-approve plans that preserve my filed contract</option>` +
|
|
2587
2640
|
`</select><span class="meta">Requires automatic filing approval. File the goal, paths, and acceptance criteria upfront. Changed scope and unanswered questions still pause.</span></label>`,
|
|
2588
|
-
`<label>
|
|
2641
|
+
`<label>If a subscription runs out<select name="allow-paid-fallback">` +
|
|
2589
2642
|
`<option value="">never switch to a paid API key on its own (the default, every preset)</option>` +
|
|
2590
2643
|
`<option value="1">allow the approved fallback — spend moves to that account</option>` +
|
|
2591
2644
|
`</select></label>`,
|
|
2592
|
-
`<button type="submit">
|
|
2645
|
+
`<button type="submit">Read the full terms</button>`,
|
|
2593
2646
|
`</form>`,
|
|
2594
2647
|
`</div>`,
|
|
2595
2648
|
].join("\n");
|
|
2596
|
-
return sendScreen(response, 200, screen("mode", [`<h1>
|
|
2649
|
+
return sendScreen(response, 200, screen("mode", [`<h1>Operating mode</h1><p>Authorize this project's automatic approvals once, for up to ${MODE_MAX_DAYS} days. Select the touchpoints below; the signature covers the exact choices. Existing modes keep their original terms.</p><p class="meta">Scope changes, agent questions, and requested revisions still require your attention. Automatic merging also needs a publication grant.</p>`, current, signForm].join("\n"), { chrome: chromeFor(project, "mode") }));
|
|
2597
2650
|
}
|
|
2598
2651
|
if (url.pathname === "/people") {
|
|
2599
2652
|
// Approvers see everyone (D7's ceiling: every fact already passed
|
|
@@ -2635,13 +2688,13 @@ export function createDecisionServer(options) {
|
|
|
2635
2688
|
? ""
|
|
2636
2689
|
: `<p class="row">watching now: ${attended.map(session => `<a href="/t/${escape(session.taskId)}">${escape(session.taskId)}</a>`).join(", ")}</p>`,
|
|
2637
2690
|
acts.length === 0
|
|
2638
|
-
? `<p class="meta">
|
|
2691
|
+
? `<p class="meta">No recorded acts yet</p>`
|
|
2639
2692
|
: acts.map(act => `<p class="row meta">${escape(act.kind)} ${escape(act.subject)} \u00b7 ${escape(act.at.slice(0, 16).replace("T", " "))}</p>`).join("\n"),
|
|
2640
2693
|
approverView && one.revokedAt === null && one.name !== who.name
|
|
2641
2694
|
? `<form method="post" action="/people/revoke" class="row">` +
|
|
2642
2695
|
`<input type="hidden" name="csrf" value="${escape(csrf)}"><input type="hidden" name="name" value="${escape(one.name)}">` +
|
|
2643
|
-
`<label>
|
|
2644
|
-
`<button type="submit">
|
|
2696
|
+
`<label>Your password<input type="password" name="token" autocomplete="current-password"></label>` +
|
|
2697
|
+
`<button type="submit">Remove ${escape(one.name)}'s sign-in</button>` +
|
|
2645
2698
|
`<span class="meta">ends their access and everything it signed \u2014 history stays</span></form>`
|
|
2646
2699
|
: "",
|
|
2647
2700
|
`</div>`,
|
|
@@ -2651,20 +2704,20 @@ export function createDecisionServer(options) {
|
|
|
2651
2704
|
const inviteRows = invites.map(one => `<p class="row">${escape(one.role)} invite · ${one.projects === null ? "all projects" : one.projects.map(repo => escape(projectName(repo))).join(", ")} \u00b7 from ${personChip(one.mintedBy)} \u00b7 expires ${escape(one.expiresAt.slice(0, 16).replace("T", " "))}${one.attempts > 0 ? ` \u00b7 ${one.attempts} failed attempt${one.attempts === 1 ? "" : "s"}` : ""}` +
|
|
2652
2705
|
` <form method="post" action="/people/invite-revoke" style="display:inline">` +
|
|
2653
2706
|
`<input type="hidden" name="csrf" value="${escape(csrf)}"><input type="hidden" name="id" value="${one.id}">` +
|
|
2654
|
-
`<label>
|
|
2655
|
-
`<button type="submit">
|
|
2707
|
+
`<label>Password <input type="password" name="token" autocomplete="current-password" style="width:8rem"></label>` +
|
|
2708
|
+
`<button type="submit">Cancel it</button></form></p>`);
|
|
2656
2709
|
const inviteCard = !approverView
|
|
2657
2710
|
? ""
|
|
2658
2711
|
: [
|
|
2659
2712
|
`<div class="card">`,
|
|
2660
|
-
`<h2 style="margin-top:0">
|
|
2661
|
-
inviteRows.length === 0 ? `<p class="meta">
|
|
2713
|
+
`<h2 style="margin-top:0">Invite someone</h2>`,
|
|
2714
|
+
inviteRows.length === 0 ? `<p class="meta">No open invites</p>` : inviteRows.join("\n"),
|
|
2662
2715
|
`<form method="post" action="/people/invite" class="row">`,
|
|
2663
2716
|
`<input type="hidden" name="csrf" value="${escape(csrf)}">`,
|
|
2664
|
-
`<label>
|
|
2717
|
+
`<label>They can<select name="role"><option value="viewer">Viewer — read work</option><option value="approver">Operator — create, manage and approve work</option></select></label>`,
|
|
2665
2718
|
accessFields(project === null ? [] : [project]),
|
|
2666
|
-
`<label>
|
|
2667
|
-
`<button type="submit">
|
|
2719
|
+
`<label>Your password<input type="password" name="token" autocomplete="current-password"></label>`,
|
|
2720
|
+
`<button type="submit">Make an invite link</button>`,
|
|
2668
2721
|
`<span class="meta">single-use, expires in 72 hours</span>`,
|
|
2669
2722
|
`</form>`,
|
|
2670
2723
|
`</div>`,
|
|
@@ -2786,7 +2839,7 @@ export function createDecisionServer(options) {
|
|
|
2786
2839
|
`<p class="row"><a href="/r/${run.id}" class="mono">#${run.id}</a> <a href="${taskHref(run.taskId)}"><strong>${escape(run.title)}</strong></a>${projectChip(run.repo)} ` +
|
|
2787
2840
|
`<span class="right meta mono">${escape(run.runner)} \u00b7 ${escape(run.role)} \u00b7 ${escape(stage)} \u00b7 ${escape(relativeAge(run.startedAt, now))}</span></p>`,
|
|
2788
2841
|
options.localRunner === undefined
|
|
2789
|
-
? `<p class="meta">
|
|
2842
|
+
? `<p class="meta">The transcript is readable on the machine that runs the worker \u2014 start the console there with <code>toolroll up</code></p>`
|
|
2790
2843
|
: `<pre class="recap plan-doc live-pane" id="live-transcript-${run.id}" data-initial-offset="0">${escape(tail)}</pre><p class="meta" id="live-transcript-${run.id}-state"></p>`,
|
|
2791
2844
|
`</section>`,
|
|
2792
2845
|
].join("\n");
|
|
@@ -2799,7 +2852,7 @@ export function createDecisionServer(options) {
|
|
|
2799
2852
|
return sendScreen(response, 200, screen("peek", [
|
|
2800
2853
|
`<h1>Peek</h1>`,
|
|
2801
2854
|
panes.length === 0
|
|
2802
|
-
? `<p class="meta">
|
|
2855
|
+
? `<p class="meta">No agent is working right now \u2014 this page follows them the moment one starts</p>`
|
|
2803
2856
|
: panes.join("\n"),
|
|
2804
2857
|
].join("\n"), { chrome: chromeFor(project, "runs"), functional: { script, fetches: true } }));
|
|
2805
2858
|
}
|
|
@@ -2823,19 +2876,19 @@ export function createDecisionServer(options) {
|
|
|
2823
2876
|
...(said === null ? [] : [`<p class="meta">${escape(said)}</p>`]),
|
|
2824
2877
|
`<div id="fleet-region">${body}</div>`,
|
|
2825
2878
|
`<p class="meta" id="fleet-region-stamp"></p>`,
|
|
2826
|
-
`<h2>
|
|
2879
|
+
`<h2>Register a worker</h2>`,
|
|
2827
2880
|
`<form method="post" action="/fleet/runner/register" class="card">` +
|
|
2828
2881
|
`<input type="hidden" name="csrf" value="${escape(csrf)}">` +
|
|
2829
|
-
`<label>
|
|
2830
|
-
`<label>
|
|
2831
|
-
`<label>
|
|
2832
|
-
`<button type="submit">
|
|
2833
|
-
`<details class="arm-danger"><summary>
|
|
2882
|
+
`<label>Name<input type="text" name="name" placeholder="builder-2" maxlength="60" required></label>` +
|
|
2883
|
+
`<label>Capacity<input type="text" name="capacity" inputmode="numeric" value="1" aria-label="capacity"></label>` +
|
|
2884
|
+
`<label>Your password, typed again<input type="password" name="token" autocomplete="current-password" required></label>` +
|
|
2885
|
+
`<button type="submit">Register — its token is shown once</button></form>`,
|
|
2886
|
+
`<details class="arm-danger"><summary>Retire a worker</summary>` +
|
|
2834
2887
|
`<form method="post" action="/fleet/runner/retire" class="card">` +
|
|
2835
2888
|
`<input type="hidden" name="csrf" value="${escape(csrf)}">` +
|
|
2836
|
-
`<label>
|
|
2837
|
-
`<label>
|
|
2838
|
-
`<button type="submit" class="danger">
|
|
2889
|
+
`<label>Which worker<input type="text" name="name" placeholder="builder-1" required></label>` +
|
|
2890
|
+
`<label>Your password, typed again<input type="password" name="token" autocomplete="current-password" required></label>` +
|
|
2891
|
+
`<button type="submit" class="danger">Retire it</button></form></details>`,
|
|
2839
2892
|
].join("\n"),
|
|
2840
2893
|
// The password fields make this screen sensitive: sendScreen strips
|
|
2841
2894
|
// the chrome additions and keeps the reorder poller — the named
|
|
@@ -2863,7 +2916,7 @@ export function createDecisionServer(options) {
|
|
|
2863
2916
|
return refuse(response, who, 404, "That version is not available for this task.", taskHref(family.root.id));
|
|
2864
2917
|
if (task.taskId !== family.root.id)
|
|
2865
2918
|
return redirect(response, `${taskHref(family.root.id)}?version=${encodeURIComponent(task.taskId)}`);
|
|
2866
|
-
return taskScreen(response, who, version ?? family.current.id, null, 200);
|
|
2919
|
+
return taskScreen(response, who, version ?? family.current.id, null, 200, undefined, undefined, url.searchParams.get("edit") === "plan");
|
|
2867
2920
|
}
|
|
2868
2921
|
// v103: a task's evidence pack, as a printable page or JSON (the whole family, sealed ledger entries included).
|
|
2869
2922
|
const evidence = matchTaskPath(url.pathname, "/evidence$");
|
|
@@ -3034,6 +3087,11 @@ export function createDecisionServer(options) {
|
|
|
3034
3087
|
const tab = parseResultTab(url.searchParams.get("tab"));
|
|
3035
3088
|
return redirect(response, `${reviewHref(taskId, found.id)}${tab === "summary" ? "" : `&tab=${tab}`}`);
|
|
3036
3089
|
}
|
|
3090
|
+
// A build that failed or stopped opens on its own result page too: its changes, its checks and what went wrong.
|
|
3091
|
+
if (url.searchParams.get("record") === null && !running && (found.role === "builder" || found.role === "scout") && attemptRowFor(taskId, found.id, null) !== null) {
|
|
3092
|
+
const tab = parseResultTab(url.searchParams.get("tab"));
|
|
3093
|
+
return redirect(response, `${reviewHref(taskId, found.id)}${tab === "summary" ? "" : `&tab=${tab}`}`);
|
|
3094
|
+
}
|
|
3037
3095
|
// Pollers exist only for a LIVE run — an orphaned null-outcome run
|
|
3038
3096
|
// would otherwise be refetched forever (finding 15). The nonce is
|
|
3039
3097
|
// sendScreen's business now.
|
|
@@ -3533,6 +3591,8 @@ export function createDecisionServer(options) {
|
|
|
3533
3591
|
})();
|
|
3534
3592
|
const roomId = url.searchParams.get('conversation');
|
|
3535
3593
|
const resultLink = (href) => roomId ? href + '&conversation=' + encodeURIComponent(roomId) : href;
|
|
3594
|
+
// Accept and finish is here: the one act that accepts the person's own checks and finishes the task.
|
|
3595
|
+
const finishes = resultRun !== null && who.role === 'approver' && focusTask?.assignment?.state === 'ready-to-check' && focusTask.assignment.receipt?.runId === resultRun.id;
|
|
3536
3596
|
const resultPanel = resultRun === null
|
|
3537
3597
|
? null
|
|
3538
3598
|
: resultPanelHtml(resultDetailOf(resultRun, who, now), {
|
|
@@ -3545,9 +3605,9 @@ export function createDecisionServer(options) {
|
|
|
3545
3605
|
hrefFor: one => resultLink(chatResultHref(focusTask?.id ?? "", resultRun.id, one)),
|
|
3546
3606
|
returnTo: resultLink(chatResultHref(focusTask?.id ?? "", resultRun.id)),
|
|
3547
3607
|
back: { href: roomId ? "/chat?conversation=" + encodeURIComponent(roomId) : taskChatHref(focusTask?.id ?? ""), label: "Back to chat" },
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
? completionForm(focusTask.assignment.receipt.taskId, resultRun.id, focusTask.assignment.receipt.digest, who.session.csrf, pullRequestTargetOf(resultRun.id)) : '');
|
|
3608
|
+
finishes,
|
|
3609
|
+
}) + (finishes && focusTask?.assignment?.receipt != null
|
|
3610
|
+
? completionForm(focusTask.assignment.receipt.taskId, resultRun.id, focusTask.assignment.receipt.digest, who.session.csrf, pullRequestTargetOf(resultRun.id), owedAcceptanceOf(focusTask.assignment.receipt)) : '');
|
|
3551
3611
|
const focusProblem = requestedTask !== null && focusTask === null
|
|
3552
3612
|
? "That task is not available in this workspace."
|
|
3553
3613
|
: requestedResult !== null && focusTask !== null && resultRun === null
|
|
@@ -5317,7 +5377,7 @@ export function createDecisionServer(options) {
|
|
|
5317
5377
|
.map(task => `<a class="item${task.id === currentId ? " current" : ""}" href="${taskHref(task.id)}">` +
|
|
5318
5378
|
`<span class="t">${escape(task.title)}</span></a>`)
|
|
5319
5379
|
.join("\n");
|
|
5320
|
-
return `<h2>
|
|
5380
|
+
return `<h2>Tasks</h2>\n${items === "" ? `<p class="meta">None yet</p>` : items}`;
|
|
5321
5381
|
}
|
|
5322
5382
|
/**
|
|
5323
5383
|
* The one liveness fact (round-4 findings 14/15): a run is being built
|
|
@@ -5350,7 +5410,7 @@ export function createDecisionServer(options) {
|
|
|
5350
5410
|
`<span class="m">${runOutcomeBadge(run, live.has(run.id))}` +
|
|
5351
5411
|
`<span class="mono">${whenTime(run.startedAt)}</span></span></a>`)
|
|
5352
5412
|
.join("\n");
|
|
5353
|
-
return `<h2>
|
|
5413
|
+
return `<h2>Builds</h2>\n${items === "" ? `<p class="meta">None yet</p>` : items}`;
|
|
5354
5414
|
}
|
|
5355
5415
|
/**
|
|
5356
5416
|
* The opener: every project inside the ceiling, most recently opened
|
|
@@ -5834,6 +5894,8 @@ export function createDecisionServer(options) {
|
|
|
5834
5894
|
// available, computed fresh only when this call has no `who`.
|
|
5835
5895
|
planRevisions,
|
|
5836
5896
|
milestoneProgress: view?.milestoneProgress ?? progressOf(ref.id, planRevisions?.current?.document ?? null),
|
|
5897
|
+
// What a failed task missed, said as the task page says it.
|
|
5898
|
+
failure: task.state === "failed" ? failureOf(family.versions.flatMap(version => store.runsFor(version.refId)), view?.assignment ?? assignmentOf(store, task.id, now, workAccess(), evidenceRoot)) : null,
|
|
5837
5899
|
claimed: view?.claimed ?? store.hasLiveClaim(ref.id, now),
|
|
5838
5900
|
liveRun: live === null ? null : { id: live.id, runner: live.runner, startedAt: live.startedAt, phase: live.phase },
|
|
5839
5901
|
checkProgress: view?.checkProgress ?? (live === null && latest === null ? null : store.checkProgress((live ?? latest).id)),
|
|
@@ -5895,7 +5957,69 @@ export function createDecisionServer(options) {
|
|
|
5895
5957
|
return null;
|
|
5896
5958
|
return { taskId: rootId, view: null, offer: { taskId: receipt.taskId, runId: receipt.runId, digest: receipt.digest }, target };
|
|
5897
5959
|
}
|
|
5898
|
-
|
|
5960
|
+
/** What went wrong with the latest finished attempt (whatever its outcome, never an older failure). */
|
|
5961
|
+
function failureOf(runs, assignment, readLog = true) {
|
|
5962
|
+
const last = latestFinishedAttempt(runs);
|
|
5963
|
+
if (last === null)
|
|
5964
|
+
return { kind: "reason", line: NO_REASON_RECORDED, evidence: null, suggestion: GENERAL_SUGGESTION, link: null };
|
|
5965
|
+
const full = store.getRun(last.id);
|
|
5966
|
+
return full === null ? { kind: "reason", line: failedAttemptSentence(last.reason), evidence: null, suggestion: stopSuggestionOf(last.reason), link: null }
|
|
5967
|
+
: explainAttempt(full, assignment?.receipt?.runId === last.id ? assignment.receipt : null, readLog);
|
|
5968
|
+
}
|
|
5969
|
+
/** One finished attempt's failure in plain words: a failing check's last error line (linked to that line in its
|
|
5970
|
+
* saved log), else the first signed requirement it missed with the evidence line behind it, else its recorded reason
|
|
5971
|
+
* in plain words (or that none was recorded). Each comes with one suggestion of what to change, and a link to the
|
|
5972
|
+
* attempt's own result page, where its changes, checks and failure are; machine output reads as an internal error,
|
|
5973
|
+
* its detail behind a link to that line of the attempt's record. `readLog` false (a list's rows) reads no saved
|
|
5974
|
+
* check log: the requirement or the reason, from the database alone. */
|
|
5975
|
+
function explainAttempt(run, receipt, readLog = true) {
|
|
5976
|
+
const taskId = store.externalIdFor(run.taskRef) ?? "";
|
|
5977
|
+
const page = `${reviewHref(taskId)}&run=${run.id}`;
|
|
5978
|
+
const see = taskId === "" ? null : { label: `See build #${run.id}`, href: page };
|
|
5979
|
+
const artifacts = store.artifactsFor(run.id);
|
|
5980
|
+
const logArtifact = !readLog ? null : receipt?.checks.status === "failed" && receipt.checks.logArtifactId !== null
|
|
5981
|
+
? artifacts.find(one => one.id === receipt.checks.logArtifactId) ?? null
|
|
5982
|
+
: [...artifacts].reverse().find(one => one.kind === "check-log") ?? null;
|
|
5983
|
+
if (logArtifact !== null) {
|
|
5984
|
+
let read = null;
|
|
5985
|
+
try {
|
|
5986
|
+
read = readVerifiedArtifact(evidenceRoot, logArtifact);
|
|
5987
|
+
}
|
|
5988
|
+
catch {
|
|
5989
|
+
read = null;
|
|
5990
|
+
}
|
|
5991
|
+
const text = read !== null && read.ok ? read.content.toString("utf8") : null;
|
|
5992
|
+
const exit = text === null ? null : /^\(exit (-?[0-9]+)\)$/m.exec(text);
|
|
5993
|
+
const failedCheck = receipt?.checks.status === "failed" || (exit !== null && exit !== undefined && Number(exit[1]) !== 0);
|
|
5994
|
+
const found = text !== null && failedCheck ? lastErrorLineOf(text) : null;
|
|
5995
|
+
if (found !== null) {
|
|
5996
|
+
const line = oneLineOf(found.text, 140);
|
|
5997
|
+
return { kind: "check", line: `The check failed: ${line}`, evidence: null, suggestion: failingCheckSuggestion(line),
|
|
5998
|
+
link: taskId === "" ? null : { label: `Check output, line ${found.line}`, href: `${page}&tab=checks#check-log-L${found.line}` } };
|
|
5999
|
+
}
|
|
6000
|
+
}
|
|
6001
|
+
const verdict = store.proofVerdictFor(run.id);
|
|
6002
|
+
const missed = verdict === null ? null : missedRequirementOf(verdict.matrix);
|
|
6003
|
+
if (missed !== null) {
|
|
6004
|
+
return { kind: "requirement", line: missedRequirementLine(missed.statement), evidence: missed.evidence === null ? null : oneLineOf(missed.evidence, 240),
|
|
6005
|
+
suggestion: missedRequirementSuggestion(missed.statement), link: see };
|
|
6006
|
+
}
|
|
6007
|
+
const internal = isInternalErrorReason(run.reason);
|
|
6008
|
+
return { kind: "reason", line: failedAttemptSentence(run.reason), evidence: null, suggestion: stopSuggestionOf(run.reason),
|
|
6009
|
+
link: internal ? { label: "The recorded error", href: `/r/${run.id}?record=1#run-reason-detail` } : see };
|
|
6010
|
+
}
|
|
6011
|
+
/** Run checks in place on a result's commit: an approver's browser session, outside the demo, when the project has a
|
|
6012
|
+
* check and none is waiting or running on it. Comes back to `returnTo`. */
|
|
6013
|
+
function runChecksHere(runId, who, returnTo) {
|
|
6014
|
+
if (runId === null || who.via !== "cookie" || who.role !== "approver" || store.isDemo())
|
|
6015
|
+
return null;
|
|
6016
|
+
const run = store.getRun(runId);
|
|
6017
|
+
const followUps = run === null ? null : followUpsFor(store, evidenceRoot, run, clock());
|
|
6018
|
+
if (followUps === null || !(followUps.quick || followUps.full) || followUps.checks.some(one => one.state === "waiting" || one.state === "running"))
|
|
6019
|
+
return null;
|
|
6020
|
+
return { action: `/r/${runId}/checks`, level: followUps.full ? "full" : "quick", returnTo };
|
|
6021
|
+
}
|
|
6022
|
+
function taskScreen(response, who, taskId, problem, status, scopeDraft, cancelDraft, editPlan = false) {
|
|
5899
6023
|
const admittedFamily = familyOf(taskId);
|
|
5900
6024
|
if (admittedFamily?.problem != null)
|
|
5901
6025
|
return sendScreen(response, status, screen(admittedFamily.root.title, `<h1>${escape(admittedFamily.root.title)}</h1><p>${escape(admittedFamily.current.state)}</p><p class="problem" data-history-problem>${escape(admittedFamily.problem)}</p>`, { chrome: chromeFor(admittedFamily.root.repo, "tasks") }));
|
|
@@ -5910,6 +6034,8 @@ export function createDecisionServer(options) {
|
|
|
5910
6034
|
presentedData.scopeDraft = scopeDraft;
|
|
5911
6035
|
if (cancelDraft !== undefined)
|
|
5912
6036
|
presentedData.cancelDraft = cancelDraft;
|
|
6037
|
+
if (editPlan)
|
|
6038
|
+
presentedData.editPlan = true;
|
|
5913
6039
|
presentedData.pullRequest = taskPullRequestOf(taskId, who);
|
|
5914
6040
|
// The thread reads the whole family: the original, its revisions, and every attempt across them.
|
|
5915
6041
|
if (family !== null && family.problem === null) {
|
|
@@ -5917,6 +6043,10 @@ export function createDecisionServer(options) {
|
|
|
5917
6043
|
versions: family.versions.map(one => ({ id: one.id, title: one.title, state: one.state })),
|
|
5918
6044
|
runs: family.versions.flatMap(version => version.id === data.task.id ? data.runs.map(run => ({ ...run, taskId: version.id })) : store.runsFor(version.refId).map(run => ({ ...run, taskId: version.id }))) };
|
|
5919
6045
|
}
|
|
6046
|
+
// What went wrong reads the family's latest finished attempt, as the Tasks row does.
|
|
6047
|
+
presentedData.failure = failureOf(presentedData.family?.runs ?? data.runs, data.assignment ?? null);
|
|
6048
|
+
presentedData.runChecks = runChecksHere(data.assignment?.receipt?.runId ?? null, who, taskHref(taskId));
|
|
6049
|
+
presentedData.demo = store.isDemo();
|
|
5920
6050
|
const paneProject = restricted() ? store.lookupRef(taskId)?.repo ?? null : who.via === "cookie" ? who.session.project : null;
|
|
5921
6051
|
const page = taskPage(paneProject === null && !unscopedMode
|
|
5922
6052
|
? chromeFor(paneProject, "tasks")
|
|
@@ -6097,7 +6227,7 @@ export function createDecisionServer(options) {
|
|
|
6097
6227
|
return peekSay(seen.reason);
|
|
6098
6228
|
const stamp = clock().toISOString().slice(11, 19);
|
|
6099
6229
|
const parts = [
|
|
6100
|
-
`<p class="meta">
|
|
6230
|
+
`<p class="meta">Best-effort look at ${escape(stamp)} UTC — files can change mid-read</p>`,
|
|
6101
6231
|
];
|
|
6102
6232
|
const changed = seen.rows.filter(one => one.kind === "changed");
|
|
6103
6233
|
const deleted = seen.rows.filter(one => one.kind === "deleted");
|
|
@@ -6122,7 +6252,7 @@ export function createDecisionServer(options) {
|
|
|
6122
6252
|
for (const row of deleted)
|
|
6123
6253
|
parts.push(line(row, "−"));
|
|
6124
6254
|
if (fresh.total > 0) {
|
|
6125
|
-
parts.push(`<p class="meta">
|
|
6255
|
+
parts.push(`<p class="meta">New files · ${fresh.total}</p>`);
|
|
6126
6256
|
for (const row of fresh.rows) {
|
|
6127
6257
|
parts.push(row.collapsed
|
|
6128
6258
|
? `<p class="row mono">+ ${peekName(row.label)} <span class="meta">collapsed names — ${row.count} files</span></p>`
|
|
@@ -6133,7 +6263,7 @@ export function createDecisionServer(options) {
|
|
|
6133
6263
|
}
|
|
6134
6264
|
}
|
|
6135
6265
|
if (unchecked.length > 0) {
|
|
6136
|
-
parts.push(`<p class="meta">
|
|
6266
|
+
parts.push(`<p class="meta">Not verified this look — absence above does not mean unchanged:</p>`);
|
|
6137
6267
|
for (const row of unchecked)
|
|
6138
6268
|
parts.push(line(row, "?"));
|
|
6139
6269
|
}
|
|
@@ -6144,7 +6274,7 @@ export function createDecisionServer(options) {
|
|
|
6144
6274
|
// The byte cap is enforced AFTER escaping (finding 32): an oversize
|
|
6145
6275
|
// rendering is replaced whole by its exact counts.
|
|
6146
6276
|
fragment =
|
|
6147
|
-
`<p class="meta">
|
|
6277
|
+
`<p class="meta">Best-effort look at ${escape(stamp)} UTC</p>` +
|
|
6148
6278
|
`<p class="row">${changed.length} changed · ${deleted.length} deleted · ${fresh.total} new · ${unchecked.length} unverified — too much to render live; the final diff will hold the detail</p>`;
|
|
6149
6279
|
}
|
|
6150
6280
|
peekCache.set(key, { fragment, at: Date.now() });
|
|
@@ -8575,7 +8705,7 @@ export function createDecisionServer(options) {
|
|
|
8575
8705
|
// THE CEREMONY (M1): every resolved term in words, and the password
|
|
8576
8706
|
// signs exactly this digest — a drifted form refuses at /mode/sign.
|
|
8577
8707
|
const nonce = mintApprovalNonce(who.name, "mode-sign", `${project}:${digest}`);
|
|
8578
|
-
const bodyHtml = `<h1 style="overflow-wrap:anywhere">
|
|
8708
|
+
const bodyHtml = `<h1 style="overflow-wrap:anywhere">Sign the ${escape(name)} mode for ${escape(project)}</h1>` +
|
|
8579
8709
|
`<form method="post" action="/mode/sign" class="card approve-form">` +
|
|
8580
8710
|
`<input type="hidden" name="csrf" value="${escape(who.session.csrf)}">` +
|
|
8581
8711
|
`<input type="hidden" name="nonce" value="${escape(nonce)}">` +
|
|
@@ -8584,12 +8714,12 @@ export function createDecisionServer(options) {
|
|
|
8584
8714
|
.map(field => `<input type="hidden" name="${field}" value="${escape(body.get(field) ?? "")}">`)
|
|
8585
8715
|
.join("") +
|
|
8586
8716
|
`<input type="hidden" name="expiry" value="${escape(expiry)}">` +
|
|
8587
|
-
`<p><strong>
|
|
8717
|
+
`<p><strong>Your password signs exactly this:</strong></p>` +
|
|
8588
8718
|
modeWords(terms)
|
|
8589
8719
|
.map(words => `<p class="recap" style="margin-top:.4rem">${escape(words)}</p>`)
|
|
8590
8720
|
.join("") +
|
|
8591
|
-
`<label>
|
|
8592
|
-
`<div class="sticky-actions"><button type="submit">
|
|
8721
|
+
`<label>Your password, typed again<input type="password" name="token" autocomplete="current-password"></label>` +
|
|
8722
|
+
`<div class="sticky-actions"><button type="submit">Sign it</button></div>` +
|
|
8593
8723
|
`</form>` +
|
|
8594
8724
|
`<p class="meta"><a href="/mode">back — sign nothing</a></p>`;
|
|
8595
8725
|
return sendScreen(response, 200, screen("mode", bodyHtml, { chrome: chromeFor(project, "mode") }));
|
|
@@ -9907,7 +10037,9 @@ export function createDecisionServer(options) {
|
|
|
9907
10037
|
const found = store.getRun(id);
|
|
9908
10038
|
if (found === null || !visible(taskRepoOf(found.taskRef)))
|
|
9909
10039
|
return refuse(response, who, 404, "no such run");
|
|
9910
|
-
|
|
10040
|
+
// The task page that offered Run checks in place is a return target too: this run's own task, or its family's root.
|
|
10041
|
+
const runTask = store.externalIdFor(found.taskRef);
|
|
10042
|
+
const back = resultReturnTarget(body.get("return"), id, runTask === null ? [] : [runTask, familyOf(runTask)?.root.id ?? runTask]);
|
|
9911
10043
|
if (who.via !== "cookie")
|
|
9912
10044
|
return refuse(response, who, 403, "Sign in with a browser session to do this.", back);
|
|
9913
10045
|
if (followUp[2] === "checks") {
|
|
@@ -9919,7 +10051,8 @@ export function createDecisionServer(options) {
|
|
|
9919
10051
|
const asked = requestFollowUpChecks(store, { runId: id, level, actor: who.name }, now);
|
|
9920
10052
|
if (!asked.ok)
|
|
9921
10053
|
return refuse(response, who, 409, asked.message, back);
|
|
9922
|
-
|
|
10054
|
+
// Back where it was asked: the task page's status row says the checks are running; a result page shows them under #follow-ups.
|
|
10055
|
+
return redirect(response, back.startsWith("/t/") ? back : `${back.split("#")[0]}#follow-ups`);
|
|
9923
10056
|
}
|
|
9924
10057
|
const filed = fileAddTestsTask(store, evidenceRoot, { runId: id, actor: who.name, filedVia: "console", ...(admissionList() === null ? {} : { admittedRepos: admissionList() }) }, now);
|
|
9925
10058
|
if (!filed.ok)
|
|
@@ -10133,7 +10266,7 @@ export function createDecisionServer(options) {
|
|
|
10133
10266
|
const quickMode = quickRef?.repo == null ? null : store.activeMode(quickRef.repo, clock());
|
|
10134
10267
|
const quickTerms = quickMode === null ? null : modeTermsFromJson(quickMode.termsJson);
|
|
10135
10268
|
const quick = quickTerms?.quickMint === true && quickMode !== null && quickMode.signedBy === who.name;
|
|
10136
|
-
const body = `<h1>
|
|
10269
|
+
const body = `<h1>Run ${escape(terms.taskId)} once, while you watch</h1>` +
|
|
10137
10270
|
`<form method="post" action="${taskHref(terms.taskId)}/attend" class="card approve-form">` +
|
|
10138
10271
|
`<input type="hidden" name="csrf" value="${escape(csrf)}">` +
|
|
10139
10272
|
`<input type="hidden" name="nonce" value="${escape(nonce)}">` +
|
|
@@ -10146,10 +10279,10 @@ export function createDecisionServer(options) {
|
|
|
10146
10279
|
(inputs.posture === undefined ? "" : `<input type="hidden" name="posture" value="${escape(inputs.posture)}">`) +
|
|
10147
10280
|
(terms.parentRun == null ? "" : `<input type="hidden" name="parent" value="${terms.parentRun}">`) +
|
|
10148
10281
|
(terms.followup == null ? "" : `<input type="hidden" name="followup" value="${escape(terms.followup)}">`) +
|
|
10149
|
-
`<p><strong>
|
|
10150
|
-
`<p class="meta">
|
|
10151
|
-
`<p class="meta">
|
|
10152
|
-
`<p class="meta">
|
|
10282
|
+
`<p><strong>Your password signs exactly this:</strong></p>` +
|
|
10283
|
+
`<p class="meta">Goal</p><p class="recap" style="margin-top:0">${escape(scope?.goal ?? "")}</p>` +
|
|
10284
|
+
`<p class="meta">Not this</p><p class="recap" style="margin-top:0">${scope?.outOfScope == null ? "<em>no exclusions</em>" : escape(scope.outOfScope)}</p>` +
|
|
10285
|
+
`<p class="meta">Touches · ${scope === null || scope.touches.length === 0 ? "anything" : scope.touches.map(one => escape(one)).join(", ")}</p>` +
|
|
10153
10286
|
(() => {
|
|
10154
10287
|
const profile = JSON.parse(terms.profileJson).profile;
|
|
10155
10288
|
const posture = profile?.permissionArgv === "bypassPermissions"
|
|
@@ -10157,21 +10290,21 @@ export function createDecisionServer(options) {
|
|
|
10157
10290
|
: profile?.permissionArgv === "auto"
|
|
10158
10291
|
? "safe unattended permissions — routine project commands and edits proceed; risky acts stop"
|
|
10159
10292
|
: "legacy acceptEdits — edits proceed, commands that ask are denied unattended";
|
|
10160
|
-
return `<p class="meta">
|
|
10293
|
+
return `<p class="meta">Runs on</p><p class="recap" style="margin-top:0">claude · ${escape(String(profile?.model ?? ""))} — ${posture}</p>`;
|
|
10161
10294
|
})() +
|
|
10162
10295
|
(terms.parentRun == null
|
|
10163
10296
|
? ""
|
|
10164
|
-
: `<p class="meta">
|
|
10165
|
-
`<p class="meta">
|
|
10166
|
-
`<p class="meta">
|
|
10167
|
-
`<p class="meta">
|
|
10168
|
-
`<p class="meta">
|
|
10169
|
-
`<p class="meta">
|
|
10170
|
-
`<p class="meta">
|
|
10297
|
+
: `<p class="meta">Continues</p><p class="recap" style="margin-top:0">attempt <a href="/r/${terms.parentRun}" class="mono">#${terms.parentRun}</a>, from exactly where it finished</p>` +
|
|
10298
|
+
`<p class="meta">The follow-up — this is the instruction</p><p class="recap" style="margin-top:0">${escape(terms.followup ?? "")}</p>`) +
|
|
10299
|
+
`<p class="meta">Repository · head</p><p class="recap mono" style="margin-top:0">${escape(terms.repo)} @ ${escape(terms.head.slice(0, 12))}</p>` +
|
|
10300
|
+
`<p class="meta">Worker</p><p class="recap" style="margin-top:0">${escape(terms.runner)} (this machine)</p>` +
|
|
10301
|
+
`<p class="meta">Spending</p><p class="recap" style="margin-top:0">up to about $${(terms.budgetMicrousd / 1_000_000).toFixed(2)} — the agent stops as soon as its total crosses this; the final step may run a little past it</p>` +
|
|
10302
|
+
`<p class="meta">Conversation</p><p class="recap" style="margin-top:0">at most ${terms.maxSessionTurns} messages to the agent, this whole session; each may work up to ${Math.round(terms.turnTimeoutSeconds / 60)} minutes — one that runs past that ends the whole session. If it needs a repair, the repair uses the same session, model, and clock.</p>` +
|
|
10303
|
+
`<p class="meta">While you watch — a signed term</p><p class="recap" style="margin-top:0">your console being open is what keeps it running — any page of it, this one included; close the console and the session winds down within a minute. Everything ends by ${whenTime(terms.absoluteExpiry)} regardless. One attempt; it never converts into unattended work.</p>` +
|
|
10171
10304
|
(quick
|
|
10172
|
-
? `<p class="meta">
|
|
10173
|
-
: `<label>
|
|
10174
|
-
`<div class="sticky-actions"><button type="submit">
|
|
10305
|
+
? `<p class="meta">Your signed mode covers this mint — no password; the mode is re-proved as you confirm, and the session is stamped with its signature</p>`
|
|
10306
|
+
: `<label>Your password, typed again — a signed-in session alone cannot authorize work<input type="password" name="token" autocomplete="current-password"></label>`) +
|
|
10307
|
+
`<div class="sticky-actions"><button type="submit">Run it while I watch</button></div>` +
|
|
10175
10308
|
`</form>` +
|
|
10176
10309
|
`<p class="meta"><a href="${taskHref(terms.taskId)}">back to the task</a></p>`;
|
|
10177
10310
|
// The chrome binds to the AUTHORITY repository (surfaces round 1,
|
|
@@ -10446,10 +10579,21 @@ export function createDecisionServer(options) {
|
|
|
10446
10579
|
return redirect(response, taskHref(taskId));
|
|
10447
10580
|
}
|
|
10448
10581
|
case "requeue": {
|
|
10582
|
+
// Retry may carry a note for the next attempt: the same steering note, checked before anything changes.
|
|
10583
|
+
const note = (body.get("note") ?? "").trim();
|
|
10584
|
+
if (note !== "" && who.via !== "cookie")
|
|
10585
|
+
return refuse(response, who, 403, "steering is a browser session's act");
|
|
10586
|
+
if (note !== "" && !validateNote(note).ok)
|
|
10587
|
+
return taskScreen(response, who, taskId, "that note will not store, so nothing was retried", 400);
|
|
10449
10588
|
const requeued = store.requeueTask(taskId, who.name, now);
|
|
10450
10589
|
if (!requeued.ok) {
|
|
10451
10590
|
return taskScreen(response, who, taskId, `not requeued: ${requeued.reason}`, 409);
|
|
10452
10591
|
}
|
|
10592
|
+
if (note !== "") {
|
|
10593
|
+
const filed = store.fileSteerNote(taskId, verifiedAuthor(who.name), note, now);
|
|
10594
|
+
if (!filed.ok)
|
|
10595
|
+
return taskScreen(response, who, taskId, "Retried. The note wasn't saved; add it under Steering.", 409);
|
|
10596
|
+
}
|
|
10453
10597
|
// Allow-listed return only — never an arbitrary URL from the form.
|
|
10454
10598
|
return redirect(response, body.get("return") === "inbox" ? "/inbox" : body.get("return") === "next" ? "/next" : taskHref(taskId));
|
|
10455
10599
|
}
|
|
@@ -10708,6 +10852,13 @@ export function createDecisionServer(options) {
|
|
|
10708
10852
|
if (budgetUsd !== null && (!Number.isFinite(budgetUsd) || budgetUsd <= 0)) {
|
|
10709
10853
|
return taskScreen(response, who, taskId, "the dollar cap is a positive amount", 400);
|
|
10710
10854
|
}
|
|
10855
|
+
// The approval sheet's in-place editor carries the cap it was shown
|
|
10856
|
+
// exactly, in millionths, and "none" for no limit (never a default).
|
|
10857
|
+
const exactBudgetGiven = body.get("budget-microusd");
|
|
10858
|
+
const exactBudget = exactBudgetGiven === null ? undefined : exactBudgetGiven === "none" ? null : /^[1-9][0-9]{0,14}$/.test(exactBudgetGiven) ? Number(exactBudgetGiven) : NaN;
|
|
10859
|
+
if (Number.isNaN(exactBudget))
|
|
10860
|
+
return taskScreen(response, who, taskId, "the dollar cap is a positive amount", 400);
|
|
10861
|
+
const inPlace = body.has("requirement-new");
|
|
10711
10862
|
// The tournament controls (operator request): a count of 2–4 files
|
|
10712
10863
|
// race terms BESIDE the scope — validated and priced BEFORE anything
|
|
10713
10864
|
// saves, so a bad tournament never half-lands on a good scope.
|
|
@@ -10762,92 +10913,112 @@ export function createDecisionServer(options) {
|
|
|
10762
10913
|
if (plannedRace !== null && store.mirrorByTask(taskId) !== null) {
|
|
10763
10914
|
return taskScreen(response, who, taskId, "external work races in a follow-up release — file the tournament on a local task", 409);
|
|
10764
10915
|
}
|
|
10765
|
-
|
|
10766
|
-
|
|
10767
|
-
|
|
10768
|
-
|
|
10769
|
-
|
|
10770
|
-
|
|
10771
|
-
|
|
10772
|
-
|
|
10773
|
-
|
|
10774
|
-
|
|
10775
|
-
|
|
10776
|
-
|
|
10777
|
-
|
|
10778
|
-
|
|
10779
|
-
|
|
10780
|
-
|
|
10781
|
-
|
|
10782
|
-
|
|
10783
|
-
|
|
10784
|
-
|
|
10785
|
-
|
|
10786
|
-
|
|
10787
|
-
|
|
10788
|
-
|
|
10789
|
-
|
|
10790
|
-
|
|
10791
|
-
|
|
10792
|
-
|
|
10793
|
-
|
|
10794
|
-
|
|
10795
|
-
|
|
10796
|
-
|
|
10797
|
-
|
|
10798
|
-
|
|
10799
|
-
|
|
10800
|
-
|
|
10801
|
-
|
|
10802
|
-
|
|
10803
|
-
|
|
10804
|
-
|
|
10805
|
-
|
|
10806
|
-
|
|
10807
|
-
|
|
10808
|
-
|
|
10809
|
-
|
|
10810
|
-
|
|
10811
|
-
|
|
10812
|
-
|
|
10813
|
-
|
|
10814
|
-
|
|
10916
|
+
let saved;
|
|
10917
|
+
try {
|
|
10918
|
+
saved = store.transact(() => {
|
|
10919
|
+
if ((plannedRace !== null || plannedComparison !== null) && store.openAuthorizationFor(ref.id) !== null) {
|
|
10920
|
+
return { ok: false, status: 409, message: "an attended authorization is open on this task — revoke it before filing a tournament or comparison" };
|
|
10921
|
+
}
|
|
10922
|
+
// C1/M3: the signer's own credentialed filing auto-approves —
|
|
10923
|
+
// coverage is asked INSIDE this transaction, the escalated
|
|
10924
|
+
// default and budget ride the filing, and the seal commits
|
|
10925
|
+
// atomically with it. Plain scopes only: a tournament or
|
|
10926
|
+
// comparison keeps its own human ceremony.
|
|
10927
|
+
// COOKIE ONLY (C1's channel table; surfaces round 1, finding 1):
|
|
10928
|
+
// bearer credentials are for machines, and a machine road must
|
|
10929
|
+
// never spend the signer's mode.
|
|
10930
|
+
const coverage = who.via === "cookie" && plannedRace === null && plannedComparison === null
|
|
10931
|
+
? modeFilingCoverage(store, ref.repo, who.name, now)
|
|
10932
|
+
: null;
|
|
10933
|
+
const before = store.getScope(taskId);
|
|
10934
|
+
const proposed = proposeGuarded(store, {
|
|
10935
|
+
taskId, author: who.name,
|
|
10936
|
+
goal: body.has("goal-brief") ? `${(body.get("goal") ?? "").trim()}${body.get("goal-brief")}` : body.get("goal") ?? "",
|
|
10937
|
+
outOfScope: body.get("not") ?? null,
|
|
10938
|
+
touches: (body.get("touches") ?? "").split(/[\n,]/),
|
|
10939
|
+
// The approval sheet's in-place editor sends Done when line by line.
|
|
10940
|
+
acceptance: inPlace
|
|
10941
|
+
? requirementsFromEditor(body, before?.acceptance ?? [])
|
|
10942
|
+
: acceptanceLinesToInput((body.get("acceptance") ?? "").split("\n")),
|
|
10943
|
+
permissionMode,
|
|
10944
|
+
qualityMode,
|
|
10945
|
+
...(exactBudget !== undefined
|
|
10946
|
+
? exactBudget === null ? {} : { budgetMicrousd: exactBudget }
|
|
10947
|
+
: budgetUsd !== null
|
|
10948
|
+
? { budgetMicrousd: Math.round(budgetUsd * 1_000_000) }
|
|
10949
|
+
: coverage?.defaultBudgetMicrousd != null
|
|
10950
|
+
? { budgetMicrousd: coverage.defaultBudgetMicrousd }
|
|
10951
|
+
: {}),
|
|
10952
|
+
...(coverage?.escalated === true ? { posture: "escalated" } : {}),
|
|
10953
|
+
sawDigest: sawDigest === null || sawDigest === "" ? null : sawDigest,
|
|
10954
|
+
taskRef: ref.id,
|
|
10955
|
+
now,
|
|
10956
|
+
});
|
|
10957
|
+
if (!proposed.ok) {
|
|
10958
|
+
return {
|
|
10959
|
+
ok: false,
|
|
10960
|
+
status: proposed.reason === "changed" || proposed.reason === "claimed" ? 409 : 400,
|
|
10961
|
+
message: proposed.message ?? (proposed.reason === "acceptance-required" ? "Not saved: add at least one requirement under Done when." : `scope not saved: ${proposed.reason}`),
|
|
10962
|
+
};
|
|
10963
|
+
}
|
|
10964
|
+
// Editing the plan in place never changes what the agent may do:
|
|
10965
|
+
// a save that would (a legacy setting, a newer policy or mode)
|
|
10966
|
+
// rolls back whole.
|
|
10967
|
+
if (inPlace && before?.profile != null && proposed.scope.profile != null && levelOfProfile(before.profile) !== levelOfProfile(proposed.scope.profile)) {
|
|
10968
|
+
throw new PermissionsWouldChange();
|
|
10969
|
+
}
|
|
10970
|
+
if (coverage !== null && ref.plan === "requested") {
|
|
10971
|
+
authorizePlanUnderMode(store, taskId, who.name, now);
|
|
10972
|
+
}
|
|
10973
|
+
else if (coverage !== null) {
|
|
10974
|
+
const filedScope = store.getScope(taskId);
|
|
10975
|
+
if (filedScope?.profileState === "resolved") {
|
|
10976
|
+
const sealedUnderMode = store.sealScopeApproval(taskId, who.name, now, {}, { kind: "mode", modeDigest: coverage.digest });
|
|
10977
|
+
// The quarantine speaks at the caller (review finding 7).
|
|
10978
|
+
if (!sealedUnderMode) {
|
|
10979
|
+
return { ok: false, status: 200, message: "scope saved — coordinator-filed: mode coverage cannot admit it; sign the scope" };
|
|
10980
|
+
}
|
|
10815
10981
|
}
|
|
10816
10982
|
}
|
|
10817
|
-
|
|
10818
|
-
|
|
10983
|
+
if (plannedComparison !== null && plannedComparison.ok) {
|
|
10984
|
+
store.fileTournamentTerms({
|
|
10985
|
+
taskRef: ref.id,
|
|
10986
|
+
kind: "comparison",
|
|
10987
|
+
raceDigest: plannedComparison.plan.comparisonDigest,
|
|
10988
|
+
agents: plannedComparison.plan.agents,
|
|
10989
|
+
perAgentBudgetMicrousd: 0,
|
|
10990
|
+
overrunReserveMicrousd: 0,
|
|
10991
|
+
totalBudgetMicrousd: 0,
|
|
10992
|
+
priceVersion: 0,
|
|
10993
|
+
publicationPolicy: plannedComparison.plan.publicationPolicy,
|
|
10994
|
+
}, now);
|
|
10995
|
+
return { ok: true };
|
|
10996
|
+
}
|
|
10997
|
+
if (plannedRace === null) {
|
|
10998
|
+
// Switching back to "one agent" withdraws a standing race OR
|
|
10999
|
+
// comparison — the deactivated row survives as history, and
|
|
11000
|
+
// the approval card returns to the scope alone.
|
|
11001
|
+
store.retractTournamentTerms(ref.id);
|
|
11002
|
+
return { ok: true };
|
|
11003
|
+
}
|
|
10819
11004
|
store.fileTournamentTerms({
|
|
10820
11005
|
taskRef: ref.id,
|
|
10821
|
-
|
|
10822
|
-
|
|
10823
|
-
|
|
10824
|
-
|
|
10825
|
-
|
|
10826
|
-
|
|
10827
|
-
|
|
10828
|
-
publicationPolicy: plannedComparison.plan.publicationPolicy,
|
|
11006
|
+
raceDigest: plannedRace.raceDigest,
|
|
11007
|
+
agents: plannedRace.agents,
|
|
11008
|
+
perAgentBudgetMicrousd: plannedRace.perAgentBudgetMicrousd,
|
|
11009
|
+
overrunReserveMicrousd: plannedRace.overrunReserveMicrousd,
|
|
11010
|
+
totalBudgetMicrousd: plannedRace.totalBudgetMicrousd,
|
|
11011
|
+
priceVersion: plannedRace.priceVersion,
|
|
11012
|
+
publicationPolicy: plannedRace.publicationPolicy,
|
|
10829
11013
|
}, now);
|
|
10830
11014
|
return { ok: true };
|
|
10831
|
-
}
|
|
10832
|
-
|
|
10833
|
-
|
|
10834
|
-
|
|
10835
|
-
|
|
10836
|
-
|
|
10837
|
-
|
|
10838
|
-
}
|
|
10839
|
-
store.fileTournamentTerms({
|
|
10840
|
-
taskRef: ref.id,
|
|
10841
|
-
raceDigest: plannedRace.raceDigest,
|
|
10842
|
-
agents: plannedRace.agents,
|
|
10843
|
-
perAgentBudgetMicrousd: plannedRace.perAgentBudgetMicrousd,
|
|
10844
|
-
overrunReserveMicrousd: plannedRace.overrunReserveMicrousd,
|
|
10845
|
-
totalBudgetMicrousd: plannedRace.totalBudgetMicrousd,
|
|
10846
|
-
priceVersion: plannedRace.priceVersion,
|
|
10847
|
-
publicationPolicy: plannedRace.publicationPolicy,
|
|
10848
|
-
}, now);
|
|
10849
|
-
return { ok: true };
|
|
10850
|
-
});
|
|
11015
|
+
});
|
|
11016
|
+
}
|
|
11017
|
+
catch (error) {
|
|
11018
|
+
if (!(error instanceof PermissionsWouldChange))
|
|
11019
|
+
throw error;
|
|
11020
|
+
saved = { ok: false, status: 409, message: "Not saved: this would change what the agent may do. Change permissions in Details, under edit the scope." };
|
|
11021
|
+
}
|
|
10851
11022
|
if (!saved.ok)
|
|
10852
11023
|
return taskScreen(response, who, taskId, saved.message, saved.status, body);
|
|
10853
11024
|
return redirect(response, taskHref(taskId));
|
|
@@ -10963,6 +11134,9 @@ export function createDecisionServer(options) {
|
|
|
10963
11134
|
}
|
|
10964
11135
|
note = validated.note;
|
|
10965
11136
|
}
|
|
11137
|
+
// A failed task's result is accepted only anyway, on purpose: with a reason.
|
|
11138
|
+
if (note === null && store.getTask(taskId)?.state === "failed")
|
|
11139
|
+
return taskScreen(response, who, taskId, "Accepting anyway needs a reason.", 400);
|
|
10966
11140
|
store.acceptProof(latest.id, verifiedAuthor(who.name), note, now);
|
|
10967
11141
|
return redirect(response, body.get("return") === null ? taskHref(taskId) : safeReturn(body.get("return")));
|
|
10968
11142
|
}
|
|
@@ -10978,6 +11152,23 @@ export function createDecisionServer(options) {
|
|
|
10978
11152
|
if (current === null || current.receipt?.taskId !== taskId || !/^[0-9]{1,15}$/.test(namedRun) || current.receipt.runId !== Number(namedRun)) {
|
|
10979
11153
|
return taskScreen(response, who, taskId, "This result changed. Open the current result before marking it complete.", 409);
|
|
10980
11154
|
}
|
|
11155
|
+
if (body.get("accept") === "1") {
|
|
11156
|
+
// Accept and finish: the person's acceptance and the completion of what it accepted, in one request
|
|
11157
|
+
// and one transaction. The receipt named is the one read, before accepting.
|
|
11158
|
+
const rawNote = (body.get("note") ?? "").trim();
|
|
11159
|
+
let note = null;
|
|
11160
|
+
if (rawNote !== "") {
|
|
11161
|
+
const validated = validateNote(rawNote);
|
|
11162
|
+
if (!validated.ok)
|
|
11163
|
+
return taskScreen(response, who, taskId, validated.problem, 400);
|
|
11164
|
+
note = validated.note;
|
|
11165
|
+
}
|
|
11166
|
+
const publish = body.get("publish") === "1";
|
|
11167
|
+
const finished = acceptAndCompleteAsOperator(store, taskId, { runId: Number(namedRun), receiptDigest: digest, note }, principal, now, evidenceRoot, publish ? accepted => completeAndOpenPullRequest(store, { taskId, digest: accepted, runId: Number(namedRun), who: principal, root: evidenceRoot }, now) : undefined);
|
|
11168
|
+
if (!finished.ok)
|
|
11169
|
+
return taskScreen(response, who, taskId, finished.message, 409);
|
|
11170
|
+
return redirect(response, publish ? `${taskHref(familyOf(taskId)?.root.id ?? taskId)}#merge` : body.get("return") === null ? reviewHref(taskId, Number(namedRun)) : safeReturn(body.get("return")));
|
|
11171
|
+
}
|
|
10981
11172
|
if (body.get("publish") === "1") {
|
|
10982
11173
|
// "Complete and open a pull request": the same exact-receipt completion, plus the owed PR for this
|
|
10983
11174
|
// exact commit in one transaction. The watch process pushes and opens it; the task shows its progress.
|
|
@@ -11398,6 +11589,36 @@ export function createDecisionServer(options) {
|
|
|
11398
11589
|
const run = store.runsFor(ref.id).find(one => (one.outcome === "built" || one.outcome === "no-change") &&
|
|
11399
11590
|
one.finishedAt !== null &&
|
|
11400
11591
|
(one.role === "builder" || one.role === "scout")) ?? null;
|
|
11592
|
+
return resultRowOf(task, ref.repo, family.problem, run);
|
|
11593
|
+
}
|
|
11594
|
+
/**
|
|
11595
|
+
* A build attempt that didn't deliver a result (failed, refused, stopped,
|
|
11596
|
+
* never finished) still has a result page: its changes, its checks and what
|
|
11597
|
+
* went wrong. The row for one: the run named by `runId` when it is an ended
|
|
11598
|
+
* builder or scout attempt of this task that isn't a delivered result, or a
|
|
11599
|
+
* failed task's latest attempt; with no run named, that latest attempt. The
|
|
11600
|
+
* same admission as a completed row: in this project (or none) and inside
|
|
11601
|
+
* the ceiling. Null otherwise, which reads like a task that never existed.
|
|
11602
|
+
*/
|
|
11603
|
+
function attemptRowFor(taskId, runId, project) {
|
|
11604
|
+
const family = familyOf(taskId);
|
|
11605
|
+
const task = store.getTask(taskId);
|
|
11606
|
+
const ref = store.lookupRef(taskId);
|
|
11607
|
+
if (family === null || task === null || ref === null)
|
|
11608
|
+
return null;
|
|
11609
|
+
if (!(project === null || ref.repo === null || ref.repo === project) || !visible(ref.repo))
|
|
11610
|
+
return null;
|
|
11611
|
+
const attempts = store.runsFor(ref.id).filter(one => (one.role === "builder" || one.role === "scout") && (one.finishedAt !== null || one.outcome !== null));
|
|
11612
|
+
const latest = task.state === "failed" ? latestFinishedAttempt(attempts) : null;
|
|
11613
|
+
const run = runId !== null ? attempts.find(one => one.id === runId) ?? null : latest;
|
|
11614
|
+
// A delivered result has its completed row; a failed task's latest attempt is what failed, whatever it ended as.
|
|
11615
|
+
if (run === null || runIsLive(run) || ((run.outcome === "built" || run.outcome === "no-change") && run.id !== latest?.id))
|
|
11616
|
+
return null;
|
|
11617
|
+
return resultRowOf(task, ref.repo, family.problem, run);
|
|
11618
|
+
}
|
|
11619
|
+
function resultRowOf(task, repo, historyProblem, run) {
|
|
11620
|
+
const ref = { repo };
|
|
11621
|
+
const family = { problem: historyProblem };
|
|
11401
11622
|
const publication = run === null ? null : store.publicationForRun(run.id);
|
|
11402
11623
|
const verdict = run === null ? null : store.proofVerdictFor(run.id);
|
|
11403
11624
|
return {
|
|
@@ -11588,6 +11809,7 @@ export function createDecisionServer(options) {
|
|
|
11588
11809
|
intent,
|
|
11589
11810
|
plan,
|
|
11590
11811
|
contest: contestOf(),
|
|
11812
|
+
...(store.isDemo() ? { demo: true } : {}),
|
|
11591
11813
|
};
|
|
11592
11814
|
if (run === null) {
|
|
11593
11815
|
return { ...base, run: null, detail: null, reviewRetry: null, review: null, notes: [] };
|
|
@@ -11909,8 +12131,8 @@ export function editorFileHref(worktree, path, line) {
|
|
|
11909
12131
|
function matrixStateBadge(state) {
|
|
11910
12132
|
// Red belongs to a Failed headline alone (task-status.ts); an unmet requirement is an amber note.
|
|
11911
12133
|
const cls = state === "pass" ? "badge-done" : state === "failed" ? "badge-note" : state === "missing" ? "badge-note" : "badge-manual-review";
|
|
11912
|
-
|
|
11913
|
-
return `<span class="badge ${cls}" data-matrix-state="${escape(state)}">${escape(
|
|
12134
|
+
// The same words as the Checks tab and the card's Requirements row (task-status.ts).
|
|
12135
|
+
return `<span class="badge ${cls}" data-matrix-state="${escape(state)}">${escape(requirementWordOf({ state }))}</span>`;
|
|
11914
12136
|
}
|
|
11915
12137
|
/** v40: the bounded repair chain's own line — presented as the user-facing
|
|
11916
12138
|
* recovery it represents, without exposing the internal workflow name.
|
|
@@ -12011,8 +12233,9 @@ function criterionMatrixHtml(matrix, options = {}) {
|
|
|
12011
12233
|
const awaitingAssessment = row.assessment?.evidenceState === "pass" && row.review === null;
|
|
12012
12234
|
const confirmed = row.assessment !== undefined && state === "pass";
|
|
12013
12235
|
// Plain words (task-status brief): the retired assessment step is not a state a person acts on.
|
|
12014
|
-
|
|
12015
|
-
const
|
|
12236
|
+
// The card's Requirements row reads the same words from the same source (task-status.ts), so they agree.
|
|
12237
|
+
const label = requirementWordOf(row, options.verdict);
|
|
12238
|
+
const cls = label === "Met" ? "badge-done" : label === "You check" ? "badge-manual-review" : "badge-note";
|
|
12016
12239
|
const warnings = [];
|
|
12017
12240
|
// Only replace the known boilerplate. Other recorded failure details
|
|
12018
12241
|
// remain verbatim, so concision cannot hide a different problem.
|
|
@@ -12082,10 +12305,10 @@ function planContractHtml(view, mode) {
|
|
|
12082
12305
|
if ("problem" in view) {
|
|
12083
12306
|
return `<div class="contract-panel contract-problem"><p class="approval-label">filed contract</p><p class="meta">${escape(view.problem)} · <a href="/r/${view.run}">run ${view.run}</a></p></div>`;
|
|
12084
12307
|
}
|
|
12085
|
-
const stale = view.current ? "" : `<p class="meta">
|
|
12308
|
+
const stale = view.current ? "" : `<p class="meta">The scope was edited after this draft landed — the record below describes the draft as the planner proposed it</p>`;
|
|
12086
12309
|
if (!view.filed) {
|
|
12087
12310
|
return mode === "full"
|
|
12088
|
-
? `<div class="contract-panel contract-drafted"><p class="approval-label">filed contract</p><p class="meta">
|
|
12311
|
+
? `<div class="contract-panel contract-drafted"><p class="approval-label">filed contract</p><p class="meta">No scope was filed before planning — the planner drafted this contract from the title and the repository; review every term as new</p>${stale}</div>`
|
|
12089
12312
|
: `<p class="meta contract-note">no scope was filed before planning — every term above is the planner's proposal</p>`;
|
|
12090
12313
|
}
|
|
12091
12314
|
if (view.revision === true) {
|
|
@@ -12095,19 +12318,19 @@ function planContractHtml(view, mode) {
|
|
|
12095
12318
|
}
|
|
12096
12319
|
return (`<div class="contract-panel contract-amended"${mode === "full" ? ` id="contract-amendment"` : ""}><p class="approval-label">updated plan</p>` +
|
|
12097
12320
|
`<p><strong>${view.changes.length} change${view.changes.length === 1 ? "" : "s"} from your notes</strong> <span class="meta">— approving accepts the updated terms ${mode === "full" ? "in the scope" : "above"} · <a href="/r/${view.run}">run ${view.run}</a></span></p>` +
|
|
12098
|
-
(view.amendment === null ? "" : `<p class="recap contract-reason"><strong>
|
|
12321
|
+
(view.amendment === null ? "" : `<p class="recap contract-reason"><strong>Why:</strong> ${escape(view.amendment)}</p>`) +
|
|
12099
12322
|
`<ul class="recap contract-changes">${view.changes.map(contractChangeHtml).join("")}</ul>` +
|
|
12100
12323
|
stale +
|
|
12101
12324
|
`</div>`);
|
|
12102
12325
|
}
|
|
12103
12326
|
if (view.changes.length === 0) {
|
|
12104
|
-
return `<div class="contract-panel contract-preserved"><p class="approval-label">filed contract</p><p><strong>
|
|
12327
|
+
return `<div class="contract-panel contract-preserved"><p class="approval-label">filed contract</p><p><strong>Preserved exactly</strong> <span class="meta">the plan reproduces the filed goal, exclusions, touches, and acceptance criteria — approving binds the terms you filed · <a href="/r/${view.run}">run ${view.run}</a></span></p>${stale}</div>`;
|
|
12105
12328
|
}
|
|
12106
12329
|
return (`<div class="contract-panel contract-amended"${mode === "full" ? ` id="contract-amendment"` : ""}><p class="approval-label">filed contract · amendment proposed</p>` +
|
|
12107
12330
|
`<p><strong>${view.changes.length} change${view.changes.length === 1 ? "" : "s"} to what you filed</strong> <span class="meta">— approving binds the AMENDED terms shown ${mode === "full" ? "in the scope" : "above"}, not the ones you filed · <a href="/r/${view.run}">run ${view.run}</a></span></p>` +
|
|
12108
12331
|
(view.amendment === null
|
|
12109
|
-
? `<p class="meta">
|
|
12110
|
-
: `<p class="recap contract-reason"><strong>
|
|
12332
|
+
? `<p class="meta">The planner stated no reason for the amendment</p>`
|
|
12333
|
+
: `<p class="recap contract-reason"><strong>Why:</strong> ${escape(view.amendment)}</p>`) +
|
|
12111
12334
|
`<ul class="recap contract-changes">${view.changes.map(contractChangeHtml).join("")}</ul>` +
|
|
12112
12335
|
stale +
|
|
12113
12336
|
`</div>`);
|
|
@@ -12136,7 +12359,7 @@ function approvalPathsHtml(touches) {
|
|
|
12136
12359
|
function acceptanceCeremonyHtml(criteria) {
|
|
12137
12360
|
if (criteria.length === 0)
|
|
12138
12361
|
return "";
|
|
12139
|
-
return (`<p class="meta">
|
|
12362
|
+
return (`<p class="meta">Acceptance</p><ul class="recap acceptance-rubric">` +
|
|
12140
12363
|
criteria
|
|
12141
12364
|
.map(c => `<li><code>${escape(c.id)}</code> ${escape(c.statement)} <span class="meta">[requires: ${c.evidence.map(escape).join(", ")}]</span></li>`)
|
|
12142
12365
|
.join("") +
|
|
@@ -12254,10 +12477,20 @@ function approvalSheetHtml(input) {
|
|
|
12254
12477
|
const planner = legOf("plan");
|
|
12255
12478
|
if (planner !== undefined)
|
|
12256
12479
|
who.push(`Planner ${agentNameWords(planner.provider, planner.model)}`);
|
|
12257
|
-
|
|
12258
|
-
|
|
12259
|
-
|
|
12260
|
-
|
|
12480
|
+
// What the yes allows, in plain words, right above it: the agent's
|
|
12481
|
+
// permissions, the per-attempt limit, and an earlier version still running.
|
|
12482
|
+
const allowing = [];
|
|
12483
|
+
const allowed = permissionPlainWordsOf(scope.profile);
|
|
12484
|
+
if (allowed !== null)
|
|
12485
|
+
allowing.push(allowed);
|
|
12486
|
+
if (raceTerms === null)
|
|
12487
|
+
allowing.push(scope.budgetMicrousd === null ? "no attempt limit" : `up to ${money(scope.budgetMicrousd)} per attempt`);
|
|
12488
|
+
const earlier = earlierVersionsWords(input.earlier.active, input.earlier.running);
|
|
12489
|
+
if (earlier !== null)
|
|
12490
|
+
allowing.push(earlier);
|
|
12491
|
+
const yoursToCheck = scope.acceptance.filter(one => one.evidence.includes("manual-review")).map(one => one.statement.trim().replace(/[.\s]+$/, ""));
|
|
12492
|
+
const consent = (allowing.length === 0 ? "" : `<p class="approval-allowing" data-approval-allowing>You’re allowing: ${allowing.map(escape).join(" · ")}</p>`) +
|
|
12493
|
+
(yoursToCheck.length === 0 ? "" : `<p class="approval-you-check" data-approval-you-check>You’ll check: ${yoursToCheck.map(escape).join("; ")}</p>`);
|
|
12261
12494
|
const race = raceTerms === null
|
|
12262
12495
|
? ""
|
|
12263
12496
|
: `<p class="approval-race">${raceTerms.n} agents build this separately: ${raceTerms.agents.map(one => escape(agentNameWords(one.provider, one.model))).join(" and ")}. ` +
|
|
@@ -12284,7 +12517,7 @@ function approvalSheetHtml(input) {
|
|
|
12284
12517
|
: !contract.filed && contract.revision !== true ? `<p class="meta">Nothing was filed before planning, so every term is the planner's proposal.</p>`
|
|
12285
12518
|
: `<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
12519
|
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">
|
|
12520
|
+
: `<ul class="approval-signed-criteria">${scope.acceptance.map(one => `<li><code>${escape(one.id)}</code> ${escape(one.statement)} <span class="meta">shown by ${one.evidence.map(kind => escape(EVIDENCE_PLAIN[kind] ?? kind)).join(", ")}</span></li>`).join("")}</ul>`;
|
|
12288
12521
|
const projection = route?.projection ?? null;
|
|
12289
12522
|
const agentsDetail = route === null || route === undefined ? ""
|
|
12290
12523
|
: projection === null
|
|
@@ -12298,7 +12531,7 @@ function approvalSheetHtml(input) {
|
|
|
12298
12531
|
const limits = profileWords(scope) +
|
|
12299
12532
|
`<p class="meta">Checks level: ${escape(qualityModeTitle(scope.qualityMode ?? "default"))}${permission === null ? "" : ` · ${escape(permission)}`}</p>` +
|
|
12300
12533
|
(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>
|
|
12534
|
+
const details = `<details class="approval-details"><summary>Plan details</summary>` +
|
|
12302
12535
|
revisionDetail +
|
|
12303
12536
|
(repairText === null ? "" : detail("Signed goal", `<p>${escape(scope.goal)}</p>`)) +
|
|
12304
12537
|
detail("Plan record", contractDetail) +
|
|
@@ -12308,6 +12541,53 @@ function approvalSheetHtml(input) {
|
|
|
12308
12541
|
(scope.candidate ? detail("Saved commit", `<p><code class="approval-commit">${escape(scope.candidate)}</code></p>`) : "") +
|
|
12309
12542
|
detail("Seal", `<p class="meta">Approval binds to this exact wording. <span class="seal mono">signs ${shortDigest(scope.digest)}</span></p>`) +
|
|
12310
12543
|
`</details>`;
|
|
12544
|
+
const passwordNote = input.surface === "task" ? "approval-password-note" : "chat-approval-password-note";
|
|
12545
|
+
// Edit plan, in place (task page): the rows become fields and save
|
|
12546
|
+
// through the scope's own route. The fields belong to a form after this
|
|
12547
|
+
// one (forms never nest), so Approve never carries them.
|
|
12548
|
+
const edit = input.edit;
|
|
12549
|
+
const draft = edit?.draft ?? null;
|
|
12550
|
+
const editForm = "plan-editor-form";
|
|
12551
|
+
// Each field opens tall enough for what it holds (up to ten lines).
|
|
12552
|
+
const field = (name, value, rows, label, hint = "") => `<label class="approval-field"><span class="approval-field-label">${label}</span>${hint === "" ? "" : `<span class="approval-field-hint">${hint}</span>`}` +
|
|
12553
|
+
`<textarea name="${name}" rows="${Math.min(10, Math.max(rows, value.split("\n").reduce((sum, line) => sum + Math.max(1, Math.ceil(line.length / 80)), 0)))}" form="${editForm}">${escape(value)}</textarea></label>`;
|
|
12554
|
+
// Done when, one line per requirement in the signed order; the server
|
|
12555
|
+
// matches them back by position (the digest guard keeps that order).
|
|
12556
|
+
const requirement = (name, value, label, placeholder = "") => `<li><input type="text" name="${name}" value="${escape(value)}" aria-label="${label}"${placeholder === "" ? "" : ` placeholder="${placeholder}"`} form="${editForm}"></li>`;
|
|
12557
|
+
const requirements = draft?.getAll("requirement") ?? scope.acceptance.map(one => one.statement);
|
|
12558
|
+
const added = draft?.get("requirement-new") ?? "";
|
|
12559
|
+
const editor = edit === null ? null :
|
|
12560
|
+
`<details class="approval-edit" id="plan-editor"${draft === null && !edit.open ? "" : " open"}>` +
|
|
12561
|
+
`<summary class="approval-link"><span class="approval-edit-open">Edit plan</span><span class="approval-edit-close">Cancel</span></summary>` +
|
|
12562
|
+
`<div class="approval-editor">` +
|
|
12563
|
+
(edit.problem === null ? "" : `<p class="problem" role="alert">${escape(edit.problem)}</p>`) +
|
|
12564
|
+
field("goal", draft?.get("goal") ?? goalShown, 3, "Goal") +
|
|
12565
|
+
field("touches", draft?.get("touches") ?? scope.touches.join("\n"), 2, "Changes", "Only in these paths, one per line. Leave empty for any file.") +
|
|
12566
|
+
field("not", draft?.get("not") ?? scope.outOfScope ?? "", 2, "Won’t touch") +
|
|
12567
|
+
`<fieldset class="approval-field"><legend class="approval-field-label">Done when</legend>` +
|
|
12568
|
+
`<span class="approval-field-hint">Clear a line to drop it.</span><ul class="approval-requirements">` +
|
|
12569
|
+
requirements.map((one, index) => requirement("requirement", one, `Requirement ${index + 1}`)).join("") +
|
|
12570
|
+
requirement("requirement-new", added, "Add a requirement", "Add a requirement") +
|
|
12571
|
+
`</ul></fieldset>` +
|
|
12572
|
+
`<div class="approval-edit-act"><button type="submit" form="${editForm}">Save plan</button>` +
|
|
12573
|
+
// A plan with written steps keeps its own editor for them.
|
|
12574
|
+
(edit.stepsHref === null ? "" : `<a class="approval-link" href="${escape(edit.stepsHref)}">Edit steps</a>`) + `</div>` +
|
|
12575
|
+
`</div></details>`;
|
|
12576
|
+
const keptMode = permissionModeOfProfile(scope.profile);
|
|
12577
|
+
const editorForm = edit === null ? "" :
|
|
12578
|
+
`<form method="post" action="${edit.action}" id="${editForm}" class="approval-editor-form">` +
|
|
12579
|
+
`<input type="hidden" name="csrf" value="${escape(input.csrf)}">` +
|
|
12580
|
+
// A refused save keeps the version its draft was edited from, so a
|
|
12581
|
+
// plan that changed meanwhile is never silently overwritten.
|
|
12582
|
+
`<input type="hidden" name="sawDigest" value="${escape(draft?.get("sawDigest") ?? scope.digest)}">` +
|
|
12583
|
+
// The terms the editor doesn't show ride along unchanged: a repair's
|
|
12584
|
+
// brief after its goal (it stays in Details), permissions, checks, cap.
|
|
12585
|
+
(repairText === null ? "" : `<input type="hidden" name="goal-brief" value="${escape(repairText[0])}">`) +
|
|
12586
|
+
(keptMode === null ? "" : `<input type="hidden" name="permission-mode" value="${keptMode}">`) +
|
|
12587
|
+
`<input type="hidden" name="quality-mode" value="${escape(scope.qualityMode ?? "default")}">` +
|
|
12588
|
+
// The cap rides in exact millionths ("none" keeps no limit).
|
|
12589
|
+
`<input type="hidden" name="budget-microusd" value="${scope.budgetMicrousd === null ? "none" : String(scope.budgetMicrousd)}">` +
|
|
12590
|
+
`</form>`;
|
|
12311
12591
|
return (`<form method="post" action="${input.action}" class="approve-form approval-sheet"${input.surface === "task" ? ` id="approve"` : ""}${input.sticky ? " data-sticky" : ""}>` +
|
|
12312
12592
|
`<input type="hidden" name="csrf" value="${escape(input.csrf)}">` +
|
|
12313
12593
|
`<input type="hidden" name="nonce" value="${escape(input.nonce)}">` +
|
|
@@ -12327,13 +12607,75 @@ function approvalSheetHtml(input) {
|
|
|
12327
12607
|
approvalAmendmentHtml(contract) +
|
|
12328
12608
|
race +
|
|
12329
12609
|
(who.length === 0 ? "" : `<p class="approval-who">${who.map(escape).join(" · ")}</p>`) +
|
|
12610
|
+
consent +
|
|
12330
12611
|
`<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>` +
|
|
12612
|
+
`<label class="approval-password"><span class="visually-hidden">Your password</span><input type="password" name="token" autocomplete="current-password" placeholder="Your password" aria-describedby="${passwordNote}"></label>` +
|
|
12613
|
+
`<p class="approval-password-note" id="${passwordNote}">Your password signs this approval.</p>` +
|
|
12332
12614
|
`<button type="submit" data-primary-action>${submit}</button></div>` +
|
|
12333
12615
|
`<p class="approval-after">${after}</p>` +
|
|
12334
|
-
`<
|
|
12616
|
+
`<div class="approval-secondary">${editor ?? `<a class="approval-link" href="${escape(input.editHref)}">Edit plan</a>`}<a class="approval-link" href="${escape(input.notNowHref)}">Not now</a></div>` +
|
|
12335
12617
|
details +
|
|
12336
|
-
`</form>`
|
|
12618
|
+
`</form>` +
|
|
12619
|
+
editorForm);
|
|
12620
|
+
}
|
|
12621
|
+
/** Earlier versions of a task still in flight, in plain words: queued
|
|
12622
|
+
* while they only wait, running once one has started; null when none. */
|
|
12623
|
+
export function earlierVersionsWords(active, running) {
|
|
12624
|
+
if (active <= 0)
|
|
12625
|
+
return null;
|
|
12626
|
+
const still = running >= active ? "running" : running <= 0 ? "queued" : "queued or running";
|
|
12627
|
+
return active === 1 ? `an earlier version is still ${still}` : `${active} earlier versions are still ${still}`;
|
|
12628
|
+
}
|
|
12629
|
+
/** An in-place plan edit that would change the agent's permissions: thrown to roll the save back. */
|
|
12630
|
+
class PermissionsWouldChange extends Error {
|
|
12631
|
+
}
|
|
12632
|
+
/** Evidence kinds in plain words, for the signed criteria in Details. */
|
|
12633
|
+
const EVIDENCE_PLAIN = { check: "the project check", screenshot: "screenshots", "changed-path": "changed files", "manual-review": "your own check" };
|
|
12634
|
+
/** What an agent may do on its own, in a few plain words. */
|
|
12635
|
+
function permissionPlainWordsOf(profile) {
|
|
12636
|
+
if (profile === null || profile === undefined)
|
|
12637
|
+
return null;
|
|
12638
|
+
const full = "full access, nothing asks first";
|
|
12639
|
+
if (profile.provider === "claude") {
|
|
12640
|
+
return profile.permissionArgv === "bypassPermissions" ? full
|
|
12641
|
+
: profile.permissionArgv === "auto" ? "file edits and routine commands; anything risky stops"
|
|
12642
|
+
: "file edits only; commands are refused";
|
|
12643
|
+
}
|
|
12644
|
+
if (profile.provider === "gemini")
|
|
12645
|
+
return profile.approvalArgv === "yolo" ? full : "file edits only; other tools are refused";
|
|
12646
|
+
return profile.sandboxMode === "danger-full-access" ? full : "file edits and commands inside the project only";
|
|
12647
|
+
}
|
|
12648
|
+
/** The permission choice a sealed profile carries, as the scope form names it. */
|
|
12649
|
+
function permissionModeOfProfile(profile) {
|
|
12650
|
+
if (profile === null || profile === undefined)
|
|
12651
|
+
return null;
|
|
12652
|
+
return profile.provider === "claude"
|
|
12653
|
+
? profile.permissionArgv === "bypassPermissions" ? "bypassPermissions" : "auto"
|
|
12654
|
+
: profile.provider === "gemini"
|
|
12655
|
+
? profile.approvalArgv === "yolo" ? "bypassPermissions" : "auto"
|
|
12656
|
+
: profile.sandboxMode === "danger-full-access" ? "bypassPermissions" : "auto";
|
|
12657
|
+
}
|
|
12658
|
+
/** Done when, from the in-place editor: each line keeps its requirement's
|
|
12659
|
+
* id by position, and its evidence and guidance while its words stand; a
|
|
12660
|
+
* rewritten one is yours to check, like an added one; an emptied one is
|
|
12661
|
+
* dropped. */
|
|
12662
|
+
function requirementsFromEditor(body, current) {
|
|
12663
|
+
const used = new Set(current.map(one => one.id));
|
|
12664
|
+
let next = 1;
|
|
12665
|
+
while (used.has(`c${next}`))
|
|
12666
|
+
next++;
|
|
12667
|
+
const plain = (raw) => raw.replace(/\s+/g, " ").trim();
|
|
12668
|
+
const kept = body.getAll("requirement").slice(0, current.length).flatMap((raw, index) => {
|
|
12669
|
+
const statement = plain(raw);
|
|
12670
|
+
const criterion = current[index];
|
|
12671
|
+
if (statement === "")
|
|
12672
|
+
return [];
|
|
12673
|
+
return statement === plain(criterion.statement)
|
|
12674
|
+
? [{ id: criterion.id, statement: criterion.statement, evidence: [...criterion.evidence], how: criterion.how }]
|
|
12675
|
+
: [{ id: criterion.id, statement, evidence: ["manual-review"], how: null }];
|
|
12676
|
+
});
|
|
12677
|
+
const added = plain(body.get("requirement-new") ?? "");
|
|
12678
|
+
return added === "" ? kept : [...kept, { id: `c${next}`, statement: added, evidence: ["manual-review"], how: null }];
|
|
12337
12679
|
}
|
|
12338
12680
|
function consentDoorOf(scope, route) {
|
|
12339
12681
|
if (scope === null)
|
|
@@ -12386,31 +12728,31 @@ function consentClosedHtml(taskId, door, surface) {
|
|
|
12386
12728
|
}
|
|
12387
12729
|
function profileWords(scope) {
|
|
12388
12730
|
if (scope.profileState === "unresolved") {
|
|
12389
|
-
return `<p class="meta"><strong>
|
|
12731
|
+
return `<p class="meta"><strong>Filed but unapprovable</strong> — ${escape(scope.unresolvedReason ?? "the scope cannot say exactly what would run")}. Restate the scope to fix it.</p>`;
|
|
12390
12732
|
}
|
|
12391
12733
|
const profile = scope.profile ?? null;
|
|
12392
12734
|
if (profile === null) {
|
|
12393
12735
|
return (scope.digestVersion ?? 1) < 2
|
|
12394
|
-
? `<p class="meta">
|
|
12736
|
+
? `<p class="meta">Approved before routing was bound — pinned at upgrade to the configuration of that day</p>`
|
|
12395
12737
|
: "";
|
|
12396
12738
|
}
|
|
12397
12739
|
const repair = profile.repairModel === "inherit" ? "same model" : profile.repairModel;
|
|
12398
12740
|
const base = profile.provider === "claude"
|
|
12399
|
-
? `<p class="meta">
|
|
12741
|
+
? `<p class="meta">Runs on <span class="mono">claude · ${escape(profile.model)}</span> — ${profile.permissionArgv === "bypassPermissions"
|
|
12400
12742
|
? "FULL permissions; claude runs with --dangerously-skip-permissions and nothing asks"
|
|
12401
12743
|
: profile.permissionArgv === "auto"
|
|
12402
12744
|
? "safe unattended permissions; routine project commands and edits proceed, risky acts stop"
|
|
12403
|
-
: "legacy acceptEdits; edits proceed, commands that ask are denied unattended"}, ${profile.maxTurns}
|
|
12745
|
+
: "legacy acceptEdits; edits proceed, commands that ask are denied unattended"}, stops after ${profile.maxTurns} turns, ${Math.round(profile.timeoutSeconds / 60)} min ${profile.timeoutKind === "idle" ? "without progress" : "per attempt"}; repairs on ${escape(repair)}, ${profile.repairMaxTurns} turns / ${Math.round(profile.repairTimeoutSeconds / 60)} min</p>`
|
|
12404
12746
|
: profile.provider === "gemini"
|
|
12405
|
-
? `<p class="meta">
|
|
12406
|
-
: `<p class="meta">
|
|
12747
|
+
? `<p class="meta">Runs on <span class="mono">gemini · ${escape(profile.model)}</span> — ${profile.approvalArgv === "yolo" ? "Full access via --approval-mode yolo; every tool auto-approved" : "Auto via --approval-mode auto_edit; edits auto-approved, other tools refused"}, no turn limit (${Math.round(profile.timeoutSeconds / 60)} min ${profile.timeoutKind === "idle" ? "without progress" : "per attempt"}), spend reported in tokens only; repairs on ${escape(repair)}, ${Math.round(profile.repairTimeoutSeconds / 60)} min</p>`
|
|
12748
|
+
: `<p class="meta">Runs on <span class="mono">${escape(profile.provider)} · ${escape(profile.model)}</span> — ${profile.sandboxMode === "danger-full-access" ? "FULL permissions via --dangerously-bypass-approvals-and-sandbox; nothing asks" : "workspace-write sandbox"}, no turn limit (${Math.round(profile.timeoutSeconds / 60)} min ${profile.timeoutKind === "idle" ? "without progress" : "per attempt"}); repairs on ${escape(repair)}, ${Math.round(profile.repairTimeoutSeconds / 60)} min</p>`;
|
|
12407
12749
|
// The fallback chain rides EVERY surface these words sign (F+G review,
|
|
12408
12750
|
// finding 2): the digest binds the whole chain, so the password form —
|
|
12409
12751
|
// task page and /next alike — states every entry, credential included.
|
|
12410
12752
|
const chain = chainFromJson(scope.proposedChainJson ?? null);
|
|
12411
12753
|
const chainLine = chain === null || chain.length < 2
|
|
12412
12754
|
? ""
|
|
12413
|
-
: `<p class="meta">
|
|
12755
|
+
: `<p class="meta">If its subscription runs out: ${chain
|
|
12414
12756
|
.slice(1)
|
|
12415
12757
|
.map(one => `falls back to <span class="mono">${escape(one.profile.provider)} · ${escape(one.profile.model)}</span> — ${one.authMode === "api-key" ? "your API key; spend moves to that account" : "its subscription login"}`)
|
|
12416
12758
|
.join("; ")}</p>`;
|
|
@@ -12439,9 +12781,9 @@ function agentsSummaryWords(view) {
|
|
|
12439
12781
|
/** Neutral metadata badges: risk, posture, and standing. */
|
|
12440
12782
|
function agentsBadgesHtml(view) {
|
|
12441
12783
|
const p = view.projection;
|
|
12442
|
-
return (`<span class="badge">${escape(riskTitle(view.riskLevel))}</span>` +
|
|
12443
|
-
(p === null ? "" : `<span class="badge">${escape(p.postureWords)}</span>`) +
|
|
12444
|
-
`<span class="badge">${escape(agentsStandingWords(view))}</span>`);
|
|
12784
|
+
return (`<span class="badge">${escape(sentenceCase(riskTitle(view.riskLevel)))}</span>` +
|
|
12785
|
+
(p === null ? "" : `<span class="badge">${escape(sentenceCase(p.postureWords))}</span>`) +
|
|
12786
|
+
`<span class="badge">${escape(sentenceCase(agentsStandingWords(view)))}</span>`);
|
|
12445
12787
|
}
|
|
12446
12788
|
/** Volatile availability, per provider on the route — shown beside the
|
|
12447
12789
|
* agents, never inside the approval's terms. */
|
|
@@ -12466,7 +12808,7 @@ function agentsWhyHtml(projection, summaryLabel = "Why these agents") {
|
|
|
12466
12808
|
.map(leg => {
|
|
12467
12809
|
const chosen = chosenWords(leg);
|
|
12468
12810
|
return (`<dt>${escape(ROLE_NOUN[leg.phase])}</dt>` +
|
|
12469
|
-
`<dd><span class="mono">${escape(leg.provider)} · ${escape(leg.model)}</span> <span class="badge">${escape(chosen)}</span>` +
|
|
12811
|
+
`<dd><span class="mono">${escape(leg.provider)} · ${escape(leg.model)}</span> <span class="badge">${escape(sentenceCase(chosen))}</span>` +
|
|
12470
12812
|
`<ul class="agents-reasons">${leg.reasons.map(reason => `<li>${escape(reason)}</li>`).join("")}${leg.problem === null ? "" : `<li><strong>${escape(leg.problem)}</strong></li>`}</ul></dd>`);
|
|
12471
12813
|
})
|
|
12472
12814
|
.join("") +
|
|
@@ -12487,7 +12829,7 @@ function agentsCeremonyHtml(view) {
|
|
|
12487
12829
|
}
|
|
12488
12830
|
return (`<div class="agents-ceremony"><p class="approval-label">agents</p>` +
|
|
12489
12831
|
`<p class="agents-summary">${escape(view.projection.summary)}</p>` +
|
|
12490
|
-
`<div class="agents-badges"><span class="badge">${escape(riskTitle(view.riskLevel))}</span><span class="badge">${escape(view.projection.postureWords)}</span></div>` +
|
|
12832
|
+
`<div class="agents-badges"><span class="badge">${escape(sentenceCase(riskTitle(view.riskLevel)))}</span><span class="badge">${escape(sentenceCase(view.projection.postureWords))}</span></div>` +
|
|
12491
12833
|
`<p class="meta agents-risk-line">${escape(riskTitle(view.riskLevel))}: ${escape(riskConsequence(view.riskLevel))}.</p>` +
|
|
12492
12834
|
agentsWhyHtml(view.projection) +
|
|
12493
12835
|
`<p class="meta">These exact agents are part of what you approve; changing any of them asks for a fresh approval.</p></div>`);
|
|
@@ -12514,7 +12856,7 @@ function routineAgentsHtml(routine, approved, ceremony = false) {
|
|
|
12514
12856
|
const projection = projectRoute(route, () => null);
|
|
12515
12857
|
return (`<div class="agents-ceremony"><p class="approval-label">agents</p>` +
|
|
12516
12858
|
`<p class="agents-summary">${escape(agentsSummary(route))}</p>` +
|
|
12517
|
-
`<div class="agents-badges"><span class="badge">${escape(riskTitle(route.risk))}</span><span class="badge">${escape(postureWords(route))}</span><span class="badge">${approved ? "frozen by the approval" : "frozen when you approve"}</span></div>` +
|
|
12859
|
+
`<div class="agents-badges"><span class="badge">${escape(sentenceCase(riskTitle(route.risk)))}</span><span class="badge">${escape(sentenceCase(postureWords(route)))}</span><span class="badge">${approved ? "frozen by the approval" : "frozen when you approve"}</span></div>` +
|
|
12518
12860
|
(ceremony ? agentsWhyHtml(projection) : "") +
|
|
12519
12861
|
`<p class="meta">${approved ? "Every firing runs on exactly these agents; a configuration change cannot re-route it." : "Approving freezes exactly these agents for every firing; a configuration change afterwards cannot re-route one."}</p></div>`);
|
|
12520
12862
|
}
|
|
@@ -12544,7 +12886,7 @@ function agentsCardHtml(taskId, view, csrf, canEdit) {
|
|
|
12544
12886
|
? ""
|
|
12545
12887
|
: `<ul class="agents-overrides">${view.overrides
|
|
12546
12888
|
.map(one => `<li><span>${escape(ROLE_NOUN[one.phase])} → <span class="mono">${escape(one.provider)} · ${escape(one.model)}</span> <span class="meta">by ${escape(one.by)} ${whenTime(one.at)}</span></span>` +
|
|
12547
|
-
(canEdit && view.editable ? `<form method="post" action="${taskHref(taskId)}/route" class="agents-clear">${hidden}<input type="hidden" name="clear-phase" value="${escape(one.phase)}"><button type="submit" class="secondary" aria-label="clear the ${escape(ROLE_NOUN[one.phase].toLowerCase())} choice">
|
|
12889
|
+
(canEdit && view.editable ? `<form method="post" action="${taskHref(taskId)}/route" class="agents-clear">${hidden}<input type="hidden" name="clear-phase" value="${escape(one.phase)}"><button type="submit" class="secondary" aria-label="clear the ${escape(ROLE_NOUN[one.phase].toLowerCase())} choice">Clear</button></form>` : "") +
|
|
12548
12890
|
`</li>`)
|
|
12549
12891
|
.join("")}</ul>`;
|
|
12550
12892
|
// The controls (v48): a risk choice that says what each level does, and
|
|
@@ -12574,7 +12916,7 @@ function agentsCardHtml(taskId, view, csrf, canEdit) {
|
|
|
12574
12916
|
? `<p class="meta">${escape(view.editableWhy ?? "the agents cannot change right now")}</p>`
|
|
12575
12917
|
: `<details class="agents-change"><summary>Change agents</summary>` +
|
|
12576
12918
|
`<form method="post" action="${taskHref(taskId)}/route" class="agents-form-risk">${hidden}` +
|
|
12577
|
-
`<label>
|
|
12919
|
+
`<label>Risk<select name="risk" aria-label="declared risk">${RISK_LEVELS.map(one => `<option value="${one}"${one === view.riskLevel ? " selected" : ""}>${escape(riskTitle(one))}</option>`).join("")}</select></label>` +
|
|
12578
12920
|
`<button type="submit" class="secondary">Set risk</button></form>` +
|
|
12579
12921
|
riskGuide +
|
|
12580
12922
|
`<div class="agents-role-forms">${roleForms.join("")}</div>` +
|
|
@@ -13897,9 +14239,34 @@ ${THEME_DARK}
|
|
|
13897
14239
|
.approval-act input[type=password] { margin: 0; font-family: var(--font-sans); }
|
|
13898
14240
|
.approval-act button { margin: 0; min-height: 2.125rem; white-space: nowrap; }
|
|
13899
14241
|
.approval-after { max-width: 75ch; color: var(--muted-foreground); font-size: 13px; line-height: 1.5; }
|
|
13900
|
-
.approval-
|
|
14242
|
+
.approval-allowing, .approval-you-check { max-width: 75ch; font-size: 14px; line-height: 1.55; overflow-wrap: anywhere; }
|
|
14243
|
+
.approval-sheet .approval-allowing + .approval-you-check { margin-top: -10px; }
|
|
14244
|
+
.approval-act > button { grid-column: 2; grid-row: 1; }
|
|
14245
|
+
.approval-password-note { grid-column: 1; grid-row: 2; margin: 0; color: var(--muted-foreground); font-size: 13px; line-height: 1.5; }
|
|
14246
|
+
.approval-secondary { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
|
|
14247
|
+
details.approval-edit, details.approval-edit[open] { margin: 0; padding: 0; border: 0; border-radius: 0; background: none; }
|
|
14248
|
+
/* Edit plan, in place: the rows become fields where they stood; the
|
|
14249
|
+
approval act and Details step aside until it is saved or cancelled. */
|
|
14250
|
+
.approval-sheet { position: relative; }
|
|
14251
|
+
.approval-edit > summary { list-style: none; cursor: pointer; }
|
|
14252
|
+
.approval-edit > summary::-webkit-details-marker { display: none; }
|
|
14253
|
+
.approval-edit-close, .approval-edit[open] .approval-edit-open { display: none; }
|
|
14254
|
+
.approval-edit[open] .approval-edit-close { display: inline; }
|
|
14255
|
+
.approval-edit[open] > summary { position: absolute; top: 14px; right: 20px; }
|
|
14256
|
+
.approval-sheet:has(.approval-edit[open]) > :not(.approval-sheet-title, .approval-secondary) { display: none; }
|
|
14257
|
+
.approval-secondary:has(> .approval-edit[open]) { display: block; }
|
|
14258
|
+
.approval-secondary:has(> .approval-edit[open]) > a { display: none; }
|
|
14259
|
+
.approval-editor { display: grid; gap: 14px; }
|
|
14260
|
+
.approval-editor .problem { margin: 0; }
|
|
14261
|
+
.approval-field { display: grid; gap: 4px; min-width: 0; margin: 0; padding: 0; border: 0; }
|
|
14262
|
+
.approval-field-label { padding: 0; color: var(--muted-foreground); font-size: 13px; font-weight: 500; line-height: 1.6; }
|
|
14263
|
+
.approval-field-hint { color: var(--muted-foreground); font-size: 12px; line-height: 1.5; }
|
|
14264
|
+
.approval-field textarea, .approval-field input[type=text] { width: 100%; max-width: 75ch; margin: 0; font-family: var(--font-sans); font-size: 14px; line-height: 1.55; }
|
|
14265
|
+
.approval-requirements { display: grid; gap: 6px; margin: 2px 0 0; padding: 0; list-style: none; }
|
|
14266
|
+
.approval-edit-act { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
|
|
14267
|
+
.approval-edit-act button { margin: 0; min-height: 2.125rem; white-space: nowrap; }
|
|
13901
14268
|
.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-
|
|
14269
|
+
.approval-secondary > .approval-link:not(:first-child) { border-color: transparent; background: none; color: var(--muted-foreground); }
|
|
13903
14270
|
@media (hover: hover) and (pointer: fine) { .approval-link:hover { background: var(--muted); color: var(--foreground); } }
|
|
13904
14271
|
details.approval-details { margin: 0; padding: 0; border: 0; border-top: 1px solid var(--border); border-radius: 0; background: none; }
|
|
13905
14272
|
details.approval-details[open] { padding-bottom: 0; }
|
|
@@ -13919,6 +14286,7 @@ ${THEME_DARK}
|
|
|
13919
14286
|
.approval-sheet { gap: 12px; padding: 14px; }
|
|
13920
14287
|
.approval-row { grid-template-columns: 1fr; gap: 2px; }
|
|
13921
14288
|
.approval-act { grid-template-columns: 1fr; }
|
|
14289
|
+
.approval-act > button, .approval-password-note { grid-column: auto; grid-row: auto; }
|
|
13922
14290
|
.approval-act input[type=password], .approval-act button { min-height: 44px; font-size: 16px; }
|
|
13923
14291
|
.approval-act button { width: 100%; }
|
|
13924
14292
|
/* The one act stays under the thumb: sticky at the bottom, clear of the home indicator. */
|
|
@@ -13927,6 +14295,9 @@ ${THEME_DARK}
|
|
|
13927
14295
|
border-top: 1px solid var(--border); background: var(--card);
|
|
13928
14296
|
}
|
|
13929
14297
|
.approval-link { min-height: 44px; padding: 0 16px; }
|
|
14298
|
+
.approval-edit[open] > summary { top: 6px; right: 6px; }
|
|
14299
|
+
.approval-field textarea, .approval-field input[type=text] { font-size: 16px; }
|
|
14300
|
+
.approval-edit-act button { width: 100%; min-height: 44px; font-size: 16px; }
|
|
13930
14301
|
.approval-details > summary { min-height: 44px; }
|
|
13931
14302
|
.approval-roles > div { grid-template-columns: 1fr; gap: 2px; }
|
|
13932
14303
|
}
|
|
@@ -15442,7 +15813,7 @@ const INBOX_TABS_CSS = '.inbox-tabs{display:inline-flex;gap:2px;max-width:100%;o
|
|
|
15442
15813
|
'.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}' +
|
|
15443
15814
|
'.inbox-unread{display:none;position:absolute;top:4px;right:3px;width:6px;height:6px;border-radius:50%;background:var(--so-signal)}' +
|
|
15444
15815
|
'@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}}';
|
|
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}');
|
|
15816
|
+
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 + DISCLOSURE_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}');
|
|
15446
15817
|
/**
|
|
15447
15818
|
* The board's liveness: fetch this page's own fragment on a timer and swap
|
|
15448
15819
|
* it in place — no flicker, no scroll reset, no long-lived stream to manage,
|
|
@@ -15697,7 +16068,7 @@ FINISH: unreviewed and undocumented is unfinished; this build ends with the fini
|
|
|
15697
16068
|
* absent from sensitive pages. Navigation help in plain words — no key
|
|
15698
16069
|
* ever posts. */
|
|
15699
16070
|
const KBD_HELP = `<div class="kbd-help" hidden role="dialog" aria-label="keyboard shortcuts" tabindex="-1">` +
|
|
15700
|
-
`<h2>
|
|
16071
|
+
`<h2>Keyboard shortcuts</h2><table>` +
|
|
15701
16072
|
`<tr><td><kbd>/</kbd></td><td>jump to a page or an open task</td></tr>` +
|
|
15702
16073
|
`<tr><td><kbd>g</kbd> then <kbd>i</kbd></td><td>go to the inbox</td></tr>` +
|
|
15703
16074
|
`<tr><td><kbd>g</kbd> then <kbd>b</kbd></td><td>go to the board</td></tr>` +
|
|
@@ -15772,7 +16143,7 @@ function shell(title, body, options = {}) {
|
|
|
15772
16143
|
: `<span class="scope-status">` +
|
|
15773
16144
|
(peekCounts.waiting > 0 ? `<a class="hot" href="/work?view=needs-you">${peekCounts.waiting} needs you</a>` : `<a href="/work?view=needs-you">0 needs you</a>`) +
|
|
15774
16145
|
`<a href="/runs">${peekCounts.running} live</a><a href="/board?view=order">${peekCounts.queued} queued</a></span>`;
|
|
15775
|
-
const scopeName = effectiveScope === "project" && chrome.project !== null ? escape(projectName(chrome.project)) : "
|
|
16146
|
+
const scopeName = effectiveScope === "project" && chrome.project !== null ? escape(projectName(chrome.project)) : "All projects";
|
|
15776
16147
|
// The switcher (board pass): the scope's name opens a menu of every
|
|
15777
16148
|
// enrolled project — one tap to open one, or to widen to all — as plain
|
|
15778
16149
|
// POST forms carrying the session's csrf, returning to this screen.
|
|
@@ -15787,7 +16158,7 @@ function shell(title, body, options = {}) {
|
|
|
15787
16158
|
const allCurrent = effectiveScope !== "project";
|
|
15788
16159
|
const rows = [
|
|
15789
16160
|
`<form method="post" action="/projects/select">${hidden}<input type="hidden" name="path" value="">` +
|
|
15790
|
-
`<button type="submit"${allCurrent ? ' class="current" aria-current="true"' : ""}>
|
|
16161
|
+
`<button type="submit"${allCurrent ? ' class="current" aria-current="true"' : ""}>All projects</button></form>`,
|
|
15791
16162
|
...(chrome.projects ?? []).map(one => `<form method="post" action="/projects/open">${hidden}<input type="hidden" name="path" value="${escape(one.path)}">` +
|
|
15792
16163
|
`<button type="submit"${!allCurrent && chrome.project === one.path ? ' class="current" aria-current="true"' : ""}>${escape(one.name)}</button></form>`),
|
|
15793
16164
|
];
|
|
@@ -15831,10 +16202,10 @@ function shell(title, body, options = {}) {
|
|
|
15831
16202
|
primaryItem("flows", "/flows", "Flows"),
|
|
15832
16203
|
primaryItem("projects", "/projects", "Projects"),
|
|
15833
16204
|
`</nav>`,
|
|
15834
|
-
...(chrome.active === "code" ? [] : [`<a class="new-task" href="/tasks/new" aria-label="
|
|
16205
|
+
...(chrome.active === "code" ? [] : [`<a class="new-task" href="/tasks/new" aria-label="New task">+ New task</a>`]),
|
|
15835
16206
|
`<nav class="nav-groups">`,
|
|
15836
|
-
navGroup("tools", "
|
|
15837
|
-
navGroup("settings", "
|
|
16207
|
+
navGroup("tools", "Work tools", workToolRows(chrome.projectScoped, chrome.chat, chrome.code), TOOL_KEYS.has(chrome.active)),
|
|
16208
|
+
navGroup("settings", "Settings", settingsRows(chrome.projectScoped, chrome.settings), SETTINGS_KEYS.has(chrome.active)),
|
|
15838
16209
|
`</nav>`,
|
|
15839
16210
|
`<span class="grow"></span>`,
|
|
15840
16211
|
`</aside>`,
|
|
@@ -15843,11 +16214,11 @@ function shell(title, body, options = {}) {
|
|
|
15843
16214
|
// must not pass as production (adoption review, finding 8; the FENCE is
|
|
15844
16215
|
// the refuseDemo gate in operate.ts, this is the honest label).
|
|
15845
16216
|
const demoBanner = (chrome.demo === true
|
|
15846
|
-
? `<div class="banner"><span class="badge">
|
|
16217
|
+
? `<div class="banner"><span class="badge">Demo</span>${escape(DEMO_BANNER.replace(/^Demo: /, ""))}</div>`
|
|
15847
16218
|
: "") +
|
|
15848
16219
|
(chrome.modeBanner === undefined
|
|
15849
16220
|
? ""
|
|
15850
|
-
: `<div class="banner"><span class="badge badge-running">
|
|
16221
|
+
: `<div class="banner"><span class="badge badge-running">Mode</span>${escape(chrome.modeBanner.words)} \u00b7 <a href="/mode">the terms \u00b7 end it</a></div>`) +
|
|
15851
16222
|
(chrome.signIn ?? []).map(one => `<div class="banner sign-in-banner" data-sign-in="${escape(one.provider)}"><strong>${escape(one.title)}</strong> \u00b7 run <code>${escape(one.command)}</code> on this computer, then resume.${one.detail === "" ? "" : ` ${escape(one.detail)}`}` +
|
|
15852
16223
|
`<form method="post" action="${escape(one.resumeHref)}" class="inline"><input type="hidden" name="csrf" value="${escape(chrome.csrf ?? "")}"><button type="submit">${escape(one.resumeLabel)}</button></form></div>`).join("") +
|
|
15853
16224
|
(chrome.updateWaiting === undefined
|
|
@@ -16069,7 +16440,7 @@ function inboxPage(chrome, data) {
|
|
|
16069
16440
|
/** The row's project, worn openly in the roll-up — null is UNPLACED,
|
|
16070
16441
|
* said as such, never a silent missing chip (finding 13). */
|
|
16071
16442
|
const chip = (repo) => !data.rollup ? "" : repo === null || repo === undefined
|
|
16072
|
-
? ` <span class="badge">
|
|
16443
|
+
? ` <span class="badge">Unplaced</span>`
|
|
16073
16444
|
: ` <span class="badge">${escape(projectName(repo))}</span>`;
|
|
16074
16445
|
const empty = data.decisions.length + data.approvals.length + data.requeueables.length +
|
|
16075
16446
|
data.cancelledBlockers.length + data.gaps.length + data.needsVerification.length === 0;
|
|
@@ -16078,19 +16449,19 @@ function inboxPage(chrome, data) {
|
|
|
16078
16449
|
// the card itself (portfolio arc §2).
|
|
16079
16450
|
const decisions = data.decisions.length === 0
|
|
16080
16451
|
? ""
|
|
16081
|
-
: `<h2>
|
|
16452
|
+
: `<h2>Answer a question</h2><p class="hint">an agent stopped mid-build to ask — nothing proceeds until you answer</p>` +
|
|
16082
16453
|
data.decisions
|
|
16083
16454
|
.map(decision => data.interactive && !data.rollup
|
|
16084
16455
|
? decisionAnswerCard(decision, data.csrf, data.now, false)
|
|
16085
16456
|
: `<a class="decide-card" href="/d/${decision.id}">` +
|
|
16086
16457
|
`<p class="q">${escape(decision.question)}</p>` +
|
|
16087
16458
|
`<span class="mono meta">${escape(decision.taskId)}</span>${chip(decision.repo)}` +
|
|
16088
|
-
`${isOverdue(decision, data.now) ? ` <span class="badge badge-overdue">
|
|
16459
|
+
`${isOverdue(decision, data.now) ? ` <span class="badge badge-overdue">Overdue</span>` : ""}` +
|
|
16089
16460
|
`</a>`)
|
|
16090
16461
|
.join("\n");
|
|
16091
16462
|
const approvals = data.approvals.length === 0
|
|
16092
16463
|
? ""
|
|
16093
|
-
: `<h2>
|
|
16464
|
+
: `<h2>Approve a scope</h2><p class="hint">scopes waiting for your approval — each binds to the exact wording you sign</p>` +
|
|
16094
16465
|
data.approvals
|
|
16095
16466
|
.map(one => `<a class="decide-card" href="${taskHref(one.taskId)}">` +
|
|
16096
16467
|
`<p class="q">${escape(one.title)}</p>` +
|
|
@@ -16100,7 +16471,7 @@ function inboxPage(chrome, data) {
|
|
|
16100
16471
|
.join("\n");
|
|
16101
16472
|
const requeueables = data.requeueables.length === 0
|
|
16102
16473
|
? ""
|
|
16103
|
-
: `<h2>
|
|
16474
|
+
: `<h2>Retry stalled work</h2><p class="hint">failed builds waiting for a person — retry clears the incidents and requeues</p>` +
|
|
16104
16475
|
data.requeueables
|
|
16105
16476
|
.map(one => `<p class="row"><a href="${taskHref(one.taskId)}">${escape(one.taskId)}</a> ${escape(one.title)}${chip(one.repo)}` +
|
|
16106
16477
|
`${one.incidentCount > 0 ? ` <span class="badge badge-failed">${one.incidentCount} incident${one.incidentCount > 1 ? "s" : ""}</span>` : ""}` +
|
|
@@ -16110,11 +16481,11 @@ function inboxPage(chrome, data) {
|
|
|
16110
16481
|
: `<span class="right"><form method="post" action="${taskHref(one.taskId)}/requeue" class="inline">` +
|
|
16111
16482
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
|
|
16112
16483
|
`<input type="hidden" name="return" value="inbox">` +
|
|
16113
|
-
`<button type="submit">
|
|
16484
|
+
`<button type="submit">Retry</button></form></span></p>`))
|
|
16114
16485
|
.join("\n");
|
|
16115
16486
|
const needsVerification = data.needsVerification.length === 0
|
|
16116
16487
|
? ""
|
|
16117
|
-
: `<h2>
|
|
16488
|
+
: `<h2>Needs review</h2><p class="hint">finished, but the evidence needs your attention before accepting</p>` +
|
|
16118
16489
|
data.needsVerification
|
|
16119
16490
|
.map(one => `<p class="row"><a href="${taskHref(one.taskId)}">${escape(one.taskId)}</a> ${escape(one.title)}${chip(one.repo)}` +
|
|
16120
16491
|
` <span class="badge badge-failed">${one.verdict === "refuted" ? "conflicting evidence" : "missing evidence"}</span>${criterionMatrixSummary(one.matrix ?? [])}` +
|
|
@@ -16123,7 +16494,7 @@ function inboxPage(chrome, data) {
|
|
|
16123
16494
|
.join("\n");
|
|
16124
16495
|
const cancelled = data.cancelledBlockers.length === 0
|
|
16125
16496
|
? ""
|
|
16126
|
-
: `<h2>
|
|
16497
|
+
: `<h2>Choose how waiting tasks continue</h2><p class="hint">these tasks were waiting for work that was cancelled — open one and choose what happens next</p>` +
|
|
16127
16498
|
data.cancelledBlockers
|
|
16128
16499
|
.map(one => `<p class="row"><a href="${taskHref(one.exampleDependent)}">${escape(one.exampleDependent)}</a>${chip(one.repo)} ` +
|
|
16129
16500
|
`<span class="meta">${one.dependentCount > 1 ? `one of ${one.dependentCount} tasks waiting` : "waiting"} for cancelled task ${escape(one.blockerId)}` +
|
|
@@ -16131,7 +16502,7 @@ function inboxPage(chrome, data) {
|
|
|
16131
16502
|
.join("\n");
|
|
16132
16503
|
const gaps = data.gaps.length === 0
|
|
16133
16504
|
? ""
|
|
16134
|
-
: `<h2>
|
|
16505
|
+
: `<h2>Supply a requirement</h2><p class="hint">approved work is ready except for these — fill one and its tasks start</p>` +
|
|
16135
16506
|
data.gaps
|
|
16136
16507
|
.map(gap => `<p class="row"><a href="/caps">${escape(gap.key)}</a> ` +
|
|
16137
16508
|
`<span class="meta">frees ${gap.unblocks.length} task${gap.unblocks.length > 1 ? "s" : ""}</span>` +
|
|
@@ -16163,8 +16534,8 @@ function inboxPage(chrome, data) {
|
|
|
16163
16534
|
const tab = data.tab ?? "all";
|
|
16164
16535
|
const shows = (one) => tab === "all" || tab === one;
|
|
16165
16536
|
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");
|
|
16166
|
-
const ready = (data.ready ?? []).length === 0 ? "" : `<h2>
|
|
16167
|
-
const running = (data.running ?? []).length === 0 ? "" : `<h2>
|
|
16537
|
+
const ready = (data.ready ?? []).length === 0 ? "" : `<h2>Ready to review</h2>` + listRows(data.ready ?? []);
|
|
16538
|
+
const running = (data.running ?? []).length === 0 ? "" : `<h2>Running now</h2>` + listRows(data.running ?? []);
|
|
16168
16539
|
const asks = { decide: data.decisions.length + data.approvals.length, unblock: data.requeueables.length + data.cancelledBlockers.length + data.gaps.length };
|
|
16169
16540
|
const counts = { ...asks,
|
|
16170
16541
|
"needs-you": asks.decide + asks.unblock,
|
|
@@ -16183,7 +16554,7 @@ function inboxPage(chrome, data) {
|
|
|
16183
16554
|
? `<div class="card"><p class="meta">${tab === "needs-you" ? "Nothing needs you." : tab === "ready" ? "No results are waiting for review." : "Nothing is running."}</p></div>` : "";
|
|
16184
16555
|
return screen("inbox", [
|
|
16185
16556
|
`<h1>Inbox</h1>`,
|
|
16186
|
-
`<p class="meta">
|
|
16557
|
+
`<p class="meta">Everything that waits on you \u2014 empty means the fleet is working</p>`,
|
|
16187
16558
|
tabs,
|
|
16188
16559
|
noWorker,
|
|
16189
16560
|
wizard,
|
|
@@ -16195,21 +16566,21 @@ function inboxPage(chrome, data) {
|
|
|
16195
16566
|
shows("needs-you") ? askGroup("unblock", counts.unblock, [requeueables, cancelled, gaps]) : "",
|
|
16196
16567
|
shows("running") ? running : "",
|
|
16197
16568
|
data.rollup
|
|
16198
|
-
? `<p class="meta">
|
|
16569
|
+
? `<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>`
|
|
16199
16570
|
: "",
|
|
16200
16571
|
// Quick capture: the shortest path from "I want this done" to the
|
|
16201
16572
|
// approve card — title and goal here, the yes on the next screen. The
|
|
16202
16573
|
// one-shot form posts to the same guarded handler as the full page.
|
|
16203
|
-
data.rollup ? "" : `<h2>
|
|
16574
|
+
data.rollup ? "" : `<h2>Capture new work</h2>`,
|
|
16204
16575
|
data.rollup ? "" : `<form method="post" action="/tasks/add" class="card">`,
|
|
16205
16576
|
...(data.rollup
|
|
16206
16577
|
? []
|
|
16207
16578
|
: [
|
|
16208
16579
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
|
|
16209
16580
|
`<input type="hidden" name="projectRevision" value="${data.revision}">`,
|
|
16210
|
-
`<label>
|
|
16211
|
-
`<label>
|
|
16212
|
-
`<button type="submit">
|
|
16581
|
+
`<label>What should get done<input type="text" name="title" placeholder="task title" maxlength="200"></label>`,
|
|
16582
|
+
`<label>What success looks like <span class="meta">(becomes the scope you approve on the next screen)</span><textarea name="goal" rows="2"></textarea></label>`,
|
|
16583
|
+
`<button type="submit">Queue it \u2192 approve its scope next</button>`,
|
|
16213
16584
|
`</form>`,
|
|
16214
16585
|
]),
|
|
16215
16586
|
].join("\n"), {
|
|
@@ -16258,7 +16629,7 @@ function systemPage(chrome, data) {
|
|
|
16258
16629
|
const agentLines = data.agents
|
|
16259
16630
|
.map(one => {
|
|
16260
16631
|
if (one.problem !== undefined) {
|
|
16261
|
-
return `<p class="row"><span class="mono">${escape(one.phase)}</span> <span class="badge badge-failed">
|
|
16632
|
+
return `<p class="row"><span class="mono">${escape(one.phase)}</span> <span class="badge badge-failed">Misconfigured</span> <span class="meta">${escape(one.problem)}</span></p>`;
|
|
16262
16633
|
}
|
|
16263
16634
|
const who = one.source === "project"
|
|
16264
16635
|
? `chosen for this project${one.setBy === null ? "" : ` by ${escape(one.setBy)}`}`
|
|
@@ -16273,10 +16644,10 @@ function systemPage(chrome, data) {
|
|
|
16273
16644
|
`<p class="meta" style="margin-top:0">${PHASE_SAID[one.phase] ?? ""}</p>`);
|
|
16274
16645
|
})
|
|
16275
16646
|
.join("\n");
|
|
16276
|
-
const agentsCard = `<h2>
|
|
16277
|
-
`<p class="meta">
|
|
16647
|
+
const agentsCard = `<h2>Agents</h2>` +
|
|
16648
|
+
`<p class="meta">Which AI provider runs each phase — changed from the terminal with your credentials (<code>toolroll config</code>), never by a browser click</p>` +
|
|
16278
16649
|
`<div class="card">${agentLines}` +
|
|
16279
|
-
`<p class="meta">
|
|
16650
|
+
`<p class="meta">Repair always stays on the provider that built — only its model can differ. A routine pins its agent the moment it fires; nothing after that can re-route it.</p>` +
|
|
16280
16651
|
`</div>`;
|
|
16281
16652
|
return screen("system", [
|
|
16282
16653
|
`<h1>System</h1>`,
|
|
@@ -16285,16 +16656,16 @@ function systemPage(chrome, data) {
|
|
|
16285
16656
|
cards.length === 0
|
|
16286
16657
|
? `<p class="meta">No builder is connected yet. On the machine where the project lives, open that folder and run <code>toolroll up</code>.</p>`
|
|
16287
16658
|
: `<div class="cards">${cards.join("")}</div>`,
|
|
16288
|
-
data.outboxPending > 0 ? `<p class="meta">
|
|
16659
|
+
data.outboxPending > 0 ? `<p class="meta">Notifications: ${data.outboxPending} pending delivery</p>` : "",
|
|
16289
16660
|
(data.externalWork ?? []).length === 0
|
|
16290
16661
|
? ""
|
|
16291
|
-
: `<h2>
|
|
16662
|
+
: `<h2>External work</h2>` +
|
|
16292
16663
|
(data.externalWork ?? [])
|
|
16293
16664
|
.map(one => `<p class="row"><span class="mono">${escape(one.remoteRepo)}</span> ` +
|
|
16294
16665
|
(one.blocked !== null
|
|
16295
|
-
? `<span class="badge badge-failed">
|
|
16666
|
+
? `<span class="badge badge-failed">Dispatch blocked</span> <span class="meta">the tracker connection needs repair — \`toolroll sync\` says why</span>`
|
|
16296
16667
|
: one.openEpisode !== null
|
|
16297
|
-
? `<span class="badge badge-failed">
|
|
16668
|
+
? `<span class="badge badge-failed">Sync failing</span> <span class="meta">${escape(one.openEpisode)}</span>`
|
|
16298
16669
|
: `<span class="meta">syncing normally</span>`) +
|
|
16299
16670
|
`</p>`)
|
|
16300
16671
|
.join("\n"),
|
|
@@ -16397,7 +16768,7 @@ function boardBody(data, ciRed) {
|
|
|
16397
16768
|
queuedHeads.get(`${card.assignedRunner ?? ""}|${card.repo ?? ""}`) === card.taskId
|
|
16398
16769
|
? `<span class="badge">${card.assignedRunner === null ? "next up" : `next for ${escape(card.assignedRunner)}`}</span>`
|
|
16399
16770
|
: "",
|
|
16400
|
-
card.assignedRunner === null ? "" : `<span class="badge">
|
|
16771
|
+
card.assignedRunner === null ? "" : `<span class="badge">Reserved</span>`,
|
|
16401
16772
|
card.routineName === null ? "" : `<span class="badge">${escape(card.routineName)}</span>`,
|
|
16402
16773
|
chip(card.repo).trim(),
|
|
16403
16774
|
]) +
|
|
@@ -16425,7 +16796,7 @@ function boardBody(data, ciRed) {
|
|
|
16425
16796
|
...(claim.branch === null ? [] : [["branch", `${claim.branch}${workspace === null ? "" : ` · ${workspace}`}`]]),
|
|
16426
16797
|
]) +
|
|
16427
16798
|
chips([
|
|
16428
|
-
card.attempt === null ? "" : `<span class="badge">
|
|
16799
|
+
card.attempt === null ? "" : `<span class="badge">Attempt ${card.attempt}</span>`,
|
|
16429
16800
|
chip(card.repo).trim(),
|
|
16430
16801
|
]) +
|
|
16431
16802
|
`</a>`);
|
|
@@ -16447,7 +16818,7 @@ function boardBody(data, ciRed) {
|
|
|
16447
16818
|
["usage", runCostWords({ authMode: row.authMode, costUsd: row.costUsd, tokensIn: null, tokensOut: null })],
|
|
16448
16819
|
]) +
|
|
16449
16820
|
chips([
|
|
16450
|
-
`<span class="badge badge-done">${row.outcome === "no-change" ? "
|
|
16821
|
+
`<span class="badge badge-done">${row.outcome === "no-change" ? "No change" : "Built"}</span>`,
|
|
16451
16822
|
pr.trim(),
|
|
16452
16823
|
data.all && row.repo !== null ? `<span class="badge">${escape(projectName(row.repo))}</span>` : "",
|
|
16453
16824
|
]) +
|
|
@@ -16459,7 +16830,7 @@ function boardBody(data, ciRed) {
|
|
|
16459
16830
|
: `<p class="meta board-scope">` +
|
|
16460
16831
|
(data.all
|
|
16461
16832
|
? (data.project === null
|
|
16462
|
-
? `<strong>
|
|
16833
|
+
? `<strong>All projects</strong>`
|
|
16463
16834
|
: `<a href="/board">${escape(projectName(data.project))}</a> \u00b7 <strong>all projects</strong>`) +
|
|
16464
16835
|
` \u2014 every project this server serves, each card wearing its project`
|
|
16465
16836
|
: `<strong>${escape(projectName(data.project))}</strong> \u00b7 <a href="/board?scope=all">all projects</a>`) +
|
|
@@ -16476,7 +16847,7 @@ function boardBody(data, ciRed) {
|
|
|
16476
16847
|
const ago = (minutes) => minutes < 60 ? `${minutes}m` : minutes < 48 * 60 ? `${Math.round(minutes / 60)}h` : `${Math.round(minutes / (24 * 60))}d`;
|
|
16477
16848
|
const deltaLine = data.delta === null
|
|
16478
16849
|
? ""
|
|
16479
|
-
: `<p class="meta"><strong>
|
|
16850
|
+
: `<p class="meta"><strong>Since you last looked</strong> (${ago(data.delta.agoMinutes)} ago): ` +
|
|
16480
16851
|
[
|
|
16481
16852
|
data.delta.built > 0 ? `<span class="good">${data.delta.built} built</span>` : "",
|
|
16482
16853
|
data.delta.failed > 0 ? `<span class="bad">${data.delta.failed} failed</span>` : "",
|
|
@@ -16487,7 +16858,7 @@ function boardBody(data, ciRed) {
|
|
|
16487
16858
|
`</p>`;
|
|
16488
16859
|
return [
|
|
16489
16860
|
`<h1>Board</h1>`,
|
|
16490
|
-
data.all ? "" : `<p class="meta board-view"><strong>
|
|
16861
|
+
data.all ? "" : `<p class="meta board-view"><strong>State</strong> \u00b7 <a href="/board?view=order">order \u2192</a> <span class="meta">drag to reorder, or to reserve a task for one worker</span></p>`,
|
|
16491
16862
|
deltaLine,
|
|
16492
16863
|
toggle,
|
|
16493
16864
|
`<div class="board">`,
|
|
@@ -16509,14 +16880,14 @@ function donePage(chrome, rows, ciRed) {
|
|
|
16509
16880
|
const pr = row.prNumber === null
|
|
16510
16881
|
? row.publicationState === null
|
|
16511
16882
|
? ""
|
|
16512
|
-
: ` <span class="badge">${escape(row.publicationState)}</span>`
|
|
16883
|
+
: ` <span class="badge">${escape(sentenceCase(row.publicationState))}</span>`
|
|
16513
16884
|
: ` <a href="${escape(row.prUrl ?? "#")}" class="badge badge-open">PR #${row.prNumber}</a>` +
|
|
16514
16885
|
(ciRed(row.prNumber) ? ` <span class="badge badge-failed">CI failing</span>` : "");
|
|
16515
16886
|
const needsVerification = (row.proofVerdict === "short" || row.proofVerdict === "refuted") && !row.proofAccepted
|
|
16516
16887
|
? ` <span class="badge badge-failed">${row.proofVerdict === "refuted" ? "conflicting evidence" : "missing evidence"}</span>`
|
|
16517
16888
|
: "";
|
|
16518
16889
|
return (`<div class="card"><p><a href="${taskHref(row.taskId)}"><strong>${escape(row.title)}</strong></a>` +
|
|
16519
|
-
`${row.outcome === "no-change" ? ` <span class="badge">
|
|
16890
|
+
`${row.outcome === "no-change" ? ` <span class="badge">No change needed</span>` : ""}${pr}${needsVerification}${criterionMatrixSummary(row.proofMatrix)}</p>` +
|
|
16520
16891
|
`${row.handoff === null ? "" : `<p class="meta">${escape(row.handoff.length > 200 ? row.handoff.slice(0, 200) + "\u2026" : row.handoff)}</p>`}` +
|
|
16521
16892
|
`<p class="meta mono">${escape(row.taskId)} \u00b7 ${whenTime(row.completedAt)}${row.ranMinutes === null ? "" : ` \u00b7 ran ${row.ranMinutes}m`}${row.provider === null ? "" : ` \u00b7 ${escape(runCostWords({ authMode: row.authMode, costUsd: row.costUsd, tokensIn: null, tokensOut: null }))}`}` +
|
|
16522
16893
|
` \u00b7 <a href="${reviewHref(row.taskId)}">review \u2192</a></p></div>`);
|
|
@@ -16582,9 +16953,9 @@ function trackRow(track, all) {
|
|
|
16582
16953
|
`<p>${trackStrip(fires)}` +
|
|
16583
16954
|
`<span class="right meta">$${spend.costUsd.toFixed(2)} this week${spend.unmeasuredRuns > 0 ? ` — measured on ${spend.totalRuns - spend.unmeasuredRuns} of ${spend.totalRuns} builds` : ""}${routine.costCeilingUsd === null ? "" : ` of $${routine.costCeilingUsd.toFixed(2)}`}</span></p>` +
|
|
16584
16955
|
(blocker !== null
|
|
16585
|
-
? `<p class="meta">
|
|
16956
|
+
? `<p class="meta">Stopped behind <a href="${taskHref(blocker.taskId)}" class="mono">${escape(blocker.taskId)}</a> (${escape(blocker.state)})</p>`
|
|
16586
16957
|
: latest?.instanceTaskId !== undefined && latest.instanceTaskId !== null
|
|
16587
|
-
? `<p class="meta">
|
|
16958
|
+
? `<p class="meta">Latest: <a href="${taskHref(latest.instanceTaskId)}" class="mono">${escape(latest.instanceTaskId)}</a>${latest.instanceState === null ? "" : ` (${escape(latest.instanceState)})`}</p>`
|
|
16588
16959
|
: "") +
|
|
16589
16960
|
`</div>`);
|
|
16590
16961
|
}
|
|
@@ -16701,12 +17072,13 @@ function checkProgressHtml(progress, id = "check-progress") {
|
|
|
16701
17072
|
: Object.values(progress.suites).every(one => one.state === "passed") ? "passed" : "unknown";
|
|
16702
17073
|
return `<p id="${id}" class="check-progress mono" data-check-progress data-final="${escape(state)}" role="status" aria-live="polite">${escape(progress.line)}</p>`;
|
|
16703
17074
|
}
|
|
16704
|
-
function taskControlDetailsHtml(control, taskId, csrf, surface, inert = false) {
|
|
16705
|
-
const html = taskControlHtml(control, taskId, csrf, surface, inert);
|
|
17075
|
+
function taskControlDetailsHtml(control, taskId, csrf, surface, inert = false, stopElsewhere = false) {
|
|
17076
|
+
const html = taskControlHtml(control, taskId, csrf, surface, inert, stopElsewhere);
|
|
16706
17077
|
return control.kind === "paused" || control.kind === "review-stopped" || control.kind === "stopping"
|
|
16707
17078
|
? `<details id="task-control-details"><summary>${control.kind === "paused" ? "Preserved work and resume" : "Stop details"}</summary>${html}</details>` : html;
|
|
16708
17079
|
}
|
|
16709
|
-
|
|
17080
|
+
/** `stopElsewhere`: the Building card already carries this exact run's Stop, so the control card keeps only its details. */
|
|
17081
|
+
function taskControlHtml(control, taskId, csrf, surface, inert = false, stopElsewhere = false) {
|
|
16710
17082
|
if (control.kind === "none")
|
|
16711
17083
|
return "";
|
|
16712
17084
|
const back = `<input type="hidden" name="return" value="${surface}">`;
|
|
@@ -16720,7 +17092,7 @@ function taskControlHtml(control, taskId, csrf, surface, inert = false) {
|
|
|
16720
17092
|
if (control.kind === "stop") {
|
|
16721
17093
|
return (`<section class="card task-control" id="task-control" data-task-control="stop" data-control-run="${control.run}" aria-label="stop this attempt">` +
|
|
16722
17094
|
`<div class="task-control-copy"><details><summary>Stop details · ${escape(role(control))} #${control.run}</summary><p class="meta">Stopping ends only this attempt's own processes. Its branch, uncommitted work, evidence, and decisions stay preserved; other tasks keep running.</p></details></div>` +
|
|
16723
|
-
(guarded
|
|
17095
|
+
(stopElsewhere ? "" : guarded
|
|
16724
17096
|
? `<form method="post" action="${taskHref(taskId)}/stop" class="inline task-control-form task-stop-form"><input type="hidden" name="csrf" value="${escape(csrf)}"><input type="hidden" name="run" value="${control.run}">${back}<button type="submit" class="danger task-control-button">Stop</button></form>`
|
|
16725
17097
|
: `<button type="button" class="danger task-control-button" disabled>Stop</button>`) +
|
|
16726
17098
|
`</section>`);
|
|
@@ -16760,7 +17132,7 @@ function resumeCeremonyPage(chrome, data) {
|
|
|
16760
17132
|
`<p class="row">Continues saved work${control.committed ? " and its commit" : ""} under the current approved scope. The next attempt needs fresh evidence; the earlier handoff cannot count as a new result.</p>`,
|
|
16761
17133
|
`<p class="row">Resuming grants no new approval or publishing permission. Scope, budget, agents, risk, fallback, verification, and review limits still apply.</p>`,
|
|
16762
17134
|
`<details><summary>Stop record and saved work</summary><p>Run #${control.run} was stopped by <span class="mono">${escape(control.stop.requestedBy)}</span> at ${escape(stopWhen(control.stop.requestedAt))}${control.stop.settledAt === null ? "" : ` and settled ${escape(stopWhen(control.stop.settledAt))} (${escape(control.stop.settlement ?? "?")})`}.</p>${control.worktree === null ? "" : `<p class="mono">${escape(control.worktree)}</p>`}</details>`,
|
|
16763
|
-
data.approved ? "" : `<p class="row problem-words"><strong>
|
|
17135
|
+
data.approved ? "" : `<p class="row problem-words"><strong>The scope is not approved as it stands</strong> — resuming lifts the pause, but no worker spends until the scope is approved again</p>`,
|
|
16764
17136
|
data.gate === null ? "" : `<p class="row"><strong>Before work starts:</strong> ${escape(data.gate.summary)} — ${escape(data.gate.detail)}</p>`,
|
|
16765
17137
|
`</div>`,
|
|
16766
17138
|
`<form method="post" action="${taskHref(data.taskId)}/resume" class="card resume-form">`,
|
|
@@ -16836,12 +17208,23 @@ function taskChatApproval(focus, csrf) {
|
|
|
16836
17208
|
route: focus.route,
|
|
16837
17209
|
coordinator: approval.coordinator,
|
|
16838
17210
|
deliverable: approval.deliverable,
|
|
16839
|
-
|
|
17211
|
+
// Edit plan opens the task page's sheet with its fields open for editing.
|
|
17212
|
+
editHref: approval.raceTerms !== null ? `${taskHref(focus.id)}#${approval.planDocument === null ? "scope" : "plan-edit"}` : `${taskHref(focus.id)}?edit=plan#plan-editor`,
|
|
16840
17213
|
notNowHref: "/chat",
|
|
16841
17214
|
sticky: false,
|
|
17215
|
+
earlier: { active: focus.assignment?.earlierActive ?? 0, running: focus.assignment?.earlierRunning ?? 0 },
|
|
17216
|
+
edit: null,
|
|
16842
17217
|
}) +
|
|
16843
17218
|
`</section>`);
|
|
16844
17219
|
}
|
|
17220
|
+
/** The chat's status sentence, worded as the task page words it: what a failed attempt missed, or which step a live
|
|
17221
|
+
* build is on (and the step it is stuck on). Undefined keeps the shared status sentence. */
|
|
17222
|
+
function chatStatusSentence(focus) {
|
|
17223
|
+
if (focus.state === "failed" && focus.failure != null)
|
|
17224
|
+
return [focus.failure.line, focus.failure.evidence].filter(Boolean).join(" ");
|
|
17225
|
+
const steps = focus.liveRun === null ? null : buildProgressOf(focus.milestoneProgress);
|
|
17226
|
+
return steps === null ? undefined : steps.line;
|
|
17227
|
+
}
|
|
16845
17228
|
/** One live, server-derived journey from request to proof. The fragment is
|
|
16846
17229
|
* safe to refresh independently, so an in-progress message is never lost. */
|
|
16847
17230
|
function taskChatLiveRegion(focus, csrf, fragment = false, inert = false, approvalHref = taskChatHref(focus.id) + "#task-chat-action") {
|
|
@@ -16857,7 +17240,7 @@ function taskChatLiveRegion(focus, csrf, fragment = false, inert = false, approv
|
|
|
16857
17240
|
return (`<section id="task-chat-live" aria-live="polite" data-task="${escape(focus.id)}" data-execution="${escape(focus.executionId)}" data-source="/chat/task-status?task=${encodeURIComponent(focus.id)}" data-poll="${polling ? "1" : "0"}" data-approval="${escape(focus.approval?.digest ?? "")}" data-plan="${escape(focus.plan ?? "")}">` +
|
|
16858
17241
|
(focus.guide === true && focus.state !== "cancelled" ? firstTaskJourneyHtml(focus) : "") +
|
|
16859
17242
|
`<div class="task-live-summary">` +
|
|
16860
|
-
(focus.assignment !== null ? assignmentSummaryHtml(focus.assignment, { workStatus: focus.status, hideAction: focus.approval !== null && focus.dispatch?.action === "approve-scope", ...(receiptLeads ? { resultHref: chatResultHref(focus.id, focus.result.runId) } : {}) }) + (receiptLeads ? completionReceiptCard(focus.result, focus.id, "chat", focus.status, focus.assignment, false) : "") : receiptLeads ? completionReceiptCard(focus.result, focus.id, "chat", focus.status) : taskStatusCard(focus.status, focus.id, focus.dispatch, focus.liveRun?.id ?? null, focus.approval !== null && focus.dispatch?.action === "approve-scope")) +
|
|
17243
|
+
(focus.assignment !== null ? assignmentSummaryHtml(focus.assignment, { workStatus: focus.status, hideAction: focus.approval !== null && focus.dispatch?.action === "approve-scope", sentence: chatStatusSentence(focus), ...(receiptLeads ? { resultHref: chatResultHref(focus.id, focus.result.runId) } : {}) }) + (receiptLeads ? completionReceiptCard(focus.result, focus.id, "chat", focus.status, focus.assignment, false) : "") : receiptLeads ? completionReceiptCard(focus.result, focus.id, "chat", focus.status) : taskStatusCard(focus.status, focus.id, focus.dispatch, focus.liveRun?.id ?? null, focus.approval !== null && focus.dispatch?.action === "approve-scope")) +
|
|
16861
17244
|
checkProgressHtml(focus.checkProgress) +
|
|
16862
17245
|
// The exact-run control (v52): the SAME component the task page
|
|
16863
17246
|
// renders, refreshed with the live region — typed input in the
|
|
@@ -16968,7 +17351,7 @@ function chatFleetOverview(snapshot, projects, csrf, interactive) {
|
|
|
16968
17351
|
return (`<section class="card chat-overview" aria-label="live portfolio overview" data-card-kind="fleet-overview">` +
|
|
16969
17352
|
`<div class="chat-overview-head"><h2>${projects.length} project${projects.length === 1 ? "" : "s"}</h2>` +
|
|
16970
17353
|
(interactive
|
|
16971
|
-
? `<form method="post" action="/chat" class="inline"><input type="hidden" name="csrf" value="${escape(csrf)}"><button type="submit" name="message" value="${escape(briefing)}" class="quiet">
|
|
17354
|
+
? `<form method="post" action="/chat" class="inline"><input type="hidden" name="csrf" value="${escape(csrf)}"><button type="submit" name="message" value="${escape(briefing)}" class="quiet">Brief me</button></form>`
|
|
16972
17355
|
: `<a href="/board?scope=all" class="chat-overview-link">open board</a>`) +
|
|
16973
17356
|
`</div>` +
|
|
16974
17357
|
`<div class="chat-overview-stats">` +
|
|
@@ -17003,9 +17386,9 @@ function chatProjectRail(projects, csrf, inert) {
|
|
|
17003
17386
|
const ask = `Give me a concise status for ${one.id} (${one.label}) and recommend the next reversible action.`;
|
|
17004
17387
|
return (`<div class="chat-project-card">` +
|
|
17005
17388
|
`<div class="chat-project-name"><span class="mono">${escape(one.id)}</span><strong>${escape(one.label)}</strong>` +
|
|
17006
|
-
`<span class="badge">${escape(statusOf(peek))}</span></div>` +
|
|
17389
|
+
`<span class="badge">${escape(sentenceCase(statusOf(peek)))}</span></div>` +
|
|
17007
17390
|
(peek === null
|
|
17008
|
-
? `<p class="meta">
|
|
17391
|
+
? `<p class="meta">Pulse unavailable</p>`
|
|
17009
17392
|
: `<div class="chat-project-stats">` +
|
|
17010
17393
|
`<span${peek.waiting > 0 ? ' class="hot"' : ""}><b>${peek.waiting}</b> need you</span>` +
|
|
17011
17394
|
`<span><b>${peek.running}</b> live</span><span><b>${peek.queued}</b> queued</span>` +
|
|
@@ -17014,13 +17397,13 @@ function chatProjectRail(projects, csrf, inert) {
|
|
|
17014
17397
|
(inert
|
|
17015
17398
|
? ""
|
|
17016
17399
|
: `<form method="post" action="/chat" class="inline"><input type="hidden" name="csrf" value="${escape(csrf)}">` +
|
|
17017
|
-
`<button type="submit" name="message" value="${escape(ask)}" class="quiet" aria-label="ask about ${escape(one.label)}">
|
|
17400
|
+
`<button type="submit" name="message" value="${escape(ask)}" class="quiet" aria-label="ask about ${escape(one.label)}">Ask</button></form>`) +
|
|
17018
17401
|
`<form method="post" action="/projects/open" class="inline"><input type="hidden" name="csrf" value="${escape(csrf)}">` +
|
|
17019
17402
|
`<input type="hidden" name="path" value="${escape(one.path)}"><input type="hidden" name="return" value="/board">` +
|
|
17020
|
-
`<button type="submit" class="quiet" aria-label="open ${escape(one.label)} board">
|
|
17403
|
+
`<button type="submit" class="quiet" aria-label="open ${escape(one.label)} board">Board</button></form></div></div>`);
|
|
17021
17404
|
}).join("");
|
|
17022
17405
|
return (`<aside class="chat-projects" id="chat-project-panel" aria-label="projects in this conversation">` +
|
|
17023
|
-
`<div class="chat-projects-head"><h2>
|
|
17406
|
+
`<div class="chat-projects-head"><h2>Projects</h2><span class="badge">${projects.length}</span>` +
|
|
17024
17407
|
`<button type="button" class="chat-project-close quiet" aria-label="close projects">×</button></div>` +
|
|
17025
17408
|
`<div class="chat-project-list">${rows}</div></aside>`);
|
|
17026
17409
|
}
|
|
@@ -17033,9 +17416,9 @@ function matePromptStarters(csrf, focus = null) {
|
|
|
17033
17416
|
["new task", "Help me define a new task. Ask only about choices that materially change the result; otherwise use your judgment and sensible reversible defaults."],
|
|
17034
17417
|
]
|
|
17035
17418
|
: [
|
|
17036
|
-
["
|
|
17037
|
-
["
|
|
17038
|
-
["
|
|
17419
|
+
["What’s happening", "Read this task and explain its current status, what is blocking it, and what should happen next."],
|
|
17420
|
+
["Review results", "Review the evidence recorded for this task and tell me what is proven and what is still unverified."],
|
|
17421
|
+
["Adjust the plan", "Read this task and propose a tighter scope if that would improve the outcome. Explain why before I confirm anything."],
|
|
17039
17422
|
];
|
|
17040
17423
|
return `<div class="chat-prompts" aria-label="suggested questions">${prompts.map(([label, message]) => `<form method="post" action="/chat" class="inline"><input type="hidden" name="csrf" value="${escape(csrf)}">` +
|
|
17041
17424
|
(focus === null ? "" : `<input type="hidden" name="task" value="${escape(focus.id)}">`) +
|
|
@@ -17184,23 +17567,23 @@ function leadConfigForm(current, data) {
|
|
|
17184
17567
|
`<form method="post" action="/chat/config" class="card">`,
|
|
17185
17568
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
|
|
17186
17569
|
`<input type="hidden" name="return" value="${escape(data.returnTo)}">`,
|
|
17187
|
-
`<label>
|
|
17570
|
+
`<label>Provider<select name="provider">`,
|
|
17188
17571
|
`<option value="codex-subscription"${current?.provider === "codex-subscription" ? " selected" : ""}>Codex membership (logged-in CLI)</option>`,
|
|
17189
17572
|
`<option value="claude-subscription"${current?.provider === "claude-subscription" ? " selected" : ""}>Anthropic membership (logged-in CLI)</option>`,
|
|
17190
17573
|
`<option value="anthropic-api"${current?.provider === "anthropic-api" ? " selected" : ""}>anthropic-api (direct API)</option>`,
|
|
17191
17574
|
`<option value="openrouter-api"${current?.provider === "openrouter-api" ? " selected" : ""}>openrouter-api (direct API)</option>`,
|
|
17192
17575
|
`</select></label>`,
|
|
17193
|
-
`<label>
|
|
17576
|
+
`<label>Model <span class="meta">(use default for your membership's current model; direct API models need a pinned price)</span>` +
|
|
17194
17577
|
`<input name="model" list="chat-models" value="${escape(current?.model ?? "default")}"><datalist id="chat-models">` +
|
|
17195
17578
|
`${models.map(model => `<option value="${escape(model)}">${escape(labels.get(model) ?? "")}</option>`).join("")}</datalist></label>`,
|
|
17196
17579
|
data.openrouterModels === null
|
|
17197
|
-
? `<p class="meta">
|
|
17580
|
+
? `<p class="meta">With OPENROUTER_API_KEY in the serve environment, this list becomes OpenRouter's full live catalog — each model priced by the party that bills it</p>`
|
|
17198
17581
|
: `<p class="meta">${data.openrouterModels.length} models live from OpenRouter's catalog; saving pins today's price — re-save to re-pin</p>`,
|
|
17199
17582
|
currentSubscription
|
|
17200
|
-
? `<p class="meta"><strong>
|
|
17201
|
-
: `<label>
|
|
17583
|
+
? `<p class="meta"><strong>No dollar maximum.</strong> Membership chat uses the plan attached to the logged-in CLI; the conversation stays live until you end it, and the daily turn limit still applies.</p>`
|
|
17584
|
+
: `<label>Weekly ceiling <span class="meta">(direct API only; leave blank when choosing a membership)</span>` +
|
|
17202
17585
|
`<input type="text" name="weekly-usd" inputmode="decimal" style="width:8rem" value="${current === null ? "" : (current.weeklyCeilingMicrousd / 1_000_000).toFixed(2)}"></label>`,
|
|
17203
|
-
`<label>
|
|
17586
|
+
`<label>Daily turns <span class="meta">(default 50)</span>` +
|
|
17204
17587
|
`<input type="text" name="daily-turns" inputmode="numeric" style="width:8rem" value="${current === null ? "" : String(current.dailyTurns)}"></label>`,
|
|
17205
17588
|
currentSubscription
|
|
17206
17589
|
? `<p class="meta">Authenticate on this machine first with ${current?.provider === "codex-subscription" ? `<span class="mono">codex login</span>` : `the <span class="mono">claude</span> CLI`}. Toolroll reuses that cached login and never stores it.</p>`
|
|
@@ -17212,13 +17595,13 @@ function leadConfigForm(current, data) {
|
|
|
17212
17595
|
: `${escape(one.provider)}: stored ${escape(one.tail ?? "")}`)
|
|
17213
17596
|
.join(" · ")})</span>` +
|
|
17214
17597
|
`<input type="password" name="key" placeholder="direct API only — leave empty to keep" autocomplete="off"></label>`,
|
|
17215
|
-
`<label>
|
|
17598
|
+
`<label>Your password <span class="meta">(typed again to change the provider)</span>` +
|
|
17216
17599
|
`<input type="password" name="token" autocomplete="current-password"></label>`,
|
|
17217
17600
|
`<button type="submit">${current === null ? "turn chat on" : "save"}</button>`,
|
|
17218
17601
|
`</form>`,
|
|
17219
17602
|
currentSubscription
|
|
17220
17603
|
? `<p class="meta">The membership provider runs without repository tools in a temporary directory; Toolroll remains the only layer that can turn a proposed action into a confirmation card.</p>`
|
|
17221
|
-
: `<p class="meta">
|
|
17604
|
+
: `<p class="meta">A pasted key is written once to a mode-0600 file beside the database — never INTO the database, never shown again beyond its last characters; an environment variable (` +
|
|
17222
17605
|
`<span class="mono">ANTHROPIC_API_KEY</span> / <span class="mono">OPENROUTER_API_KEY</span>) always wins when set</p>`,
|
|
17223
17606
|
].join("\n");
|
|
17224
17607
|
}
|
|
@@ -17247,7 +17630,7 @@ function chatPage(chrome, data) {
|
|
|
17247
17630
|
? `<div class="card" id="latest" data-lead-off><p><strong>The lead is off.</strong></p><p class="meta">${data.canManage ? `Turn it on in${settingsLink}.` : "An approver can turn it on."}</p></div>`
|
|
17248
17631
|
: code === "unpriced" || code === "no-key"
|
|
17249
17632
|
? `<div class="card" id="latest"><p><strong>The lead can’t run yet.</strong></p><p class="meta">${escape(data.enabled.why)}${data.canManage ? ` ·${settingsLink}` : ""}</p></div>`
|
|
17250
|
-
: `<div class="card" id="latest"><p><strong>
|
|
17633
|
+
: `<div class="card" id="latest"><p><strong>Chat is off.</strong></p><p class="meta">${escape(data.enabled.why)}</p></div>`);
|
|
17251
17634
|
return screen("chat", chatWorkspace(parts.join("\n"), data.projects, data.csrf, true, data.focusTask, data.resultPanel ?? null), { chrome, functional: { script: CHAT_UI_SCRIPT + (data.focusTask === null ? "" : RESULT_REVIEW_SCRIPT), fetches: data.focusTask !== null } });
|
|
17252
17635
|
}
|
|
17253
17636
|
const config = data.enabled.config;
|
|
@@ -17263,7 +17646,7 @@ function chatPage(chrome, data) {
|
|
|
17263
17646
|
parts.push(data.coordinatorProposals);
|
|
17264
17647
|
parts.push(data.focusTask === null && !data.catchUp ? `<details class="chat-fleet-context">${chatOverviewSummaryHtml(data.projects, data.fleetSnapshot?.attentionCount)}${chatFleetOverview(data.fleetSnapshot, data.projects, data.csrf, false)}</details>` : "", chatLimitsHtml({ provider: config.provider, model: config.model, turnsToday: data.turnsToday, dailyTurns: config.dailyTurns, subscription, weekly: subscription ? null : { spent: data.weeklySpent, ceiling: config.weeklyCeilingMicrousd } }));
|
|
17265
17648
|
for (const turn of data.latched) {
|
|
17266
|
-
parts.push(`<div class="problem"><strong>
|
|
17649
|
+
parts.push(`<div class="problem"><strong>Unknown spend blocks chat.</strong> turn #${turn.id} may have cost up to ${chatMoney(turn.reservedMicrousd)} — ` +
|
|
17267
17650
|
`<a href="/chat/ack/${turn.id}">read and acknowledge it</a> to re-enable this credential.</div>`);
|
|
17268
17651
|
}
|
|
17269
17652
|
if (data.pending !== null) {
|
|
@@ -17278,7 +17661,7 @@ function chatPage(chrome, data) {
|
|
|
17278
17661
|
}
|
|
17279
17662
|
else if (last.reply !== null) {
|
|
17280
17663
|
parts.push(`<div class="card" id="latest">${renderChatText(last.reply)}` +
|
|
17281
|
-
(last.proposalsDiscarded ? `<p class="meta">
|
|
17664
|
+
(last.proposalsDiscarded ? `<p class="meta">A draft block in this answer was malformed and was discarded whole</p>` : "") +
|
|
17282
17665
|
`</div>`);
|
|
17283
17666
|
}
|
|
17284
17667
|
}
|
|
@@ -17286,21 +17669,21 @@ function chatPage(chrome, data) {
|
|
|
17286
17669
|
for (const one of candidates) {
|
|
17287
17670
|
const draft = one.draft;
|
|
17288
17671
|
parts.push(`<div class="card">` +
|
|
17289
|
-
`<p><strong>${draft.kind === "task" ? escape(draft.title) : escape(draft.name)}</strong> <span class="badge">
|
|
17290
|
-
`<p class="meta">
|
|
17672
|
+
`<p><strong>${draft.kind === "task" ? escape(draft.title) : escape(draft.name)}</strong> <span class="badge">Draft ${escape(draft.kind)}</span></p>` +
|
|
17673
|
+
`<p class="meta">Drafted by the model from fleet context — nothing is filed; drafts do not survive a restart</p>` +
|
|
17291
17674
|
`<p style="white-space:pre-wrap">${escape(draft.goal)}</p>` +
|
|
17292
|
-
(draft.outOfScope === null ? "" : `<p class="meta">
|
|
17293
|
-
(draft.touches.length > 0 ? `<p class="meta">
|
|
17294
|
-
(draft.kind === "routine" ? `<p class="meta">
|
|
17295
|
-
`<p class="meta">
|
|
17675
|
+
(draft.outOfScope === null ? "" : `<p class="meta">Not: ${escape(draft.outOfScope)}</p>`) +
|
|
17676
|
+
(draft.touches.length > 0 ? `<p class="meta">Touches: ${escape(draft.touches.join(", "))}</p>` : "") +
|
|
17677
|
+
(draft.kind === "routine" ? `<p class="meta">Schedule: ${escape(draft.schedule)}</p>` : "") +
|
|
17678
|
+
`<p class="meta">Repo: <span class="mono">${escape(projectName(one.repoPath))}</span></p>` +
|
|
17296
17679
|
`<form method="post" action="/chat/file/${escape(one.key)}" class="inline">` +
|
|
17297
17680
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
|
|
17298
17681
|
`<input type="password" name="token" placeholder="your password" autocomplete="current-password">` +
|
|
17299
|
-
`<button type="submit">
|
|
17682
|
+
`<button type="submit">File unapproved</button></form>` +
|
|
17300
17683
|
`</div>`);
|
|
17301
17684
|
}
|
|
17302
17685
|
if (!subscription && data.canManage && data.focusTask === null) {
|
|
17303
|
-
parts.push(`<h2>
|
|
17686
|
+
parts.push(`<h2>Ask</h2>`, `<form method="post" action="/chat" class="card">`, `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`, `<label>Message<textarea name="message" rows="3" maxlength="2000"></textarea></label>`, `<label>Your password <span class="meta">(every message — chat spends)</span><input type="password" name="token" autocomplete="current-password"></label>`, `<button type="submit">Ask</button>`, `</form>`);
|
|
17304
17687
|
}
|
|
17305
17688
|
if (data.canManage) {
|
|
17306
17689
|
parts.push(`<p class="meta" id="chat-settings"><a href="/settings/lead">Lead settings</a> · provider, model and limits</p>`);
|
|
@@ -17323,12 +17706,12 @@ function chatAckPage(chrome, turn, nonce, csrf) {
|
|
|
17323
17706
|
`<p>Turn <span class="mono">#${turn.id}</span> on <span class="mono">${escape(turn.provider)} · ${escape(turn.model)}</span> ` +
|
|
17324
17707
|
`may have started before it failed (${escape(turn.failureReason ?? "crashed")}), and its cost could not be measured.</p>`,
|
|
17325
17708
|
`<p><strong>Acknowledging charges the reserved worst case, ${chatMoney(turn.reservedMicrousd)}, to the ledger and re-enables chat on this credential.</strong></p>`,
|
|
17326
|
-
`<p class="meta">
|
|
17709
|
+
`<p class="meta">Check the provider's own usage dashboard if you want the exact figure first; the ledger keeps whichever is known.</p>`,
|
|
17327
17710
|
`<form method="post" action="/chat/ack/${turn.id}">`,
|
|
17328
17711
|
`<input type="hidden" name="csrf" value="${escape(csrf)}">`,
|
|
17329
17712
|
`<input type="hidden" name="nonce" value="${escape(nonce)}">`,
|
|
17330
|
-
`<label>
|
|
17331
|
-
`<button type="submit">
|
|
17713
|
+
`<label>Your password<input type="password" name="token" autocomplete="current-password"></label>`,
|
|
17714
|
+
`<button type="submit">Accept the charge — re-enable chat</button>`,
|
|
17332
17715
|
`</form>`,
|
|
17333
17716
|
`</div>`,
|
|
17334
17717
|
].join("\n"), { chrome });
|
|
@@ -17535,10 +17918,10 @@ function proposalCardParts(view, csrf, inert, decision, returnTo = null) {
|
|
|
17535
17918
|
const decisionId = typeof payload["decision"] === "number" ? payload["decision"] : 0;
|
|
17536
17919
|
const pick = text("option");
|
|
17537
17920
|
const options = decision === null
|
|
17538
|
-
? `<p class="meta">
|
|
17921
|
+
? `<p class="meta">The decision is gone</p>`
|
|
17539
17922
|
: `<ul class="answer-options">` +
|
|
17540
17923
|
decision.options
|
|
17541
|
-
.map(one => `<li${one.id === pick ? ' class="picked"' : ""}><strong>${escape(one.label)}</strong>${one.reversible ? "" : ' <span class="badge">
|
|
17924
|
+
.map(one => `<li${one.id === pick ? ' class="picked"' : ""}><strong>${escape(one.label)}</strong>${one.reversible ? "" : ' <span class="badge">Irreversible</span>'}` +
|
|
17542
17925
|
`${one.id === decision.recommendation ? ' <span class="meta">— the builder recommends this</span>' : ""}` +
|
|
17543
17926
|
`${one.id === pick ? ' <span class="meta">— proposed</span>' : ""}` +
|
|
17544
17927
|
`<p class="meta">${escape(one.consequence)}</p></li>`)
|
|
@@ -17568,13 +17951,13 @@ function proposalCardParts(view, csrf, inert, decision, returnTo = null) {
|
|
|
17568
17951
|
: view.kind === "control"
|
|
17569
17952
|
? (isChatControl(payload["control"]) ? `<a class="button-link" href="${escape(chatControlHref(payload["control"], task, payload["run"], payload["project"]))}">${escape(CHAT_CONTROLS[payload["control"]].label)}</a>` : `<p>Control unavailable.</p>`)
|
|
17570
17953
|
: view.kind === "cancel"
|
|
17571
|
-
? `<p class="meta">
|
|
17572
|
-
`<form method="post" action="${view.actionBase}/${view.id}/dismiss" class="inline"><input type="hidden" name="csrf" value="${escape(csrf)}">${returnField}<button type="submit" class="quiet">
|
|
17954
|
+
? `<p class="meta">Cancelling is armed on the task itself — <a href="${taskHref(task)}">open ${escape(task)}</a></p>` +
|
|
17955
|
+
`<form method="post" action="${view.actionBase}/${view.id}/dismiss" class="inline"><input type="hidden" name="csrf" value="${escape(csrf)}">${returnField}<button type="submit" class="quiet">Dismiss</button></form>`
|
|
17573
17956
|
: `<div class="acts">` +
|
|
17574
17957
|
`<form method="post" action="${view.actionBase}/${view.id}/confirm" class="inline"><input type="hidden" name="csrf" value="${escape(csrf)}">${returnField}` +
|
|
17575
17958
|
(irreversible ? `<label class="arm"><input type="checkbox" name="confirm" value="yes"> I understand this cannot be undone</label>` : "") +
|
|
17576
17959
|
`<button type="submit">${escape(presentation.action)}</button></form>` +
|
|
17577
|
-
`<form method="post" action="${view.actionBase}/${view.id}/dismiss" class="inline"><input type="hidden" name="csrf" value="${escape(csrf)}">${returnField}<button type="submit" class="quiet">
|
|
17960
|
+
`<form method="post" action="${view.actionBase}/${view.id}/dismiss" class="inline"><input type="hidden" name="csrf" value="${escape(csrf)}">${returnField}<button type="submit" class="quiet">Dismiss</button></form>` +
|
|
17578
17961
|
`</div>`;
|
|
17579
17962
|
}
|
|
17580
17963
|
else if (view.state === "pending") {
|
|
@@ -17605,7 +17988,7 @@ function proposalCardParts(view, csrf, inert, decision, returnTo = null) {
|
|
|
17605
17988
|
const html = (`<article class="card proposal proposal-${escape(view.kind)} ${escape(view.state)}" data-card-kind="${escape(view.kind)}">` +
|
|
17606
17989
|
`<header class="proposal-head"><span class="proposal-icon">${strokeIcon(presentation.icon)}</span>` +
|
|
17607
17990
|
`<span><strong>${escape(presentation.label)}</strong><small>proposed by ${provenance}</small></span>` +
|
|
17608
|
-
`<span class="badge ${stateClass}">${escape(view.state)}</span></header>` +
|
|
17991
|
+
`<span class="badge ${stateClass}">${escape(sentenceCase(view.state))}</span></header>` +
|
|
17609
17992
|
`<div class="proposal-body">${what}</div>` +
|
|
17610
17993
|
`<footer class="proposal-actions">${acts}</footer></article>`);
|
|
17611
17994
|
// The same card as data (chat cards): one act, in the door's own words.
|
|
@@ -17653,7 +18036,7 @@ function relativeAge(iso, now) {
|
|
|
17653
18036
|
function coordinatorProposalsSection(proposals, decisions, csrf, now, heading = true, returnTo = null) {
|
|
17654
18037
|
if (proposals.length === 0)
|
|
17655
18038
|
return "";
|
|
17656
|
-
return ((heading ? `<h2>
|
|
18039
|
+
return ((heading ? `<h2>Proposed by coordinators <span class="meta">${proposals.length}</span></h2>` : "") +
|
|
17657
18040
|
`<div class="coordinator-proposals">` +
|
|
17658
18041
|
proposals.map(one => coordinatorProposalCard(one, csrf, now, decisions.get(typeof one.payload["decision"] === "number" ? one.payload["decision"] : -1) ?? null, returnTo)).join("") +
|
|
17659
18042
|
`</div>`);
|
|
@@ -17777,10 +18160,10 @@ function matePage(chrome, data) {
|
|
|
17777
18160
|
`<span>this week ${chatMoney(data.weeklySpent)} of ${chatMoney(data.config.weeklyCeilingMicrousd)}</span>`) +
|
|
17778
18161
|
`</div>`,
|
|
17779
18162
|
`<p class="meta">Started ${escape(data.session.mintedAt.slice(0, 16).replace("T", " "))}Z. It stays open until you end it; only bounded recent context is sent to the model.</p>`,
|
|
17780
|
-
`<form method="post" action="/chat/mate/end" class="inline"><input type="hidden" name="csrf" value="${escape(data.csrf)}"><input type="hidden" name="return" value="${escape(returnTo)}"><button type="submit" class="quiet">
|
|
18163
|
+
`<form method="post" action="/chat/mate/end" class="inline"><input type="hidden" name="csrf" value="${escape(data.csrf)}"><input type="hidden" name="return" value="${escape(returnTo)}"><button type="submit" class="quiet">End the conversation and forget the thread</button></form>`,
|
|
17781
18164
|
data.recent.length === 0
|
|
17782
18165
|
? ""
|
|
17783
|
-
: `<p class="meta">
|
|
18166
|
+
: `<p class="meta">Recent turns: ${data.recent
|
|
17784
18167
|
.map(turn => `<span class="mono">#${turn.id}</span> ${escape(turn.state)}${turn.failureReason === null ? "" : ` · ${escape(turn.failureReason)}`} · ${subscription ? "membership" : chatMoney(turn.settledMicrousd ?? turn.reservedMicrousd)}`)
|
|
17785
18168
|
.join(" · ")}</p>`,
|
|
17786
18169
|
`<p class="meta"><a href="/settings/lead">Lead settings</a> · provider, model and limits</p>`,
|
|
@@ -17797,7 +18180,7 @@ function matePage(chrome, data) {
|
|
|
17797
18180
|
if (data.problem !== null)
|
|
17798
18181
|
conversation.push(`<div class="problem">${escape(data.problem)}</div>`);
|
|
17799
18182
|
for (const turn of data.latched) {
|
|
17800
|
-
conversation.push(`<div class="problem"><strong>
|
|
18183
|
+
conversation.push(`<div class="problem"><strong>Unknown spend blocks chat.</strong> turn #${turn.id} may have cost up to ${chatMoney(turn.reservedMicrousd)} — ` +
|
|
17801
18184
|
`<a href="/chat/ack/${turn.id}">read and acknowledge it</a> to re-enable this credential.</div>`);
|
|
17802
18185
|
}
|
|
17803
18186
|
const lastMessage = data.messages.at(-1);
|
|
@@ -17807,7 +18190,7 @@ function matePage(chrome, data) {
|
|
|
17807
18190
|
// The New update action (package 2): hidden until a live update lands
|
|
17808
18191
|
// while the reader is above the latest message; a real button, so the
|
|
17809
18192
|
// keyboard reaches it. Only this act moves the reader.
|
|
17810
|
-
`<div class="chat-new-update-holder"><button type="button" class="chat-new-update" id="chat-new-update" hidden>New update ↓</button></div>`, `<form method="post" action="/chat" class="card composer" id="${latestReply === null && data.pending === null ? "latest" : "chat-composer"}" aria-label="message the mate" data-chat-session="${data.session.id}" data-chat-task="${escape(data.focusTask?.id ?? "")}" data-chat-user="${escape(data.session.approver)}" data-chat-busy="${data.pending === null ? "0" : "1"}" data-chat-version="${mateChatVersion(data)}" data-chat-approval="${escape(data.focusTask?.approval?.digest ?? "")}">`, `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`, `<input type="hidden" name="request" value="${randomBytes(16).toString("hex")}"><input type="hidden" name="request-session" value="${data.session.id}">`, data.focusTask === null ? "" : `<input type="hidden" name="task" value="${escape(data.focusTask.id)}">`, chatProject === null ? "" : `<input type="hidden" name="project" value="${escape(chatProject)}">`, data.resultRunId == null ? "" : `<input type="hidden" name="result" value="${data.resultRunId}">`, `<label>
|
|
18193
|
+
`<div class="chat-new-update-holder"><button type="button" class="chat-new-update" id="chat-new-update" hidden>New update ↓</button></div>`, `<form method="post" action="/chat" class="card composer" id="${latestReply === null && data.pending === null ? "latest" : "chat-composer"}" aria-label="message the mate" data-chat-session="${data.session.id}" data-chat-task="${escape(data.focusTask?.id ?? "")}" data-chat-user="${escape(data.session.approver)}" data-chat-busy="${data.pending === null ? "0" : "1"}" data-chat-version="${mateChatVersion(data)}" data-chat-approval="${escape(data.focusTask?.approval?.digest ?? "")}">`, `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`, `<input type="hidden" name="request" value="${randomBytes(16).toString("hex")}"><input type="hidden" name="request-session" value="${data.session.id}">`, data.focusTask === null ? "" : `<input type="hidden" name="task" value="${escape(data.focusTask.id)}">`, chatProject === null ? "" : `<input type="hidden" name="project" value="${escape(chatProject)}">`, data.resultRunId == null ? "" : `<input type="hidden" name="result" value="${data.resultRunId}">`, `<label>Message<textarea name="message" rows="1" maxlength="${MATE_MESSAGE_MAX_CHARS}" placeholder="${data.focusTask !== null ? "Ask about this task…" : chatProject !== null ? `Ask about ${escape(projectName(chatProject))}…` : "Describe what you want done…"}"></textarea></label>`, `<button type="submit" aria-label="${data.pending === null ? "send message" : "wait for the current reply before sending"}"${data.pending === null ? "" : " disabled"}>Send</button>`, `</form>`,
|
|
17811
18194
|
// Concise pass (2026-09-13): the status line speaks only when there is
|
|
17812
18195
|
// a state to report — a reply in progress here, the connection from
|
|
17813
18196
|
// the continuity script — and the composer carries no second intro.
|
|
@@ -17839,25 +18222,25 @@ function routinesPage(chrome, tracks, form) {
|
|
|
17839
18222
|
const capture = chrome.project === null
|
|
17840
18223
|
? ""
|
|
17841
18224
|
: [
|
|
17842
|
-
`<h2>
|
|
18225
|
+
`<h2>File a standing order</h2>`,
|
|
17843
18226
|
form.problem === null ? "" : `<div class="problem">${escape(form.problem)}</div>`,
|
|
17844
18227
|
`<form method="post" action="/routines/add" class="card">`,
|
|
17845
18228
|
`<input type="hidden" name="csrf" value="${escape(form.csrf)}">`,
|
|
17846
18229
|
`<input type="hidden" name="projectRevision" value="${form.revision}">`,
|
|
17847
18230
|
fill === null
|
|
17848
|
-
? `<p class="meta">
|
|
18231
|
+
? `<p class="meta">Start from a template: ${TEMPLATES.filter(one => one.kind === "routine")
|
|
17849
18232
|
.map(one => `<a href="/routines?template=${escape(one.name)}">${escape(one.name)}</a>`)
|
|
17850
18233
|
.join(" · ")}</p>`
|
|
17851
18234
|
: `<p class="meta">${form.values === undefined ? "pre-filled from a template — edit anything" : "Your entries are kept below"}; nothing fires until you approve the standing order</p>`,
|
|
17852
|
-
`<label>
|
|
17853
|
-
`<label>
|
|
17854
|
-
`<label>
|
|
17855
|
-
`<label>
|
|
17856
|
-
`<label>
|
|
18235
|
+
`<label>Name <span class="meta">(lowercase-with-dashes — it names each instance)</span><input type="text" name="name" placeholder="nightly-deps" maxlength="41" value="${fill === null ? "" : escape(fill.name)}"></label>`,
|
|
18236
|
+
`<label>Goal <span class="meta">(what every firing is allowed to do)</span><textarea name="goal" rows="2">${fill === null ? "" : escape(fill.goal)}</textarea></label>`,
|
|
18237
|
+
`<label>Not this <span class="meta">(optional)</span><input type="text" name="not" value="${fill === null ? "" : escape(fill.not)}"></label>`,
|
|
18238
|
+
`<label>Touches <span class="meta">(paths, comma-separated, optional)</span><input type="text" name="touches" value="${fill === null ? "" : escape(fill.touches)}"></label>`,
|
|
18239
|
+
`<label>Acceptance <span class="meta">(required — one criterion per line: <code>statement | evidence,kinds | how</code>; evidence kinds are check, screenshot, changed-path, manual-review; id is optional and auto-numbered)</span><textarea name="acceptance" rows="3" placeholder="The full test suite passes | check">${fill === null ? "" : escape(fill.acceptance)}</textarea></label>`,
|
|
17857
18240
|
scheduleEditorHtml(fill?.schedule ?? null, form.values),
|
|
17858
|
-
`<label>
|
|
17859
|
-
`<button type="submit">
|
|
17860
|
-
`<p class="meta">
|
|
18241
|
+
`<label>Budget <span class="meta">(dollars per rolling 7 days, optional — needs a provider that reports cost)</span><input type="text" name="ceiling" inputmode="decimal" style="width:8rem" value="${escape(form.values?.get("ceiling") ?? "")}"></label>`,
|
|
18242
|
+
`<button type="submit">File it \u2192 approve the standing order next</button>`,
|
|
18243
|
+
`<p class="meta">Filing is cheap — nothing fires until you approve the template on the next screen, password and all</p>`,
|
|
17861
18244
|
`</form>`,
|
|
17862
18245
|
].join("\n");
|
|
17863
18246
|
const list = tracks.length === 0
|
|
@@ -17877,19 +18260,19 @@ function routineScreenPage(chrome, data) {
|
|
|
17877
18260
|
const schedule = parseSchedule(routine.schedule);
|
|
17878
18261
|
const scheduleSaid = schedule === null ? routine.schedule : describeSchedule(schedule);
|
|
17879
18262
|
const terms = `<div class="card">` +
|
|
17880
|
-
`<p class="meta">
|
|
17881
|
-
`<p class="meta">
|
|
17882
|
-
`<p class="meta">
|
|
18263
|
+
`<p class="meta">Goal</p><p class="recap" style="margin-top:0">${escape(routine.goal)}</p>` +
|
|
18264
|
+
`<p class="meta">Not this</p><p class="recap" style="margin-top:0">${routine.outOfScope === null ? "<em>no exclusions</em>" : escape(routine.outOfScope)}</p>` +
|
|
18265
|
+
`<p class="meta">Touches · ${routine.touches.length === 0 ? "anything" : routine.touches.map(one => escape(one)).join(", ")}</p>` +
|
|
17883
18266
|
(routine.acceptance.length === 0
|
|
17884
18267
|
? ""
|
|
17885
|
-
: `<p class="meta">
|
|
18268
|
+
: `<p class="meta">Acceptance</p><ul class="recap">${routine.acceptance
|
|
17886
18269
|
.map(c => `<li><code>${escape(c.id)}</code> ${escape(c.statement)} <span class="meta">[requires: ${c.evidence.map(escape).join(", ")}]</span></li>`)
|
|
17887
18270
|
.join("")}</ul>`) +
|
|
17888
|
-
`<p class="meta">
|
|
18271
|
+
`<p class="meta">Needs · ${routine.requirements.length === 0 ? "nothing beyond the repository" : routine.requirements.map(one => escape(one)).join(", ")}</p>` +
|
|
17889
18272
|
routineAgentsHtml(routine, approved) +
|
|
17890
|
-
`<p class="meta">
|
|
17891
|
-
`<p class="meta">
|
|
17892
|
-
`<p class="meta">
|
|
18273
|
+
`<p class="meta">Schedule · ${escape(scheduleSaid)}</p>` +
|
|
18274
|
+
`<p class="meta">Budget · ${routine.costCeilingUsd === null ? "no ceiling" : `$${routine.costCeilingUsd.toFixed(2)} per rolling 7 days`}</p>` +
|
|
18275
|
+
`<p class="meta">One at a time — a firing skips while the previous instance is unfinished</p>` +
|
|
17893
18276
|
`</div>`;
|
|
17894
18277
|
// THE CONSENT DOOR (v48): the password and the approve act exist only
|
|
17895
18278
|
// where an exact, readable route can be restated — never over agents
|
|
@@ -17921,13 +18304,13 @@ function routineScreenPage(chrome, data) {
|
|
|
17921
18304
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
|
|
17922
18305
|
`<input type="hidden" name="nonce" value="${escape(data.nonce)}">`,
|
|
17923
18306
|
`<input type="hidden" name="digest" value="${escape(routine.digest)}">`,
|
|
17924
|
-
`<p><strong>
|
|
18307
|
+
`<p><strong>Approve this standing order:</strong></p>`,
|
|
17925
18308
|
`<p class="recap">Each firing creates a task under exactly the terms above and BUILDS IT WITHOUT ASKING — ${escape(scheduleSaid)}, until you pause it. Questions and failures still reach you like any other work.</p>`,
|
|
17926
18309
|
// The agents the yes freezes (v48): the same concise block every
|
|
17927
18310
|
// approval shows, before the password.
|
|
17928
18311
|
routineAgentsHtml(routine, false, true),
|
|
17929
|
-
`<label>
|
|
17930
|
-
`<button type="submit">
|
|
18312
|
+
`<label>Your password, typed again — a signed-in session alone cannot agree to standing work<input type="password" name="token" autocomplete="current-password"></label>` +
|
|
18313
|
+
`<button type="submit">Approve this routine</button>`,
|
|
17931
18314
|
`</form>`,
|
|
17932
18315
|
].join("\n");
|
|
17933
18316
|
const verb = (name, label, danger = false) => `<form method="post" action="${routineHref(routine.id)}/${name}" class="inline">` +
|
|
@@ -17937,7 +18320,7 @@ function routineScreenPage(chrome, data) {
|
|
|
17937
18320
|
? `<form method="post" action="${routineHref(routine.id)}/run-now" class="inline">` +
|
|
17938
18321
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
|
|
17939
18322
|
`<input type="password" name="token" autocomplete="current-password" class="inline" placeholder="your password" aria-label="password for run now" style="width:11rem"> ` +
|
|
17940
|
-
`<button type="submit">
|
|
18323
|
+
`<button type="submit">Run now</button></form>`
|
|
17941
18324
|
: "";
|
|
17942
18325
|
const acts = `<div class="card">` +
|
|
17943
18326
|
(routine.paused ? verb("resume", "resume") : verb("pause", "pause")) +
|
|
@@ -17951,7 +18334,7 @@ function routineScreenPage(chrome, data) {
|
|
|
17951
18334
|
const said = fire.outcome === "fired"
|
|
17952
18335
|
? fire.instanceTaskId === null
|
|
17953
18336
|
? "fired"
|
|
17954
|
-
: `<a href="${taskHref(fire.instanceTaskId)}" class="mono">${escape(fire.instanceTaskId)}</a>${fire.instanceState === null ? "" : ` <span class="badge badge-${escape(fire.instanceState)}">${escape(fire.instanceState)}</span>`}`
|
|
18337
|
+
: `<a href="${taskHref(fire.instanceTaskId)}" class="mono">${escape(fire.instanceTaskId)}</a>${fire.instanceState === null ? "" : ` <span class="badge badge-${escape(fire.instanceState)}">${escape(sentenceCase(fire.instanceState))}</span>`}`
|
|
17955
18338
|
: `<span class="meta">skipped — ${escape(fire.reason ?? "")}</span>`;
|
|
17956
18339
|
const slot = fire.scheduledFor.replace(/^manual:/, "");
|
|
17957
18340
|
return `<p class="row">${said}<span class="right meta mono">${fire.reason === "manual" ? "run now · " : ""}${whenTime(slot)}</span></p>`;
|
|
@@ -17966,14 +18349,14 @@ function routineScreenPage(chrome, data) {
|
|
|
17966
18349
|
? `<div class="problem">stopped behind <a href="${taskHref(data.blocker.taskId)}" class="mono">${escape(data.blocker.taskId)}</a> (${escape(data.blocker.state)}) — the track resumes when it finishes or is cancelled</div>`
|
|
17967
18350
|
: "",
|
|
17968
18351
|
approved && !routine.paused && routine.nextFireAt !== null
|
|
17969
|
-
? `<p class="meta">
|
|
18352
|
+
? `<p class="meta">Next fire ${whenTime(routine.nextFireAt)}</p>`
|
|
17970
18353
|
: "",
|
|
17971
|
-
"<h2>
|
|
18354
|
+
"<h2>The standing order</h2>",
|
|
17972
18355
|
terms,
|
|
17973
18356
|
approveForm,
|
|
17974
|
-
"<h2>
|
|
18357
|
+
"<h2>Acts</h2>",
|
|
17975
18358
|
acts,
|
|
17976
|
-
"<h2>
|
|
18359
|
+
"<h2>Firings</h2>",
|
|
17977
18360
|
ledger,
|
|
17978
18361
|
].join("\n"), { chrome });
|
|
17979
18362
|
}
|
|
@@ -17992,21 +18375,21 @@ function homePage(chrome, data) {
|
|
|
17992
18375
|
.map(decision => `<a class="decide-card" href="/d/${decision.id}">` +
|
|
17993
18376
|
`<p class="q">${escape(decision.question)}</p>` +
|
|
17994
18377
|
`<span class="mono meta">${escape(decision.taskId)}</span>` +
|
|
17995
|
-
`${isOverdue(decision, data.now) ? ` <span class="badge badge-overdue">
|
|
18378
|
+
`${isOverdue(decision, data.now) ? ` <span class="badge badge-overdue">Overdue</span>` : ""}` +
|
|
17996
18379
|
`</a>`)
|
|
17997
18380
|
.join("\n");
|
|
17998
18381
|
const incidents = data.incidents.length === 0
|
|
17999
18382
|
? ""
|
|
18000
|
-
: `<h2>
|
|
18383
|
+
: `<h2>Incidents</h2><p class="hint">builds that stopped and need a person — resolve here, or open the task to retry it</p>` +
|
|
18001
18384
|
data.incidents
|
|
18002
18385
|
.map(one => `<p class="row"><a href="${taskHref(one.taskId)}">${escape(one.taskId)}</a> — ${escape(incidentWords(one.kind))}` +
|
|
18003
18386
|
`<span class="right"><form method="post" action="/i/${one.id}/resolve" class="inline">` +
|
|
18004
18387
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
|
|
18005
|
-
`<button type="submit">
|
|
18388
|
+
`<button type="submit">Resolve</button></form></span></p>`)
|
|
18006
18389
|
.join("\n");
|
|
18007
18390
|
const stranded = data.stranded.length === 0
|
|
18008
18391
|
? ""
|
|
18009
|
-
: `<h2>
|
|
18392
|
+
: `<h2>Tasks waiting on failed work</h2><p class="hint">open a task and choose whether to try the failed work again, wait for something else, or continue without it</p>` +
|
|
18010
18393
|
data.stranded
|
|
18011
18394
|
.map(one => `<p class="row"><a href="${taskHref(one.id)}">${escape(one.id)}</a> waits on ${one.blockedBy
|
|
18012
18395
|
.map(blocker => `<a href="${taskHref(blocker)}">${escape(blocker)}</a>`)
|
|
@@ -18016,7 +18399,7 @@ function homePage(chrome, data) {
|
|
|
18016
18399
|
? ""
|
|
18017
18400
|
: data.gaps.length === 0
|
|
18018
18401
|
? ""
|
|
18019
|
-
: `<h2>
|
|
18402
|
+
: `<h2>Missing requirements</h2><p class="hint">tools or credentials builds need — checked before any money is spent</p>` +
|
|
18020
18403
|
data.gaps
|
|
18021
18404
|
.map(gap => `<p class="row"><a href="/caps">${escape(gap.key)}</a> — ${escape(gap.state)}<span class="right meta">how to fix →</span></p>`)
|
|
18022
18405
|
.join("\n") +
|
|
@@ -18028,7 +18411,7 @@ function homePage(chrome, data) {
|
|
|
18028
18411
|
// an operator actually watches this page, so it re-renders itself.
|
|
18029
18412
|
const building = data.building.length === 0
|
|
18030
18413
|
? ""
|
|
18031
|
-
: `<h2>
|
|
18414
|
+
: `<h2>Building now</h2><p class="hint">live builds — this page refreshes itself every 10 seconds while anything runs</p><div class="cards">` +
|
|
18032
18415
|
data.building
|
|
18033
18416
|
.map(claim => `<a class="stat-card" href="${taskHref(claim.taskId)}" style="text-decoration:none">` +
|
|
18034
18417
|
`<span class="k"><span class="dot dot-ok pulse"></span>${escape(claim.taskId)}</span>` +
|
|
@@ -18064,8 +18447,8 @@ function homePage(chrome, data) {
|
|
|
18064
18447
|
: `last window: ${data.episode.built} built, ${data.episode.broke} broke \u00b7 ended ${whenTime(data.episode.endedAt)}`}</span></div>`;
|
|
18065
18448
|
const fleetCards = [...runnerCards, watchCard, ...worktreeCards].filter(one => one !== "");
|
|
18066
18449
|
const fleet = fleetCards.length === 0
|
|
18067
|
-
? `<h2>
|
|
18068
|
-
: `<h2>
|
|
18450
|
+
? `<h2>System status</h2><p class="hint">No builder is connected yet. On the machine where the project lives, open that folder and run <code>toolroll up</code>.</p>`
|
|
18451
|
+
: `<h2>System status</h2><p class="hint">builders execute tasks in isolated temporary copies of each project</p><div class="cards">${fleetCards.join("")}</div>`;
|
|
18069
18452
|
const startHere = data.taskCount === 0
|
|
18070
18453
|
? [
|
|
18071
18454
|
`<div class="card">`,
|
|
@@ -18085,10 +18468,10 @@ function homePage(chrome, data) {
|
|
|
18085
18468
|
: `<p class="meta"><strong>${escape(projectName(data.repo))}</strong> — the last 24 hours, honestly labeled: a rolling window, whatever your hours are</p>`,
|
|
18086
18469
|
startHere,
|
|
18087
18470
|
ledger,
|
|
18088
|
-
`<p class="meta">
|
|
18089
|
-
data.outboxPending > 0 ? `<p class="meta">
|
|
18471
|
+
`<p class="meta">Spend: ${escape(consoleSpend(summary))}</p>`,
|
|
18472
|
+
data.outboxPending > 0 ? `<p class="meta">Notifications: ${data.outboxPending} pending delivery</p>` : "",
|
|
18090
18473
|
building,
|
|
18091
|
-
`<h2>
|
|
18474
|
+
`<h2>Needs your decision</h2><p class="hint">an agent stopped mid-build to ask — nothing proceeds until you answer</p>`,
|
|
18092
18475
|
decide,
|
|
18093
18476
|
incidents,
|
|
18094
18477
|
stranded,
|
|
@@ -18157,21 +18540,21 @@ function taskComposerHtml(data) {
|
|
|
18157
18540
|
`<details class="task-options"${prefill === null ? "" : " open"}>`,
|
|
18158
18541
|
`<summary><span>Edit details</span><small>optional · defaults are remembered</small></summary>`,
|
|
18159
18542
|
`<div class="task-options-grid">`,
|
|
18160
|
-
`<label class="wide">
|
|
18543
|
+
`<label class="wide">Goal <span class="meta">— provide upfront for automatic approval of an unchanged plan</span>` +
|
|
18161
18544
|
`<textarea name="goal" rows="3" placeholder="What success looks like">${prefill === null ? "" : escape(prefill.goal)}</textarea></label>`,
|
|
18162
|
-
`<label class="wide">
|
|
18545
|
+
`<label class="wide">Acceptance <span class="meta">— required with a goal; one per line: <code>statement | evidence,kinds | how</code></span>` +
|
|
18163
18546
|
`<textarea name="acceptance" rows="3" placeholder="Requests over the limit return 429 | check">${prefill === null ? "" : escape(prefill.acceptance)}</textarea></label>`,
|
|
18164
|
-
`<label>
|
|
18165
|
-
`<label>
|
|
18547
|
+
`<label>Not this <span class="meta">— optional boundary</span><input type="text" name="not" value="${prefill === null ? "" : escape(prefill.not)}"></label>`,
|
|
18548
|
+
`<label>Likely touches <span class="meta">— paths, comma-separated</span><input type="text" name="touches" value="${prefill === null ? "" : escape(prefill.touches)}"></label>`,
|
|
18166
18549
|
`<label class="wide task-check"><input type="checkbox" name="plan-first" value="1"${values === undefined || values.get("plan-first") === "1" ? " checked" : ""}><span><strong>Let the planner inspect first</strong><small class="meta">Recommended. It drafts the goal, acceptance criteria, and implementation approach, and asks only when a missing answer materially changes the work.</small></span></label>`,
|
|
18167
18550
|
`<label class="wide task-check"><input type="checkbox" name="scout" value="1"${values?.get("scout") === "1" ? " checked" : ""}><span><strong>Research only</strong><small class="meta">Deliver a read-only report instead of changing the repository.</small></span></label>`,
|
|
18168
|
-
`<label>
|
|
18551
|
+
`<label>Task id <span class="meta">— optional</span><input type="text" name="id" value="${escape(values?.get("id") ?? "")}" placeholder="made from the request"></label>`,
|
|
18169
18552
|
candidates.length === 0
|
|
18170
18553
|
? ""
|
|
18171
|
-
: `<label>
|
|
18554
|
+
: `<label>Starts after <span class="meta">— optional</span><select name="after"><option value="">right away</option>` +
|
|
18172
18555
|
candidates.map(one => `<option value="${escape(one.id)}"${one.id === after ? " selected" : ""}>${escape(one.id)} — ${escape(one.title)}</option>`).join("") +
|
|
18173
18556
|
`</select></label>`,
|
|
18174
|
-
`<fieldset class="permission-field"><legend>
|
|
18557
|
+
`<fieldset class="permission-field"><legend>Agent permissions</legend>${permissionModeChoices("permission-mode", values?.get("permission-mode") === "bypassPermissions" ? "bypassPermissions" : values?.get("permission-mode") === "auto" ? "auto" : data.permissionDefault)}` +
|
|
18175
18558
|
`<p class="meta permission-note">Inherited from Settings. You can still change it on the proposed scope before approval.</p></fieldset>`,
|
|
18176
18559
|
`</div>`,
|
|
18177
18560
|
`</details>`,
|
|
@@ -18267,9 +18650,9 @@ function workPage(chrome, data) {
|
|
|
18267
18650
|
project: data.multiProject || row.repo === null ? (row.repo === null ? 'Unplaced' : projectName(row.repo)) : null,
|
|
18268
18651
|
age: relativeAge(row.updatedAt, data.now),
|
|
18269
18652
|
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'),
|
|
18653
|
+
ask: row.ask, chip: row.chip, group: row.ask ?? (row.status.rank === 1 ? 'building' : 'rest'),
|
|
18271
18654
|
action: actionHref === null || row.primaryAction === null ? null : { label: row.primaryAction.label, href: actionHref },
|
|
18272
|
-
detail: needsYouDetail ? row.status.detail : null,
|
|
18655
|
+
detail: needsYouDetail ? row.status.detail : row.progress ?? null,
|
|
18273
18656
|
problem: row.familyProblem,
|
|
18274
18657
|
notes: (row.status.diagnostics ?? []).map(one => `${one.label} · ${one.detail}`),
|
|
18275
18658
|
};
|
|
@@ -18297,12 +18680,12 @@ function tasksPage(chrome, tasks, state, csrf, problem, repo = null, prefill = n
|
|
|
18297
18680
|
.join("\n");
|
|
18298
18681
|
return screen("tasks", [
|
|
18299
18682
|
"<h1>Tasks</h1>",
|
|
18300
|
-
`<p class="meta">
|
|
18683
|
+
`<p class="meta">Work you want done${repo === null ? "" : ` in <strong>${escape(projectName(repo))}</strong>`} \u2014 a task builds unattended only after its scope is approved; open one to write or approve its scope</p>`,
|
|
18301
18684
|
repo === null ? "" : `<p class="meta path-words"><span class="mono">${escape(repo)}</span></p>`,
|
|
18302
18685
|
problem === null ? "" : `<div class="problem">${escape(problem)}</div>`,
|
|
18303
|
-
`<p class="meta">
|
|
18686
|
+
`<p class="meta">Filter: <a href="/tasks">all</a> · ${filters}</p>`,
|
|
18304
18687
|
rows,
|
|
18305
|
-
`<h2>
|
|
18688
|
+
`<h2>Add a task</h2>`,
|
|
18306
18689
|
taskComposerHtml({ csrf, project: repo, prefill, permissionDefault, qualityDefault }),
|
|
18307
18690
|
].join("\n"), { chrome });
|
|
18308
18691
|
}
|
|
@@ -18312,21 +18695,21 @@ function browsePage(chrome, data) {
|
|
|
18312
18695
|
`<form method="post" action="/projects/open" class="inline">`,
|
|
18313
18696
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
|
|
18314
18697
|
`<input type="hidden" name="path" value="${escape(path)}">`,
|
|
18315
|
-
`<button type="submit">
|
|
18698
|
+
`<button type="submit">Open</button>`,
|
|
18316
18699
|
`</form>`,
|
|
18317
18700
|
].join("");
|
|
18318
18701
|
return screen("projects", [
|
|
18319
18702
|
`<h1>Choose a folder</h1>`,
|
|
18320
18703
|
`<p class="meta">git repositories float to the top and can be opened; anything else can be entered — only folders under ${data.roots.length === 1 ? `<span class="mono">${escape(projectName(data.root))}</span>` : "the configured roots"} are visible here</p>`,
|
|
18321
18704
|
data.roots.length > 1
|
|
18322
|
-
? `<p class="meta">
|
|
18705
|
+
? `<p class="meta">Roots: ${data.roots.map(one => `<a href="/projects/browse?at=${encodeURIComponent(one)}" class="mono">${escape(projectName(one))}</a>`).join(" · ")}</p>`
|
|
18323
18706
|
: "",
|
|
18324
18707
|
`<p class="mono meta">${escape(crumb)}</p>`,
|
|
18325
18708
|
data.parent === null
|
|
18326
18709
|
? ""
|
|
18327
18710
|
: `<p class="row"><a href="/projects/browse?at=${encodeURIComponent(data.parent)}">\u2190 up one level</a></p>`,
|
|
18328
18711
|
data.entries.length === 0
|
|
18329
|
-
? `<p class="meta">
|
|
18712
|
+
? `<p class="meta">No folders here</p>`
|
|
18330
18713
|
: data.entries
|
|
18331
18714
|
.map(one => `<p class="row">` +
|
|
18332
18715
|
`<a href="/projects/browse?at=${encodeURIComponent(one.path)}"><strong>${escape(one.name)}</strong></a>` +
|
|
@@ -18344,7 +18727,7 @@ function browsePage(chrome, data) {
|
|
|
18344
18727
|
* switching context must never depend on remembering which repo is open.
|
|
18345
18728
|
*/
|
|
18346
18729
|
function workbenchRail(data) {
|
|
18347
|
-
const workspace = (repo) => `<span class="badge">${repo === null ? "
|
|
18730
|
+
const workspace = (repo) => `<span class="badge">${repo === null ? "Unplaced" : escape(projectName(repo))}</span>`;
|
|
18348
18731
|
const row = (card, reason) => `<a class="wb-row${card.taskId === data.selected ? " wb-selected" : ""}" href="/workbench?t=${encodeURIComponent(card.taskId)}"` +
|
|
18349
18732
|
`${card.taskId === data.selected ? ` aria-current="true"` : ""}>` +
|
|
18350
18733
|
`<span class="wb-title">${escape(card.title)}</span>` +
|
|
@@ -18354,7 +18737,7 @@ function workbenchRail(data) {
|
|
|
18354
18737
|
(cards.length === 0 ? `<p class="meta">${empty}</p>` : cards.slice(0, 100).map(render).join("\n")) +
|
|
18355
18738
|
`</section>`;
|
|
18356
18739
|
const parts = [];
|
|
18357
|
-
parts.push(`<div class="wb-rail-head"><div><span class="eyebrow">portfolio</span><h2>
|
|
18740
|
+
parts.push(`<div class="wb-rail-head"><div><span class="eyebrow">portfolio</span><h2>All projects</h2></div>` +
|
|
18358
18741
|
`<a href="/projects">manage →</a></div>`);
|
|
18359
18742
|
parts.push(group("needs you", data.attention, "Nothing needs your input.", card => row(card, escape(card.reason))));
|
|
18360
18743
|
parts.push(group("in progress", data.building, "No agent is working right now.", card => {
|
|
@@ -18366,23 +18749,23 @@ function workbenchRail(data) {
|
|
|
18366
18749
|
parts.push(group("blocked & waiting", data.waiting, "Nothing is blocked or paused.", card => row(card, escape(card.reason))));
|
|
18367
18750
|
parts.push(group("up next", data.queued, "The ready queue is empty.", card => row(card, `${escape(card.reason)}${card.assignedRunner === null ? "" : ` · reserved for ${escape(card.assignedRunner)}`}`)));
|
|
18368
18751
|
if (data.done.length > 0) {
|
|
18369
|
-
parts.push(`<section class="wb-group"><h2>
|
|
18752
|
+
parts.push(`<section class="wb-group"><h2>Just finished <span class="lane-count">${data.done.length}</span></h2>`);
|
|
18370
18753
|
parts.push(data.done
|
|
18371
18754
|
.map(one => `<a class="wb-row${one.taskId === data.selected ? " wb-selected" : ""}" href="/workbench?t=${encodeURIComponent(one.taskId)}">` +
|
|
18372
18755
|
`<span class="wb-title">${escape(one.title)}</span>` +
|
|
18373
18756
|
`<span class="wb-meta"><span class="mono meta">${escape(one.taskId)}</span>${workspace(one.repo)} ` +
|
|
18374
|
-
`<span class="badge badge-${one.outcome === "built" || one.outcome === "no-change" ? "done" : "failed"}">${escape(one.outcome ?? "?")}</span></span></a>`)
|
|
18757
|
+
`<span class="badge badge-${one.outcome === "built" || one.outcome === "no-change" ? "done" : "failed"}">${escape(sentenceCase(one.outcome ?? "?"))}</span></span></a>`)
|
|
18375
18758
|
.join("\n"));
|
|
18376
18759
|
parts.push(`</section>`);
|
|
18377
18760
|
}
|
|
18378
18761
|
if (data.saturated)
|
|
18379
|
-
parts.push(`<p class="meta">
|
|
18762
|
+
parts.push(`<p class="meta">More exists — this rail is capped; the <a href="/board?scope=all">board</a> holds the rest</p>`);
|
|
18380
18763
|
return parts.join("\n");
|
|
18381
18764
|
}
|
|
18382
18765
|
/** The project chip every all-scope row wears: null is UNPLACED, said so. */
|
|
18383
18766
|
function projectChip(repo) {
|
|
18384
18767
|
return repo === null || repo === undefined
|
|
18385
|
-
? ` <span class="badge">
|
|
18768
|
+
? ` <span class="badge">Unplaced</span>`
|
|
18386
18769
|
: ` <span class="badge">${escape(projectName(repo))}</span>`;
|
|
18387
18770
|
}
|
|
18388
18771
|
/**
|
|
@@ -18400,11 +18783,11 @@ function decisionAnswerCard(decision, csrf, now, chip, returnTo = null) {
|
|
|
18400
18783
|
const options = decision.options
|
|
18401
18784
|
.map(option => {
|
|
18402
18785
|
const recommended = option.id === decision.recommendation
|
|
18403
|
-
? ` <span class="badge">
|
|
18786
|
+
? ` <span class="badge">Recommended</span>`
|
|
18404
18787
|
: "";
|
|
18405
18788
|
if (!option.reversible) {
|
|
18406
18789
|
return (`<p class="decide-option"><a href="/d/${decision.id}${returnTo === null ? "" : `?return=${encodeURIComponent(returnTo)}`}">${escape(option.label)}</a>` +
|
|
18407
|
-
` <span class="badge badge-overdue">
|
|
18790
|
+
` <span class="badge badge-overdue">Irreversible</span>${recommended}` +
|
|
18408
18791
|
` <span class="meta">${escape(option.consequence)}</span></p>`);
|
|
18409
18792
|
}
|
|
18410
18793
|
return (`<form class="decide-option decide-inline" method="post" action="/d/${decision.id}/answer">` +
|
|
@@ -18419,7 +18802,7 @@ function decisionAnswerCard(decision, csrf, now, chip, returnTo = null) {
|
|
|
18419
18802
|
`<p class="q">${escape(decision.question)}</p>` +
|
|
18420
18803
|
`<details class="decision-context"><summary>Context</summary><p class="meta">${escape(decision.recap)}</p><span class="meta mono">${escape(decision.taskId)}</span></details>` +
|
|
18421
18804
|
`<p class="meta">${chip ? projectChip(decision.repo) : ""}` +
|
|
18422
|
-
`${isOverdue(decision, now) ? ` <span class="badge badge-overdue">
|
|
18805
|
+
`${isOverdue(decision, now) ? ` <span class="badge badge-overdue">Overdue</span>` : ""}` +
|
|
18423
18806
|
` <a href="/d/${decision.id}${returnTo === null ? "" : `?return=${encodeURIComponent(returnTo)}`}">View details →</a></p>` +
|
|
18424
18807
|
`<div class="decide-options">${options}</div></div>`);
|
|
18425
18808
|
}
|
|
@@ -18500,10 +18883,10 @@ function portfolioOverview(data) {
|
|
|
18500
18883
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
|
|
18501
18884
|
`<input type="hidden" name="path" value="${escape(one.repo)}">` +
|
|
18502
18885
|
`<input type="hidden" name="return" value="/board">` +
|
|
18503
|
-
`<button type="submit">
|
|
18886
|
+
`<button type="submit">Board →</button></form>`;
|
|
18504
18887
|
return (`<div class="workspace-card${one.attention > 0 ? " hot" : ""}">` +
|
|
18505
18888
|
`<div class="workspace-head"><span class="workspace-name">${escape(workspace(one.repo))}</span>` +
|
|
18506
|
-
`<span class="badge ${status.cls}">${status.word}</span>${boardForm}${one.repo === null || data.csrf === "" ? "" : `<form method="post" action="/projects/open" class="inline">${hiddenFields({ csrf: data.csrf, path: one.repo, return: "/control" })}<button>
|
|
18889
|
+
`<span class="badge ${status.cls}">${sentenceCase(status.word)}</span>${boardForm}${one.repo === null || data.csrf === "" ? "" : `<form method="post" action="/projects/open" class="inline">${hiddenFields({ csrf: data.csrf, path: one.repo, return: "/control" })}<button>Set up →</button></form>`}</div>` +
|
|
18507
18890
|
`<div class="workspace-stats">` +
|
|
18508
18891
|
`<span class="pulse-stat${one.attention > 0 ? " hot" : ""}"><b>${one.attention}</b> need you</span>` +
|
|
18509
18892
|
`<span class="pulse-stat"><b>${one.building}</b> live</span>` +
|
|
@@ -18555,7 +18938,7 @@ function portfolioOverview(data) {
|
|
|
18555
18938
|
// ---- running: current live claims, project chips on ----
|
|
18556
18939
|
const liveRows = data.live
|
|
18557
18940
|
.map(one => `<p class="row"><a href="${taskHref(one.taskId)}">${escape(one.taskId)}</a>${projectChip(one.repo)} ` +
|
|
18558
|
-
`<span class="badge badge-running">
|
|
18941
|
+
`<span class="badge badge-running">Running</span> ` +
|
|
18559
18942
|
`<span class="mono meta">${escape(one.runner)}${one.model === null ? "" : ` · ${escape(one.model)}`} · ` +
|
|
18560
18943
|
`<time data-elapsed-since="${escape(one.claimedAt)}"></time></span></p>`)
|
|
18561
18944
|
.join("\n");
|
|
@@ -18563,8 +18946,8 @@ function portfolioOverview(data) {
|
|
|
18563
18946
|
const chipClass = (outcome) => outcome === "built" || outcome === "no-change" ? "badge-done" : outcome === "failed" ? "badge-failed" : "";
|
|
18564
18947
|
const ledgerRows = data.ledger
|
|
18565
18948
|
.map(one => `<p class="row"><a href="/r/${one.runId}">${escape(one.title)}</a>${projectChip(one.repo)} ` +
|
|
18566
|
-
`<span class="badge ${chipClass(one.outcome)}">${escape(one.outcome)}</span> ` +
|
|
18567
|
-
`${one.role === "scout" ? `<a class="badge" href="${taskHref(one.taskId)}#report">
|
|
18949
|
+
`<span class="badge ${chipClass(one.outcome)}">${escape(sentenceCase(one.outcome))}</span> ` +
|
|
18950
|
+
`${one.role === "scout" ? `<a class="badge" href="${taskHref(one.taskId)}#report">Report</a> ` : ""}` +
|
|
18568
18951
|
`<span class="mono meta">${one.provider === null ? "" : escape(one.provider)}${one.model === null ? "" : ` · ${escape(one.model)}`}` +
|
|
18569
18952
|
`${one.ranMinutes === null ? "" : ` · ${one.ranMinutes}m`}` +
|
|
18570
18953
|
` · ${escape(runCostWords({ authMode: one.authMode, costUsd: one.costUsd, tokensIn: null, tokensOut: null }))}` +
|
|
@@ -18578,11 +18961,11 @@ function portfolioOverview(data) {
|
|
|
18578
18961
|
})()}</span></p>`)
|
|
18579
18962
|
.join("\n");
|
|
18580
18963
|
return [
|
|
18581
|
-
`<div class="control-room-head"><div><h1>
|
|
18582
|
-
`<p class="meta">
|
|
18583
|
-
`<div class="actions"><a class="badge" href="/tasks/new">+
|
|
18964
|
+
`<div class="control-room-head"><div><h1>Portfolio</h1>` +
|
|
18965
|
+
`<p class="meta">Every project and live build in one place</p></div>` +
|
|
18966
|
+
`<div class="actions"><a class="badge" href="/tasks/new">+ New task</a><a class="badge" href="/board?scope=all">Full board →</a></div></div>`,
|
|
18584
18967
|
data.saturated ? `<div class="problem">This overview reached its 200-task display cap; the task list holds the rest.</div>` : "",
|
|
18585
|
-
`<h2>
|
|
18968
|
+
`<h2>Waits on you</h2>`,
|
|
18586
18969
|
waitCount === 0
|
|
18587
18970
|
? `<div class="answered"><strong>Nothing needs you.</strong> <span class="meta">You can leave this open; live state updates in the rail.</span></div>`
|
|
18588
18971
|
: [
|
|
@@ -18594,17 +18977,17 @@ function portfolioOverview(data) {
|
|
|
18594
18977
|
`<p class="meta"><a href="/next">clear the queue → one thing at a time</a></p>`,
|
|
18595
18978
|
].filter(part => part !== "").join("\n"),
|
|
18596
18979
|
data.gapsProject === null
|
|
18597
|
-
? `<p class="meta">
|
|
18980
|
+
? `<p class="meta">Requirement gaps are checked one project at a time — open a project to see and fill its gaps · <a href="/projects">open a project</a></p>`
|
|
18598
18981
|
: "",
|
|
18599
|
-
`<h2>
|
|
18982
|
+
`<h2>Project pulse</h2>`,
|
|
18600
18983
|
`<p class="hint">one row per repository</p>`,
|
|
18601
18984
|
pulseRows.length === 0
|
|
18602
18985
|
? `<div class="card"><p><strong>No active work yet.</strong></p><p class="meta">Queue a task and its progress will show here across every workspace.</p></div>`
|
|
18603
18986
|
: `<div class="workspace-pulse">${workspaceRows}</div>`,
|
|
18604
|
-
`<h2>
|
|
18987
|
+
`<h2>The last 24 hours</h2>`,
|
|
18605
18988
|
`<p class="hint">runs started in the last 24 hours</p>`,
|
|
18606
18989
|
data.runs24.length === 0
|
|
18607
|
-
? `<p class="meta">
|
|
18990
|
+
? `<p class="meta">No runs started in the window</p>`
|
|
18608
18991
|
: `<p class="row"><span class="meta">runs started</span> <span class="mono">${data.runs24.length}</span></p>` +
|
|
18609
18992
|
`<p class="row"><span class="meta">outcomes</span> <span class="mono">${escape(outcomeWords)}</span></p>` +
|
|
18610
18993
|
`<p class="row"><span class="meta">spend</span> <span class="mono">${escape(spendLine(summary))}</span></p>` +
|
|
@@ -18614,10 +18997,10 @@ function portfolioOverview(data) {
|
|
|
18614
18997
|
(summary.tokens > 0
|
|
18615
18998
|
? `<p class="row"><span class="meta">tokens</span> <span class="mono">${summary.tokens.toLocaleString()}</span></p>`
|
|
18616
18999
|
: ""),
|
|
18617
|
-
`<h2>
|
|
18618
|
-
data.live.length === 0 ? `<p class="meta">
|
|
18619
|
-
`<h2>
|
|
18620
|
-
data.ledger.length === 0 ? `<p class="meta">
|
|
19000
|
+
`<h2>Running</h2>`,
|
|
19001
|
+
data.live.length === 0 ? `<p class="meta">No agent is working right now</p>` : liveRows,
|
|
19002
|
+
`<h2>Terminal runs started in the last 24 hours</h2>`,
|
|
19003
|
+
data.ledger.length === 0 ? `<p class="meta">None yet</p>` : ledgerRows,
|
|
18621
19004
|
].join("\n");
|
|
18622
19005
|
}
|
|
18623
19006
|
/** Dollars for a screen: micro-USD stated as money, unknowables in words. */
|
|
@@ -18713,7 +19096,7 @@ function contestPage(chrome, data) {
|
|
|
18713
19096
|
const glance = summaries.length < 2
|
|
18714
19097
|
? ""
|
|
18715
19098
|
: `<div class="scroll-x"><table class="contest-glance">` +
|
|
18716
|
-
`<tr><td></td>${summaries.map(one => `<th>
|
|
19099
|
+
`<tr><td></td>${summaries.map(one => `<th>Agent ${one.contestant.ordinal}<span class="meta"> · ${escape(one.contestant.provider)} · ${escape(one.contestant.model)}</span>${one.winner ? ` <span class="badge badge-done">Picked</span>` : ""}</th>`).join("")}</tr>` +
|
|
18717
19100
|
`<tr><td class="meta">outcome</td>${summaries.map(one => `<td>${escape(one.outcome)}</td>`).join("")}</tr>` +
|
|
18718
19101
|
`<tr><td class="meta">changed</td>${summaries.map(one => `<td>${escape(one.diffWords)}</td>`).join("")}</tr>` +
|
|
18719
19102
|
`<tr><td class="meta">time</td>${summaries.map(one => `<td>${one.minutes === null ? "—" : `${one.minutes} min`}</td>`).join("")}</tr>` +
|
|
@@ -18726,22 +19109,22 @@ function contestPage(chrome, data) {
|
|
|
18726
19109
|
const agent = summary.agent;
|
|
18727
19110
|
const parts = [
|
|
18728
19111
|
`<div class="card${winner ? " picked" : ""}">`,
|
|
18729
|
-
`<p><strong>
|
|
18730
|
-
`${winner ? ` <span class="badge badge-done">
|
|
19112
|
+
`<p><strong>Agent ${contestant.ordinal}</strong> <span class="meta">${escape(contestant.provider)} · ${escape(contestant.model)}</span>` +
|
|
19113
|
+
`${winner ? ` <span class="badge badge-done">Picked</span>` : ""}</p>`,
|
|
18731
19114
|
`<p class="row">${escape(outcome)}` +
|
|
18732
19115
|
`${minutes === null ? "" : ` <span class="meta">· ${minutes} min</span>`}` +
|
|
18733
19116
|
`${asked > 0 ? ` <span class="meta">· asked ${asked} question${asked > 1 ? "s" : ""}</span>` : ""}` +
|
|
18734
19117
|
`${run === null ? "" : ` <span class="meta">· <a href="/r/${run.id}">the build</a></span>`}</p>`,
|
|
18735
|
-
`<p class="row"><strong>
|
|
18736
|
-
run === null || run.handoff === null ? "" : `<p><strong>
|
|
18737
|
-
diff === null ? `<p class="meta">
|
|
19118
|
+
`<p class="row"><strong>Cost</strong> ${escape(cost)}</p>`,
|
|
19119
|
+
run === null || run.handoff === null ? "" : `<p><strong>Its own conclusion</strong></p><p class="recap">${escape(run.handoff)}</p>`,
|
|
19120
|
+
diff === null ? `<p class="meta">No diff was captured</p>` : terminalDiffCard(diff, run === null ? 0 : run.id),
|
|
18738
19121
|
];
|
|
18739
19122
|
if (picking) {
|
|
18740
19123
|
if (agent.pickable) {
|
|
18741
|
-
parts.push(`<form method="post" action="/contest/${contest.id}/arm" class="inline">`, `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`, `<input type="hidden" name="choice" value="${contestant.id}">`, `<button type="submit">
|
|
19124
|
+
parts.push(`<form method="post" action="/contest/${contest.id}/arm" class="inline">`, `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`, `<input type="hidden" name="choice" value="${contestant.id}">`, `<button type="submit">Pick this result…</button>`, `</form>`, `<p class="meta">Picking continues to a confirmation screen — nothing happens yet</p>`);
|
|
18742
19125
|
}
|
|
18743
19126
|
else {
|
|
18744
|
-
parts.push(`<p class="meta">
|
|
19127
|
+
parts.push(`<p class="meta">Cannot be picked — ${escape(agent.unpickableReason ?? "")}</p>`);
|
|
18745
19128
|
}
|
|
18746
19129
|
}
|
|
18747
19130
|
parts.push(`</div>`);
|
|
@@ -18753,11 +19136,11 @@ function contestPage(chrome, data) {
|
|
|
18753
19136
|
`only one result will be kept as the task's outcome; the rest stay archived with their evidence</p>`,
|
|
18754
19137
|
data.problem === null ? "" : `<div class="problem">${escape(data.problem)}</div>`,
|
|
18755
19138
|
`<p class="row"><strong>${escape((CONTEST_STATE_WORDS[contest.state] ?? "the tournament is in an unexpected state — the records have the detail").replace(/tournament/g, contestNoun(contest.kind)))}</strong></p>`,
|
|
18756
|
-
contest.pickedBy === null ? "" : `<p class="meta">
|
|
19139
|
+
contest.pickedBy === null ? "" : `<p class="meta">Picked by ${escape(contest.pickedBy)} at ${whenTime(contest.pickedAt ?? "")}</p>`,
|
|
18757
19140
|
contest.kind === "comparison"
|
|
18758
|
-
? `<p class="row"><strong>
|
|
19141
|
+
? `<p class="row"><strong>Spend</strong> ${escape(contestDollars(data.totalMicrousd))} measured on the lanes that report dollars` +
|
|
18759
19142
|
`${data.anyUnknown ? ` <span class="meta">— the rest report tokens only</span>` : ""}</p>`
|
|
18760
|
-
: `<p class="row"><strong>
|
|
19143
|
+
: `<p class="row"><strong>Charged so far</strong> ${escape(contestDollars(data.totalMicrousd))}` +
|
|
18761
19144
|
`${data.anyUnknown ? ` <span class="meta">— includes at least one agent charged its full reservation because the exact figure was unknowable</span>` : ""}</p>`,
|
|
18762
19145
|
glance,
|
|
18763
19146
|
`<div class="contest-compare">${summaries.map(agentCard).join("\n")}</div>`,
|
|
@@ -18767,9 +19150,9 @@ function contestPage(chrome, data) {
|
|
|
18767
19150
|
`<form method="post" action="/contest/${contest.id}/arm" class="inline">`,
|
|
18768
19151
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
|
|
18769
19152
|
`<input type="hidden" name="act" value="abandon">`,
|
|
18770
|
-
`<button type="submit">
|
|
19153
|
+
`<button type="submit">Abandon the ${contestNoun(contest.kind)}…</button>`,
|
|
18771
19154
|
`</form>`,
|
|
18772
|
-
`<p class="meta">
|
|
19155
|
+
`<p class="meta">Abandoning picks nothing: the task is marked failed (it can be re-queued), and every agent's branch and evidence is kept</p>`,
|
|
18773
19156
|
`</div>`,
|
|
18774
19157
|
].join("\n")
|
|
18775
19158
|
: "",
|
|
@@ -18786,7 +19169,7 @@ function contestCeremonyPage(chrome, data) {
|
|
|
18786
19169
|
const back = `<p class="meta"><a href="/contest/${data.contestId}">back — decide nothing</a></p>`;
|
|
18787
19170
|
if (data.kind === "abandon") {
|
|
18788
19171
|
return screen("tournament", [
|
|
18789
|
-
`<h1>
|
|
19172
|
+
`<h1>Abandon this ${contestNoun(data.contestKind)}?</h1>`,
|
|
18790
19173
|
`<div class="card">`,
|
|
18791
19174
|
`<p class="row">${data.agents} agents ${data.contestKind === "comparison" ? "built independently" : "raced"} on <strong>${escape(data.taskTitle)}</strong>. Abandoning picks nothing:</p>`,
|
|
18792
19175
|
`<p class="row">— the task is marked <strong>failed</strong> and can be re-queued later</p>`,
|
|
@@ -18798,20 +19181,20 @@ function contestCeremonyPage(chrome, data) {
|
|
|
18798
19181
|
`<form method="post" action="/contest/${data.contestId}/abandon" class="card">`,
|
|
18799
19182
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
|
|
18800
19183
|
`<input type="hidden" name="nonce" value="${escape(data.nonceValue)}">`,
|
|
18801
|
-
`<label>
|
|
18802
|
-
`<button type="submit" class="danger">
|
|
19184
|
+
`<label>Your password, typed again<input type="password" name="token" autocomplete="current-password"></label>`,
|
|
19185
|
+
`<button type="submit" class="danger">Abandon the ${contestNoun(data.contestKind)}</button>`,
|
|
18803
19186
|
`</form>`,
|
|
18804
19187
|
back,
|
|
18805
19188
|
].join("\n"), { chrome });
|
|
18806
19189
|
}
|
|
18807
19190
|
const agent = data.chosen;
|
|
18808
19191
|
if (agent === undefined || agent.run === null)
|
|
18809
|
-
return screen("tournament", `<p class="meta">
|
|
19192
|
+
return screen("tournament", `<p class="meta">Nothing to confirm</p>`, { chrome });
|
|
18810
19193
|
const run = agent.run;
|
|
18811
19194
|
return screen("tournament", [
|
|
18812
|
-
`<h1>
|
|
19195
|
+
`<h1>Pick agent ${agent.contestant.ordinal}'s result?</h1>`,
|
|
18813
19196
|
`<div class="card">`,
|
|
18814
|
-
`<p class="row"><strong>
|
|
19197
|
+
`<p class="row"><strong>Agent ${agent.contestant.ordinal}</strong> — ${escape(agent.contestant.provider)} · ${escape(agent.contestant.model)}</p>`,
|
|
18815
19198
|
`<p class="row">its result becomes the outcome of <strong>${escape(data.taskTitle)}</strong> — the task is marked done, keyed to <a href="/r/${run.id}">this build</a></p>`,
|
|
18816
19199
|
run.outcome === "no-change"
|
|
18817
19200
|
? `<p class="row">the result is a verified <strong>no change</strong> — the agent concluded nothing needed doing, and its checkout still matches the starting point</p>`
|
|
@@ -18821,8 +19204,8 @@ function contestCeremonyPage(chrome, data) {
|
|
|
18821
19204
|
? ""
|
|
18822
19205
|
: `<p class="row">the diff being picked: <span class="mono">${escape(agent.diff.sha256.slice(0, 12))}</span> · ${agent.diff.bytesStored} bytes, verified</p>`,
|
|
18823
19206
|
data.publication === null || data.publication === undefined
|
|
18824
|
-
? `<p class="row"><strong>
|
|
18825
|
-
: `<p class="row"><strong>
|
|
19207
|
+
? `<p class="row"><strong>Nothing is published</strong> — the branch stays local to this machine</p>`
|
|
19208
|
+
: `<p class="row"><strong>A ${data.publication.draft ? "draft " : ""}pull request will be opened</strong> on ` +
|
|
18826
19209
|
`<span class="mono">${escape(data.publication.githubRepo)}</span> from <span class="mono">${escape(data.publication.branch)}</span></p>`,
|
|
18827
19210
|
data.contestKind === "comparison"
|
|
18828
19211
|
? `<p class="row">this agent's spend: ${agent.contestant.unknownSpend ? "unmeasured — its harness reports tokens, not dollars" : `${escape(contestDollars(agent.contestant.measuredMicrousd))} measured`}` +
|
|
@@ -18836,8 +19219,8 @@ function contestCeremonyPage(chrome, data) {
|
|
|
18836
19219
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
|
|
18837
19220
|
`<input type="hidden" name="nonce" value="${escape(data.nonceValue)}">`,
|
|
18838
19221
|
`<input type="hidden" name="choice" value="${agent.contestant.id}">`,
|
|
18839
|
-
`<label>
|
|
18840
|
-
`<div class="sticky-actions"><button type="submit">
|
|
19222
|
+
`<label>Your password, typed again<input type="password" name="token" autocomplete="current-password"></label>`,
|
|
19223
|
+
`<div class="sticky-actions"><button type="submit">Yes — pick this result</button></div>`,
|
|
18841
19224
|
`</form>`,
|
|
18842
19225
|
back,
|
|
18843
19226
|
].filter(one => one !== "").join("\n"), { chrome });
|
|
@@ -18928,18 +19311,18 @@ function githubReposPage(chrome, data) {
|
|
|
18928
19311
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
|
|
18929
19312
|
`<input type="hidden" name="repo" value="${escape(nameWithOwner)}">`,
|
|
18930
19313
|
`<input type="hidden" name="root" value="0">`,
|
|
18931
|
-
`<button type="submit">
|
|
19314
|
+
`<button type="submit">Clone here →</button>`,
|
|
18932
19315
|
`</form>`,
|
|
18933
19316
|
].join("");
|
|
18934
19317
|
const rows = !data.listed.ok
|
|
18935
19318
|
? `<p class="meta">${escape(data.listed.message)}</p>`
|
|
18936
19319
|
: data.listed.repos.length === 0
|
|
18937
|
-
? `<p class="meta">
|
|
19320
|
+
? `<p class="meta">The signed-in GitHub account has no repositories to list</p>`
|
|
18938
19321
|
: data.listed.repos
|
|
18939
19322
|
.map(repo => {
|
|
18940
19323
|
const localPath = data.local.get(repo.nameWithOwner.toLowerCase()) ?? null;
|
|
18941
19324
|
const action = localPath !== null && data.openProject === localPath
|
|
18942
|
-
? `<span class="badge badge-done">
|
|
19325
|
+
? `<span class="badge badge-done">Open now</span>`
|
|
18943
19326
|
: localPath !== null
|
|
18944
19327
|
? openForm(localPath, data.registered.has(localPath) ? "open →" : "add + open →")
|
|
18945
19328
|
: data.cloneReady
|
|
@@ -18947,10 +19330,10 @@ function githubReposPage(chrome, data) {
|
|
|
18947
19330
|
: `<span class="meta">choose a projects folder first</span>`;
|
|
18948
19331
|
return [
|
|
18949
19332
|
`<div class="card project-card">`,
|
|
18950
|
-
`<div class="row"><strong>${escape(repo.nameWithOwner)}</strong>${repo.isPrivate ? ` <span class="badge">
|
|
19333
|
+
`<div class="row"><strong>${escape(repo.nameWithOwner)}</strong>${repo.isPrivate ? ` <span class="badge">Private</span>` : ""}`,
|
|
18951
19334
|
`<span class="right">${action}</span></div>`,
|
|
18952
19335
|
localPath === null
|
|
18953
|
-
? `<p class="meta">
|
|
19336
|
+
? `<p class="meta">Not on this machine yet${/^\d{4}-\d{2}-\d{2}T/.test(repo.updatedAt) ? ` · pushed ${whenTime(repo.updatedAt)}` : ""}</p>`
|
|
18954
19337
|
: `<p class="meta mono" style="overflow-wrap:anywhere;margin:.2rem 0">${escape(localPath)}</p>`,
|
|
18955
19338
|
repo.description === "" ? "" : `<p class="meta">${escape(repo.description)}</p>`,
|
|
18956
19339
|
`</div>`,
|
|
@@ -18958,8 +19341,8 @@ function githubReposPage(chrome, data) {
|
|
|
18958
19341
|
})
|
|
18959
19342
|
.join("\n");
|
|
18960
19343
|
return screen("projects", [
|
|
18961
|
-
`<h1>
|
|
18962
|
-
`<p class="meta">
|
|
19344
|
+
`<h1>Your GitHub repositories</h1>`,
|
|
19345
|
+
`<p class="meta">Repositories available to the GitHub account signed in on this machine — open one you already have, or clone a new one after a quick preview.</p>`,
|
|
18963
19346
|
rows,
|
|
18964
19347
|
`<p class="row" style="margin-top:.6rem"><a class="badge" href="/projects">← back to projects</a></p>`,
|
|
18965
19348
|
].join("\n"), { chrome });
|
|
@@ -18974,16 +19357,16 @@ function projectsPage(chrome, recent, candidates, open, csrf, problem, _unscoped
|
|
|
18974
19357
|
? `<p class="meta">${escape(onboard.why)}</p>`
|
|
18975
19358
|
: [
|
|
18976
19359
|
`<details class="project-add-more"${onboard.record === null ? '' : ' open'}><summary>${onboard.record === null ? 'Paste a GitHub link' : 'Review repository'}</summary>`,
|
|
18977
|
-
`<p class="meta">
|
|
19360
|
+
`<p class="meta">Paste a GitHub repository — you will preview it before anything is downloaded. It goes into your saved projects folder and the builder connects automatically. Large-file (LFS) objects are not downloaded.</p>`,
|
|
18978
19361
|
onboard.record === null
|
|
18979
19362
|
? [
|
|
18980
19363
|
`<form method="post" action="/projects/onboard-preview" class="card">`,
|
|
18981
19364
|
`<input type="hidden" name="csrf" value="${escape(csrf)}">`,
|
|
18982
|
-
`<label>
|
|
19365
|
+
`<label>Repository <input type="text" name="repo" placeholder="owner/name or https://github.com/owner/name"></label>`,
|
|
18983
19366
|
onboard.roots.length > 1
|
|
18984
|
-
? `<label>
|
|
18985
|
-
: `<input type="hidden" name="root" value="0"><p class="meta">
|
|
18986
|
-
`<button type="submit">
|
|
19367
|
+
? `<label>Into <select name="root">${onboard.roots.map((one, index) => `<option value="${index}">${escape(one)}</option>`).join("")}</select></label>`
|
|
19368
|
+
: `<input type="hidden" name="root" value="0"><p class="meta">Into ${escape(onboard.roots[0] ?? "")}</p>`,
|
|
19369
|
+
`<button type="submit">Preview</button>`,
|
|
18987
19370
|
`</form>`,
|
|
18988
19371
|
].join("\n")
|
|
18989
19372
|
: [
|
|
@@ -18995,8 +19378,8 @@ function projectsPage(chrome, recent, candidates, open, csrf, problem, _unscoped
|
|
|
18995
19378
|
onboard.record[1].large
|
|
18996
19379
|
? `<label class="row"><input type="checkbox" name="big-ok" value="1"> this is a large repository (or its size is unknown) — clone it anyway</label>`
|
|
18997
19380
|
: "",
|
|
18998
|
-
`<label>
|
|
18999
|
-
`<div class="sticky-actions"><button type="submit">
|
|
19381
|
+
`<label>Your password, typed again <input type="password" name="token" autocomplete="current-password"></label>`,
|
|
19382
|
+
`<div class="sticky-actions"><button type="submit">Clone and open</button></div>`,
|
|
19000
19383
|
`</form>`,
|
|
19001
19384
|
`</div>`,
|
|
19002
19385
|
].join("\n"),
|
|
@@ -19038,7 +19421,7 @@ function projectsPage(chrome, recent, candidates, open, csrf, problem, _unscoped
|
|
|
19038
19421
|
`<div class="row">${open !== null && open === one.path
|
|
19039
19422
|
? `<a class="project-name" href="${escape(returnTo)}"><strong>${escape(one.name)}</strong></a>`
|
|
19040
19423
|
: openForm(one.path, one.name, returnTo, "project-name")}`,
|
|
19041
|
-
`<span class="right">${open !== null && open === one.path ? `<span class="badge badge-done">
|
|
19424
|
+
`<span class="right">${open !== null && open === one.path ? `<span class="badge badge-done">Open now</span>` : openForm(one.path, "Open", returnTo, "button-link")}</span></div>`,
|
|
19042
19425
|
`<p class="meta mono" style="overflow-wrap:anywhere;margin:.2rem 0">${escape(one.path)}</p>`,
|
|
19043
19426
|
`<p class="row" style="gap:.35rem;flex-wrap:wrap">${peekChips(one.path, one.peek)}</p>`,
|
|
19044
19427
|
`<p class="meta row" style="justify-content:space-between;margin-bottom:0"><span>${escape(one.note)}</span><a href="/settings/knowledge?repo=${encodeURIComponent(one.path)}" style="display:inline-flex;align-items:center;min-height:44px">Knowledge</a></p>`,
|
|
@@ -19080,13 +19463,13 @@ function projectsPage(chrome, recent, candidates, open, csrf, problem, _unscoped
|
|
|
19080
19463
|
`<form method="post" action="/projects/open" class="card">`,
|
|
19081
19464
|
`<input type="hidden" name="csrf" value="${escape(csrf)}">`,
|
|
19082
19465
|
`<input type="hidden" name="return" value="${escape(returnTo)}">`,
|
|
19083
|
-
`<label>
|
|
19084
|
-
`<button type="submit">
|
|
19466
|
+
`<label>Path on this server<input type="text" name="path" placeholder="/Users/you/code/your-repo"></label>`,
|
|
19467
|
+
`<button type="submit">Open project</button>`,
|
|
19085
19468
|
`</form></details>`,
|
|
19086
19469
|
].join("\n");
|
|
19087
19470
|
const addCard = [
|
|
19088
19471
|
`<div class="card project-add-card">`,
|
|
19089
|
-
`<h2 class="project-add-title">
|
|
19472
|
+
`<h2 class="project-add-title">Add a project</h2>`,
|
|
19090
19473
|
`<div class="project-add-actions">`,
|
|
19091
19474
|
browsable
|
|
19092
19475
|
? addAction("/projects/browse", FOLDER_PATHS, "Choose a local folder", "Browse the project folders on this machine")
|
|
@@ -19107,8 +19490,8 @@ function projectsPage(chrome, recent, candidates, open, csrf, problem, _unscoped
|
|
|
19107
19490
|
recentItems.length === 0 && candidateItems.length === 0
|
|
19108
19491
|
? `<div class="card"><p><strong>Nothing to open yet.</strong></p><p class="meta">Add one below \u2014 opening it registers it here for next time.</p></div>`
|
|
19109
19492
|
: "",
|
|
19110
|
-
recentItems.length > 0 ? `<h2>
|
|
19111
|
-
candidateItems.length > 0 ? `<h2>
|
|
19493
|
+
recentItems.length > 0 ? `<h2>Recent</h2>${cards(recentItems)}` : "",
|
|
19494
|
+
candidateItems.length > 0 ? `<h2>Available</h2>${cards(candidateItems)}` : "",
|
|
19112
19495
|
addCard,
|
|
19113
19496
|
].join("\n"), { chrome, workspace: { view: {
|
|
19114
19497
|
kind: "projects", choosing, problem, returnTo,
|
|
@@ -19280,25 +19663,25 @@ const NAV_ICONS = {
|
|
|
19280
19663
|
function workToolRows(scoped = false, _offersChat = false, offersCode = false) {
|
|
19281
19664
|
const rows = [
|
|
19282
19665
|
...(offersCode ? [{ key: "code", href: "/code", label: "Coding sessions", hint: "Direct coding sessions and their saved results" }] : []),
|
|
19283
|
-
{ key: "inbox", href: "/inbox", label: "
|
|
19284
|
-
{ key: "board", href: "/board", label: "
|
|
19285
|
-
{ key: "tasks", href: "/tasks", label: "
|
|
19286
|
-
{ key: "recipes", href: "/recipes", label: "
|
|
19287
|
-
{ key: "routines", href: "/routines", label: "
|
|
19288
|
-
{ key: "workbench", href: "/workbench", label: "
|
|
19289
|
-
{ key: "ledger", href: "/ledger", label: "
|
|
19290
|
-
{ key: "spend", href: "/spend", label: "
|
|
19666
|
+
{ key: "inbox", href: "/inbox", label: "Inbox", hint: "questions, approvals, and repairs to act on" },
|
|
19667
|
+
{ key: "board", href: "/board", label: "Board", hint: "lanes by state, with the order view" },
|
|
19668
|
+
{ key: "tasks", href: "/tasks", label: "Task list", hint: "everything, filterable by state" },
|
|
19669
|
+
{ key: "recipes", href: "/recipes", label: "Recipes", hint: "choose, customize, and reuse a workflow" },
|
|
19670
|
+
{ key: "routines", href: "/routines", label: "Routines", hint: "scheduled tracks and their firings" },
|
|
19671
|
+
{ key: "workbench", href: "/workbench", label: "Portfolio", hint: "every project and live build in one place" },
|
|
19672
|
+
{ key: "ledger", href: "/ledger", label: "Action ledger", hint: "who acted, what happened, and the result" },
|
|
19673
|
+
{ key: "spend", href: "/spend", label: "Spend", hint: "what agent work cost, and monthly budgets" },
|
|
19291
19674
|
];
|
|
19292
19675
|
return scoped ? rows.filter(row => row.key !== "workbench" && row.key !== "spend") : rows;
|
|
19293
19676
|
}
|
|
19294
19677
|
function settingsRows(scoped = false, offersSettings = false) {
|
|
19295
19678
|
const rows = [
|
|
19296
|
-
...(offersSettings ? [{ key: "settings", href: "/settings", label: "
|
|
19297
|
-
{ key: "fleet", href: "/fleet", label: "
|
|
19298
|
-
{ key: "caps", href: "/caps", label: "
|
|
19299
|
-
{ key: "people", href: "/people", label: "
|
|
19300
|
-
{ key: "mode", href: "/mode", label: "
|
|
19301
|
-
{ key: "system", href: "/system", label: "
|
|
19679
|
+
...(offersSettings ? [{ key: "settings", href: "/settings", label: "Settings", hint: "agent defaults, alerts, credentials" }] : []),
|
|
19680
|
+
{ key: "fleet", href: "/fleet", label: "Fleet", hint: "who is working, and on what" },
|
|
19681
|
+
{ key: "caps", href: "/caps", label: "Requirements", hint: "tools and credentials builds need" },
|
|
19682
|
+
{ key: "people", href: "/people", label: "People", hint: "who can sign in, and what they have done" },
|
|
19683
|
+
{ key: "mode", href: "/mode", label: "Operating mode", hint: "the signed posture this repository runs under" },
|
|
19684
|
+
{ key: "system", href: "/system", label: "System", hint: "workers, providers, and grants" },
|
|
19302
19685
|
];
|
|
19303
19686
|
return scoped ? rows.filter(row => row.key === "people" || row.key === "settings") : rows;
|
|
19304
19687
|
}
|
|
@@ -19328,10 +19711,10 @@ function queueCard(one, csrf, revision, queueRevision, workers, column) {
|
|
|
19328
19711
|
// and the reservation owner. Money is not in this query and is not
|
|
19329
19712
|
// invented here — it stays on the task page, labeled.
|
|
19330
19713
|
const state = one.taken
|
|
19331
|
-
? `<span class="badge">
|
|
19714
|
+
? `<span class="badge">Being taken — keeps its claim</span>`
|
|
19332
19715
|
: column === "anyone"
|
|
19333
|
-
? `<span class="badge">
|
|
19334
|
-
: `<span class="badge">
|
|
19716
|
+
? `<span class="badge">Queued</span>`
|
|
19717
|
+
: `<span class="badge">Reserved for ${escape(column)}</span>`;
|
|
19335
19718
|
const chips = ` ${state}` +
|
|
19336
19719
|
`${one.dispatch === null || one.dispatch === undefined ? "" : ` <a class="badge" href="${taskHref(one.id)}" title="${escape(one.dispatch.summary)}">${escape(dispatchHeadline(one.dispatch))}</a>`}`;
|
|
19337
19720
|
const hidden = `<input type="hidden" name="csrf" value="${escape(csrf)}">` +
|
|
@@ -19348,7 +19731,7 @@ function queueCard(one, csrf, revision, queueRevision, workers, column) {
|
|
|
19348
19731
|
`<select name="column" aria-label="reserve for">` +
|
|
19349
19732
|
`<option value="anyone"${column === "anyone" ? " selected" : ""}>anyone</option>` +
|
|
19350
19733
|
workers.filter(worker => !worker.retired).map(worker => `<option value="${escape(worker.name)}"${column === worker.name ? " selected" : ""}>${escape(worker.name)}</option>`).join("") +
|
|
19351
|
-
`</select><button type="submit">
|
|
19734
|
+
`</select><button type="submit">Move</button></form>`;
|
|
19352
19735
|
return (`<div class="card queue-card" data-task="${escape(one.id)}" data-taken="${one.taken ? "1" : "0"}"${one.dispatch === null || one.dispatch === undefined ? "" : ` data-dispatch-status="${escape(one.dispatch.code)}"`}>` +
|
|
19353
19736
|
`<p class="row">${one.taken ? "" : `${GRIP_HANDLE}`}` +
|
|
19354
19737
|
`<a href="${taskHref(one.id)}">${escape(one.title)}</a>${chips}</p>` +
|
|
@@ -19370,22 +19753,22 @@ function queueBody(tasks, workers, csrf, revision, queueRevision) {
|
|
|
19370
19753
|
? `<p class="meta lane-empty">${escape(empty)}</p>`
|
|
19371
19754
|
: rows
|
|
19372
19755
|
.map((one, index) => index === 0 && key === "anyone" && allWorkersBusy && !one.taken
|
|
19373
|
-
? queueCard(one, csrf, revision, queueRevision, workers, key).replace('</p>\n', "</p>").replace(`<p class="row meta">`, `<p class="meta">
|
|
19756
|
+
? queueCard(one, csrf, revision, queueRevision, workers, key).replace('</p>\n', "</p>").replace(`<p class="row meta">`, `<p class="meta">Every worker has reserved work — this waits until a column empties</p><p class="row meta">`)
|
|
19374
19757
|
: queueCard(one, csrf, revision, queueRevision, workers, key))
|
|
19375
19758
|
.join("\n")) +
|
|
19376
19759
|
`</section>`;
|
|
19377
19760
|
const noteForm = (worker) => worker.retired
|
|
19378
|
-
? `<p class="meta">
|
|
19761
|
+
? `<p class="meta">This worker is retired — drag these elsewhere, or register the name again</p>`
|
|
19379
19762
|
: `<p class="meta mono">${worker.building} building in this project · unattended capacity ${worker.capacity}</p>` +
|
|
19380
19763
|
`<form method="post" action="/queue/note" class="row">` +
|
|
19381
19764
|
`<input type="hidden" name="csrf" value="${escape(csrf)}">` +
|
|
19382
19765
|
`<input type="hidden" name="projectRevision" value="${revision}">` +
|
|
19383
19766
|
`<input type="hidden" name="runner" value="${escape(worker.name)}">` +
|
|
19384
19767
|
`<input type="text" name="note" value="${worker.note === null ? "" : escape(worker.note)}" data-initial="${worker.note === null ? "" : escape(worker.note)}" placeholder="what this worker is working through" aria-label="column note" maxlength="200">` +
|
|
19385
|
-
`<button type="submit">
|
|
19386
|
-
`<p class="meta">
|
|
19768
|
+
`<button type="submit">Save</button></form>` +
|
|
19769
|
+
`<p class="meta">Takes from the shared queue when this column is empty</p>`;
|
|
19387
19770
|
return (`<div class="lanes" data-queue-revision="${queueRevision}">` +
|
|
19388
|
-
column("shared queue", "anyone", `<p class="meta">
|
|
19771
|
+
column("shared queue", "anyone", `<p class="meta">Workers take from here when their column is empty — top card first</p>`, shared, "nothing waiting — every task is reserved or running") +
|
|
19389
19772
|
workers
|
|
19390
19773
|
.map(worker => column(worker.name + (worker.retired ? " (retired)" : ""), worker.name, noteForm(worker), columnOf(worker.name), "nothing queued — this worker will take from the shared queue"))
|
|
19391
19774
|
.join("\n") +
|
|
@@ -19407,7 +19790,7 @@ function fleetBody(queued, building, runners, csrf, queueRevision, visibleRepo)
|
|
|
19407
19790
|
const live = building.filter(one => one.runner === runner.name);
|
|
19408
19791
|
const retired = runner.retiredAt !== null;
|
|
19409
19792
|
const head = retired
|
|
19410
|
-
? `<p class="meta">
|
|
19793
|
+
? `<p class="meta">This worker is retired — drag these elsewhere, or register the name again</p>`
|
|
19411
19794
|
: `<form method="post" action="/queue/note" class="row">` +
|
|
19412
19795
|
`<input type="hidden" name="csrf" value="${escape(csrf)}">` +
|
|
19413
19796
|
`<input type="hidden" name="from" value="fleet">` +
|
|
@@ -19424,9 +19807,9 @@ function fleetBody(queued, building, runners, csrf, queueRevision, visibleRepo)
|
|
|
19424
19807
|
`<p class="row">${one.taken ? "" : `${GRIP_HANDLE}`}` +
|
|
19425
19808
|
`<a href="${taskHref(one.id)}">${escape(one.title)}</a></p>` +
|
|
19426
19809
|
`<p class="row meta"><span class="mono">${escape(one.id)}</span>${chip(one.repo)}` +
|
|
19427
|
-
`${one.approved ? "" : ` <span class="badge">
|
|
19428
|
-
`${one.blockers > 0 ? ` <span class="badge">
|
|
19429
|
-
`${one.taken ? ` <span class="badge">
|
|
19810
|
+
`${one.approved ? "" : ` <span class="badge">Unapproved scope</span>`}` +
|
|
19811
|
+
`${one.blockers > 0 ? ` <span class="badge">Waits for ${one.blockers}</span>` : ""}` +
|
|
19812
|
+
`${one.taken ? ` <span class="badge">Being taken</span>` : ""}</p></div>`)
|
|
19430
19813
|
.join("\n");
|
|
19431
19814
|
const empty = live.length === 0 && own.length === 0
|
|
19432
19815
|
? `<p class="meta lane-empty">${retired ? "nothing left" : "idle — will take from the shared queue"}</p>`
|
|
@@ -19441,12 +19824,12 @@ function fleetBody(queued, building, runners, csrf, queueRevision, visibleRepo)
|
|
|
19441
19824
|
`<p class="row">${one.taken ? "" : `${GRIP_HANDLE}`}` +
|
|
19442
19825
|
`<a href="${taskHref(one.id)}">${escape(one.title)}</a></p>` +
|
|
19443
19826
|
`<p class="row meta"><span class="mono">${escape(one.id)}</span>${chip(one.repo)}` +
|
|
19444
|
-
`${one.approved ? "" : ` <span class="badge">
|
|
19445
|
-
`${one.blockers > 0 ? ` <span class="badge">
|
|
19446
|
-
`${one.taken ? ` <span class="badge">
|
|
19827
|
+
`${one.approved ? "" : ` <span class="badge">Unapproved scope</span>`}` +
|
|
19828
|
+
`${one.blockers > 0 ? ` <span class="badge">Waits for ${one.blockers}</span>` : ""}` +
|
|
19829
|
+
`${one.taken ? ` <span class="badge">Being taken</span>` : ""}</p></div>`)
|
|
19447
19830
|
.join("\n");
|
|
19448
|
-
const sharedLane = `<section class="lane queue-column" data-column="anyone"><h2>
|
|
19449
|
-
`<p class="meta">
|
|
19831
|
+
const sharedLane = `<section class="lane queue-column" data-column="anyone"><h2>Shared queue</h2>` +
|
|
19832
|
+
`<p class="meta">Any free worker takes from here, top first</p>${sharedCards}` +
|
|
19450
19833
|
(shared.length === 0 ? `<p class="meta lane-empty">nothing waiting — every task is reserved or running</p>` : "") +
|
|
19451
19834
|
`</section>`;
|
|
19452
19835
|
return (`<div class="lanes" data-queue-revision="${queueRevision}">` +
|
|
@@ -19588,13 +19971,15 @@ function newTaskPage(chrome, project, csrf, projectRevision, problem, candidates
|
|
|
19588
19971
|
* brief, but the console renders its known structure as a useful control
|
|
19589
19972
|
* surface. Historical free-form plans keep their safe plain-text fallback. */
|
|
19590
19973
|
const MILESTONE_STATE_WORDS = {
|
|
19591
|
-
pending: "
|
|
19592
|
-
current: "
|
|
19593
|
-
completed: "
|
|
19594
|
-
blocked: "
|
|
19974
|
+
pending: "Pending",
|
|
19975
|
+
current: "In progress",
|
|
19976
|
+
completed: "Completed",
|
|
19977
|
+
blocked: "Blocked",
|
|
19595
19978
|
};
|
|
19979
|
+
/** Badges, labels and buttons read in sentence case, whatever word a record stores. */
|
|
19980
|
+
const sentenceCase = (word) => word.charAt(0).toUpperCase() + word.slice(1);
|
|
19596
19981
|
function milestoneStateWord(state) {
|
|
19597
|
-
return MILESTONE_STATE_WORDS[state] ?? "
|
|
19982
|
+
return MILESTONE_STATE_WORDS[state] ?? "Pending";
|
|
19598
19983
|
}
|
|
19599
19984
|
/** Adaptive execution plans (v44): the live milestone projection a running
|
|
19600
19985
|
* (or finished) build has reported. A shared render used by BOTH the task
|
|
@@ -19653,9 +20038,9 @@ function planRevisionLedgerHtml(ledger, taskId, csrf) {
|
|
|
19653
20038
|
`</div>`;
|
|
19654
20039
|
const history = ledger.history.length <= 1
|
|
19655
20040
|
? ""
|
|
19656
|
-
: `<details class="plan-revision-history"><summary>
|
|
20041
|
+
: `<details class="plan-revision-history"><summary>Revision history (${ledger.history.length})</summary>` +
|
|
19657
20042
|
ledger.history
|
|
19658
|
-
.map(one => `<p class="row"><span class="mono">rev ${one.revision}</span> <span class="badge">${escape(one.status)}</span> ` +
|
|
20043
|
+
.map(one => `<p class="row"><span class="mono">rev ${one.revision}</span> <span class="badge">${escape(sentenceCase(one.status))}</span> ` +
|
|
19659
20044
|
`<span class="meta">${escape(one.author)} · ${whenTime(one.createdAt)}</span> — ${escape(one.reason)}</p>`)
|
|
19660
20045
|
.join("\n") +
|
|
19661
20046
|
`</details>`;
|
|
@@ -19774,25 +20159,24 @@ function taskBodyParts(data) {
|
|
|
19774
20159
|
if (liveRunId === null || liveRun === undefined)
|
|
19775
20160
|
return "";
|
|
19776
20161
|
const minutes = Math.max(0, Math.round((data.now.getTime() - new Date(liveRun.startedAt).getTime()) / 60_000));
|
|
19777
|
-
const head = `<p><strong>
|
|
20162
|
+
const head = `<p><strong>Build #${liveRun.id} · ${escape(liveRun.runner)} · running ` +
|
|
19778
20163
|
`<time data-elapsed-since="${escape(liveRun.startedAt)}">${minutes}m</time></strong></p>`;
|
|
19779
20164
|
const door = `<p class="row"><a href="/r/${liveRun.id}">full build view →</a></p>`;
|
|
19780
20165
|
if (data.degraded !== undefined) {
|
|
19781
20166
|
return `<div class="card attempt-live" data-live-run="${liveRun.id}">${head}` +
|
|
19782
|
-
`<p class="meta">${data.degraded === "sensitive" ? "
|
|
20167
|
+
`<p class="meta">${data.degraded === "sensitive" ? "This page carries a password ceremony, so the live view stays on the build page" : "The live view is on the build page"}</p>${door}</div>`;
|
|
19783
20168
|
}
|
|
19784
|
-
|
|
19785
|
-
|
|
19786
|
-
|
|
19787
|
-
|
|
19788
|
-
|
|
19789
|
-
|
|
19790
|
-
|
|
19791
|
-
|
|
19792
|
-
|
|
19793
|
-
|
|
19794
|
-
|
|
19795
|
-
`<p class="meta" id="live-transcript-state"></p>`;
|
|
20169
|
+
// With the live file view off there is nothing live to show here: no panel (the build's own page says why).
|
|
20170
|
+
if (data.peekable !== true)
|
|
20171
|
+
return "";
|
|
20172
|
+
const peek = `<p class="meta">What is changing right now</p>` +
|
|
20173
|
+
`<div id="run-peek"><p class="meta">Watching\u2026 the first look lands within 15 seconds</p></div>` +
|
|
20174
|
+
`<p class="meta" id="run-peek-stamp"></p>`;
|
|
20175
|
+
const transcript = liveRun.provider !== "claude"
|
|
20176
|
+
? `<p class="meta">The live transcript needs the claude harness for now \u2014 this build runs on ${escape(liveRun.provider)}</p>`
|
|
20177
|
+
: `<p class="meta">What the agent is saying · display only \u2014 this is not evidence, and the machine running the agent could alter it</p>` +
|
|
20178
|
+
`<pre id="live-transcript" class="mono" style="max-height:18rem;overflow:auto;white-space:pre-wrap"></pre>` +
|
|
20179
|
+
`<p class="meta" id="live-transcript-state"></p>`;
|
|
19796
20180
|
return `<div class="card attempt-live" data-live-run="${liveRun.id}">${head}${peek}${transcript}${door}</div>`;
|
|
19797
20181
|
})();
|
|
19798
20182
|
// One truthful answer to the first question on a queued task: "will this
|
|
@@ -19828,7 +20212,8 @@ function taskBodyParts(data) {
|
|
|
19828
20212
|
case "approve-scope": return ` <a href="#approve">Review and sign the exact scope</a>.`;
|
|
19829
20213
|
case "answer-decision": return ` <a href="#decisions">Answer the waiting question</a>.`;
|
|
19830
20214
|
case "unhold": return ` Use <strong>Remove hold</strong> when it can continue.`;
|
|
19831
|
-
|
|
20215
|
+
// The reason is the detail itself; Retry is the status card's own act, said once.
|
|
20216
|
+
case "retry-task": return "";
|
|
19832
20217
|
case "repair-capability": return ` <a href="/caps">Repair the requirement</a>.`;
|
|
19833
20218
|
case "repair-dependency":
|
|
19834
20219
|
return blocker?.admitted === true
|
|
@@ -19871,7 +20256,8 @@ function taskBodyParts(data) {
|
|
|
19871
20256
|
: `<p class="meta">This task resumes automatically when the builder reconnects. You do not need to file or approve it again.</p>`) +
|
|
19872
20257
|
`<p><a href="/system">See connection status →</a></p></div></details>`);
|
|
19873
20258
|
}
|
|
19874
|
-
|
|
20259
|
+
// Never a link to this page's own acts: Retry sits on the status card and below.
|
|
20260
|
+
const href = diagnosis.action === "retry-task" ? null : taskRecoveryHref(task.id, diagnosis);
|
|
19875
20261
|
return href === null ? "" : `<a class="button-link dispatch-action-link" href="${href}">${escape(dispatchActionLabel(diagnosis))}</a>`;
|
|
19876
20262
|
})();
|
|
19877
20263
|
const positive = diagnosis.code === "running" || diagnosis.code === "ready" || diagnosis.code === "planning-ready" || diagnosis.code === "scouting-ready";
|
|
@@ -19893,7 +20279,7 @@ function taskBodyParts(data) {
|
|
|
19893
20279
|
`<p class="meta">Saved assessment history. Current task status and checks are shown above.</p>` +
|
|
19894
20280
|
(accepted === null ? "" : `<p class="meta">Accepted with an exception by ${escape(accepted.approver)}. Check results are unchanged.${accepted.note === null ? "" : ` ${escape(accepted.note)}`}</p>`) +
|
|
19895
20281
|
(proof.proofReasons.length === 0 ? "" : `<ul>${[...new Set(proof.proofReasons.map(plainReasonWords))].map(reason => `<li>${escape(reason)}</li>`).join("")}</ul>`) +
|
|
19896
|
-
criterionMatrixHtml(proof.proofMatrix, { compact: true, runId: proof.runId, links: proof.proofMatrixLinks }) +
|
|
20282
|
+
criterionMatrixHtml(proof.proofMatrix, { compact: true, runId: proof.runId, links: proof.proofMatrixLinks, verdict: proof.proofVerdict }) +
|
|
19897
20283
|
semanticCoverageHtml(proof.proofMatrix, proof.qualityMode ?? "default") + `</div></details>`;
|
|
19898
20284
|
}
|
|
19899
20285
|
// v50: the independent review's own card — attempt counts, what is
|
|
@@ -19945,7 +20331,7 @@ function taskBodyParts(data) {
|
|
|
19945
20331
|
const proofDetails = proof.proofMatrix.length === 0 && machineNote === "" && chainHtml === ""
|
|
19946
20332
|
? ""
|
|
19947
20333
|
: `<details class="dispatch-proof-details"><summary>${proof.proofMatrix.length === 0 ? "Verification details" : `${proof.proofMatrix.length} requirement${proof.proofMatrix.length === 1 ? "" : "s"}`} · View details</summary>` +
|
|
19948
|
-
`<div class="dispatch-proof-body">${criterionMatrixHtml(proof.proofMatrix, { compact: true, runId: proof.runId, links: proof.proofMatrixLinks })}${coverageHtml}${machineNote}${chainHtml}</div></details>`;
|
|
20334
|
+
`<div class="dispatch-proof-body">${criterionMatrixHtml(proof.proofMatrix, { compact: true, runId: proof.runId, links: proof.proofMatrixLinks, verdict: proof.proofVerdict })}${coverageHtml}${machineNote}${chainHtml}</div></details>`;
|
|
19949
20335
|
// The publication fact is separate from the evidence fact: a PR or
|
|
19950
20336
|
// an observed merge is named from its record; nothing is "deployed".
|
|
19951
20337
|
const publicationWords = (data.publication === null || data.publication === undefined ? "" : ` ${escape(receiptPublicationWords(publicationFactsOf(data.publication)))}`) +
|
|
@@ -20005,26 +20391,30 @@ function taskBodyParts(data) {
|
|
|
20005
20391
|
: `<form method="post" action="${taskHref(task.id)}/steer" class="row">` +
|
|
20006
20392
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
|
|
20007
20393
|
`<input type="text" name="note" placeholder="guidance for the next attempt" aria-label="steering note" style="width:100%;max-width:28rem">` +
|
|
20008
|
-
`<button type="submit">
|
|
20009
|
-
`<p class="meta">
|
|
20010
|
-
const steeringCard = steerRows === "" && steerForm === "" ? "" : `<h2>
|
|
20394
|
+
`<button type="submit">Steer</button></form>` +
|
|
20395
|
+
`<p class="meta">Lands when the next attempt starts — a running agent is not interrupted, and a note cannot widen the approved scope</p>`;
|
|
20396
|
+
const steeringCard = steerRows === "" && steerForm === "" ? "" : `<h2>Steering</h2>${steerRows}${steerForm}`;
|
|
20011
20397
|
const holds = data.holds.length === 0
|
|
20012
20398
|
? ""
|
|
20013
|
-
: `<h2>
|
|
20399
|
+
: `<h2>Holds</h2>` +
|
|
20014
20400
|
data.holds
|
|
20015
20401
|
.map(hold => `<p class="row">${escape(holdOwnerWords(hold.ownerKind))} — ${escape(hold.reason)}` +
|
|
20016
20402
|
`${hold.until === null ? "" : ` <span class="meta">until ${whenTime(hold.until)}</span>`}</p>`)
|
|
20017
20403
|
.join("\n") +
|
|
20018
|
-
`<p class="meta">
|
|
20404
|
+
`<p class="meta">Only your hold can be lifted here — waits caused by questions, incidents, or retry delays clear on their own</p>`;
|
|
20019
20405
|
const approval = approvalOf(scope);
|
|
20406
|
+
// A refused save from the sheet's in-place editor reopens that editor with
|
|
20407
|
+
// its draft; the full scope form below stays as it was.
|
|
20408
|
+
const inlineScopeDraft = data.scopeDraft?.has("requirement-new") === true;
|
|
20409
|
+
const scopeDraft = inlineScopeDraft ? undefined : data.scopeDraft;
|
|
20020
20410
|
const scopeCard = scope === null
|
|
20021
|
-
? `<p class="meta">
|
|
20411
|
+
? `<p class="meta">No scope proposed — nothing builds this until one is approved</p>`
|
|
20022
20412
|
: [
|
|
20023
20413
|
`<div class="card">`,
|
|
20024
|
-
`<p><strong>
|
|
20025
|
-
scope.outOfScope === null ? "" : `<p><strong>
|
|
20026
|
-
scope.touches.length === 0 ? "" : `<p class="scope-paths"><strong>
|
|
20027
|
-
`<p><strong>
|
|
20414
|
+
`<p><strong>Goal</strong></p><p class="recap">${escape(scope.goal)}</p>`,
|
|
20415
|
+
scope.outOfScope === null ? "" : `<p><strong>Not this</strong></p><p class="recap">${escape(scope.outOfScope)}</p>`,
|
|
20416
|
+
scope.touches.length === 0 ? "" : `<p class="scope-paths"><strong>Touches</strong> ${scope.touches.map(one => escape(one)).join(", ")}</p>`,
|
|
20417
|
+
`<p><strong>Quality</strong> ${escape(qualityModeTitle(scope.qualityMode ?? "default"))}</p>`,
|
|
20028
20418
|
// The fallback chain, on the card the yes reads (Layer F): the
|
|
20029
20419
|
// digest binds the WHOLE chain, so every entry — credential
|
|
20030
20420
|
// included — is said before anyone signs.
|
|
@@ -20032,7 +20422,7 @@ function taskBodyParts(data) {
|
|
|
20032
20422
|
const chain = chainFromJson(scope.proposedChainJson ?? null);
|
|
20033
20423
|
if (chain === null || chain.length < 2)
|
|
20034
20424
|
return "";
|
|
20035
|
-
return `<p><strong>
|
|
20425
|
+
return `<p><strong>If its subscription runs out</strong></p><p class="recap">${chain
|
|
20036
20426
|
.slice(1)
|
|
20037
20427
|
.map(one => `falls back to ${escape(one.profile.provider)} (${escape(one.profile.model)}) — ${one.authMode === "api-key" ? "your API key; spend moves to that account" : "its subscription login"}`)
|
|
20038
20428
|
.join("; ")}</p>`;
|
|
@@ -20050,10 +20440,10 @@ function taskBodyParts(data) {
|
|
|
20050
20440
|
// blank — the same rule as the revision brief.
|
|
20051
20441
|
const reportCard = data.report === null || data.report === undefined
|
|
20052
20442
|
? data.deliverable === "report"
|
|
20053
|
-
? `<div class="card"><p><strong>
|
|
20443
|
+
? `<div class="card"><p><strong>Scout task</strong> <span class="meta">delivers a report, never a branch — a read-only session investigates the goal and its report appears here when it finishes</span></p></div>`
|
|
20054
20444
|
: ""
|
|
20055
20445
|
: !data.report.ok
|
|
20056
|
-
? `<div class="card"><p><strong>
|
|
20446
|
+
? `<div class="card"><p><strong>The report</strong></p><p class="meta">${escape(data.report.problem)} · <a href="/r/${data.report.run}">run ${data.report.run}</a></p></div>`
|
|
20057
20447
|
: [
|
|
20058
20448
|
`<div class="card report">`,
|
|
20059
20449
|
`<p><strong>${escape(data.report.report.title)}</strong> <span class="meta">the scout's report · <a href="/r/${data.report.run}">run ${data.report.run}</a></span></p>`,
|
|
@@ -20062,11 +20452,11 @@ function taskBodyParts(data) {
|
|
|
20062
20452
|
...(data.report.report.followUps.length === 0
|
|
20063
20453
|
? []
|
|
20064
20454
|
: [
|
|
20065
|
-
`<p><strong>
|
|
20455
|
+
`<p><strong>Follow-ups the scout proposes</strong> <span class="meta">each files as a task in this repository; its scope still needs your approval</span></p>`,
|
|
20066
20456
|
...data.report.report.followUps.map((one, index) => `<div class="follow-up"><p><strong>${escape(one.title)}</strong></p><p class="meta">${escape(one.goal)}</p>` +
|
|
20067
20457
|
(data.csrf === "" || data.repo === null
|
|
20068
20458
|
? `<p class="meta">${data.repo === null ? "this task has no repository — file it by hand" : ""}</p>`
|
|
20069
|
-
: `<form method="post" action="${taskHref(task.id)}/follow-up" class="inline"><input type="hidden" name="csrf" value="${escape(data.csrf)}"><input type="hidden" name="index" value="${index}"><button type="submit">
|
|
20459
|
+
: `<form method="post" action="${taskHref(task.id)}/follow-up" class="inline"><input type="hidden" name="csrf" value="${escape(data.csrf)}"><input type="hidden" name="index" value="${index}"><button type="submit">File this follow-up</button></form>`) +
|
|
20070
20460
|
`</div>`),
|
|
20071
20461
|
]),
|
|
20072
20462
|
`</div>`,
|
|
@@ -20098,7 +20488,7 @@ function taskBodyParts(data) {
|
|
|
20098
20488
|
? data.revision != null && !("problem" in data.revision)
|
|
20099
20489
|
? `<div class="card planner-status"><span class="planner-orb" aria-hidden="true"></span><p><strong>Updating the plan</strong>` +
|
|
20100
20490
|
`<span class="meta">Adding your notes to the plan. You approve any change before it builds.</span></p></div>`
|
|
20101
|
-
: `<div class="card planner-status"><span class="planner-orb" aria-hidden="true"></span><p><strong>
|
|
20491
|
+
: `<div class="card planner-status"><span class="planner-orb" aria-hidden="true"></span><p><strong>Planning requested</strong>` +
|
|
20102
20492
|
`<span class="meta">${data.planAuto ? "Automatic approval is enabled for a verified plan that preserves your filed contract. Amendments and unanswered questions still pause." : "The agent is inspecting the repository and drafting the goal, acceptance criteria, and approach. It will ask only if a missing answer changes the work."}</span></p></div>`
|
|
20103
20493
|
: ""
|
|
20104
20494
|
: `${approval.approved ? `<details class="card planner-plan planner-plan-collapsed"><summary class="execution-plan-head">` : `<section class="card planner-plan"><div class="execution-plan-head">`}` +
|
|
@@ -20111,7 +20501,7 @@ function taskBodyParts(data) {
|
|
|
20111
20501
|
? ""
|
|
20112
20502
|
: `<details class="plan-editor" id="plan-edit"><summary>Edit plan</summary><form method="post" action="${taskHref(task.id)}/plan-edit">` +
|
|
20113
20503
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}"><input type="hidden" name="saw-plan" value="${escape(data.planSha)}">` +
|
|
20114
|
-
`<label>
|
|
20504
|
+
`<label>Plan details <span class="meta">Keep the five headings. Approval locks this version for the build.</span>` +
|
|
20115
20505
|
`<textarea name="plan-document" rows="14">${escape(data.planDocument)}</textarea></label>` +
|
|
20116
20506
|
`<button type="submit">Save plan</button></form></details>`) +
|
|
20117
20507
|
`${approval.approved ? `</div></details>` : `</section>`}`;
|
|
@@ -20123,7 +20513,7 @@ function taskBodyParts(data) {
|
|
|
20123
20513
|
const revisionCard = data.revision === null || data.revision === undefined
|
|
20124
20514
|
? ""
|
|
20125
20515
|
: "problem" in data.revision
|
|
20126
|
-
? `<div class="card"><p><strong>
|
|
20516
|
+
? `<div class="card"><p><strong>Revision brief</strong></p><p class="meta">${escape(data.revision.problem)}</p></div>`
|
|
20127
20517
|
: [
|
|
20128
20518
|
`<div class="revision-card" data-revision-feedback>`,
|
|
20129
20519
|
`<p><strong>${data.revision.kind === "ci-repair" ? "CI repair" : data.revision.kind === "criterion-repair" ? "Criterion repair" : "Revision feedback"}</strong> <span class="meta">from <a href="/r/${data.revision.sourceRun}">build #${data.revision.sourceRun}</a></span></p>`,
|
|
@@ -20151,7 +20541,7 @@ function taskBodyParts(data) {
|
|
|
20151
20541
|
: scope.profileState === "unresolved"
|
|
20152
20542
|
? `<div class="card approve-form" id="approve"><p><strong>This task is waiting on you: its scope cannot be approved yet.</strong></p>` +
|
|
20153
20543
|
profileWords(scope) +
|
|
20154
|
-
`<p class="ceremony-road"><a class="button-link" href="#scope">
|
|
20544
|
+
`<p class="ceremony-road"><a class="button-link" href="#scope">Edit the scope to fix it →</a></p></div>`
|
|
20155
20545
|
: !consentDoor.open
|
|
20156
20546
|
? consentClosedHtml(task.id, consentDoor, "task")
|
|
20157
20547
|
: approvalSheetHtml({
|
|
@@ -20174,6 +20564,11 @@ function taskBodyParts(data) {
|
|
|
20174
20564
|
editHref: data.csrf !== "" && data.planSha != null && data.planDocument !== null ? "#plan-edit" : "#scope",
|
|
20175
20565
|
notNowHref: "/work",
|
|
20176
20566
|
sticky: data.dispatch?.action === "approve-scope",
|
|
20567
|
+
earlier: { active: data.assignment?.earlierActive ?? 0, running: data.assignment?.earlierRunning ?? 0 },
|
|
20568
|
+
// A tournament's terms live in the full scope editor; every other plan edits in place.
|
|
20569
|
+
edit: data.csrf === "" || (data.raceTerms ?? null) !== null ? null
|
|
20570
|
+
: { action: `${taskHref(task.id)}/scope`, draft: inlineScopeDraft ? data.scopeDraft ?? null : null, problem: inlineScopeDraft ? data.problem : null,
|
|
20571
|
+
open: data.editPlan === true, stepsHref: data.planSha != null && data.planDocument !== null ? "#plan-edit" : null },
|
|
20177
20572
|
});
|
|
20178
20573
|
// The attended road (Phase 2E): beside the approval, never replacing it.
|
|
20179
20574
|
// The mint button leads to the CONFIRM screen where every term renders
|
|
@@ -20186,12 +20581,12 @@ function taskBodyParts(data) {
|
|
|
20186
20581
|
: attended.open !== null
|
|
20187
20582
|
? [
|
|
20188
20583
|
`<div class="card" data-attended="${escape(attended.open.id)}">`,
|
|
20189
|
-
`<p><strong>
|
|
20584
|
+
`<p><strong>Attended session</strong> <span class="meta">${escape(attended.open.state)}</span></p>`,
|
|
20190
20585
|
`<p class="meta">${attended.open.running ? "the agent is running — your open console keeps it live" : "waiting to dispatch to this machine"} · ${attended.open.turnsUsed}/${attended.open.cap} messages · $${(attended.open.spentMicrousd / 1_000_000).toFixed(2)} of $${(attended.open.budgetMicrousd / 1_000_000).toFixed(2)} · everything ends by ${whenTime(attended.open.expiresAt)}</p>`,
|
|
20191
|
-
`<p class="meta">
|
|
20586
|
+
`<p class="meta">This page being open keeps it alive — close it and the session winds down within a minute</p>`,
|
|
20192
20587
|
`<form method="post" action="${taskHref(task.id)}/attend-revoke" class="inline">`,
|
|
20193
20588
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
|
|
20194
|
-
`<button type="submit">
|
|
20589
|
+
`<button type="submit">Revoke — stop the session</button>`,
|
|
20195
20590
|
`</form>`,
|
|
20196
20591
|
`</div>`,
|
|
20197
20592
|
].join("\n")
|
|
@@ -20199,36 +20594,36 @@ function taskBodyParts(data) {
|
|
|
20199
20594
|
? [
|
|
20200
20595
|
`<form method="post" action="${taskHref(task.id)}/attend-preview" class="card">`,
|
|
20201
20596
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
|
|
20202
|
-
`<p><strong>
|
|
20203
|
-
`<p class="meta">
|
|
20597
|
+
`<p><strong>Or run it once while you watch</strong></p>`,
|
|
20598
|
+
`<p class="meta">No approval filed: one attempt, on this machine, only while this page is open. You will read every term before ${attended.mint?.quick === true ? "you confirm it" : "your password signs it"}.</p>`,
|
|
20204
20599
|
attended.mint === undefined || attended.mint.models.length <= 1
|
|
20205
20600
|
? ""
|
|
20206
|
-
: `<label>
|
|
20601
|
+
: `<label>Model <span class="meta">(watched sessions run claude only)</span><select name="model">` +
|
|
20207
20602
|
attended.mint.models
|
|
20208
20603
|
.map(model => `<option value="${escape(model)}"${model === attended.mint?.pinnedModel ? " selected" : ""}>${escape(model)}</option>`)
|
|
20209
20604
|
.join("") +
|
|
20210
20605
|
`</select></label>`,
|
|
20211
20606
|
attended.mint === undefined
|
|
20212
20607
|
? ""
|
|
20213
|
-
: `<label>
|
|
20608
|
+
: `<label>Permissions<select name="posture">` +
|
|
20214
20609
|
`<option value="auto"${attended.mint.posture === "auto" ? " selected" : ""}>safe unattended — routine project commands and edits proceed</option>` +
|
|
20215
20610
|
`<option value="acceptEdits"${attended.mint.posture === "acceptEdits" ? " selected" : ""}>legacy acceptEdits — commands that ask are denied unattended</option>` +
|
|
20216
20611
|
`<option value="bypassPermissions"${attended.mint.posture === "bypassPermissions" ? " selected" : ""}>full permissions — nothing asks</option>` +
|
|
20217
20612
|
`</select></label>`,
|
|
20218
|
-
`<button type="submit">
|
|
20613
|
+
`<button type="submit">Read the terms</button>`,
|
|
20219
20614
|
`</form>`,
|
|
20220
20615
|
].join("\n")
|
|
20221
20616
|
: "";
|
|
20222
20617
|
const scopeForm = [
|
|
20223
|
-
`<details${scope === null ||
|
|
20618
|
+
`<details${scope === null || scopeDraft !== undefined ? " open" : ""}><summary>${scope === null ? "Write the scope" : "Edit the scope"}${approval.approved ? " (editing voids the approval)" : ""}</summary>`,
|
|
20224
20619
|
`<form method="post" action="${taskHref(task.id)}/scope" class="scope-editor">`,
|
|
20225
|
-
|
|
20620
|
+
scopeDraft === undefined || data.problem === null ? "" : `<div class="problem" role="alert" id="scope-error">${escape(data.problem)}</div>`,
|
|
20226
20621
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
|
|
20227
|
-
`<input type="hidden" name="sawDigest" value="${escape(
|
|
20228
|
-
`<label>
|
|
20229
|
-
`<label>
|
|
20230
|
-
`<label>
|
|
20231
|
-
`<label>
|
|
20622
|
+
`<input type="hidden" name="sawDigest" value="${escape(scopeDraft?.get("sawDigest") ?? scope?.digest ?? "")}">`,
|
|
20623
|
+
`<label>Goal<textarea name="goal" rows="3"${scopeDraft === undefined ? "" : ' autofocus aria-describedby="scope-error"'}>${escape(scopeDraft?.get("goal") ?? scope?.goal ?? "")}</textarea></label>`,
|
|
20624
|
+
`<label>Not this<textarea name="not" rows="2">${escape(scopeDraft?.get("not") ?? scope?.outOfScope ?? "")}</textarea></label>`,
|
|
20625
|
+
`<label>Touches <span class="meta">(one per line)</span><textarea name="touches" rows="2">${escape(scopeDraft?.get("touches") ?? (scope?.touches ?? []).join("\n"))}</textarea></label>`,
|
|
20626
|
+
`<label>Acceptance <span class="meta">(required — one criterion per line: <code>statement | evidence,kinds | how</code>; evidence kinds are check, screenshot, changed-path, manual-review; id is optional and auto-numbered)</span><textarea name="acceptance" rows="3" placeholder="The button opens the settings panel | screenshot">${escape(scopeDraft?.get("acceptance") ?? acceptanceToLines(scope?.acceptance ?? []).join("\n"))}</textarea></label>`,
|
|
20232
20627
|
(() => {
|
|
20233
20628
|
const defaults = data.spendDefaults ?? null;
|
|
20234
20629
|
const budgetPrefill = scope?.budgetMicrousd != null
|
|
@@ -20236,8 +20631,8 @@ function taskBodyParts(data) {
|
|
|
20236
20631
|
: defaults?.buildPerRunMicrousd != null
|
|
20237
20632
|
? (defaults.buildPerRunMicrousd / 1_000_000).toFixed(2)
|
|
20238
20633
|
: "";
|
|
20239
|
-
return `<label>
|
|
20240
|
-
`<input type="number" name="budget-usd" step="0.01" min="0.01" value="${escape(
|
|
20634
|
+
return `<label>Agent-reported usage cap <span class="meta">(optional — leave blank for uncapped subscription work)</span>` +
|
|
20635
|
+
`<input type="number" name="budget-usd" step="0.01" min="0.01" value="${escape(scopeDraft?.get("budget-usd") ?? budgetPrefill)}" placeholder="no cap"></label>` +
|
|
20241
20636
|
`<p class="meta">Claude expresses this limiter in API-equivalent dollars even on a membership. It does not switch the run to API billing.</p>`;
|
|
20242
20637
|
})(),
|
|
20243
20638
|
(() => {
|
|
@@ -20248,14 +20643,14 @@ function taskBodyParts(data) {
|
|
|
20248
20643
|
: scope?.profile?.provider === "codex" || scope?.profile?.provider === "openrouter"
|
|
20249
20644
|
? scope.profile.sandboxMode === "danger-full-access" ? "bypassPermissions" : "auto"
|
|
20250
20645
|
: null;
|
|
20251
|
-
const selected =
|
|
20252
|
-
return `<fieldset class="permission-field"><legend>
|
|
20646
|
+
const selected = scopeDraft?.get("permission-mode") === "bypassPermissions" ? "bypassPermissions" : scopeDraft?.get("permission-mode") === "auto" ? "auto" : data.permissionMode ?? profileMode ?? data.permissionDefault ?? "auto";
|
|
20647
|
+
return `<fieldset class="permission-field"><legend>Agent permissions</legend>` +
|
|
20253
20648
|
permissionModeChoices("permission-mode", selected) +
|
|
20254
20649
|
`<p class="meta permission-note">This task’s choice is sealed into its scope. Full access prevents permission prompts or sandbox limits from pausing supported unattended agents.</p></fieldset>`;
|
|
20255
20650
|
})(),
|
|
20256
20651
|
(() => {
|
|
20257
|
-
const selected =
|
|
20258
|
-
return `<fieldset class="permission-field"><legend>
|
|
20652
|
+
const selected = scopeDraft?.get("quality-mode") === "strict" ? "strict" : scopeDraft?.get("quality-mode") === "default" ? "default" : scope?.qualityMode ?? data.qualityMode ?? data.qualityDefault ?? "default";
|
|
20653
|
+
return `<fieldset class="permission-field"><legend>Quality</legend>` +
|
|
20259
20654
|
qualityModeChoices("quality-mode", selected) +
|
|
20260
20655
|
`<p class="meta permission-note">This choice is signed into the scope. Inspect the saved work and actual check results when it is ready. Publication and deployment need their own authorization.</p></fieldset>`;
|
|
20261
20656
|
})(),
|
|
@@ -20266,8 +20661,8 @@ function taskBodyParts(data) {
|
|
|
20266
20661
|
// approval above restates and covers both.
|
|
20267
20662
|
const defaults = data.spendDefaults ?? null;
|
|
20268
20663
|
const terms = data.raceTerms ?? null;
|
|
20269
|
-
const selectedCount =
|
|
20270
|
-
const selectedModel =
|
|
20664
|
+
const selectedCount = scopeDraft === undefined ? terms !== null ? terms.n : defaults?.raceAgents ?? 0 : Number(scopeDraft.get("race-count") ?? "0");
|
|
20665
|
+
const selectedModel = scopeDraft?.get("race-model") ?? terms?.agents[0]?.model ?? "claude-sonnet-5";
|
|
20271
20666
|
const perPrefill = terms !== null
|
|
20272
20667
|
? (terms.perAgentBudgetMicrousd / 1_000_000).toFixed(2)
|
|
20273
20668
|
: defaults?.racePerAgentMicrousd != null
|
|
@@ -20279,31 +20674,31 @@ function taskBodyParts(data) {
|
|
|
20279
20674
|
? (defaults.raceTotalMicrousd / 1_000_000).toFixed(2)
|
|
20280
20675
|
: "";
|
|
20281
20676
|
return [
|
|
20282
|
-
`<details class="more-agents"${selectedCount > 0 ? " open" : ""}><summary>
|
|
20283
|
-
`<label>
|
|
20677
|
+
`<details class="more-agents"${selectedCount > 0 ? " open" : ""}><summary>More than one agent (optional)</summary>`,
|
|
20678
|
+
`<label>How many agents compete <span class="meta">(a tournament builds the task independently N times — you compare and pick one)</span>` +
|
|
20284
20679
|
`<select name="race-count">` +
|
|
20285
20680
|
`<option value=""${selectedCount === 0 ? " selected" : ""}>one agent — no tournament</option>` +
|
|
20286
20681
|
[2, 3, 4].map(count => `<option value="${count}"${selectedCount === count ? " selected" : ""}>${count} agents</option>`).join("") +
|
|
20287
20682
|
`</select></label>`,
|
|
20288
|
-
`<label>
|
|
20683
|
+
`<label>Competing model <select name="race-model">` +
|
|
20289
20684
|
PRICED_BUILD_MODELS.map(model => `<option value="${escape(model)}"${model === selectedModel ? " selected" : ""}>${escape(model)}</option>`).join("") +
|
|
20290
20685
|
`</select></label>`,
|
|
20291
|
-
`<label>
|
|
20292
|
-
`<label>
|
|
20686
|
+
`<label>Each competing agent may spend ($)<input type="number" name="race-per-usd" step="0.01" min="0.01" value="${escape(scopeDraft?.get("race-per-usd") ?? perPrefill)}"></label>`,
|
|
20687
|
+
`<label>The whole tournament may spend ($)<input type="number" name="race-total-usd" step="0.01" min="0.01" value="${escape(scopeDraft?.get("race-total-usd") ?? totalPrefill)}"></label>`,
|
|
20293
20688
|
// The comparison lanes (Phase 3 slice B): 2-4 rows, any registered
|
|
20294
20689
|
// provider, exact model required — no dollar fields exist. Blank
|
|
20295
20690
|
// rows are unused; filling any row files a comparison INSTEAD of a
|
|
20296
20691
|
// tournament, and mixing the two refuses in words.
|
|
20297
20692
|
`<p class="meta" style="margin-top:.75rem">or compare different agents side by side — no dollar caps; each agent's clock is its bound, and spend lands measured only where the harness reports dollars:</p>`,
|
|
20298
|
-
...[1, 2, 3, 4].map(lane => `<div class="row"><label>
|
|
20693
|
+
...[1, 2, 3, 4].map(lane => `<div class="row"><label>Agent ${lane} <select name="compare-provider-${lane}">` +
|
|
20299
20694
|
`<option value="">—</option>` +
|
|
20300
|
-
PROVIDER_IDS.map(provider => `<option value="${escape(provider)}"${
|
|
20695
|
+
PROVIDER_IDS.map(provider => `<option value="${escape(provider)}"${scopeDraft?.get(`compare-provider-${lane}`) === provider ? " selected" : ""}>${escape(provider)}</option>`).join("") +
|
|
20301
20696
|
`</select></label>` +
|
|
20302
|
-
`<label>
|
|
20697
|
+
`<label>Its exact model<input type="text" name="compare-model-${lane}" value="${escape(scopeDraft?.get(`compare-model-${lane}`) ?? "")}" placeholder="e.g. gemini-2.5-pro"></label></div>`),
|
|
20303
20698
|
`</details>`,
|
|
20304
20699
|
].join("\n");
|
|
20305
20700
|
})(),
|
|
20306
|
-
`<button type="submit">
|
|
20701
|
+
`<button type="submit">Save scope</button>`,
|
|
20307
20702
|
`</form></details>`,
|
|
20308
20703
|
].join("\n");
|
|
20309
20704
|
// 41_237 → "41k": token counts read at a glance; exactness lives on the run page.
|
|
@@ -20325,7 +20720,7 @@ function taskBodyParts(data) {
|
|
|
20325
20720
|
// duration · tokens · measured-or-unmeasured cost.
|
|
20326
20721
|
const runs = data.runs.length === 0
|
|
20327
20722
|
? ""
|
|
20328
|
-
: `<h2>
|
|
20723
|
+
: `<h2>Attempts</h2>` +
|
|
20329
20724
|
data.runs
|
|
20330
20725
|
.map(run => {
|
|
20331
20726
|
const bits = [
|
|
@@ -20518,20 +20913,20 @@ function taskBodyParts(data) {
|
|
|
20518
20913
|
})();
|
|
20519
20914
|
const decisions = data.decisions.length === 0
|
|
20520
20915
|
? ""
|
|
20521
|
-
: `<h2>
|
|
20916
|
+
: `<h2>Decisions</h2>` +
|
|
20522
20917
|
data.decisions
|
|
20523
20918
|
.map(decision => `<p class="row"><a href="/d/${decision.id}">${escape(decision.question)}</a> ` +
|
|
20524
20919
|
`<span class="meta">${escape(decision.state)}${isOverdue(decision, data.now) ? " · overdue" : ""}</span></p>`)
|
|
20525
20920
|
.join("\n");
|
|
20526
20921
|
const incidents = data.incidents.length === 0
|
|
20527
20922
|
? ""
|
|
20528
|
-
: `<h2>
|
|
20923
|
+
: `<h2>Incidents</h2>` +
|
|
20529
20924
|
data.incidents
|
|
20530
20925
|
.map(one => one.resolvedAt === null
|
|
20531
20926
|
? `<p class="row">${escape(incidentWords(one.kind))} ` +
|
|
20532
20927
|
`<form method="post" action="/i/${one.id}/resolve" class="inline">` +
|
|
20533
20928
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
|
|
20534
|
-
`<button type="submit">
|
|
20929
|
+
`<button type="submit">Resolve</button></form></p>`
|
|
20535
20930
|
: `<p class="row meta">${escape(incidentWords(one.kind))} — resolved by ${escape(one.resolvedBy ?? "?")}</p>`)
|
|
20536
20931
|
.join("\n");
|
|
20537
20932
|
const stalled = task.state === "failed" || data.incidents.some(one => one.resolvedAt === null);
|
|
@@ -20543,11 +20938,11 @@ function taskBodyParts(data) {
|
|
|
20543
20938
|
const waitsFor = data.waitsFor ?? [];
|
|
20544
20939
|
const candidates = (data.waitCandidates ?? []).filter(one => !waitsFor.some(existing => existing.id === one.id));
|
|
20545
20940
|
const waitRows = waitsFor
|
|
20546
|
-
.map(one => `<p class="row">${one.admitted ? `<a href="${taskHref(one.id)}" class="mono">${escape(one.id)}</a>` : `<span class="mono">${escape(one.id)}</span>`}${one.state === null ? "" : ` <span class="badge badge-${escape(one.state)}">${escape(one.state)}</span>`}` +
|
|
20941
|
+
.map(one => `<p class="row">${one.admitted ? `<a href="${taskHref(one.id)}" class="mono">${escape(one.id)}</a>` : `<span class="mono">${escape(one.id)}</span>`}${one.state === null ? "" : ` <span class="badge badge-${escape(one.state)}">${escape(sentenceCase(one.state))}</span>`}` +
|
|
20547
20942
|
`<form method="post" action="${taskHref(task.id)}/unblock" class="inline">` +
|
|
20548
20943
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
|
|
20549
20944
|
`<input type="hidden" name="on" value="${escape(one.id)}">` +
|
|
20550
|
-
`<button type="submit">
|
|
20945
|
+
`<button type="submit">Don't wait for this</button></form></p>`)
|
|
20551
20946
|
.join("\n");
|
|
20552
20947
|
const waitAdd = data.csrf === "" || candidates.length === 0
|
|
20553
20948
|
? ""
|
|
@@ -20556,11 +20951,11 @@ function taskBodyParts(data) {
|
|
|
20556
20951
|
`<select name="on" aria-label="task to wait for">` +
|
|
20557
20952
|
candidates.map(one => `<option value="${escape(one.id)}">${escape(one.id)} — ${escape(one.title)}</option>`).join("") +
|
|
20558
20953
|
`</select>` +
|
|
20559
|
-
`<button type="submit">
|
|
20954
|
+
`<button type="submit">Wait for this task</button>` +
|
|
20560
20955
|
`<span class="meta"> — this task starts only after it finishes</span></form>`;
|
|
20561
20956
|
const waitsForCard = waitRows === "" && waitAdd === ""
|
|
20562
20957
|
? ""
|
|
20563
|
-
: `<h2>
|
|
20958
|
+
: `<h2>Waits for</h2>${waitRows === "" ? `<p class="meta">Nothing — it starts when a worker is free</p>` : waitRows}${waitAdd}`;
|
|
20564
20959
|
// The acts bar (task page pass): every verb in one row under the title,
|
|
20565
20960
|
// the one that resolves this task's state first and primary — retry on a
|
|
20566
20961
|
// stalled task, build-next in the queue, plan-first with no scope. The
|
|
@@ -20569,8 +20964,10 @@ function taskBodyParts(data) {
|
|
|
20569
20964
|
// hold (the hold governs the NEXT start), and requeue refuses while a
|
|
20570
20965
|
// claim is live — so the words say exactly when each becomes real.
|
|
20571
20966
|
const canPlan = data.plan === null && !approval.approved && !data.claimed && task.state === "queued" && (data.coordinator === null || data.coordinator === undefined);
|
|
20572
|
-
|
|
20573
|
-
|
|
20967
|
+
// A failed task's status card carries Retry itself (the same requeue); here it would be a second ink act.
|
|
20968
|
+
const cardRetries = task.state === "failed" && data.assignment != null && data.csrf !== "";
|
|
20969
|
+
const primaryAct = stopControlsActive || cardRetries ? null : stalled && !data.claimed
|
|
20970
|
+
? { html: act("requeue", "Retry, keeping the branch and workspace"), why: "Resolves the incidents, clears the failed attempts and queues the task again. The branch and workspace are kept.", whyClass: "retry" }
|
|
20574
20971
|
: canPlan
|
|
20575
20972
|
? { html: act("plan", "plan first"), why: "plan first sends an agent to read the repository, ask you questions, and propose a scope — nothing builds until you approve it", whyClass: "plan" }
|
|
20576
20973
|
: data.plan === "requested"
|
|
@@ -20581,7 +20978,7 @@ function taskBodyParts(data) {
|
|
|
20581
20978
|
const holdAct = `<form method="post" action="${taskHref(task.id)}/hold" class="inline act-hold">` +
|
|
20582
20979
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
|
|
20583
20980
|
`<input type="text" name="reason" class="inline" placeholder="reason (optional)" aria-label="hold reason">` +
|
|
20584
|
-
`<button type="submit">
|
|
20981
|
+
`<button type="submit">Hold the next attempt</button></form>`;
|
|
20585
20982
|
const canHold = task.state === "queued" || task.state === "running" || task.state === "failed";
|
|
20586
20983
|
const actsBar = [
|
|
20587
20984
|
`<span id="task-actions"></span><div class="acts-bar">`,
|
|
@@ -20598,7 +20995,7 @@ function taskBodyParts(data) {
|
|
|
20598
20995
|
`</div>`,
|
|
20599
20996
|
primaryAct === null ? "" : `<p class="meta acts-why acts-why-${primaryAct.whyClass}">${primaryAct.why}</p>`,
|
|
20600
20997
|
data.claimed && !stopControlsActive
|
|
20601
|
-
? `<p class="meta acts-why">a worker is building this right now — <em>
|
|
20998
|
+
? `<p class="meta acts-why">a worker is building this right now — <em>Hold the next attempt</em> stops the one after it; cancel waits for the current build to finish${stalled ? "; retry becomes available after this attempt finishes" : ""}</p>`
|
|
20602
20999
|
: "",
|
|
20603
21000
|
].join("\n");
|
|
20604
21001
|
// Cancel gets the same ceremony as an irreversible answer: armed behind
|
|
@@ -20620,9 +21017,10 @@ function taskBodyParts(data) {
|
|
|
20620
21017
|
const section = (title, html, open, count, id = title.replace(/\s+/g, "-")) => {
|
|
20621
21018
|
if (html === "")
|
|
20622
21019
|
return "";
|
|
20623
|
-
const
|
|
20624
|
-
|
|
20625
|
-
|
|
21020
|
+
const heading = sentenceCase(title);
|
|
21021
|
+
const inner = html.replace(`<h2>${title}</h2>`, "").replace(`<h2>${heading}</h2>`, "");
|
|
21022
|
+
sectionParts.push({ id, title: heading, html: inner, open, count: count ?? null });
|
|
21023
|
+
return `<details class="section" id="${id}"${open ? " open" : ""}><summary><h2>${heading}${count === undefined ? "" : ` <span class="lane-count">${count}</span>`}</h2></summary>` + inner + `</details>`;
|
|
20626
21024
|
};
|
|
20627
21025
|
const receiptLeads = data.assignment == null && task.state === "done" && data.completion?.receipt != null;
|
|
20628
21026
|
// Exact identity stays available in Task options; failures stay in the
|
|
@@ -20634,7 +21032,7 @@ function taskBodyParts(data) {
|
|
|
20634
21032
|
? ` · filed by <span class="mono">${escape(data.coordinator.label)}</span>${data.coordinator.filedAgo === null ? "" : ` ${escape(data.coordinator.filedAgo)}`}`
|
|
20635
21033
|
: data.filedVia === null || data.filedVia === undefined
|
|
20636
21034
|
? ""
|
|
20637
|
-
: ` · filed via ${escape(data.filedVia)}`}${data.deliverable === "report" ? ` · <span class="badge">
|
|
21035
|
+
: ` · filed via ${escape(data.filedVia)}`}${data.deliverable === "report" ? ` · <span class="badge">Scout</span>` : ""}</p>`;
|
|
20638
21036
|
// A pull request that can be merged (or opened) owns the one primary action; the result stays in its section.
|
|
20639
21037
|
const pullRequestActs = data.csrf !== "" && (data.pullRequest?.view?.canMerge === true || (data.pullRequest?.view == null && data.pullRequest?.offer != null));
|
|
20640
21038
|
const pullRequestFact = data.pullRequest?.fact != null && data.pullRequest.view?.runId === data.assignment?.receipt?.runId ? data.pullRequest.fact : undefined;
|
|
@@ -20675,12 +21073,12 @@ function taskBodyParts(data) {
|
|
|
20675
21073
|
: "gone from the tracker";
|
|
20676
21074
|
const reopenable = mirror.remoteState === "open" && !mirror.dispatchOk && mirror.closeGeneration !== null &&
|
|
20677
21075
|
mirror.syncGeneration > mirror.closeGeneration && ["cancelled", "failed", "queued"].includes(task.state) && data.csrf !== "";
|
|
20678
|
-
return (`<div class="card"><p><strong>
|
|
21076
|
+
return (`<div class="card"><p><strong>External work</strong> <span class="meta">${link} · ${escape(state)}</span></p>` +
|
|
20679
21077
|
(reopenable
|
|
20680
21078
|
? `<form method="post" action="${taskHref(task.id)}/reopen" class="row">` +
|
|
20681
21079
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
|
|
20682
21080
|
`<input type="password" name="token" placeholder="your password" aria-label="your password" autocomplete="current-password">` +
|
|
20683
|
-
`<button type="submit">
|
|
21081
|
+
`<button type="submit">Reopen — the approved scope stands</button></form>`
|
|
20684
21082
|
: "") +
|
|
20685
21083
|
`</div>`);
|
|
20686
21084
|
})();
|
|
@@ -20694,7 +21092,7 @@ function taskBodyParts(data) {
|
|
|
20694
21092
|
// is as fenced as the builder on a coordinator filing, so "plan
|
|
20695
21093
|
// first" would recommend a road that refuses.
|
|
20696
21094
|
? `<div class="card"><p><strong>This task is waiting on you: an agent filed it, and it has no scope.</strong></p>` +
|
|
20697
|
-
`<p class="meta">
|
|
21095
|
+
`<p class="meta">Filed by <span class="mono">${escape(data.coordinator.label)}</span> — nothing plans, claims, or runs until you write a scope below and sign it. Your signature runs their request.</p></div>`
|
|
20698
21096
|
: `<div class="card task-scope-needed"><p><strong>No approved scope yet</strong></p>` +
|
|
20699
21097
|
`<p class="meta"><strong>Plan first</strong> drafts it from the repository, or <a href="#scope">write it yourself</a>.</p></div>`
|
|
20700
21098
|
: "";
|
|
@@ -20729,7 +21127,7 @@ function taskBodyParts(data) {
|
|
|
20729
21127
|
attemptPanel,
|
|
20730
21128
|
data.coordinatorProposals == null || data.coordinatorProposals.rows.length === 0
|
|
20731
21129
|
? ""
|
|
20732
|
-
: section("proposals", `<h2>
|
|
21130
|
+
: section("proposals", `<h2>Proposed by coordinators</h2>` +
|
|
20733
21131
|
coordinatorProposalsSection(data.coordinatorProposals.rows, data.coordinatorProposals.decisions, data.csrf, data.coordinatorProposals.now, false).replace(/<form method="post" action="\/proposals\/(\d+)\/(confirm|dismiss)" class="inline">/g, (_m, id, verb) => `<form method="post" action="/proposals/${id}/${verb}" class="inline"><input type="hidden" name="return" value="${escape(taskHref(data.task.id))}">`), true, data.coordinatorProposals.rows.length),
|
|
20734
21132
|
section("decisions", decisions, true, data.decisions.length),
|
|
20735
21133
|
section("incidents", incidents, true, data.incidents.length),
|
|
@@ -20741,13 +21139,13 @@ function taskBodyParts(data) {
|
|
|
20741
21139
|
? ` · CI ${data.publication.lastCheckState} at last observation`
|
|
20742
21140
|
: " · no checks observed"}</span></p>`,
|
|
20743
21141
|
section("report", reportCard, true),
|
|
20744
|
-
data.assignment == null ? section("attempts", runs, true, data.runs.length) : section("Build activity", runs.replace("<h2>
|
|
21142
|
+
data.assignment == null ? section("attempts", runs, true, data.runs.length) : section("Build activity", runs.replace("<h2>Attempts</h2>", ""), false, data.runs.length, "attempts"),
|
|
20745
21143
|
section("usage", spendCard, false),
|
|
20746
21144
|
section("steering", steeringCard, (data.steering ?? []).length > 0, (data.steering ?? []).length),
|
|
20747
21145
|
section("scope",
|
|
20748
21146
|
// The recipe road rides with the scope it reuses (UI polish
|
|
20749
21147
|
// 2026-09-13), off the title-to-action path.
|
|
20750
|
-
["<h2>
|
|
21148
|
+
["<h2>Scope</h2>", scopeCard, data.repo !== null && data.scope !== null ? `<p class="meta"><a href="/recipes/from-task?task=${encodeURIComponent(task.id)}">Reuse this scope as a recipe →</a></p>` : "", agentsCardHtml(task.id, data.route, data.csrf, data.canEditRoute === true), revisionInApproval ? "" : revisionCard, data.completion != null ? "" : repairChainHtml(data.repairChain ?? null), attendedCard, scopeForm].join("\n"), scopeDraft !== undefined || (data.plan !== "requested" && approveForm === "" && !(scope === null && canPlan))),
|
|
20751
21149
|
dependencyChoiceNeeded ? "" : section("waits for", waitsForCard, (data.waitsFor ?? []).length > 0, (data.waitsFor ?? []).length),
|
|
20752
21150
|
section("holds", holds, true, data.holds.length),
|
|
20753
21151
|
cancelAct,
|
|
@@ -20767,7 +21165,19 @@ function taskBodyParts(data) {
|
|
|
20767
21165
|
thread.push({ key: "filed", at: rootFiled, kind: "filed", who: "person", author: filedBy ?? "", title: "Filed the task",
|
|
20768
21166
|
text: (() => { const goal = data.family?.root.goal ?? scope?.goal ?? null; return goal === null ? null : oneLineOf(goal, 280); })(), link: null, html: "", more: null });
|
|
20769
21167
|
const newestBuilt = familyRuns.filter(run => run.outcome === "built" && run.role !== "planner" && run.role !== "reviewer").sort((a, b) => b.id - a.id)[0];
|
|
21168
|
+
// Failed: the card says what went wrong in one line, and its one act is Retry itself (the same requeue), never a link to
|
|
21169
|
+
// this page; someone who can't retry here (a viewer, a live claim) gets no act at all.
|
|
21170
|
+
const statusCard = data.assignment != null && assignmentOptions !== null ? assignmentCardOf(data.assignment, assignmentOptions) : null;
|
|
21171
|
+
// A live build says which step it is on, or which step it is stuck on and why.
|
|
21172
|
+
const building = statusCard?.status.headline === "Building" && liveRun !== undefined && liveRun.role !== "planner" && liveRun.role !== "reviewer";
|
|
21173
|
+
// While a build runs, the attempts that stopped before it fold into one quiet line under its Building card. Only
|
|
21174
|
+
// there: with any other card (or none) they stay in the thread, never vanish.
|
|
21175
|
+
const earlierStopped = !building ? []
|
|
21176
|
+
: familyRuns.filter(run => run.id < liveRun.id && (run.role === "builder" || run.role === "scout") && run.outcome !== "built" && run.outcome !== "no-change" && run.id !== liveHistoryRunId);
|
|
21177
|
+
const foldedAttempts = new Set(earlierStopped.map(run => run.id));
|
|
20770
21178
|
for (const run of familyRuns) {
|
|
21179
|
+
if (foldedAttempts.has(run.id))
|
|
21180
|
+
continue;
|
|
20771
21181
|
const live = run.id === liveHistoryRunId || (run.outcome === null && run.id === liveRunId);
|
|
20772
21182
|
const noun = runNoun(run);
|
|
20773
21183
|
const href = { label: `${noun[0].toUpperCase()}${noun.slice(1)} #${run.id}`, href: `/r/${run.id}` };
|
|
@@ -20791,9 +21201,9 @@ function taskBodyParts(data) {
|
|
|
20791
21201
|
: run.outcome === null ? "Attempt never finished"
|
|
20792
21202
|
: `${noun[0].toUpperCase()}${noun.slice(1)} ${run.outcome}`;
|
|
20793
21203
|
thread.push({ key: `run-${run.id}`, at: run.finishedAt ?? run.startedAt, kind: "result", who: "agent", author: run.runner,
|
|
20794
|
-
title: `${title}${revision}`, text: run.handoff === null ? (run.reason === null || run.outcome === "built" ? null : reasonWords(run.reason)) : oneLineOf(run.handoff, 600),
|
|
21204
|
+
title: `${title}${revision}`, text: run.handoff === null ? (run.outcome === "failed" ? failedAttemptSentence(run.reason) : run.reason === null || run.outcome === "built" ? null : reasonWords(run.reason)) : oneLineOf(run.handoff, 600),
|
|
20795
21205
|
link: href, html: "",
|
|
20796
|
-
more: previousResult !== null && run.id === newestBuilt?.id && run.taskId === task.id ? { summary:
|
|
21206
|
+
more: previousResult !== null && run.id === newestBuilt?.id && run.taskId === task.id ? { summary: STATUS_MORE, html: previousResult.html } : null });
|
|
20797
21207
|
}
|
|
20798
21208
|
// What the agent is working through now, and how its plan changed.
|
|
20799
21209
|
const liveAt = liveRun?.startedAt ?? task.updatedAt;
|
|
@@ -20809,7 +21219,7 @@ function taskBodyParts(data) {
|
|
|
20809
21219
|
const feedback = data.revision;
|
|
20810
21220
|
thread.push({ key: "revision", at: task.createdAt, kind: "reply", who: "person", author: feedback.comments[0]?.author ?? "You",
|
|
20811
21221
|
title: feedback.kind === "ci-repair" ? "Asked for a CI repair" : feedback.kind === "criterion-repair" ? "Asked for a repair" : "Asked for changes",
|
|
20812
|
-
text: feedback.comments.map(one => `${one.path === null ? "" : `${one.path}${one.line === null ? "" : `:${one.line}`} — `}${one.note}`).join("\n"),
|
|
21222
|
+
text: feedback.comments.map(one => `${one.path === null ? "" : `${one.path}${one.line === null ? "" : `:${one.line}`} — `}${one.note.trim()}`).filter(one => one !== "").join("\n") || null,
|
|
20813
21223
|
link: { label: `From build #${feedback.sourceRun}`, href: `/r/${feedback.sourceRun}` }, html: "", more: null });
|
|
20814
21224
|
}
|
|
20815
21225
|
for (const decision of data.decisions) {
|
|
@@ -20886,6 +21296,45 @@ function taskBodyParts(data) {
|
|
|
20886
21296
|
// needs a person, then facts, then folds; the mechanics under Manage.
|
|
20887
21297
|
const MANAGE = new Set(["steering", "waits-for", "holds"]);
|
|
20888
21298
|
const finished = task.state === "done" || task.state === "cancelled";
|
|
21299
|
+
const failedCard = statusCard?.status.headline === "Failed";
|
|
21300
|
+
// A link to this very page, with no section to open, goes nowhere: a card never offers one.
|
|
21301
|
+
const bareSelfLink = (href) => {
|
|
21302
|
+
if (href.includes("#"))
|
|
21303
|
+
return false;
|
|
21304
|
+
const [path, query = ""] = href.split("?");
|
|
21305
|
+
const version = new URLSearchParams(query).get("version");
|
|
21306
|
+
return [data.assignment?.rootId, data.rootId, task.id].some(id => id != null && path === `/t/${encodeURIComponent(id)}`) && (version === null || version === task.id);
|
|
21307
|
+
};
|
|
21308
|
+
// Build again, in place, names its form after the card's own act.
|
|
21309
|
+
const rebuild = data.csrf !== "" && data.assignment?.primaryAction?.code === "retry-task" && data.assignment.need != null && "key" in data.assignment.need && data.assignment.need.key === "rebuild"
|
|
21310
|
+
? { action: `${taskHref(data.assignment.rootId)}/requeue` } : null;
|
|
21311
|
+
const retry = failedCard && task.state === "failed" && !data.claimed && !stopControlsActive && data.csrf !== ""
|
|
21312
|
+
? { action: `${taskHref(task.id)}/requeue`, ...(data.failure == null ? {} : { note: retryNoteOf(data.failure.suggestion) }) } : null;
|
|
21313
|
+
// Run checks happens in place: a status row's Run checks posts here and comes back; with nothing to run, the row offers nothing.
|
|
21314
|
+
const runChecks = data.runChecks ?? null;
|
|
21315
|
+
// A row's link to a result in Chat opens that result's own page instead: Chat would only lead back here.
|
|
21316
|
+
const resultPageOf = (href) => {
|
|
21317
|
+
const chat = href === null ? null : /^\/chat\?task=([A-Za-z0-9._~%-]{1,200})&result=([0-9]{1,15})(?:&tab=(changes|checks))?(#[A-Za-z0-9_-]{1,80})?$/.exec(href);
|
|
21318
|
+
return chat === null ? href : `/review?result=${chat[1]}&run=${chat[2]}${chat[3] === undefined ? "" : `&tab=${chat[3]}`}${chat[4] ?? ""}`;
|
|
21319
|
+
};
|
|
21320
|
+
const inPlaceChecks = (card) => {
|
|
21321
|
+
const status = data.demo === true ? demoChecksOf(card.status) : card.status;
|
|
21322
|
+
return { ...card, status: { ...status, details: status.details.map(one => one.action?.href?.endsWith("#follow-ups") === true ? { ...one, action: runChecks === null ? null : { label: one.action.label, href: runChecks.action } }
|
|
21323
|
+
: { ...one, href: resultPageOf(one.href), action: one.action === null ? null : { ...one.action, href: resultPageOf(one.action.href) } }) } };
|
|
21324
|
+
};
|
|
21325
|
+
const steps = building ? buildProgressOf(data.milestoneProgress ?? null) : null;
|
|
21326
|
+
const progress = steps === null ? null : { line: steps.line,
|
|
21327
|
+
stuck: steps.stuck === null ? null : { ...steps.stuck, action: data.csrf === "" ? null : { label: "Send the agent a note", href: "#steering" } } };
|
|
21328
|
+
// The Building card carries Stop for the build it describes (the same form), in place of a second card below it.
|
|
21329
|
+
const stop = building && data.control?.kind === "stop" && data.control.run === liveRun.id && data.csrf !== ""
|
|
21330
|
+
? { action: `${taskHref(task.id)}/stop`, run: data.control.run } : null;
|
|
21331
|
+
// The Building card keeps its way to the build's own record (/r/<id>), whatever act it shows.
|
|
21332
|
+
const record = building ? { label: `Build #${liveRun.id} record`, href: `/r/${liveRun.id}` } : null;
|
|
21333
|
+
const earlier = earlierStopped.length === 0 ? null : {
|
|
21334
|
+
summary: earlierAttemptsWords(earlierStopped.length),
|
|
21335
|
+
attempts: [...earlierStopped].sort((a, b) => a.id - b.id).map(run => ({ label: `${runNoun(run)[0].toUpperCase()}${runNoun(run).slice(1)} #${run.id}`, href: `/r/${run.id}`,
|
|
21336
|
+
text: run.outcome === null ? "Never finished." : run.outcome === "failed" ? failedAttemptSentence(run.reason) : run.reason === null ? null : (words => `${words.charAt(0).toUpperCase()}${words.slice(1)}.`)(reasonWords(run.reason)) })),
|
|
21337
|
+
};
|
|
20889
21338
|
const view = {
|
|
20890
21339
|
kind: "task",
|
|
20891
21340
|
id: task.id,
|
|
@@ -20897,16 +21346,24 @@ function taskBodyParts(data) {
|
|
|
20897
21346
|
{ label: "Ask", href: taskChatHref(data.rootId ?? task.id), active: false },
|
|
20898
21347
|
],
|
|
20899
21348
|
version: data.versionLabel == null ? null : { label: data.versionLabel, current: { label: "Current work", href: taskHref(data.rootId ?? task.id) } },
|
|
20900
|
-
status:
|
|
21349
|
+
status: statusCard === null ? null : inPlaceChecks(failedCard ? { ...statusCard, action: null, status: { ...statusCard.status, sentence: data.failure?.line ?? statusCard.status.sentence } }
|
|
21350
|
+
: progress !== null || record !== null ? { ...statusCard, action: statusCard.action?.href === record?.href ? null : statusCard.action, status: { ...statusCard.status, sentence: progress?.line ?? statusCard.status.sentence } }
|
|
21351
|
+
: statusCard.action !== null && rebuild === null && bareSelfLink(statusCard.action.href) ? { ...statusCard, action: null } : statusCard),
|
|
20901
21352
|
statusHtml,
|
|
21353
|
+
failure: failedCard && data.failure != null ? { line: data.failure.line, evidence: data.failure.evidence, suggestion: data.failure.suggestion, link: data.failure.link } : null,
|
|
21354
|
+
retry,
|
|
21355
|
+
runChecks,
|
|
21356
|
+
progress,
|
|
21357
|
+
stop,
|
|
21358
|
+
record,
|
|
21359
|
+
earlier,
|
|
20902
21360
|
approval: approvalHtml,
|
|
20903
21361
|
confirmStopped: data.csrf !== "" && data.assignment?.primaryAction?.code === "confirm-stopped" && data.assignment.primaryAction.target.runId !== null
|
|
20904
21362
|
? { action: `${taskHref(data.assignment.rootId)}/confirm-stopped`, run: data.assignment.primaryAction.target.runId, checked: needsCheck(data.assignment) } : null,
|
|
20905
|
-
rebuild
|
|
20906
|
-
? { action: `${taskHref(data.assignment.rootId)}/requeue` } : null,
|
|
21363
|
+
rebuild,
|
|
20907
21364
|
// The plan, progress and plan changes are thread entries now; the rest still needs a person here.
|
|
20908
21365
|
lead: [
|
|
20909
|
-
{ key: "history", html: data.history ?? "" }, { key: "control", html: controlHtml }, { key: "problem", html: problemHtml },
|
|
21366
|
+
{ key: "history", html: data.history ?? "" }, { key: "control", html: stop === null ? controlHtml : taskControlDetailsHtml(data.control, task.id, data.csrf, "task", false, true) }, { key: "problem", html: problemHtml },
|
|
20910
21367
|
{ key: "pull-request", html: pullRequestCard },
|
|
20911
21368
|
{ key: "needs-scope", html: needsScopeCard },
|
|
20912
21369
|
{ key: "mirror", html: mirrorCard }, { key: "contest", html: contestCard }, { key: "attempt", html: attemptPanel },
|
|
@@ -20917,7 +21374,7 @@ function taskBodyParts(data) {
|
|
|
20917
21374
|
sections: [
|
|
20918
21375
|
...(previousResult === null || thread.some(one => one.more !== null) ? [] : [{ id: "result", title: previousResult.title, html: previousResult.html, open: false, count: null }]),
|
|
20919
21376
|
// Questions are in the thread; the scope and the ledgers fold here until asked.
|
|
20920
|
-
...sectionParts.filter(one => !MANAGE.has(one.id) && one.id !== "decisions").map(one => one.id === "scope" &&
|
|
21377
|
+
...sectionParts.filter(one => !MANAGE.has(one.id) && one.id !== "decisions").map(one => one.id === "scope" && scopeDraft === undefined && (finished || approval.approved) ? { ...one, open: false } : one),
|
|
20921
21378
|
],
|
|
20922
21379
|
manage: [
|
|
20923
21380
|
...sectionParts.filter(one => MANAGE.has(one.id)),
|
|
@@ -21363,26 +21820,27 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
|
|
|
21363
21820
|
const intent = view.intent;
|
|
21364
21821
|
let intentView = null;
|
|
21365
21822
|
if (intent === null) {
|
|
21366
|
-
parts.push(`<section class="card cockpit-section" data-cockpit-section="intent"><h3>
|
|
21823
|
+
parts.push(`<section class="card cockpit-section" data-cockpit-section="intent"><h3>Approved scope</h3><p class="meta">No scope was filed for this task, so there is no approved goal or boundary to review.</p></section>`);
|
|
21367
21824
|
}
|
|
21368
21825
|
else {
|
|
21826
|
+
// The time itself goes to the page as a stamp: the one formatter words it in the viewer's zone.
|
|
21369
21827
|
const approvalWords = intent.approval.approved
|
|
21370
|
-
? `
|
|
21828
|
+
? `Approved by ${intent.approvedBy ?? "an operator"}`
|
|
21371
21829
|
: intent.approval.reason === "changed"
|
|
21372
|
-
? "
|
|
21373
|
-
: "
|
|
21830
|
+
? "The scope changed after approval. The words below are the current text, not the signed one."
|
|
21831
|
+
: "Never approved. The result was built without a signed scope.";
|
|
21374
21832
|
const approval = intent.approval.approved
|
|
21375
|
-
? `
|
|
21376
|
-
: approvalWords;
|
|
21377
|
-
const body = `<p class="recap" style="margin-top:.25rem"><strong>
|
|
21378
|
-
(intent.outOfScope === null ? `<p class="meta">
|
|
21833
|
+
? `Approved by ${escape(intent.approvedBy ?? "an operator")} · ${whenTime(intent.approval.at)}`
|
|
21834
|
+
: escape(approvalWords);
|
|
21835
|
+
const body = `<p class="recap" style="margin-top:.25rem"><strong>Goal</strong> ${escape(intent.goal)}</p>` +
|
|
21836
|
+
(intent.outOfScope === null ? `<p class="meta">No boundary was stated</p>` : `<p class="recap"><strong>Not this</strong> ${escape(intent.outOfScope)}</p>`) +
|
|
21379
21837
|
(intent.touches.length === 0
|
|
21380
|
-
? `<p class="meta">
|
|
21381
|
-
: `<p class="row"><span class="meta">
|
|
21838
|
+
? `<p class="meta">No expected paths were signed — every changed file reads as in bounds</p>`
|
|
21839
|
+
: `<p class="row"><span class="meta">Expected to touch</span> ${intent.touches.map(one => `<span class="mono">${escape(one)}</span>`).join(" ")}</p>`) +
|
|
21382
21840
|
(view.plan === null
|
|
21383
21841
|
? ""
|
|
21384
|
-
: `<p class="meta">
|
|
21385
|
-
intentView = { approval: approvalWords, html: body };
|
|
21842
|
+
: `<p class="meta">Plan revision ${view.plan.revision} · <span class="mono" title="${escape(view.plan.sha256)}">${escape(view.plan.sha256.slice(0, 12))}…</span>${view.plan.approach === null ? "" : ` — ${escape(view.plan.approach)}`}</p>`);
|
|
21843
|
+
intentView = { approval: approvalWords, approvedAt: intent.approval.approved ? intent.approval.at ?? null : null, html: body };
|
|
21386
21844
|
parts.push(`<details class="card cockpit-section cockpit-disclosure" data-cockpit-section="intent"><summary><h3>Approved scope<small>What this build was asked to do · ${approval}</small></h3><span class="cockpit-disclosure-action">View</span></summary>` +
|
|
21387
21845
|
`<div class="cockpit-disclosure-body">` + body + `</div></details>`);
|
|
21388
21846
|
}
|
|
@@ -21393,7 +21851,7 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
|
|
|
21393
21851
|
problem: view.detail === null ? view.historyProblem : null,
|
|
21394
21852
|
next: reviewNextActionOf(view, csrf), complete: null, decision: null, checks: null, intent: intentView, noRun: null, panel: null, contest: "",
|
|
21395
21853
|
notes: view.notes.map(one => ({ author: one.author, at: one.createdAt, note: one.note })),
|
|
21396
|
-
acts: { primary: null, secondary: null, line: null }, runChecks: null,
|
|
21854
|
+
acts: { primary: null, secondary: null, line: null }, runChecks: null, mismatch: null,
|
|
21397
21855
|
// The raw run record lives under Details now (2026-10-02): /r/<id> for this result redirects here.
|
|
21398
21856
|
record: run === null ? null : { build: run.id, href: `/r/${run.id}?record=1`, facts: [
|
|
21399
21857
|
{ label: "Agent", value: [run.provider, run.model].filter(Boolean).join(" · ") },
|
|
@@ -21404,7 +21862,7 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
|
|
|
21404
21862
|
] },
|
|
21405
21863
|
};
|
|
21406
21864
|
if (run === null || view.detail === null) {
|
|
21407
|
-
parts.push(`<section class="card cockpit-section" id="verification" data-cockpit-section="proof"><h3>
|
|
21865
|
+
parts.push(`<section class="card cockpit-section" id="verification" data-cockpit-section="proof"><h3>Verification</h3>` +
|
|
21408
21866
|
`<p class="meta">This task has no finished build record, so there are no captured changes or checks to review.</p>` +
|
|
21409
21867
|
`<p class="row"><a href="${taskHref(view.taskId)}">Open the task →</a></p></section>`);
|
|
21410
21868
|
return { html: `<section class="cockpit-detail">${parts.join("\n")}</section>`, selected: { ...selected, noRun: "This task has no finished build record, so there are no captured changes or checks to review." } };
|
|
@@ -21437,24 +21895,31 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
|
|
|
21437
21895
|
});
|
|
21438
21896
|
const contest = view.contest === null ? "" : `<p class="row"><a href="/contest/${view.contest.id}">${view.contest.state === "pick-wait" ? `compare the ${contestNoun(view.contest.kind)} and pick →` : `the ${contestNoun(view.contest.kind)} (${escape(view.contest.state)}) →`}</a></p>`;
|
|
21439
21897
|
parts.push(`<div id="verification" data-cockpit-section="result">` + panel.html + contest + `</div>`);
|
|
21898
|
+
const blocked = cantAcceptYetOf(proof?.verdict ?? null, proof?.reasons ?? [], accepted);
|
|
21899
|
+
const youCheck = panel.panel.youCheck;
|
|
21900
|
+
// Accept and finish posts the completion; when an acceptance is owed (the person's own checks, or a reason a
|
|
21901
|
+
// report that doesn't match its changes asks for), the same request records it first.
|
|
21902
|
+
const owed = accepted ? null : youCheck?.accept != null ? { note: null } : panel.panel.need?.accept != null ? { note: panel.panel.need.accept.note }
|
|
21903
|
+
: blocked === ACCEPT_NEEDS_REASON ? { note: "Why is this safe to accept?" } : null;
|
|
21440
21904
|
const complete = assignment?.state === "ready-to-check" && assignment.receipt !== null && canRetryReview && csrf !== ""
|
|
21441
|
-
? { action: `${taskHref(view.taskId)}/complete`, receipt: assignment.receipt.digest, run: run.id } : null;
|
|
21905
|
+
? { action: `${taskHref(view.taskId)}/complete`, receipt: assignment.receipt.digest, run: run.id, accept: owed } : null;
|
|
21442
21906
|
if (complete !== null)
|
|
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;
|
|
21907
|
+
parts.push(completionForm(view.taskId, run.id, complete.receipt, csrf, view.detail?.pullRequestTo ?? null, owed));
|
|
21908
|
+
// The one decision, after the evidence: Accept and finish only when every requirement is met and the checks passed.
|
|
21909
|
+
const acceptsHere = complete !== null || panel.panel.need?.accept != null || youCheck?.accept != null;
|
|
21446
21910
|
const matrix = proof === null || proof.proofProblem !== null ? [] : proof.matrix;
|
|
21447
|
-
const
|
|
21448
|
-
const
|
|
21449
|
-
|
|
21450
|
-
|
|
21451
|
-
|
|
21452
|
-
|
|
21453
|
-
|
|
21454
|
-
|
|
21455
|
-
|
|
21456
|
-
|
|
21457
|
-
const decision =
|
|
21911
|
+
const unanswered = accepted || youCheck == null ? [] : youCheck.items.length > 0 ? youCheck.items.map(one => one.statement === "" ? one.words : one.statement) : youCheck.lines;
|
|
21912
|
+
const base = !acceptsHere ? null : acceptWordsOf({
|
|
21913
|
+
checks: checks === undefined ? null : checks.running != null ? "running" : checks.level === "off" && checks.status !== "passed" ? "off" : checks.status,
|
|
21914
|
+
unmet: matrix.filter(row => row.state !== "pass" && row.state !== "manual-review").length,
|
|
21915
|
+
action: complete !== null ? "complete" : "accept",
|
|
21916
|
+
publishing: view.detail?.publishing ?? "other",
|
|
21917
|
+
proof: proof !== null && proof.proof !== null && proof.proofProblem === null,
|
|
21918
|
+
});
|
|
21919
|
+
// Refuted: plain Accept and finish stays as allowed, in outline, and the one line before the acts (acts.line) says why.
|
|
21920
|
+
const settled = base === null || blocked === null ? base : { ...base, label: "Accept and finish", ready: false };
|
|
21921
|
+
const decision = settled === null ? null : { sentence: RESULT_DECISION_SENTENCE, ...settled, ...acceptWithChecksOf(settled, { unanswered, notRight: [] }),
|
|
21922
|
+
base: { label: settled.label, ready: settled.ready, why: settled.why } };
|
|
21458
21923
|
// 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
21924
|
const followUps = view.detail.followUps ?? null;
|
|
21460
21925
|
const checksRunning = checks?.running != null || (followUps?.checks.some(one => one.state === "waiting" || one.state === "running") ?? false);
|
|
@@ -21464,35 +21929,107 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
|
|
|
21464
21929
|
? { action: `/r/${run.id}/checks`, level: followUps.full ? "full" : "quick", returnTo: `${here}&run=${run.id}&tab=checks` } : null;
|
|
21465
21930
|
const need = panel.panel.need;
|
|
21466
21931
|
const nextKind = selected.next?.kind;
|
|
21467
|
-
|
|
21468
|
-
|
|
21469
|
-
|
|
21470
|
-
|
|
21471
|
-
|
|
21472
|
-
|
|
21473
|
-
|
|
21474
|
-
|
|
21475
|
-
|
|
21932
|
+
// A failed build: what went wrong is the card, Retry the act; a link to this same page goes nowhere, so it isn't one.
|
|
21933
|
+
const failure = view.failure == null ? null : { line: view.failure.line, evidence: view.failure.evidence, suggestion: view.failure.suggestion, retry: view.failure.retry,
|
|
21934
|
+
link: view.failure.link === null || (view.failure.link.href.startsWith(`${here}&run=${run.id}`) && !view.failure.link.href.includes("#")) ? null : view.failure.link,
|
|
21935
|
+
...(view.failure.acceptAnyway == null ? {} : { acceptAnyway: { ...view.failure.acceptAnyway, returnTo: `${here}&run=${run.id}` } }) };
|
|
21936
|
+
const actFacts = failure !== null
|
|
21937
|
+
? { accept: null, runChecks: runChecks !== null, checksRunning, blocked: null, canRequest: false, need: null, next: null, failed: { retry: failure.retry !== null, acceptAnyway: failure.acceptAnyway !== undefined } }
|
|
21938
|
+
: {
|
|
21939
|
+
accept: settled === null ? null : { ready: settled.ready },
|
|
21940
|
+
runChecks: runChecks !== null,
|
|
21941
|
+
checksRunning,
|
|
21942
|
+
blocked,
|
|
21943
|
+
canRequest: panel.panel.canRequest && panel.panel.request !== null,
|
|
21944
|
+
need: need === null || need.accept != null ? null : need.rebuild != null ? "rebuild" : need.confirm !== null ? "confirm-stopped" : null,
|
|
21945
|
+
next: nextKind === "revise" || nextKind === "draft-repair" ? nextKind : null,
|
|
21946
|
+
unanswered: settled === null ? 0 : unanswered.length,
|
|
21947
|
+
notRight: 0,
|
|
21948
|
+
};
|
|
21949
|
+
const acts = resultActsOf(actFacts);
|
|
21950
|
+
// A refuted result's card lists every recorded disagreement, each tied to its lines; when the report itself doesn't
|
|
21951
|
+
// match the changes (not a failed check), that is the headline.
|
|
21952
|
+
const mismatch = failure !== null || proof === null || accepted || proof.verdict !== "refuted" ? null : (() => {
|
|
21953
|
+
const patch = view.detail?.terminal?.patch ?? null;
|
|
21954
|
+
const files = patch === null || "problem" in patch ? [] : parseReviewDiff(patch.text).files;
|
|
21955
|
+
// Each file's first change: the lines it added (else every line it shows), and where that change starts (its link target).
|
|
21956
|
+
const hunkStart = new Map();
|
|
21957
|
+
const changes = new Map(files.map(file => {
|
|
21958
|
+
const shown = file.hunks[0]?.lines.filter(line => line.newLine !== null && line.kind !== "meta") ?? [];
|
|
21959
|
+
if (shown[0]?.newLine != null)
|
|
21960
|
+
hunkStart.set(file.path, shown[0].newLine);
|
|
21961
|
+
const added = shown.filter(line => line.kind === "addition");
|
|
21962
|
+
const lines = (added.length > 0 ? added : shown).map(line => line.newLine);
|
|
21963
|
+
return [file.path, lines.length === 0 ? null : { from: Math.min(...lines), to: Math.max(...lines) }];
|
|
21964
|
+
}));
|
|
21965
|
+
const changesHref = `${here}&run=${run.id}&tab=changes`;
|
|
21966
|
+
const found = reportMismatchesOf(proof.reasons, proof.proofProblem === null ? proof.matrix : [], changes);
|
|
21967
|
+
// The saved report itself can't be read: that is the evidence problem, said once.
|
|
21968
|
+
if (proof.proofProblem !== null)
|
|
21969
|
+
found.push({ text: `The saved report can't be read: ${proof.proofProblem}`, path: null, lines: null, inChanges: null, note: null, reason: proof.proofProblem });
|
|
21970
|
+
if (found.length === 0)
|
|
21971
|
+
return null;
|
|
21972
|
+
return {
|
|
21973
|
+
headline: evidenceProblemOf(proof.verdict, proof.reasons) === "mismatched" ? MISMATCH_HEADLINE : null,
|
|
21974
|
+
rows: found.map(one => ({
|
|
21975
|
+
text: one.text, path: one.path, absent: one.inChanges === false,
|
|
21976
|
+
lines: one.lines === null ? null : one.lines.from === one.lines.to ? `line ${one.lines.from}` : `lines ${one.lines.from}–${one.lines.to}`,
|
|
21977
|
+
href: one.path !== null && one.inChanges === true ? `${changesHref}#${diffFileAnchor(one.path)}${hunkStart.has(one.path) ? `-L${hunkStart.get(one.path)}` : ""}`
|
|
21978
|
+
: one.note !== null && (proof.proof?.caveats.length ?? 0) >= one.note ? `${here}&run=${run.id}#report-note-${one.note}` : null,
|
|
21979
|
+
noteLabel: one.path === null && one.note !== null ? `The report, note ${one.note}` : null,
|
|
21980
|
+
})),
|
|
21981
|
+
said: [...new Set([...proof.reasons, ...found.map(one => one.reason)].flatMap(reason => [reason, plainReasonWords(reason)]))],
|
|
21982
|
+
};
|
|
21983
|
+
})();
|
|
21476
21984
|
if (view.notes.length > 0) {
|
|
21477
|
-
parts.push(`<section class="card cockpit-section" data-cockpit-section="notes"><h3>
|
|
21985
|
+
parts.push(`<section class="card cockpit-section" data-cockpit-section="notes"><h3>Operator notes</h3>` +
|
|
21478
21986
|
view.notes.map(one => `<p class="row"><span class="meta">${escape(one.author)} · ${whenTime(one.createdAt)}</span> ${escape(one.note)}</p>`).join("\n") +
|
|
21479
21987
|
`</section>`);
|
|
21480
21988
|
}
|
|
21481
21989
|
return {
|
|
21482
21990
|
html: `<section class="cockpit-detail">${parts.join("\n")}</section>`,
|
|
21483
21991
|
selected: {
|
|
21484
|
-
...selected, complete, decision
|
|
21992
|
+
...selected, complete: failure === null ? complete : null, decision: failure === null ? decision : null, contest, acts, actFacts, runChecks, mismatch, failure,
|
|
21993
|
+
// A failed build reads Failed, whatever its task has done since; its outcome is what went wrong.
|
|
21994
|
+
// Run checks is the decision's own act on this page; a status row never sends the person to Chat for it.
|
|
21995
|
+
panel: (() => {
|
|
21996
|
+
// A Chat link to this same result would lead back here: it isn't one.
|
|
21997
|
+
const elsewhere = (href) => href !== null && !href.endsWith("#follow-ups") && !new RegExp(`^/chat\\?task=[^&]+&result=${run.id}(?:&|$)`).test(href);
|
|
21998
|
+
const shown = panel.panel.status === null ? null : view.demo === true ? demoChecksOf(panel.panel.status) : panel.panel.status;
|
|
21999
|
+
const missed = proof === null || proof.proofProblem !== null ? 0 : requirementsOf(proof.matrix)?.missed ?? 0;
|
|
22000
|
+
const status = shown === null ? null : { ...shown, details: shown.details.map(one => ({ ...one,
|
|
22001
|
+
href: elsewhere(one.href) ? one.href : null, action: one.action !== null && elsewhere(one.action.href) ? one.action : null })) };
|
|
22002
|
+
return failure === null ? { ...panel.panel, status }
|
|
22003
|
+
: { ...panel.panel, outcome: failure.line, need: null, youCheck: null, status: status === null ? null : { ...status, headline: "Failed", tone: "danger", sentence: failure.line, need: null,
|
|
22004
|
+
// Read as Failed, the Requirements row counts what it missed, as the task's card does.
|
|
22005
|
+
details: status.details.map(one => one.key !== "requirements" || missed === 0 ? one : { ...one, text: `${missed} missed`, mark: "failed", action: null }) },
|
|
22006
|
+
// What it missed is said once, in plain words above; the recorded wording stays under Details.
|
|
22007
|
+
attention: panel.panel.attention.filter(one => !(proof?.reasons ?? []).includes(one) && !(proof?.matrix ?? []).some(row => row.detail.includes(one))) };
|
|
22008
|
+
})(),
|
|
22009
|
+
...(failure === null ? {} : { status: { label: "Failed", tone: "problem", token: "failed" } }),
|
|
21485
22010
|
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}` },
|
|
21486
22011
|
},
|
|
21487
22012
|
};
|
|
21488
22013
|
}
|
|
21489
|
-
|
|
21490
|
-
|
|
22014
|
+
/** What Accept and finish owes before it completes, as the form asks for it: the person's own check (no
|
|
22015
|
+
* reason), an exception a report that doesn't match its changes needs a reason for, or nothing. */
|
|
22016
|
+
function owedAcceptanceOf(receipt) {
|
|
22017
|
+
if (receipt === null || receipt.proofAcceptance !== null)
|
|
22018
|
+
return null;
|
|
22019
|
+
if (personCheckPending(receipt))
|
|
22020
|
+
return { note: null };
|
|
22021
|
+
return cantAcceptYetOf(receipt.proof?.verdict ?? null, receipt.proof?.reasons ?? [], false) === ACCEPT_NEEDS_REASON ? { note: "Why is this safe to accept?" } : null;
|
|
22022
|
+
}
|
|
22023
|
+
function completionForm(taskId, runId, digest, csrf, pullRequestTo = null, accept = null) {
|
|
22024
|
+
// Accept and finish, in one request: with an acceptance owed (the person's own checks, or an exception and
|
|
22025
|
+
// its reason, asked for right here), the same post records it first.
|
|
22026
|
+
const hidden = `<input type="hidden" name="csrf" value="${escape(csrf)}"><input type="hidden" name="receipt" value="${escape(digest)}"><input type="hidden" name="run" value="${runId}">${accept === null ? "" : `<input type="hidden" name="accept" value="1">`}` +
|
|
22027
|
+
(accept?.note == null ? "" : `<label class="meta" for="complete-reason-${runId}">${escape(ACCEPT_NEEDS_REASON)}</label><input type="text" id="complete-reason-${runId}" name="note" maxlength="500" required placeholder="${escape(accept.note)}">`);
|
|
21491
22028
|
if (pullRequestTo !== null) {
|
|
21492
|
-
// With pull requests set up: the PR is the primary road, a bare
|
|
21493
|
-
return `<form method="post" action="${taskHref(taskId)}/complete" class="card result-complete">${hidden}<p class="meta">
|
|
22029
|
+
// With pull requests set up: the PR is the primary road, a bare finish the quiet one beside it.
|
|
22030
|
+
return `<form method="post" action="${taskHref(taskId)}/complete" class="card result-complete">${hidden}<p class="meta">Finishes the task. A pull request opens on ${escape(pullRequestTo)} from this exact commit when you ask for one.</p><div class="result-complete-actions"><button type="submit" name="publish" value="1" style="min-height:44px">Complete and open a pull request</button><button type="submit" class="secondary" style="min-height:44px">Accept and finish</button></div></form>`;
|
|
21494
22031
|
}
|
|
21495
|
-
return `<form method="post" action="${taskHref(taskId)}/complete" class="card result-complete">${hidden}<p class="meta">
|
|
22032
|
+
return `<form method="post" action="${taskHref(taskId)}/complete" class="card result-complete">${hidden}<p class="meta">Finishes the task. Checks stay unchanged; nothing is published or deployed.</p><button type="submit" style="min-height:44px">Accept and finish</button></form>`;
|
|
21496
22033
|
}
|
|
21497
22034
|
/** The pull request, in one card: its state and link, one line of detail, and the one action it needs — Merge
|
|
21498
22035
|
* behind the password when checks passed, or Open a pull request for a result completed without one. */
|
|
@@ -21576,38 +22113,9 @@ function homePhaseWords(run) {
|
|
|
21576
22113
|
function phaseWords(phase) {
|
|
21577
22114
|
return PHASE_WORDS[phase] ?? "the agent is working";
|
|
21578
22115
|
}
|
|
21579
|
-
|
|
21580
|
-
agent: "the agent failed",
|
|
21581
|
-
"agent-reported": "the agent reported it could not finish",
|
|
21582
|
-
"no-op": "nothing changed when something should have",
|
|
21583
|
-
"no-handoff": "the agent stopped before handing off; its work was kept and it is being resumed",
|
|
21584
|
-
"moved-head": "the branch moved underneath the build",
|
|
21585
|
-
"moved-branch": "the branch moved underneath the build",
|
|
21586
|
-
timeout: "ran out of time",
|
|
21587
|
-
git: "a git step failed",
|
|
21588
|
-
"malformed-decision": "the agent's question was malformed",
|
|
21589
|
-
"malformed-plan": "the plan was malformed",
|
|
21590
|
-
fenced: "another worker took the task over",
|
|
21591
|
-
unapproved: "the scope was not approved",
|
|
21592
|
-
"scope-changed": "the scope changed after approval",
|
|
21593
|
-
capability: "a requirement was missing",
|
|
21594
|
-
setup: "the workspace preparation step failed",
|
|
21595
|
-
"provider-init": "the agent could not start",
|
|
21596
|
-
"commit-failure": "the commit failed",
|
|
21597
|
-
"protected-branch": "refused to touch a protected branch",
|
|
21598
|
-
"wrong-branch": "the checkout was on the wrong branch",
|
|
21599
|
-
"not-leased": "the lease was not valid",
|
|
21600
|
-
"no-claim": "the lease was not valid",
|
|
21601
|
-
"not-yours": "the lease was not valid",
|
|
21602
|
-
"no-run-record": "the run record was missing",
|
|
21603
|
-
"missing-mailbox": "the resume mailbox could not be read",
|
|
21604
|
-
"unreadable-mailbox": "the resume mailbox could not be read",
|
|
21605
|
-
"revision-brief": "the revision brief could not be read",
|
|
21606
|
-
"repaired-park": "resumed from a parked question",
|
|
21607
|
-
stopped: "stopped by the operator",
|
|
21608
|
-
};
|
|
22116
|
+
/** A recorded reason in one plain line; machine output (a stack trace, a path, "Error:" text) waits on the run record. */
|
|
21609
22117
|
function reasonWords(reason) {
|
|
21610
|
-
return
|
|
22118
|
+
return isInternalErrorReason(reason) ? "stopped with an internal error" : oneLineOf(runReasonWords(reason.trim().split(/\r?\n/, 1)[0] ?? ""), 140);
|
|
21611
22119
|
}
|
|
21612
22120
|
const INCIDENT_WORDS = {
|
|
21613
22121
|
"malformed-decision": "the agent's question was malformed",
|
|
@@ -21659,13 +22167,13 @@ function shortDigest(digest) {
|
|
|
21659
22167
|
* orphaned run keeps saying what actually became of it. */
|
|
21660
22168
|
function runOutcomeBadge(run, live) {
|
|
21661
22169
|
if (!live && run.role === "planner" && run.reason === "plan-drafted") {
|
|
21662
|
-
return `<span class="badge">
|
|
22170
|
+
return `<span class="badge">Planned</span>`;
|
|
21663
22171
|
}
|
|
21664
22172
|
if (!live && run.reason === "interrupted")
|
|
21665
|
-
return `<span class="badge">
|
|
22173
|
+
return `<span class="badge">Interrupted</span>`;
|
|
21666
22174
|
return live
|
|
21667
|
-
? `<span class="badge badge-running">
|
|
21668
|
-
: `<span class="badge badge-${escape(run.outcome ?? "cut")}">${escape(run.outcome ?? "never finished")}</span>`;
|
|
22175
|
+
? `<span class="badge badge-running">Running</span>`
|
|
22176
|
+
: `<span class="badge badge-${escape(run.outcome ?? "cut")}">${escape(sentenceCase(run.outcome ?? "never finished"))}</span>`;
|
|
21669
22177
|
}
|
|
21670
22178
|
const runNoun = (run) => run.role === "planner" ? "plan" : run.role === "reviewer" ? "review" : run.role === "scout" ? "report" : "build";
|
|
21671
22179
|
function runsPage(chrome, rows, liveIds, nextCursor, verdicts = new Map(), accepted = new Set()) {
|
|
@@ -21680,7 +22188,7 @@ function runsPage(chrome, rows, liveIds, nextCursor, verdicts = new Map(), accep
|
|
|
21680
22188
|
return (`<p class="row"><a href="/r/${run.id}" class="mono">#${run.id}</a> ` +
|
|
21681
22189
|
`<a href="${taskHref(run.taskId)}" class="mono">${escape(run.taskId)}</a> ` +
|
|
21682
22190
|
runOutcomeBadge(run, liveIds.has(run.id)) +
|
|
21683
|
-
`${run.qualityMode === "strict" ? ` <span class="badge">
|
|
22191
|
+
`${run.qualityMode === "strict" ? ` <span class="badge">Strict review</span>` : ""}` +
|
|
21684
22192
|
needsVerification +
|
|
21685
22193
|
criterionMatrixSummary(verdicts.get(run.id)?.matrix ?? []) +
|
|
21686
22194
|
`${run.provider === "claude" ? "" : ` <span class="meta mono">${escape(run.provider)}</span>`}` +
|
|
@@ -21689,7 +22197,7 @@ function runsPage(chrome, rows, liveIds, nextCursor, verdicts = new Map(), accep
|
|
|
21689
22197
|
})
|
|
21690
22198
|
.join("\n");
|
|
21691
22199
|
const older = nextCursor === null ? "" : `<p><a href="/runs?before=${nextCursor}">older →</a></p>`;
|
|
21692
|
-
return screen("builds", [`<h1>
|
|
22200
|
+
return screen("builds", [`<h1>Builds <a class="badge" href="/peek">Peek at the live ones \u2192</a></h1>`, buildsViews("builds"), `<p class="hint">one build = one attempt by an agent to complete a task, on its own branch</p>`, list, older].join("\n"), { chrome });
|
|
21693
22201
|
}
|
|
21694
22202
|
function structuredHandoffView(artifacts, root) {
|
|
21695
22203
|
const artifact = [...artifacts].reverse().find(one => one.kind === "handoff");
|
|
@@ -22044,7 +22552,7 @@ function reviewDiffHtml(patch, stat, runId, commentable, anchors = new Map()) {
|
|
|
22044
22552
|
const parsed = parseReviewDiff(patch.text);
|
|
22045
22553
|
const structured = parsed.files.some(file => file.hunks.length > 0);
|
|
22046
22554
|
if (!structured) {
|
|
22047
|
-
return (`<details><summary>
|
|
22555
|
+
return (`<details><summary>The patch${patch.truncated ? " (TRUNCATED — the raw record says how much was cut)" : ""}</summary>` +
|
|
22048
22556
|
`<pre class="mono" style="overflow-x:auto">${escape(patch.text)}</pre></details>` +
|
|
22049
22557
|
storedDownloadLink(runId, patch.artifactId, "diff", patch.truncated));
|
|
22050
22558
|
}
|
|
@@ -22057,18 +22565,23 @@ function reviewDiffHtml(patch, stat, runId, commentable, anchors = new Map()) {
|
|
|
22057
22565
|
: counts.additions === null || counts.deletions === null
|
|
22058
22566
|
? `<span class="diff-file-counts">binary</span>`
|
|
22059
22567
|
: `<span class="diff-file-counts"><b>+${counts.additions}</b><i>−${counts.deletions}</i></span>`;
|
|
22060
|
-
const
|
|
22061
|
-
|
|
22062
|
-
|
|
22063
|
-
|
|
22064
|
-
|
|
22065
|
-
|
|
22066
|
-
|
|
22067
|
-
|
|
22068
|
-
|
|
22069
|
-
|
|
22070
|
-
|
|
22071
|
-
|
|
22568
|
+
const fileAnchor = anchors.get(one.path);
|
|
22569
|
+
// Each change is a link target of its own (#<file anchor>-L<first new line>): a result names the exact lines.
|
|
22570
|
+
const hunks = one.hunks.map(hunk => {
|
|
22571
|
+
const first = hunk.lines.find(line => line.newLine !== null && line.kind !== "meta")?.newLine ?? null;
|
|
22572
|
+
return `<section class="diff-hunk"${fileAnchor === undefined || first === null ? "" : ` id="${fileAnchor}-L${first}"`}><div class="diff-hunk-head">${escape(hunk.header)}</div>` +
|
|
22573
|
+
`<div class="diff-lines">${hunk.lines.map(line => {
|
|
22574
|
+
const lineNumber = line.newLine ?? line.oldLine;
|
|
22575
|
+
const side = line.newLine === null && line.oldLine !== null ? "old" : "new";
|
|
22576
|
+
const annotate = !commentable || lineNumber === null || line.kind === "meta"
|
|
22577
|
+
? `<span class="diff-annotate-space"></span>`
|
|
22578
|
+
: `<button type="button" class="diff-annotate pick-line" data-path="${escape(one.path)}" data-line="${lineNumber}" data-side="${side}" aria-label="Annotate ${escape(one.path)}, ${side} line ${lineNumber}" title="Annotate this line">${annotateIcon}</button>`;
|
|
22579
|
+
const marker = line.kind === "addition" ? "+" : line.kind === "deletion" ? "−" : line.kind === "context" ? " " : "·";
|
|
22580
|
+
return (`<div class="diff-line diff-${line.kind}">${annotate}` +
|
|
22581
|
+
`<span class="diff-gutter">${line.oldLine ?? ""}</span><span class="diff-gutter">${line.newLine ?? ""}</span>` +
|
|
22582
|
+
`<code><b aria-hidden="true">${marker}</b>${escape(line.text)}</code></div>`);
|
|
22583
|
+
}).join("")}</div></section>`;
|
|
22584
|
+
}).join("");
|
|
22072
22585
|
const anchor = anchors.get(one.path);
|
|
22073
22586
|
return (`<details class="diff-file"${fileIndex === 0 ? " open" : ""}${anchor === undefined ? "" : ` id="${anchor}"`}>` +
|
|
22074
22587
|
`<summary><span class="diff-file-name">${escape(one.path)}</span>${countWords}</summary>` +
|
|
@@ -22096,7 +22609,9 @@ function checkLogHtml(log, runId, attributes) {
|
|
|
22096
22609
|
if (log.problem !== null) {
|
|
22097
22610
|
return `<p class="problem" data-check-log="damaged"${attributes}>The check log no longer verifies (${escape(log.problem)}). Its output is not shown, and there is nothing to download.</p>`;
|
|
22098
22611
|
}
|
|
22099
|
-
return (`<details data-check-log="${log.truncated ? "shortened" : "ok"}"${attributes}><summary>Check output${log.truncated ? ` (shortened — ${log.bytesStored} of ${log.bytesOriginal} bytes stored)` : ""}</summary><pre class="mono" style="overflow-x:auto;max-height:18rem">${
|
|
22612
|
+
return (`<details data-check-log="${log.truncated ? "shortened" : "ok"}"${attributes}><summary>Check output${log.truncated ? ` (shortened — ${log.bytesStored} of ${log.bytesOriginal} bytes stored)` : ""}</summary><pre class="mono" style="overflow-x:auto;max-height:18rem">${
|
|
22613
|
+
// Each line is a link target (#check-log-L<n>): a failed task names the exact line its check ended on.
|
|
22614
|
+
log.text.split("\n").map((line, index) => `<span id="check-log-L${index + 1}">${escape(line)}</span>`).join("\n")}</pre></details>` +
|
|
22100
22615
|
storedDownloadLink(runId, log.artifactId, "check log", log.truncated));
|
|
22101
22616
|
}
|
|
22102
22617
|
/** The download link for a stored record (repair 2026-09-14): a shortened
|
|
@@ -22111,12 +22626,12 @@ function storedDownloadLink(runId, artifactId, what, truncated) {
|
|
|
22111
22626
|
* page's prefill script reads — a real button, keyboard-reachable, separate
|
|
22112
22627
|
* from the link so the two never fight over one click (finding 4). */
|
|
22113
22628
|
function terminalDiffCard(view, runId, editor = null, commentable = false) {
|
|
22114
|
-
const parts = ["<h2>
|
|
22629
|
+
const parts = ["<h2>What changed</h2>"];
|
|
22115
22630
|
if (view.stat === null) {
|
|
22116
|
-
parts.push(`<p class="meta">
|
|
22631
|
+
parts.push(`<p class="meta">No change summary was captured for this build</p>`);
|
|
22117
22632
|
}
|
|
22118
22633
|
else if ("problem" in view.stat) {
|
|
22119
|
-
parts.push(`<p class="meta">
|
|
22634
|
+
parts.push(`<p class="meta">Stat: ${escape(view.stat.problem)}</p>`);
|
|
22120
22635
|
}
|
|
22121
22636
|
else {
|
|
22122
22637
|
const s = view.stat;
|
|
@@ -22138,23 +22653,23 @@ function terminalDiffCard(view, runId, editor = null, commentable = false) {
|
|
|
22138
22653
|
.slice(0, 40)
|
|
22139
22654
|
.map(file => `<p class="row mono">${fileName(file.path)}${file.renamedFrom === undefined ? "" : ` (was ${escape(file.renamedFrom)})`} ` +
|
|
22140
22655
|
`<span class="meta">${file.additions === null || file.deletions === null ? "binary" : `+${file.additions} −${file.deletions}`}</span>` +
|
|
22141
|
-
`${commentable ? ` <button type="button" class="pick-file" data-path="${escape(file.path)}">
|
|
22656
|
+
`${commentable ? ` <button type="button" class="pick-file" data-path="${escape(file.path)}">Comment</button>` : ""}</p>`)
|
|
22142
22657
|
.join("\n") +
|
|
22143
22658
|
(s.files.length > 40 ? `<p class="meta">…and ${s.files.length - 40} more file(s)</p>` : "") +
|
|
22144
22659
|
`</div>`);
|
|
22145
22660
|
if (editor !== null) {
|
|
22146
|
-
parts.push(`<p class="meta">
|
|
22661
|
+
parts.push(`<p class="meta">File links open in VS Code on THIS device — if the build's worktree is gone, a link opens nothing</p>`);
|
|
22147
22662
|
}
|
|
22148
22663
|
}
|
|
22149
22664
|
}
|
|
22150
22665
|
if (view.patch === null) {
|
|
22151
|
-
parts.push(`<p class="meta">
|
|
22666
|
+
parts.push(`<p class="meta">The final diff was not captured for this build</p>`);
|
|
22152
22667
|
}
|
|
22153
22668
|
else if ("problem" in view.patch) {
|
|
22154
|
-
parts.push(`<p class="meta">
|
|
22669
|
+
parts.push(`<p class="meta">Patch: ${escape(view.patch.problem)}</p>`);
|
|
22155
22670
|
}
|
|
22156
22671
|
else if (view.patch.text.trim() === "") {
|
|
22157
|
-
parts.push(`<p class="meta">
|
|
22672
|
+
parts.push(`<p class="meta">Empty diff — captured successfully, nothing changed</p>`);
|
|
22158
22673
|
}
|
|
22159
22674
|
else {
|
|
22160
22675
|
parts.push(reviewDiffHtml(view.patch, view.stat, runId, commentable));
|
|
@@ -22173,7 +22688,7 @@ function evidenceBundleCard(view, run, publication = null, review = null) {
|
|
|
22173
22688
|
if (view === null)
|
|
22174
22689
|
return "";
|
|
22175
22690
|
const runId = run.id;
|
|
22176
|
-
const parts = ["<h2>
|
|
22691
|
+
const parts = ["<h2>Verification and evidence</h2>"];
|
|
22177
22692
|
if (view.verdict !== null) {
|
|
22178
22693
|
// The same status line every other surface renders for this run
|
|
22179
22694
|
// (workspace package 1), beside the verdict's own explanation.
|
|
@@ -22183,7 +22698,7 @@ function evidenceBundleCard(view, run, publication = null, review = null) {
|
|
|
22183
22698
|
if (view.accepted !== null) {
|
|
22184
22699
|
parts.push(`<p class="meta">Accepted with an exception by <span class="mono">${escape(view.accepted.by)}</span> · ${whenTime(view.accepted.at)}${view.accepted.note === null ? "" : ` — ${escape(view.accepted.note)}`}</p>`);
|
|
22185
22700
|
}
|
|
22186
|
-
parts.push(criterionMatrixHtml(view.matrix, { runId, links: view.matrixLinks }));
|
|
22701
|
+
parts.push(criterionMatrixHtml(view.matrix, { runId, links: view.matrixLinks, verdict: view.verdict }));
|
|
22187
22702
|
parts.push(semanticCoverageHtml(view.matrix, view.qualityMode));
|
|
22188
22703
|
if (reviewConflict(view.matrix, view.machineVerdict, view.verdict)) {
|
|
22189
22704
|
parts.push(`<p class="meta">An independent review found conflicting evidence.</p>`);
|
|
@@ -22194,25 +22709,25 @@ function evidenceBundleCard(view, run, publication = null, review = null) {
|
|
|
22194
22709
|
}
|
|
22195
22710
|
else if (view.proof !== null) {
|
|
22196
22711
|
if (view.proof.criteria.length > 0) {
|
|
22197
|
-
parts.push(`<div class="result-section"><strong>
|
|
22712
|
+
parts.push(`<div class="result-section"><strong>Acceptance criteria</strong><ul>` +
|
|
22198
22713
|
view.proof.criteria
|
|
22199
|
-
.map(one => `<li><span class="badge${one.verdict === "met" ? " badge-done" : one.verdict === "not-met" ? " badge-failed" : ""}">${escape(one.verdict)}</span> ${escape(one.statement)} <span class="meta">— ${escape(one.how)}</span></li>`)
|
|
22714
|
+
.map(one => `<li><span class="badge${one.verdict === "met" ? " badge-done" : one.verdict === "not-met" ? " badge-failed" : ""}">${escape(sentenceCase(one.verdict))}</span> ${escape(one.statement)} <span class="meta">— ${escape(one.how)}</span></li>`)
|
|
22200
22715
|
.join("") +
|
|
22201
22716
|
`</ul></div>`);
|
|
22202
22717
|
}
|
|
22203
22718
|
if (view.proof.checks.length > 0) {
|
|
22204
|
-
parts.push(`<div class="result-section"><strong>
|
|
22719
|
+
parts.push(`<div class="result-section"><strong>Checks reported by the agent</strong><ul>` +
|
|
22205
22720
|
view.proof.checks.map(one => `<li><span class="mono">${escape(one.command)}</span> <span class="meta">(exit ${one.exitCode}) — ${escape(one.summary)}</span></li>`).join("") +
|
|
22206
22721
|
`</ul></div>`);
|
|
22207
22722
|
}
|
|
22208
22723
|
if (view.proof.caveats.length > 0) {
|
|
22209
|
-
parts.push(`<div class="result-section"><strong>
|
|
22724
|
+
parts.push(`<div class="result-section"><strong>Caveats</strong><ul>${view.proof.caveats.map(one => `<li>${escape(one)}</li>`).join("")}</ul></div>`);
|
|
22210
22725
|
}
|
|
22211
22726
|
}
|
|
22212
22727
|
if (view.checkLog !== null)
|
|
22213
22728
|
parts.push(checkLogHtml(view.checkLog, runId, ""));
|
|
22214
22729
|
if (view.screenshots.length > 0) {
|
|
22215
|
-
parts.push(`<div class="result-section"><strong>
|
|
22730
|
+
parts.push(`<div class="result-section"><strong>Screenshots</strong>` +
|
|
22216
22731
|
view.screenshots
|
|
22217
22732
|
.map(shot => `<p class="row"><a href="/r/${runId}/evidence/${shot.artifactId}">` +
|
|
22218
22733
|
`<img src="/r/${runId}/evidence/${shot.artifactId}" alt="${escape(shot.caption)}" style="max-width:12rem;max-height:9rem;border-radius:var(--radius);border:1px solid var(--border)"></a> ` +
|
|
@@ -22329,7 +22844,7 @@ function completionReceiptCard(view, taskId, place, standing = receiptStatusOf(v
|
|
|
22329
22844
|
(view.publication?.state === "failed" ? `<p class="receipt-note">${statusIconSvg("note")} Pull request couldn't open. The commit is safe locally.</p>` : "") +
|
|
22330
22845
|
caveats +
|
|
22331
22846
|
coverage +
|
|
22332
|
-
`<details class="receipt-details"><summary
|
|
22847
|
+
`<details class="receipt-details"><summary>${STATUS_MORE}</summary>` +
|
|
22333
22848
|
(view.summary !== null && view.summary !== conciseOutcomeOf(view.summary) ? `<p class="recap">${escape(view.summary)}</p>` : "") +
|
|
22334
22849
|
`<p class="receipt-publication" data-receipt-publication="${escape(facts.publicationState)}">${escape(facts.publicationWords)}</p>` +
|
|
22335
22850
|
// A review in flight is the receipt's primary status too (its chip
|
|
@@ -22460,9 +22975,10 @@ function resultPanelParts(detail, o) {
|
|
|
22460
22975
|
items: personCheckItems(proof, patchOk ? patch.text : null, shown, runId),
|
|
22461
22976
|
accept: acceptable && need?.accept == null ? { action: `${taskHref(detail.taskId)}/accept-proof`, run: run.id, returnTo: o.returnTo } : null,
|
|
22462
22977
|
};
|
|
22978
|
+
const youCheckForm = youCheck?.accept != null && o.finishes !== true ? youCheck.accept : null;
|
|
22463
22979
|
const youCheckHtml = youCheck === null ? "" :
|
|
22464
22980
|
`<div class="result-you-check" data-result-you-check="${youCheck.lines.length}"><ul>${youCheck.lines.map(one => `<li>${escape(one)}</li>`).join("")}</ul>` +
|
|
22465
|
-
(
|
|
22981
|
+
(youCheckForm === null ? "" : `<form method="post" action="${escape(youCheckForm.action)}"><input type="hidden" name="csrf" value="${escape(o.csrf)}"><input type="hidden" name="run" value="${youCheckForm.run}"><input type="hidden" name="return" value="${escape(youCheckForm.returnTo)}"><button type="submit" data-accept-result>Accept</button></form>`) +
|
|
22466
22982
|
`</div>`;
|
|
22467
22983
|
const attentionHtml = attention.length === 0
|
|
22468
22984
|
? ""
|
|
@@ -22512,6 +23028,8 @@ function resultPanelParts(detail, o) {
|
|
|
22512
23028
|
...(conclusion !== outcome ? [`<p class="recap">${escape(conclusion)}</p>`] : []),
|
|
22513
23029
|
...(handoff !== null && handoff.changes.length > 0 ? [`<ul class="result-changes">${handoff.changes.map(one => `<li>${escape(one)}</li>`).join("")}</ul>`] : []),
|
|
22514
23030
|
...(handoff !== null && handoff.verification.length > 0 ? [`<p class="meta">Checked by the agent:</p><ul class="result-changes" data-cockpit-source="agent-words">${handoff.verification.map(one => `<li>${escape(one)}</li>`).join("")}</ul>`] : []),
|
|
23031
|
+
// The report's own notes, each a link target (#report-note-<n>) a mismatch can point at.
|
|
23032
|
+
...(proof?.proof == null || proof.proof.caveats.length === 0 ? [] : [`<p class="meta">Its notes:</p><ol class="result-changes" data-report-notes>${proof.proof.caveats.map((one, index) => `<li id="report-note-${index + 1}">${escape(one)}</li>`).join("")}</ol>`]),
|
|
22515
23033
|
];
|
|
22516
23034
|
if (agentAccount.length > 0)
|
|
22517
23035
|
summaryParts.push(`<details class="result-notes" data-result-notes-agent><summary>What the agent reported</summary>${agentAccount.join("")}</details>`);
|
|
@@ -22587,9 +23105,9 @@ function resultPanelParts(detail, o) {
|
|
|
22587
23105
|
detail.files
|
|
22588
23106
|
.map(file => {
|
|
22589
23107
|
const counts = file.additions === null || file.deletions === null ? "binary" : `+${file.additions} −${file.deletions}`;
|
|
22590
|
-
const flags = (file.outsideTouches ? ` <span class="badge badge-failed" data-outside-touches="1">
|
|
23108
|
+
const flags = (file.outsideTouches ? ` <span class="badge badge-failed" data-outside-touches="1">Outside touches</span>` : "") + (file.cited ? ` <span class="badge badge-done">Cited</span>` : "");
|
|
22591
23109
|
const why = file.priority.reasons.length === 0 ? "" : ` <span class="meta">— ${file.priority.reasons.map(escape).join("; ")}</span>`;
|
|
22592
|
-
return `<li data-file-priority="${file.priority.band}">${fileName(file)}${file.renamedFrom === null ? "" : ` <span class="meta">(was ${escape(file.renamedFrom)})</span>`} <span class="meta">${counts}</span>${flags}${detail.canAnnotate ? ` <button type="button" class="pick-file" data-path="${escape(file.path)}">
|
|
23110
|
+
return `<li data-file-priority="${file.priority.band}">${fileName(file)}${file.renamedFrom === null ? "" : ` <span class="meta">(was ${escape(file.renamedFrom)})</span>`} <span class="meta">${counts}</span>${flags}${detail.canAnnotate ? ` <button type="button" class="pick-file" data-path="${escape(file.path)}">Comment</button>` : ""}${why}</li>`;
|
|
22593
23111
|
})
|
|
22594
23112
|
.join("") +
|
|
22595
23113
|
`</ol>` +
|
|
@@ -22630,14 +23148,14 @@ function resultPanelParts(detail, o) {
|
|
|
22630
23148
|
checkParts.push(detail.signedCriteria > 0 ? `<p class="meta">The approved scope has ${detail.signedCriteria} requirement${detail.signedCriteria === 1 ? "" : "s"}, but this build has no requirement-by-requirement verification.</p>` : `<p class="meta">This scope signed no acceptance checks.</p>`);
|
|
22631
23149
|
}
|
|
22632
23150
|
else {
|
|
22633
|
-
checkParts.push(criterionMatrixHtml(proof.matrix, { runId, links: proof.matrixLinks, fileAnchors: detail.fileAnchors }));
|
|
23151
|
+
checkParts.push(criterionMatrixHtml(proof.matrix, { runId, links: proof.matrixLinks, fileAnchors: detail.fileAnchors, verdict: proof.verdict }));
|
|
22634
23152
|
}
|
|
22635
23153
|
checkParts.push(semanticCoverageHtml(proof.matrix, proof.qualityMode));
|
|
22636
23154
|
if (reviewConflict(proof.matrix, proof.machineVerdict, proof.verdict))
|
|
22637
23155
|
checkParts.push(`<p class="meta">An independent review found conflicting evidence.</p>`);
|
|
22638
23156
|
checkParts.push(repairChainHtml(proof.repairChain));
|
|
22639
23157
|
if (detail.reviewerFindings.length > 0) {
|
|
22640
|
-
checkParts.push(`<div class="result-section" data-cockpit-source="reviewer"><strong>
|
|
23158
|
+
checkParts.push(`<div class="result-section" data-cockpit-source="reviewer"><strong>Independent review</strong><ul>` +
|
|
22641
23159
|
reviewerNotes +
|
|
22642
23160
|
`</ul></div>`);
|
|
22643
23161
|
}
|
|
@@ -22666,7 +23184,7 @@ function resultPanelParts(detail, o) {
|
|
|
22666
23184
|
// The handoff's own account of its checks — the agent's words, labeled
|
|
22667
23185
|
// as such, whether or not a proof exists.
|
|
22668
23186
|
if (!directAssessment && handoff !== null && handoff.verification.length > 0)
|
|
22669
|
-
checkParts.push(`<div class="result-section" data-cockpit-source="agent-words"><strong>
|
|
23187
|
+
checkParts.push(`<div class="result-section" data-cockpit-source="agent-words"><strong>The agent's own account</strong><ul>${handoff.verification.map(one => `<li>${escape(one)}</li>`).join("")}</ul></div>`);
|
|
22670
23188
|
if (o.extraChecks !== undefined)
|
|
22671
23189
|
checkParts.push(o.extraChecks);
|
|
22672
23190
|
// ---- request changes: both feedback styles, one sealed road ------------
|
|
@@ -22711,8 +23229,8 @@ function resultPanelParts(detail, o) {
|
|
|
22711
23229
|
`<textarea name="note" rows="2" maxlength="${LIMITS.note}" placeholder="Describe the change…" aria-label="review comment" aria-describedby="comment-note-limit"${o.noted ? " autofocus" : ""}></textarea>` +
|
|
22712
23230
|
`<span class="meta diff-comment-limit" id="comment-note-limit">up to ${LIMITS.note} characters</span>` +
|
|
22713
23231
|
`<details class="result-pin"><summary>Attach to a file or line</summary>` +
|
|
22714
|
-
`<div class="diff-comment-target"><label>
|
|
22715
|
-
`<label>
|
|
23232
|
+
`<div class="diff-comment-target"><label>File<input type="text" name="path" placeholder="src/…" aria-label="file" class="mono"></label>` +
|
|
23233
|
+
`<label>Line<input type="text" name="line" placeholder="—" aria-label="line" inputmode="numeric"></label></div></details>` +
|
|
22716
23234
|
`<div class="result-feedback-actions"><button type="submit" name="intent" value="revise" data-request-changes>Request changes</button><button type="submit" name="intent" value="note" class="quiet" data-save-feedback>Save for later</button></div></form></details>`);
|
|
22717
23235
|
}
|
|
22718
23236
|
// ---- the panel ---------------------------------------------------------
|
|
@@ -22754,6 +23272,8 @@ function resultPanelParts(detail, o) {
|
|
|
22754
23272
|
reviewHistory: REVIEW_TOKENS.has(status.token) ? status.detail : null,
|
|
22755
23273
|
attention: attention.filter(one => !shortenedMaterialReason(one)).map(plainReasonWords),
|
|
22756
23274
|
youCheck,
|
|
23275
|
+
// The same counts as the Requirements row (a refuted report verifies none of them, and says Unverified).
|
|
23276
|
+
requirements: proof === null || proof.verdict === "refuted" ? null : requirementsOf(proof.matrix),
|
|
22757
23277
|
limits: attention.filter(one => shortenedMaterialReason(one)),
|
|
22758
23278
|
tabs: RESULT_TABS.map(tab => ({ key: tab.key, label: tab.key === "checks" && proof?.matrix.some(row => row.assessment !== undefined) ? "Requirements" : tab.label, count: tabCounts[tab.key], href: o.hrefFor(tab.key), active: tab.key === o.tab })),
|
|
22759
23279
|
views: [{ key: "summary", html: summaryParts.join("\n") }, { key: "changes", html: changeParts.join("\n") }, { key: "checks", html: checkParts.join("\n") }],
|
|
@@ -22901,7 +23421,7 @@ function runFactsRows(run, taskId, live, route = null) {
|
|
|
22901
23421
|
["quality", qualityModeTitle(run.qualityMode ?? "default")],
|
|
22902
23422
|
["outcome", live ? "running" : (run.outcome ?? "never finished")],
|
|
22903
23423
|
["phase", live && run.phase !== null ? phaseWords(run.phase) : null],
|
|
22904
|
-
["reason", run.reason === null ? null : reasonWords(run.reason)],
|
|
23424
|
+
["reason", run.reason === null ? null : isInternalErrorReason(run.reason) ? "stopped with an internal error (below)" : reasonWords(run.reason)],
|
|
22905
23425
|
["runner", run.runner, true],
|
|
22906
23426
|
["branch", run.branch, true],
|
|
22907
23427
|
["model", run.model, true],
|
|
@@ -22927,7 +23447,11 @@ function runFactsRows(run, taskId, live, route = null) {
|
|
|
22927
23447
|
.filter((fact) => fact[1] !== null && fact[1] !== "")
|
|
22928
23448
|
.map(([label, value, mono]) => `<p class="row"><span class="meta" style="min-width:8.5rem">${escape(label)}</span> ` +
|
|
22929
23449
|
`<span${mono === true ? ` class="mono"` : ""}>${escape(value)}</span></p>`)
|
|
22930
|
-
.join("\n") + elapsed
|
|
23450
|
+
.join("\n") + elapsed +
|
|
23451
|
+
// Machine output (a stack trace, a path, "Error:" text) reads as an internal error everywhere else; its detail is here, as recorded.
|
|
23452
|
+
(run.reason !== null && isInternalErrorReason(run.reason)
|
|
23453
|
+
? `<p class="row"><span class="meta" style="min-width:8.5rem">recorded error</span></p><pre class="mono" id="run-reason-detail" style="white-space:pre-wrap;overflow-wrap:anywhere">${escape(run.reason)}</pre>`
|
|
23454
|
+
: ""));
|
|
22931
23455
|
}
|
|
22932
23456
|
function runPage(chrome, run, taskId, artifacts, terminal = null, notes = [], csrf = "", comments = [], ciRepair = null, liveScript, peekable = false, running = false, editor = null, editorToggle = null, noted = false, heldTurns = null, continueOffer = null, structuredHandoff = null, proofBundle = null, route = null, publicationRow = null, review = null, result = null, sourceDigest = null) {
|
|
22933
23457
|
const rows = runFactsRows(run, taskId, running, route);
|
|
@@ -22944,9 +23468,9 @@ function runPage(chrome, run, taskId, artifacts, terminal = null, notes = [], cs
|
|
|
22944
23468
|
: " · the agent is working on this";
|
|
22945
23469
|
const conversation = heldTurns === null
|
|
22946
23470
|
? ""
|
|
22947
|
-
: `<h2>
|
|
23471
|
+
: `<h2>Conversation</h2>` +
|
|
22948
23472
|
(heldTurns.turns.length === 0
|
|
22949
|
-
? `<p class="meta">
|
|
23473
|
+
? `<p class="meta">Nothing said yet</p>`
|
|
22950
23474
|
: heldTurns.turns
|
|
22951
23475
|
.map(turn => `<p class="row"><span class="meta">${turn.sourceKind === "operator"
|
|
22952
23476
|
? escape(turn.author ?? "operator")
|
|
@@ -22960,11 +23484,11 @@ function runPage(chrome, run, taskId, artifacts, terminal = null, notes = [], cs
|
|
|
22960
23484
|
? `<form method="post" action="/r/${run.id}/turn" class="row">` +
|
|
22961
23485
|
`<input type="hidden" name="csrf" value="${escape(csrf)}">` +
|
|
22962
23486
|
`<input type="text" name="text" maxlength="500" placeholder="say something to the agent — it reads this as its next instruction, inside the approved scope" aria-label="turn" style="width:100%;max-width:34rem">` +
|
|
22963
|
-
`<button type="submit">
|
|
23487
|
+
`<button type="submit">Send</button></form>` +
|
|
22964
23488
|
`<p class="meta">${escape(heldTurns.state)} · ${heldTurns.turns.length}/${heldTurns.cap} messages · a waiting question must be answered before free-form messages</p>`
|
|
22965
23489
|
: heldTurns.open
|
|
22966
23490
|
? ""
|
|
22967
|
-
: `<p class="meta">
|
|
23491
|
+
: `<p class="meta">The session has ended — the record above is complete</p>`);
|
|
22968
23492
|
// The live peek region (A2): the poller fills it only on a serve that
|
|
22969
23493
|
// asserted its runner. Without the assertion the section still appears
|
|
22970
23494
|
// for a running build and says honestly why it is empty \u2014 a page that
|
|
@@ -22973,24 +23497,24 @@ function runPage(chrome, run, taskId, artifacts, terminal = null, notes = [], cs
|
|
|
22973
23497
|
const peek = !running
|
|
22974
23498
|
? ""
|
|
22975
23499
|
: peekable
|
|
22976
|
-
? `<h2>
|
|
22977
|
-
`<div id="run-peek"><p class="meta">
|
|
23500
|
+
? `<h2>What is changing right now</h2>` +
|
|
23501
|
+
`<div id="run-peek"><p class="meta">Watching\u2026 the first look lands within 15 seconds</p></div>` +
|
|
22978
23502
|
`<p class="meta" id="run-peek-stamp"></p>`
|
|
22979
|
-
: `<h2>
|
|
22980
|
-
`<p class="meta">
|
|
23503
|
+
: `<h2>What is changing right now</h2>` +
|
|
23504
|
+
`<p class="meta">The live file view is off \u2014 start serve with ${escape("--runner <name>")} naming this machine's worker, and it appears here</p>`;
|
|
22981
23505
|
// The live transcript (arc 1): the agent's own words, streamed to a file
|
|
22982
23506
|
// beside the run and polled as raw text. Honesty stated on the surface:
|
|
22983
23507
|
// this is display only, and the machine running the agent could alter it.
|
|
22984
23508
|
const transcript = !running
|
|
22985
23509
|
? ""
|
|
22986
23510
|
: !peekable
|
|
22987
|
-
? `<h2>
|
|
22988
|
-
`<p class="meta">
|
|
23511
|
+
? `<h2>What the agent is saying</h2>` +
|
|
23512
|
+
`<p class="meta">The live transcript is off \u2014 start serve with ${escape("--runner <name>")} naming this machine's worker, and it appears here</p>`
|
|
22989
23513
|
: run.provider !== "claude"
|
|
22990
|
-
? `<h2>
|
|
22991
|
-
`<p class="meta">
|
|
22992
|
-
: `<h2>
|
|
22993
|
-
`<p class="meta">
|
|
23514
|
+
? `<h2>What the agent is saying</h2>` +
|
|
23515
|
+
`<p class="meta">The live transcript needs the claude harness for now \u2014 this build runs on ${escape(run.provider)}</p>`
|
|
23516
|
+
: `<h2>What the agent is saying</h2>` +
|
|
23517
|
+
`<p class="meta">Display only \u2014 this is not evidence, and the machine running the agent could alter it</p>` +
|
|
22994
23518
|
`<pre id="live-transcript" class="mono" style="max-height:24rem;overflow:auto;white-space:pre-wrap"></pre>` +
|
|
22995
23519
|
`<p class="meta" id="live-transcript-state"></p>`;
|
|
22996
23520
|
const resultSummary = structuredHandoff?.conclusion ?? run.handoff;
|
|
@@ -22999,7 +23523,7 @@ function runPage(chrome, run, taskId, artifacts, terminal = null, notes = [], cs
|
|
|
22999
23523
|
: `<div class="result-section"><strong>${escape(label)}</strong><ul>${items.map(one => `<li>${escape(one)}</li>`).join("")}</ul></div>`;
|
|
23000
23524
|
const handoff = resultSummary === null
|
|
23001
23525
|
? ""
|
|
23002
|
-
: `<section class="card result-card"><h2>
|
|
23526
|
+
: `<section class="card result-card"><h2>Result</h2><p class="recap">${escape(resultSummary)}</p>` +
|
|
23003
23527
|
(structuredHandoff === null
|
|
23004
23528
|
? ""
|
|
23005
23529
|
: resultList("completed", structuredHandoff.changes) +
|
|
@@ -23035,9 +23559,9 @@ function runPage(chrome, run, taskId, artifacts, terminal = null, notes = [], cs
|
|
|
23035
23559
|
? ""
|
|
23036
23560
|
: `<form method="post" action="/r/${run.id}/comment" class="card diff-comment-form" id="comment-form">` +
|
|
23037
23561
|
`<input type="hidden" name="csrf" value="${escape(csrf)}">` +
|
|
23038
|
-
`<div class="diff-comment-target"><label>
|
|
23039
|
-
`<label>
|
|
23040
|
-
`<label>
|
|
23562
|
+
`<div class="diff-comment-target"><label>File<input type="text" name="path" placeholder="select a line above" aria-label="file" class="mono"></label>` +
|
|
23563
|
+
`<label>Line<input type="text" name="line" placeholder="—" aria-label="line" inputmode="numeric"></label></div>` +
|
|
23564
|
+
`<label>Change requested<textarea name="note" rows="2" maxlength="${LIMITS.note}" placeholder="Explain what should change and why" aria-label="review comment" aria-describedby="comment-note-limit"${noted ? " autofocus" : ""}></textarea></label>` +
|
|
23041
23565
|
`<span class="meta diff-comment-limit" id="comment-note-limit">up to ${LIMITS.note} characters</span>` +
|
|
23042
23566
|
`<button type="submit">Add annotation</button></form>`;
|
|
23043
23567
|
// The device-side half of the editor-link activation (arc 6, finding 1):
|
|
@@ -23065,7 +23589,7 @@ function runPage(chrome, run, taskId, artifacts, terminal = null, notes = [], cs
|
|
|
23065
23589
|
: `<div class="card"><p><strong>CI is failing on PR #${ciRepair.pr}</strong> <span class="meta">observed by the episode watcher</span></p>` +
|
|
23066
23590
|
`<form method="post" action="/r/${run.id}/draft-repair">` +
|
|
23067
23591
|
`<input type="hidden" name="csrf" value="${escape(csrf)}">` +
|
|
23068
|
-
`<button type="submit">
|
|
23592
|
+
`<button type="submit">Draft a repair task</button>` +
|
|
23069
23593
|
`<span class="meta"> — one unapproved task; you approve its scope before anything builds</span></form></div>`;
|
|
23070
23594
|
const reviewCard = (commentRows === "" && commentForm === "" ? "" : `<h2 id="review">Review and revise</h2>` +
|
|
23071
23595
|
`<p class="meta">Annotate the diff above. When the batch is ready, create one scoped revision task from it.</p>` +
|
|
@@ -23081,8 +23605,8 @@ function runPage(chrome, run, taskId, artifacts, terminal = null, notes = [], cs
|
|
|
23081
23605
|
: `<form method="post" action="/r/${run.id}/note" class="row">` +
|
|
23082
23606
|
`<input type="hidden" name="csrf" value="${escape(csrf)}">` +
|
|
23083
23607
|
`<input type="text" name="note" placeholder="a note for whoever reads this run next" aria-label="run note" style="width:100%;max-width:28rem">` +
|
|
23084
|
-
`<button type="submit">
|
|
23085
|
-
const notesCard = noteRows === "" && noteForm === "" ? "" : `<h2>
|
|
23608
|
+
`<button type="submit">Add note</button></form>`;
|
|
23609
|
+
const notesCard = noteRows === "" && noteForm === "" ? "" : `<h2>Operator notes</h2>${noteRows}${noteForm}`;
|
|
23086
23610
|
// Continuation (Phase 2E, A4): a finished attempt offers a watched
|
|
23087
23611
|
// follow-up — the text you type here enters the SIGNED terms on the
|
|
23088
23612
|
// confirm screen; nothing runs until your password agrees to exactly it.
|
|
@@ -23222,7 +23746,7 @@ function capsPage(chrome, caps, gaps, repo, now) {
|
|
|
23222
23746
|
if (caps === null) {
|
|
23223
23747
|
return screen("requirements", [
|
|
23224
23748
|
`<h1>Requirements</h1>`,
|
|
23225
|
-
`<p class="meta">
|
|
23749
|
+
`<p class="meta">Open a project to see its requirements — <a href="/projects">projects</a></p>`,
|
|
23226
23750
|
].join("\n"), { chrome });
|
|
23227
23751
|
}
|
|
23228
23752
|
const list = caps.length === 0
|
|
@@ -23232,7 +23756,7 @@ function capsPage(chrome, caps, gaps, repo, now) {
|
|
|
23232
23756
|
`${escape(describeCapability(capability, now ?? new Date()))}</p>`)
|
|
23233
23757
|
.join("\n");
|
|
23234
23758
|
const blocked = gaps.length === 0
|
|
23235
|
-
? `<p class="meta">
|
|
23759
|
+
? `<p class="meta">No gaps — everything recorded is verified</p>`
|
|
23236
23760
|
: gaps
|
|
23237
23761
|
.map(gap => `<div class="card"><p>${escape(gap.key)} — ${escape(gap.state)}</p>` +
|
|
23238
23762
|
`<p class="meta">${gap.unblocks.length > 0
|
|
@@ -23240,16 +23764,16 @@ function capsPage(chrome, caps, gaps, repo, now) {
|
|
|
23240
23764
|
: gap.alsoBlocks.length > 0
|
|
23241
23765
|
? `part of what holds: ${gap.alsoBlocks.map(one => escape(one)).join(", ")}`
|
|
23242
23766
|
: "nothing queued needs it yet"}</p>` +
|
|
23243
|
-
`<p class="meta">
|
|
23767
|
+
`<p class="meta">Verify: ${escape(gap.verify)}</p>` +
|
|
23244
23768
|
`<p class="meta">${escape(gap.instructions)}</p></div>`)
|
|
23245
23769
|
.join("\n");
|
|
23246
23770
|
return screen("requirements", [
|
|
23247
23771
|
`<h1>Requirements</h1>`,
|
|
23248
23772
|
`<p class="hint">tools and credentials builds need — each is probed on the worker before any build spends money; values never leave your machine</p>`,
|
|
23249
23773
|
list,
|
|
23250
|
-
`<h2>
|
|
23774
|
+
`<h2>Missing, ranked by what filling them frees</h2>`,
|
|
23251
23775
|
blocked,
|
|
23252
|
-
`<p class="meta">
|
|
23776
|
+
`<p class="meta">Read-only here: checks are shell commands you wrote, and a web button that runs shell would need its own security review</p>`,
|
|
23253
23777
|
].join("\n"), { chrome });
|
|
23254
23778
|
}
|
|
23255
23779
|
/** v98: how the bot's messages reach Toolroll, in words: pushed to the public address, or asked for. */
|
|
@@ -23278,7 +23802,7 @@ function settingsPage(chrome, existing, hasEnv, csrf, problem, messaging = null,
|
|
|
23278
23802
|
`<input type="hidden" name="csrf" value="${escape(csrf)}">` +
|
|
23279
23803
|
permissionModeChoices("permission-mode", permissionDefault.mode) +
|
|
23280
23804
|
`<button type="submit">Save default</button></form>`
|
|
23281
|
-
: `<div class="card"><p><strong>${permissionDefault.mode === "bypassPermissions" ? "Full access" : "Auto"}</strong></p><p class="meta">
|
|
23805
|
+
: `<div class="card"><p><strong>${permissionDefault.mode === "bypassPermissions" ? "Full access" : "Auto"}</strong></p><p class="meta">An approver can change this default</p></div>`,
|
|
23282
23806
|
permissionDefault.updatedAt === null
|
|
23283
23807
|
? ""
|
|
23284
23808
|
: `<p class="meta settings-changed">Changed ${whenTime(permissionDefault.updatedAt)}${permissionDefault.updatedBy === null ? "" : ` by ${escape(permissionDefault.updatedBy)}`}</p>`,
|
|
@@ -23292,7 +23816,7 @@ function settingsPage(chrome, existing, hasEnv, csrf, problem, messaging = null,
|
|
|
23292
23816
|
`<input type="hidden" name="csrf" value="${escape(csrf)}">` +
|
|
23293
23817
|
qualityModeChoices("quality-mode", qualityDefault.mode) +
|
|
23294
23818
|
`<button type="submit">Save default</button></form>`
|
|
23295
|
-
: `<div class="card"><p><strong>${escape(qualityModeTitle(qualityDefault.mode))}</strong></p><p class="meta">
|
|
23819
|
+
: `<div class="card"><p><strong>${escape(qualityModeTitle(qualityDefault.mode))}</strong></p><p class="meta">An approver can change this default</p></div>`,
|
|
23296
23820
|
qualityDefault.updatedAt === null
|
|
23297
23821
|
? ""
|
|
23298
23822
|
: `<p class="meta settings-changed">Changed ${whenTime(qualityDefault.updatedAt)}${qualityDefault.updatedBy === null ? "" : ` by ${escape(qualityDefault.updatedBy)}`}</p>`,
|
|
@@ -23682,18 +24206,18 @@ function nextPage(chrome, data) {
|
|
|
23682
24206
|
`<p>${escape(item.approval.title)}</p>` +
|
|
23683
24207
|
(data.planDocument === null
|
|
23684
24208
|
? ""
|
|
23685
|
-
: `<div class="card"><p><strong>
|
|
24209
|
+
: `<div class="card"><p><strong>The plan</strong> <span class="meta">drafted by a planning session</span></p>${executionPlanHtml(data.planDocument, true)}${planContractHtml(data.planContract ?? null, "ceremony")}</div>`) +
|
|
23686
24210
|
`<form method="post" action="${taskHref(item.approval.taskId)}/approve" class="card approve-form">` +
|
|
23687
24211
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
|
|
23688
24212
|
`<input type="hidden" name="nonce" value="${escape(data.nonce)}">` +
|
|
23689
24213
|
`<input type="hidden" name="digest" value="${escape(data.approvalDigest ?? item.approval.digest)}">` +
|
|
23690
24214
|
`<input type="hidden" name="return" value="next">` +
|
|
23691
|
-
`<p><strong>
|
|
23692
|
-
(data.deliverable === "report" ? `<p class="meta"><span class="badge">
|
|
24215
|
+
`<p><strong>Approve exactly this:</strong></p>` +
|
|
24216
|
+
(data.deliverable === "report" ? `<p class="meta"><span class="badge">Scout</span> a read-only session investigates this goal and delivers a report — no branch, nothing changes in the repository</p>` : "") +
|
|
23693
24217
|
(scope === null || scope.profileState !== "unresolved" ? "" : profileWords(scope)) +
|
|
23694
|
-
`<p class="meta">
|
|
23695
|
-
`<p class="meta">
|
|
23696
|
-
`<p class="meta">
|
|
24218
|
+
`<p class="meta">Goal</p><p class="recap" style="margin-top:0">${escape(scope?.goal ?? item.approval.goal)}</p>` +
|
|
24219
|
+
`<p class="meta">Not this</p><p class="recap" style="margin-top:0">${scope?.outOfScope == null ? "<em>no exclusions</em>" : escape(scope.outOfScope)}</p>` +
|
|
24220
|
+
`<p class="meta">Touches · ${scope === null || scope.touches.length === 0 ? "anything" : scope.touches.map(one => escape(one)).join(", ")}</p>` +
|
|
23697
24221
|
(scope === null ? "" : acceptanceCeremonyHtml(scope.acceptance)) +
|
|
23698
24222
|
(data.raceTerms === null
|
|
23699
24223
|
? ""
|
|
@@ -23703,8 +24227,8 @@ function nextPage(chrome, data) {
|
|
|
23703
24227
|
// tap away, never in the way.
|
|
23704
24228
|
agentsCeremonyHtml(data.route) +
|
|
23705
24229
|
(scope === null ? "" : runtimeDetailsHtml(scope)) +
|
|
23706
|
-
`<label>
|
|
23707
|
-
`<div class="sticky-actions"><button type="submit">
|
|
24230
|
+
`<label>Your password, typed again — a signed-in session alone cannot agree to work<input type="password" name="token" autocomplete="current-password"></label>` +
|
|
24231
|
+
`<div class="sticky-actions"><button type="submit">Approve this scope</button></div>` +
|
|
23708
24232
|
`</form>` +
|
|
23709
24233
|
`<p class="meta"><a href="${taskHref(item.approval.taskId)}">open the full task</a> to edit the scope first</p>`;
|
|
23710
24234
|
}
|
|
@@ -23712,22 +24236,22 @@ function nextPage(chrome, data) {
|
|
|
23712
24236
|
card =
|
|
23713
24237
|
`<h1>${escape(item.stalled.taskId)}</h1>` +
|
|
23714
24238
|
`<p>${escape(item.stalled.title)}</p>` +
|
|
23715
|
-
`<p class="meta">
|
|
24239
|
+
`<p class="meta">Stopped — ${item.stalled.incidentCount} incident(s)${item.stalled.strikes > 0 ? ` after ${item.stalled.strikes} attempt(s)` : ""}</p>` +
|
|
23716
24240
|
`<form method="post" action="${taskHref(item.stalled.taskId)}/requeue" class="card">` +
|
|
23717
24241
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
|
|
23718
24242
|
`<input type="hidden" name="return" value="next">` +
|
|
23719
|
-
`<p class="meta">
|
|
23720
|
-
`<button type="submit">
|
|
24243
|
+
`<p class="meta">Requeue resolves the incidents, clears the failed attempts, and puts it back in line</p>` +
|
|
24244
|
+
`<button type="submit">Retry this work</button>` +
|
|
23721
24245
|
`</form>` +
|
|
23722
24246
|
`<p class="meta"><a href="${taskHref(item.stalled.taskId)}">open the full task</a> to read the runs first</p>`;
|
|
23723
24247
|
}
|
|
23724
24248
|
else {
|
|
23725
24249
|
const { gap } = item;
|
|
23726
24250
|
card =
|
|
23727
|
-
`<h1>
|
|
24251
|
+
`<h1>Supply ${escape(gap.key)}</h1>` +
|
|
23728
24252
|
`<p class="meta">${escape(gap.state)}</p>` +
|
|
23729
24253
|
`<p>Filling this starts ${gap.unblocks.length} task(s): ${gap.unblocks.map(one => `<span class="mono">${escape(one)}</span>`).join(", ")}</p>` +
|
|
23730
|
-
`<div class="card"><p class="meta">
|
|
24254
|
+
`<div class="card"><p class="meta">Prove it filled from the terminal:</p><pre class="recap">${escape(gap.verify)}</pre></div>`;
|
|
23731
24255
|
}
|
|
23732
24256
|
return screen("next", [header, card].join("\n"), { chrome });
|
|
23733
24257
|
}
|
|
@@ -23748,9 +24272,9 @@ function decisionOptionForms(decision, csrf, returnTo) {
|
|
|
23748
24272
|
`<input type="hidden" name="choice" value="${escape(option.id)}">`,
|
|
23749
24273
|
...(returnTo === null ? [] : [`<input type="hidden" name="return" value="${escape(returnTo)}">`]),
|
|
23750
24274
|
...(option.reversible ? [] : [`<input type="hidden" name="confirm" value="yes">`]),
|
|
23751
|
-
recommended ? `<p class="meta" style="margin:0 0 .375rem"><span class="badge">
|
|
24275
|
+
recommended ? `<p class="meta" style="margin:0 0 .375rem"><span class="badge">Recommended</span></p>` : "",
|
|
23752
24276
|
`<p class="consequence">${escape(option.consequence)}</p>`,
|
|
23753
|
-
`<button type="submit">${escape(option.label)}${option.reversible ? "" : ` <span class="badge badge-overdue">
|
|
24277
|
+
`<button type="submit">${escape(option.label)}${option.reversible ? "" : ` <span class="badge badge-overdue">Irreversible</span>`}</button>`,
|
|
23754
24278
|
`<input type="text" name="note" placeholder="optional note — travels with this answer" aria-label="optional note">`,
|
|
23755
24279
|
`</form>`,
|
|
23756
24280
|
].join("\n");
|
|
@@ -23768,14 +24292,14 @@ function decisionPage(chrome, decision, taskId, artifacts, who, now, returnTo =
|
|
|
23768
24292
|
: "";
|
|
23769
24293
|
const evidence = artifacts.length === 0
|
|
23770
24294
|
? ""
|
|
23771
|
-
: `<div class="evidence"><strong>
|
|
24295
|
+
: `<div class="evidence"><strong>Evidence</strong>` +
|
|
23772
24296
|
artifacts
|
|
23773
24297
|
.map(artifact => `<a href="/d/${decision.id}/evidence/${artifact.id}">${escape(evidenceWords(artifact.kind))}` +
|
|
23774
24298
|
`${artifact.truncated ? " (truncated)" : ""} · ${artifact.bytesStored} bytes</a>`)
|
|
23775
24299
|
.join("\n") +
|
|
23776
24300
|
"</div>";
|
|
23777
24301
|
return screen(`decide \u00b7 ${taskId}`, [
|
|
23778
|
-
`<h1>${escape(taskId)} <span class="badge badge-${escape(decision.state)}">${escape(decision.state)}</span>${isOverdue(decision, now) ? ` <span class="badge badge-overdue">
|
|
24302
|
+
`<h1>${escape(taskId)} <span class="badge badge-${escape(decision.state)}">${escape(decision.state)}</span>${isOverdue(decision, now) ? ` <span class="badge badge-overdue">Overdue</span>` : ""}${decision.deadline === null ? "" : ` <span class="meta">deadline ${escape(decision.deadline)}</span>`}</h1>`,
|
|
23779
24303
|
`<div class="recap">${escape(decision.recap)}</div>`,
|
|
23780
24304
|
`<div class="question">${escape(decision.question)}</div>`,
|
|
23781
24305
|
decision.state === "answered" ? answered : options,
|