toolroll 0.9.14 → 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 +15 -0
- package/dist/assignment-ui.d.ts +1 -0
- package/dist/assignment-ui.js +6 -4
- 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 +2 -1
- package/dist/browser-workspace.d.ts +88 -18
- 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/lead-status.js +2 -2
- package/dist/needs-you.d.ts +48 -0
- package/dist/needs-you.js +93 -1
- package/dist/result-acts.d.ts +49 -5
- package/dist/result-acts.js +34 -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 +1121 -677
- package/dist/slack-chat.js +1 -1
- package/dist/task-status.d.ts +21 -4
- package/dist/task-status.js +41 -16
- 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 +13 -3
- package/dist/workspace-motion.js +3 -3
- package/dist/workspace-ui.d.ts +43 -7
- package/dist/workspace-ui.js +59 -15
- 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, NO_REASON_RECORDED, failedAttemptSentence, isInternalErrorReason, latestFinishedAttempt, runReasonWords } 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, evidenceProblemOf, lastErrorLineOf, reportMismatchesOf, ACCEPT_NEEDS_REASON, MISMATCH_HEADLINE, 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,34 +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
|
-
/** What went wrong with the latest finished attempt (whatever its outcome, never an older failure)
|
|
5899
|
-
|
|
5900
|
-
* words, else that no reason was recorded. A reason that is machine output reads as an internal error, its detail
|
|
5901
|
-
* behind a link to that line of the attempt's record; nothing links to this task's own page. */
|
|
5902
|
-
function failureOf(runs, assignment) {
|
|
5960
|
+
/** What went wrong with the latest finished attempt (whatever its outcome, never an older failure). */
|
|
5961
|
+
function failureOf(runs, assignment, readLog = true) {
|
|
5903
5962
|
const last = latestFinishedAttempt(runs);
|
|
5904
5963
|
if (last === null)
|
|
5905
|
-
return { line: NO_REASON_RECORDED, link: null };
|
|
5906
|
-
const
|
|
5907
|
-
|
|
5908
|
-
|
|
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) {
|
|
5909
5984
|
let read = null;
|
|
5910
5985
|
try {
|
|
5911
|
-
read =
|
|
5986
|
+
read = readVerifiedArtifact(evidenceRoot, logArtifact);
|
|
5912
5987
|
}
|
|
5913
5988
|
catch {
|
|
5914
5989
|
read = null;
|
|
5915
5990
|
}
|
|
5916
|
-
const
|
|
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;
|
|
5917
5995
|
if (found !== null) {
|
|
5918
|
-
|
|
5919
|
-
|
|
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}` } };
|
|
5920
5999
|
}
|
|
5921
6000
|
}
|
|
5922
|
-
|
|
5923
|
-
|
|
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 };
|
|
5924
6010
|
}
|
|
5925
|
-
|
|
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) {
|
|
5926
6023
|
const admittedFamily = familyOf(taskId);
|
|
5927
6024
|
if (admittedFamily?.problem != null)
|
|
5928
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") }));
|
|
@@ -5937,6 +6034,8 @@ export function createDecisionServer(options) {
|
|
|
5937
6034
|
presentedData.scopeDraft = scopeDraft;
|
|
5938
6035
|
if (cancelDraft !== undefined)
|
|
5939
6036
|
presentedData.cancelDraft = cancelDraft;
|
|
6037
|
+
if (editPlan)
|
|
6038
|
+
presentedData.editPlan = true;
|
|
5940
6039
|
presentedData.pullRequest = taskPullRequestOf(taskId, who);
|
|
5941
6040
|
// The thread reads the whole family: the original, its revisions, and every attempt across them.
|
|
5942
6041
|
if (family !== null && family.problem === null) {
|
|
@@ -5946,6 +6045,8 @@ export function createDecisionServer(options) {
|
|
|
5946
6045
|
}
|
|
5947
6046
|
// What went wrong reads the family's latest finished attempt, as the Tasks row does.
|
|
5948
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();
|
|
5949
6050
|
const paneProject = restricted() ? store.lookupRef(taskId)?.repo ?? null : who.via === "cookie" ? who.session.project : null;
|
|
5950
6051
|
const page = taskPage(paneProject === null && !unscopedMode
|
|
5951
6052
|
? chromeFor(paneProject, "tasks")
|
|
@@ -6126,7 +6227,7 @@ export function createDecisionServer(options) {
|
|
|
6126
6227
|
return peekSay(seen.reason);
|
|
6127
6228
|
const stamp = clock().toISOString().slice(11, 19);
|
|
6128
6229
|
const parts = [
|
|
6129
|
-
`<p class="meta">
|
|
6230
|
+
`<p class="meta">Best-effort look at ${escape(stamp)} UTC — files can change mid-read</p>`,
|
|
6130
6231
|
];
|
|
6131
6232
|
const changed = seen.rows.filter(one => one.kind === "changed");
|
|
6132
6233
|
const deleted = seen.rows.filter(one => one.kind === "deleted");
|
|
@@ -6151,7 +6252,7 @@ export function createDecisionServer(options) {
|
|
|
6151
6252
|
for (const row of deleted)
|
|
6152
6253
|
parts.push(line(row, "−"));
|
|
6153
6254
|
if (fresh.total > 0) {
|
|
6154
|
-
parts.push(`<p class="meta">
|
|
6255
|
+
parts.push(`<p class="meta">New files · ${fresh.total}</p>`);
|
|
6155
6256
|
for (const row of fresh.rows) {
|
|
6156
6257
|
parts.push(row.collapsed
|
|
6157
6258
|
? `<p class="row mono">+ ${peekName(row.label)} <span class="meta">collapsed names — ${row.count} files</span></p>`
|
|
@@ -6162,7 +6263,7 @@ export function createDecisionServer(options) {
|
|
|
6162
6263
|
}
|
|
6163
6264
|
}
|
|
6164
6265
|
if (unchecked.length > 0) {
|
|
6165
|
-
parts.push(`<p class="meta">
|
|
6266
|
+
parts.push(`<p class="meta">Not verified this look — absence above does not mean unchanged:</p>`);
|
|
6166
6267
|
for (const row of unchecked)
|
|
6167
6268
|
parts.push(line(row, "?"));
|
|
6168
6269
|
}
|
|
@@ -6173,7 +6274,7 @@ export function createDecisionServer(options) {
|
|
|
6173
6274
|
// The byte cap is enforced AFTER escaping (finding 32): an oversize
|
|
6174
6275
|
// rendering is replaced whole by its exact counts.
|
|
6175
6276
|
fragment =
|
|
6176
|
-
`<p class="meta">
|
|
6277
|
+
`<p class="meta">Best-effort look at ${escape(stamp)} UTC</p>` +
|
|
6177
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>`;
|
|
6178
6279
|
}
|
|
6179
6280
|
peekCache.set(key, { fragment, at: Date.now() });
|
|
@@ -8604,7 +8705,7 @@ export function createDecisionServer(options) {
|
|
|
8604
8705
|
// THE CEREMONY (M1): every resolved term in words, and the password
|
|
8605
8706
|
// signs exactly this digest — a drifted form refuses at /mode/sign.
|
|
8606
8707
|
const nonce = mintApprovalNonce(who.name, "mode-sign", `${project}:${digest}`);
|
|
8607
|
-
const bodyHtml = `<h1 style="overflow-wrap:anywhere">
|
|
8708
|
+
const bodyHtml = `<h1 style="overflow-wrap:anywhere">Sign the ${escape(name)} mode for ${escape(project)}</h1>` +
|
|
8608
8709
|
`<form method="post" action="/mode/sign" class="card approve-form">` +
|
|
8609
8710
|
`<input type="hidden" name="csrf" value="${escape(who.session.csrf)}">` +
|
|
8610
8711
|
`<input type="hidden" name="nonce" value="${escape(nonce)}">` +
|
|
@@ -8613,12 +8714,12 @@ export function createDecisionServer(options) {
|
|
|
8613
8714
|
.map(field => `<input type="hidden" name="${field}" value="${escape(body.get(field) ?? "")}">`)
|
|
8614
8715
|
.join("") +
|
|
8615
8716
|
`<input type="hidden" name="expiry" value="${escape(expiry)}">` +
|
|
8616
|
-
`<p><strong>
|
|
8717
|
+
`<p><strong>Your password signs exactly this:</strong></p>` +
|
|
8617
8718
|
modeWords(terms)
|
|
8618
8719
|
.map(words => `<p class="recap" style="margin-top:.4rem">${escape(words)}</p>`)
|
|
8619
8720
|
.join("") +
|
|
8620
|
-
`<label>
|
|
8621
|
-
`<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>` +
|
|
8622
8723
|
`</form>` +
|
|
8623
8724
|
`<p class="meta"><a href="/mode">back — sign nothing</a></p>`;
|
|
8624
8725
|
return sendScreen(response, 200, screen("mode", bodyHtml, { chrome: chromeFor(project, "mode") }));
|
|
@@ -9936,7 +10037,9 @@ export function createDecisionServer(options) {
|
|
|
9936
10037
|
const found = store.getRun(id);
|
|
9937
10038
|
if (found === null || !visible(taskRepoOf(found.taskRef)))
|
|
9938
10039
|
return refuse(response, who, 404, "no such run");
|
|
9939
|
-
|
|
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]);
|
|
9940
10043
|
if (who.via !== "cookie")
|
|
9941
10044
|
return refuse(response, who, 403, "Sign in with a browser session to do this.", back);
|
|
9942
10045
|
if (followUp[2] === "checks") {
|
|
@@ -9948,7 +10051,8 @@ export function createDecisionServer(options) {
|
|
|
9948
10051
|
const asked = requestFollowUpChecks(store, { runId: id, level, actor: who.name }, now);
|
|
9949
10052
|
if (!asked.ok)
|
|
9950
10053
|
return refuse(response, who, 409, asked.message, back);
|
|
9951
|
-
|
|
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`);
|
|
9952
10056
|
}
|
|
9953
10057
|
const filed = fileAddTestsTask(store, evidenceRoot, { runId: id, actor: who.name, filedVia: "console", ...(admissionList() === null ? {} : { admittedRepos: admissionList() }) }, now);
|
|
9954
10058
|
if (!filed.ok)
|
|
@@ -10162,7 +10266,7 @@ export function createDecisionServer(options) {
|
|
|
10162
10266
|
const quickMode = quickRef?.repo == null ? null : store.activeMode(quickRef.repo, clock());
|
|
10163
10267
|
const quickTerms = quickMode === null ? null : modeTermsFromJson(quickMode.termsJson);
|
|
10164
10268
|
const quick = quickTerms?.quickMint === true && quickMode !== null && quickMode.signedBy === who.name;
|
|
10165
|
-
const body = `<h1>
|
|
10269
|
+
const body = `<h1>Run ${escape(terms.taskId)} once, while you watch</h1>` +
|
|
10166
10270
|
`<form method="post" action="${taskHref(terms.taskId)}/attend" class="card approve-form">` +
|
|
10167
10271
|
`<input type="hidden" name="csrf" value="${escape(csrf)}">` +
|
|
10168
10272
|
`<input type="hidden" name="nonce" value="${escape(nonce)}">` +
|
|
@@ -10175,10 +10279,10 @@ export function createDecisionServer(options) {
|
|
|
10175
10279
|
(inputs.posture === undefined ? "" : `<input type="hidden" name="posture" value="${escape(inputs.posture)}">`) +
|
|
10176
10280
|
(terms.parentRun == null ? "" : `<input type="hidden" name="parent" value="${terms.parentRun}">`) +
|
|
10177
10281
|
(terms.followup == null ? "" : `<input type="hidden" name="followup" value="${escape(terms.followup)}">`) +
|
|
10178
|
-
`<p><strong>
|
|
10179
|
-
`<p class="meta">
|
|
10180
|
-
`<p class="meta">
|
|
10181
|
-
`<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>` +
|
|
10182
10286
|
(() => {
|
|
10183
10287
|
const profile = JSON.parse(terms.profileJson).profile;
|
|
10184
10288
|
const posture = profile?.permissionArgv === "bypassPermissions"
|
|
@@ -10186,21 +10290,21 @@ export function createDecisionServer(options) {
|
|
|
10186
10290
|
: profile?.permissionArgv === "auto"
|
|
10187
10291
|
? "safe unattended permissions — routine project commands and edits proceed; risky acts stop"
|
|
10188
10292
|
: "legacy acceptEdits — edits proceed, commands that ask are denied unattended";
|
|
10189
|
-
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>`;
|
|
10190
10294
|
})() +
|
|
10191
10295
|
(terms.parentRun == null
|
|
10192
10296
|
? ""
|
|
10193
|
-
: `<p class="meta">
|
|
10194
|
-
`<p class="meta">
|
|
10195
|
-
`<p class="meta">
|
|
10196
|
-
`<p class="meta">
|
|
10197
|
-
`<p class="meta">
|
|
10198
|
-
`<p class="meta">
|
|
10199
|
-
`<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>` +
|
|
10200
10304
|
(quick
|
|
10201
|
-
? `<p class="meta">
|
|
10202
|
-
: `<label>
|
|
10203
|
-
`<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>` +
|
|
10204
10308
|
`</form>` +
|
|
10205
10309
|
`<p class="meta"><a href="${taskHref(terms.taskId)}">back to the task</a></p>`;
|
|
10206
10310
|
// The chrome binds to the AUTHORITY repository (surfaces round 1,
|
|
@@ -10748,6 +10852,13 @@ export function createDecisionServer(options) {
|
|
|
10748
10852
|
if (budgetUsd !== null && (!Number.isFinite(budgetUsd) || budgetUsd <= 0)) {
|
|
10749
10853
|
return taskScreen(response, who, taskId, "the dollar cap is a positive amount", 400);
|
|
10750
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");
|
|
10751
10862
|
// The tournament controls (operator request): a count of 2–4 files
|
|
10752
10863
|
// race terms BESIDE the scope — validated and priced BEFORE anything
|
|
10753
10864
|
// saves, so a bad tournament never half-lands on a good scope.
|
|
@@ -10802,92 +10913,112 @@ export function createDecisionServer(options) {
|
|
|
10802
10913
|
if (plannedRace !== null && store.mirrorByTask(taskId) !== null) {
|
|
10803
10914
|
return taskScreen(response, who, taskId, "external work races in a follow-up release — file the tournament on a local task", 409);
|
|
10804
10915
|
}
|
|
10805
|
-
|
|
10806
|
-
|
|
10807
|
-
|
|
10808
|
-
|
|
10809
|
-
|
|
10810
|
-
|
|
10811
|
-
|
|
10812
|
-
|
|
10813
|
-
|
|
10814
|
-
|
|
10815
|
-
|
|
10816
|
-
|
|
10817
|
-
|
|
10818
|
-
|
|
10819
|
-
|
|
10820
|
-
|
|
10821
|
-
|
|
10822
|
-
|
|
10823
|
-
|
|
10824
|
-
|
|
10825
|
-
|
|
10826
|
-
|
|
10827
|
-
|
|
10828
|
-
|
|
10829
|
-
|
|
10830
|
-
|
|
10831
|
-
|
|
10832
|
-
|
|
10833
|
-
|
|
10834
|
-
|
|
10835
|
-
|
|
10836
|
-
|
|
10837
|
-
|
|
10838
|
-
|
|
10839
|
-
|
|
10840
|
-
|
|
10841
|
-
|
|
10842
|
-
|
|
10843
|
-
|
|
10844
|
-
|
|
10845
|
-
|
|
10846
|
-
|
|
10847
|
-
|
|
10848
|
-
|
|
10849
|
-
|
|
10850
|
-
|
|
10851
|
-
|
|
10852
|
-
|
|
10853
|
-
|
|
10854
|
-
|
|
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
|
+
}
|
|
10855
10981
|
}
|
|
10856
10982
|
}
|
|
10857
|
-
|
|
10858
|
-
|
|
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
|
+
}
|
|
10859
11004
|
store.fileTournamentTerms({
|
|
10860
11005
|
taskRef: ref.id,
|
|
10861
|
-
|
|
10862
|
-
|
|
10863
|
-
|
|
10864
|
-
|
|
10865
|
-
|
|
10866
|
-
|
|
10867
|
-
|
|
10868
|
-
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,
|
|
10869
11013
|
}, now);
|
|
10870
11014
|
return { ok: true };
|
|
10871
|
-
}
|
|
10872
|
-
|
|
10873
|
-
|
|
10874
|
-
|
|
10875
|
-
|
|
10876
|
-
|
|
10877
|
-
|
|
10878
|
-
}
|
|
10879
|
-
store.fileTournamentTerms({
|
|
10880
|
-
taskRef: ref.id,
|
|
10881
|
-
raceDigest: plannedRace.raceDigest,
|
|
10882
|
-
agents: plannedRace.agents,
|
|
10883
|
-
perAgentBudgetMicrousd: plannedRace.perAgentBudgetMicrousd,
|
|
10884
|
-
overrunReserveMicrousd: plannedRace.overrunReserveMicrousd,
|
|
10885
|
-
totalBudgetMicrousd: plannedRace.totalBudgetMicrousd,
|
|
10886
|
-
priceVersion: plannedRace.priceVersion,
|
|
10887
|
-
publicationPolicy: plannedRace.publicationPolicy,
|
|
10888
|
-
}, now);
|
|
10889
|
-
return { ok: true };
|
|
10890
|
-
});
|
|
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
|
+
}
|
|
10891
11022
|
if (!saved.ok)
|
|
10892
11023
|
return taskScreen(response, who, taskId, saved.message, saved.status, body);
|
|
10893
11024
|
return redirect(response, taskHref(taskId));
|
|
@@ -11003,6 +11134,9 @@ export function createDecisionServer(options) {
|
|
|
11003
11134
|
}
|
|
11004
11135
|
note = validated.note;
|
|
11005
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);
|
|
11006
11140
|
store.acceptProof(latest.id, verifiedAuthor(who.name), note, now);
|
|
11007
11141
|
return redirect(response, body.get("return") === null ? taskHref(taskId) : safeReturn(body.get("return")));
|
|
11008
11142
|
}
|
|
@@ -11018,6 +11152,23 @@ export function createDecisionServer(options) {
|
|
|
11018
11152
|
if (current === null || current.receipt?.taskId !== taskId || !/^[0-9]{1,15}$/.test(namedRun) || current.receipt.runId !== Number(namedRun)) {
|
|
11019
11153
|
return taskScreen(response, who, taskId, "This result changed. Open the current result before marking it complete.", 409);
|
|
11020
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
|
+
}
|
|
11021
11172
|
if (body.get("publish") === "1") {
|
|
11022
11173
|
// "Complete and open a pull request": the same exact-receipt completion, plus the owed PR for this
|
|
11023
11174
|
// exact commit in one transaction. The watch process pushes and opens it; the task shows its progress.
|
|
@@ -11438,6 +11589,36 @@ export function createDecisionServer(options) {
|
|
|
11438
11589
|
const run = store.runsFor(ref.id).find(one => (one.outcome === "built" || one.outcome === "no-change") &&
|
|
11439
11590
|
one.finishedAt !== null &&
|
|
11440
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 };
|
|
11441
11622
|
const publication = run === null ? null : store.publicationForRun(run.id);
|
|
11442
11623
|
const verdict = run === null ? null : store.proofVerdictFor(run.id);
|
|
11443
11624
|
return {
|
|
@@ -11628,6 +11809,7 @@ export function createDecisionServer(options) {
|
|
|
11628
11809
|
intent,
|
|
11629
11810
|
plan,
|
|
11630
11811
|
contest: contestOf(),
|
|
11812
|
+
...(store.isDemo() ? { demo: true } : {}),
|
|
11631
11813
|
};
|
|
11632
11814
|
if (run === null) {
|
|
11633
11815
|
return { ...base, run: null, detail: null, reviewRetry: null, review: null, notes: [] };
|
|
@@ -11949,8 +12131,8 @@ export function editorFileHref(worktree, path, line) {
|
|
|
11949
12131
|
function matrixStateBadge(state) {
|
|
11950
12132
|
// Red belongs to a Failed headline alone (task-status.ts); an unmet requirement is an amber note.
|
|
11951
12133
|
const cls = state === "pass" ? "badge-done" : state === "failed" ? "badge-note" : state === "missing" ? "badge-note" : "badge-manual-review";
|
|
11952
|
-
|
|
11953
|
-
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>`;
|
|
11954
12136
|
}
|
|
11955
12137
|
/** v40: the bounded repair chain's own line — presented as the user-facing
|
|
11956
12138
|
* recovery it represents, without exposing the internal workflow name.
|
|
@@ -12051,8 +12233,9 @@ function criterionMatrixHtml(matrix, options = {}) {
|
|
|
12051
12233
|
const awaitingAssessment = row.assessment?.evidenceState === "pass" && row.review === null;
|
|
12052
12234
|
const confirmed = row.assessment !== undefined && state === "pass";
|
|
12053
12235
|
// Plain words (task-status brief): the retired assessment step is not a state a person acts on.
|
|
12054
|
-
|
|
12055
|
-
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";
|
|
12056
12239
|
const warnings = [];
|
|
12057
12240
|
// Only replace the known boilerplate. Other recorded failure details
|
|
12058
12241
|
// remain verbatim, so concision cannot hide a different problem.
|
|
@@ -12122,10 +12305,10 @@ function planContractHtml(view, mode) {
|
|
|
12122
12305
|
if ("problem" in view) {
|
|
12123
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>`;
|
|
12124
12307
|
}
|
|
12125
|
-
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>`;
|
|
12126
12309
|
if (!view.filed) {
|
|
12127
12310
|
return mode === "full"
|
|
12128
|
-
? `<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>`
|
|
12129
12312
|
: `<p class="meta contract-note">no scope was filed before planning — every term above is the planner's proposal</p>`;
|
|
12130
12313
|
}
|
|
12131
12314
|
if (view.revision === true) {
|
|
@@ -12135,19 +12318,19 @@ function planContractHtml(view, mode) {
|
|
|
12135
12318
|
}
|
|
12136
12319
|
return (`<div class="contract-panel contract-amended"${mode === "full" ? ` id="contract-amendment"` : ""}><p class="approval-label">updated plan</p>` +
|
|
12137
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>` +
|
|
12138
|
-
(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>`) +
|
|
12139
12322
|
`<ul class="recap contract-changes">${view.changes.map(contractChangeHtml).join("")}</ul>` +
|
|
12140
12323
|
stale +
|
|
12141
12324
|
`</div>`);
|
|
12142
12325
|
}
|
|
12143
12326
|
if (view.changes.length === 0) {
|
|
12144
|
-
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>`;
|
|
12145
12328
|
}
|
|
12146
12329
|
return (`<div class="contract-panel contract-amended"${mode === "full" ? ` id="contract-amendment"` : ""}><p class="approval-label">filed contract · amendment proposed</p>` +
|
|
12147
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>` +
|
|
12148
12331
|
(view.amendment === null
|
|
12149
|
-
? `<p class="meta">
|
|
12150
|
-
: `<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>`) +
|
|
12151
12334
|
`<ul class="recap contract-changes">${view.changes.map(contractChangeHtml).join("")}</ul>` +
|
|
12152
12335
|
stale +
|
|
12153
12336
|
`</div>`);
|
|
@@ -12176,7 +12359,7 @@ function approvalPathsHtml(touches) {
|
|
|
12176
12359
|
function acceptanceCeremonyHtml(criteria) {
|
|
12177
12360
|
if (criteria.length === 0)
|
|
12178
12361
|
return "";
|
|
12179
|
-
return (`<p class="meta">
|
|
12362
|
+
return (`<p class="meta">Acceptance</p><ul class="recap acceptance-rubric">` +
|
|
12180
12363
|
criteria
|
|
12181
12364
|
.map(c => `<li><code>${escape(c.id)}</code> ${escape(c.statement)} <span class="meta">[requires: ${c.evidence.map(escape).join(", ")}]</span></li>`)
|
|
12182
12365
|
.join("") +
|
|
@@ -12294,10 +12477,20 @@ function approvalSheetHtml(input) {
|
|
|
12294
12477
|
const planner = legOf("plan");
|
|
12295
12478
|
if (planner !== undefined)
|
|
12296
12479
|
who.push(`Planner ${agentNameWords(planner.provider, planner.model)}`);
|
|
12297
|
-
|
|
12298
|
-
|
|
12299
|
-
|
|
12300
|
-
|
|
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>`);
|
|
12301
12494
|
const race = raceTerms === null
|
|
12302
12495
|
? ""
|
|
12303
12496
|
: `<p class="approval-race">${raceTerms.n} agents build this separately: ${raceTerms.agents.map(one => escape(agentNameWords(one.provider, one.model))).join(" and ")}. ` +
|
|
@@ -12324,7 +12517,7 @@ function approvalSheetHtml(input) {
|
|
|
12324
12517
|
: !contract.filed && contract.revision !== true ? `<p class="meta">Nothing was filed before planning, so every term is the planner's proposal.</p>`
|
|
12325
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>`;
|
|
12326
12519
|
const criteriaDetail = scope.acceptance.length === 0 ? ""
|
|
12327
|
-
: `<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>`;
|
|
12328
12521
|
const projection = route?.projection ?? null;
|
|
12329
12522
|
const agentsDetail = route === null || route === undefined ? ""
|
|
12330
12523
|
: projection === null
|
|
@@ -12338,7 +12531,7 @@ function approvalSheetHtml(input) {
|
|
|
12338
12531
|
const limits = profileWords(scope) +
|
|
12339
12532
|
`<p class="meta">Checks level: ${escape(qualityModeTitle(scope.qualityMode ?? "default"))}${permission === null ? "" : ` · ${escape(permission)}`}</p>` +
|
|
12340
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>`);
|
|
12341
|
-
const details = `<details class="approval-details"><summary>
|
|
12534
|
+
const details = `<details class="approval-details"><summary>Plan details</summary>` +
|
|
12342
12535
|
revisionDetail +
|
|
12343
12536
|
(repairText === null ? "" : detail("Signed goal", `<p>${escape(scope.goal)}</p>`)) +
|
|
12344
12537
|
detail("Plan record", contractDetail) +
|
|
@@ -12348,6 +12541,53 @@ function approvalSheetHtml(input) {
|
|
|
12348
12541
|
(scope.candidate ? detail("Saved commit", `<p><code class="approval-commit">${escape(scope.candidate)}</code></p>`) : "") +
|
|
12349
12542
|
detail("Seal", `<p class="meta">Approval binds to this exact wording. <span class="seal mono">signs ${shortDigest(scope.digest)}</span></p>`) +
|
|
12350
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>`;
|
|
12351
12591
|
return (`<form method="post" action="${input.action}" class="approve-form approval-sheet"${input.surface === "task" ? ` id="approve"` : ""}${input.sticky ? " data-sticky" : ""}>` +
|
|
12352
12592
|
`<input type="hidden" name="csrf" value="${escape(input.csrf)}">` +
|
|
12353
12593
|
`<input type="hidden" name="nonce" value="${escape(input.nonce)}">` +
|
|
@@ -12367,13 +12607,75 @@ function approvalSheetHtml(input) {
|
|
|
12367
12607
|
approvalAmendmentHtml(contract) +
|
|
12368
12608
|
race +
|
|
12369
12609
|
(who.length === 0 ? "" : `<p class="approval-who">${who.map(escape).join(" · ")}</p>`) +
|
|
12610
|
+
consent +
|
|
12370
12611
|
`<div class="approval-act" id="${input.surface === "task" ? "approval-confirm" : "chat-approval-confirm"}">` +
|
|
12371
|
-
`<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>` +
|
|
12372
12614
|
`<button type="submit" data-primary-action>${submit}</button></div>` +
|
|
12373
12615
|
`<p class="approval-after">${after}</p>` +
|
|
12374
|
-
`<
|
|
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>` +
|
|
12375
12617
|
details +
|
|
12376
|
-
`</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 }];
|
|
12377
12679
|
}
|
|
12378
12680
|
function consentDoorOf(scope, route) {
|
|
12379
12681
|
if (scope === null)
|
|
@@ -12426,31 +12728,31 @@ function consentClosedHtml(taskId, door, surface) {
|
|
|
12426
12728
|
}
|
|
12427
12729
|
function profileWords(scope) {
|
|
12428
12730
|
if (scope.profileState === "unresolved") {
|
|
12429
|
-
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>`;
|
|
12430
12732
|
}
|
|
12431
12733
|
const profile = scope.profile ?? null;
|
|
12432
12734
|
if (profile === null) {
|
|
12433
12735
|
return (scope.digestVersion ?? 1) < 2
|
|
12434
|
-
? `<p class="meta">
|
|
12736
|
+
? `<p class="meta">Approved before routing was bound — pinned at upgrade to the configuration of that day</p>`
|
|
12435
12737
|
: "";
|
|
12436
12738
|
}
|
|
12437
12739
|
const repair = profile.repairModel === "inherit" ? "same model" : profile.repairModel;
|
|
12438
12740
|
const base = profile.provider === "claude"
|
|
12439
|
-
? `<p class="meta">
|
|
12741
|
+
? `<p class="meta">Runs on <span class="mono">claude · ${escape(profile.model)}</span> — ${profile.permissionArgv === "bypassPermissions"
|
|
12440
12742
|
? "FULL permissions; claude runs with --dangerously-skip-permissions and nothing asks"
|
|
12441
12743
|
: profile.permissionArgv === "auto"
|
|
12442
12744
|
? "safe unattended permissions; routine project commands and edits proceed, risky acts stop"
|
|
12443
|
-
: "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>`
|
|
12444
12746
|
: profile.provider === "gemini"
|
|
12445
|
-
? `<p class="meta">
|
|
12446
|
-
: `<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>`;
|
|
12447
12749
|
// The fallback chain rides EVERY surface these words sign (F+G review,
|
|
12448
12750
|
// finding 2): the digest binds the whole chain, so the password form —
|
|
12449
12751
|
// task page and /next alike — states every entry, credential included.
|
|
12450
12752
|
const chain = chainFromJson(scope.proposedChainJson ?? null);
|
|
12451
12753
|
const chainLine = chain === null || chain.length < 2
|
|
12452
12754
|
? ""
|
|
12453
|
-
: `<p class="meta">
|
|
12755
|
+
: `<p class="meta">If its subscription runs out: ${chain
|
|
12454
12756
|
.slice(1)
|
|
12455
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"}`)
|
|
12456
12758
|
.join("; ")}</p>`;
|
|
@@ -12479,9 +12781,9 @@ function agentsSummaryWords(view) {
|
|
|
12479
12781
|
/** Neutral metadata badges: risk, posture, and standing. */
|
|
12480
12782
|
function agentsBadgesHtml(view) {
|
|
12481
12783
|
const p = view.projection;
|
|
12482
|
-
return (`<span class="badge">${escape(riskTitle(view.riskLevel))}</span>` +
|
|
12483
|
-
(p === null ? "" : `<span class="badge">${escape(p.postureWords)}</span>`) +
|
|
12484
|
-
`<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>`);
|
|
12485
12787
|
}
|
|
12486
12788
|
/** Volatile availability, per provider on the route — shown beside the
|
|
12487
12789
|
* agents, never inside the approval's terms. */
|
|
@@ -12506,7 +12808,7 @@ function agentsWhyHtml(projection, summaryLabel = "Why these agents") {
|
|
|
12506
12808
|
.map(leg => {
|
|
12507
12809
|
const chosen = chosenWords(leg);
|
|
12508
12810
|
return (`<dt>${escape(ROLE_NOUN[leg.phase])}</dt>` +
|
|
12509
|
-
`<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>` +
|
|
12510
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>`);
|
|
12511
12813
|
})
|
|
12512
12814
|
.join("") +
|
|
@@ -12527,7 +12829,7 @@ function agentsCeremonyHtml(view) {
|
|
|
12527
12829
|
}
|
|
12528
12830
|
return (`<div class="agents-ceremony"><p class="approval-label">agents</p>` +
|
|
12529
12831
|
`<p class="agents-summary">${escape(view.projection.summary)}</p>` +
|
|
12530
|
-
`<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>` +
|
|
12531
12833
|
`<p class="meta agents-risk-line">${escape(riskTitle(view.riskLevel))}: ${escape(riskConsequence(view.riskLevel))}.</p>` +
|
|
12532
12834
|
agentsWhyHtml(view.projection) +
|
|
12533
12835
|
`<p class="meta">These exact agents are part of what you approve; changing any of them asks for a fresh approval.</p></div>`);
|
|
@@ -12554,7 +12856,7 @@ function routineAgentsHtml(routine, approved, ceremony = false) {
|
|
|
12554
12856
|
const projection = projectRoute(route, () => null);
|
|
12555
12857
|
return (`<div class="agents-ceremony"><p class="approval-label">agents</p>` +
|
|
12556
12858
|
`<p class="agents-summary">${escape(agentsSummary(route))}</p>` +
|
|
12557
|
-
`<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>` +
|
|
12558
12860
|
(ceremony ? agentsWhyHtml(projection) : "") +
|
|
12559
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>`);
|
|
12560
12862
|
}
|
|
@@ -12584,7 +12886,7 @@ function agentsCardHtml(taskId, view, csrf, canEdit) {
|
|
|
12584
12886
|
? ""
|
|
12585
12887
|
: `<ul class="agents-overrides">${view.overrides
|
|
12586
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>` +
|
|
12587
|
-
(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>` : "") +
|
|
12588
12890
|
`</li>`)
|
|
12589
12891
|
.join("")}</ul>`;
|
|
12590
12892
|
// The controls (v48): a risk choice that says what each level does, and
|
|
@@ -12614,7 +12916,7 @@ function agentsCardHtml(taskId, view, csrf, canEdit) {
|
|
|
12614
12916
|
? `<p class="meta">${escape(view.editableWhy ?? "the agents cannot change right now")}</p>`
|
|
12615
12917
|
: `<details class="agents-change"><summary>Change agents</summary>` +
|
|
12616
12918
|
`<form method="post" action="${taskHref(taskId)}/route" class="agents-form-risk">${hidden}` +
|
|
12617
|
-
`<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>` +
|
|
12618
12920
|
`<button type="submit" class="secondary">Set risk</button></form>` +
|
|
12619
12921
|
riskGuide +
|
|
12620
12922
|
`<div class="agents-role-forms">${roleForms.join("")}</div>` +
|
|
@@ -13937,9 +14239,34 @@ ${THEME_DARK}
|
|
|
13937
14239
|
.approval-act input[type=password] { margin: 0; font-family: var(--font-sans); }
|
|
13938
14240
|
.approval-act button { margin: 0; min-height: 2.125rem; white-space: nowrap; }
|
|
13939
14241
|
.approval-after { max-width: 75ch; color: var(--muted-foreground); font-size: 13px; line-height: 1.5; }
|
|
13940
|
-
.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; }
|
|
13941
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; }
|
|
13942
|
-
.approval-
|
|
14269
|
+
.approval-secondary > .approval-link:not(:first-child) { border-color: transparent; background: none; color: var(--muted-foreground); }
|
|
13943
14270
|
@media (hover: hover) and (pointer: fine) { .approval-link:hover { background: var(--muted); color: var(--foreground); } }
|
|
13944
14271
|
details.approval-details { margin: 0; padding: 0; border: 0; border-top: 1px solid var(--border); border-radius: 0; background: none; }
|
|
13945
14272
|
details.approval-details[open] { padding-bottom: 0; }
|
|
@@ -13959,6 +14286,7 @@ ${THEME_DARK}
|
|
|
13959
14286
|
.approval-sheet { gap: 12px; padding: 14px; }
|
|
13960
14287
|
.approval-row { grid-template-columns: 1fr; gap: 2px; }
|
|
13961
14288
|
.approval-act { grid-template-columns: 1fr; }
|
|
14289
|
+
.approval-act > button, .approval-password-note { grid-column: auto; grid-row: auto; }
|
|
13962
14290
|
.approval-act input[type=password], .approval-act button { min-height: 44px; font-size: 16px; }
|
|
13963
14291
|
.approval-act button { width: 100%; }
|
|
13964
14292
|
/* The one act stays under the thumb: sticky at the bottom, clear of the home indicator. */
|
|
@@ -13967,6 +14295,9 @@ ${THEME_DARK}
|
|
|
13967
14295
|
border-top: 1px solid var(--border); background: var(--card);
|
|
13968
14296
|
}
|
|
13969
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; }
|
|
13970
14301
|
.approval-details > summary { min-height: 44px; }
|
|
13971
14302
|
.approval-roles > div { grid-template-columns: 1fr; gap: 2px; }
|
|
13972
14303
|
}
|
|
@@ -15482,7 +15813,7 @@ const INBOX_TABS_CSS = '.inbox-tabs{display:inline-flex;gap:2px;max-width:100%;o
|
|
|
15482
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}' +
|
|
15483
15814
|
'.inbox-unread{display:none;position:absolute;top:4px;right:3px;width:6px;height:6px;border-radius:50%;background:var(--so-signal)}' +
|
|
15484
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}}';
|
|
15485
|
-
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}');
|
|
15486
15817
|
/**
|
|
15487
15818
|
* The board's liveness: fetch this page's own fragment on a timer and swap
|
|
15488
15819
|
* it in place — no flicker, no scroll reset, no long-lived stream to manage,
|
|
@@ -15737,7 +16068,7 @@ FINISH: unreviewed and undocumented is unfinished; this build ends with the fini
|
|
|
15737
16068
|
* absent from sensitive pages. Navigation help in plain words — no key
|
|
15738
16069
|
* ever posts. */
|
|
15739
16070
|
const KBD_HELP = `<div class="kbd-help" hidden role="dialog" aria-label="keyboard shortcuts" tabindex="-1">` +
|
|
15740
|
-
`<h2>
|
|
16071
|
+
`<h2>Keyboard shortcuts</h2><table>` +
|
|
15741
16072
|
`<tr><td><kbd>/</kbd></td><td>jump to a page or an open task</td></tr>` +
|
|
15742
16073
|
`<tr><td><kbd>g</kbd> then <kbd>i</kbd></td><td>go to the inbox</td></tr>` +
|
|
15743
16074
|
`<tr><td><kbd>g</kbd> then <kbd>b</kbd></td><td>go to the board</td></tr>` +
|
|
@@ -15812,7 +16143,7 @@ function shell(title, body, options = {}) {
|
|
|
15812
16143
|
: `<span class="scope-status">` +
|
|
15813
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>`) +
|
|
15814
16145
|
`<a href="/runs">${peekCounts.running} live</a><a href="/board?view=order">${peekCounts.queued} queued</a></span>`;
|
|
15815
|
-
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";
|
|
15816
16147
|
// The switcher (board pass): the scope's name opens a menu of every
|
|
15817
16148
|
// enrolled project — one tap to open one, or to widen to all — as plain
|
|
15818
16149
|
// POST forms carrying the session's csrf, returning to this screen.
|
|
@@ -15827,7 +16158,7 @@ function shell(title, body, options = {}) {
|
|
|
15827
16158
|
const allCurrent = effectiveScope !== "project";
|
|
15828
16159
|
const rows = [
|
|
15829
16160
|
`<form method="post" action="/projects/select">${hidden}<input type="hidden" name="path" value="">` +
|
|
15830
|
-
`<button type="submit"${allCurrent ? ' class="current" aria-current="true"' : ""}>
|
|
16161
|
+
`<button type="submit"${allCurrent ? ' class="current" aria-current="true"' : ""}>All projects</button></form>`,
|
|
15831
16162
|
...(chrome.projects ?? []).map(one => `<form method="post" action="/projects/open">${hidden}<input type="hidden" name="path" value="${escape(one.path)}">` +
|
|
15832
16163
|
`<button type="submit"${!allCurrent && chrome.project === one.path ? ' class="current" aria-current="true"' : ""}>${escape(one.name)}</button></form>`),
|
|
15833
16164
|
];
|
|
@@ -15871,10 +16202,10 @@ function shell(title, body, options = {}) {
|
|
|
15871
16202
|
primaryItem("flows", "/flows", "Flows"),
|
|
15872
16203
|
primaryItem("projects", "/projects", "Projects"),
|
|
15873
16204
|
`</nav>`,
|
|
15874
|
-
...(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>`]),
|
|
15875
16206
|
`<nav class="nav-groups">`,
|
|
15876
|
-
navGroup("tools", "
|
|
15877
|
-
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)),
|
|
15878
16209
|
`</nav>`,
|
|
15879
16210
|
`<span class="grow"></span>`,
|
|
15880
16211
|
`</aside>`,
|
|
@@ -15883,11 +16214,11 @@ function shell(title, body, options = {}) {
|
|
|
15883
16214
|
// must not pass as production (adoption review, finding 8; the FENCE is
|
|
15884
16215
|
// the refuseDemo gate in operate.ts, this is the honest label).
|
|
15885
16216
|
const demoBanner = (chrome.demo === true
|
|
15886
|
-
? `<div class="banner"><span class="badge">
|
|
16217
|
+
? `<div class="banner"><span class="badge">Demo</span>${escape(DEMO_BANNER.replace(/^Demo: /, ""))}</div>`
|
|
15887
16218
|
: "") +
|
|
15888
16219
|
(chrome.modeBanner === undefined
|
|
15889
16220
|
? ""
|
|
15890
|
-
: `<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>`) +
|
|
15891
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)}`}` +
|
|
15892
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("") +
|
|
15893
16224
|
(chrome.updateWaiting === undefined
|
|
@@ -16109,7 +16440,7 @@ function inboxPage(chrome, data) {
|
|
|
16109
16440
|
/** The row's project, worn openly in the roll-up — null is UNPLACED,
|
|
16110
16441
|
* said as such, never a silent missing chip (finding 13). */
|
|
16111
16442
|
const chip = (repo) => !data.rollup ? "" : repo === null || repo === undefined
|
|
16112
|
-
? ` <span class="badge">
|
|
16443
|
+
? ` <span class="badge">Unplaced</span>`
|
|
16113
16444
|
: ` <span class="badge">${escape(projectName(repo))}</span>`;
|
|
16114
16445
|
const empty = data.decisions.length + data.approvals.length + data.requeueables.length +
|
|
16115
16446
|
data.cancelledBlockers.length + data.gaps.length + data.needsVerification.length === 0;
|
|
@@ -16118,19 +16449,19 @@ function inboxPage(chrome, data) {
|
|
|
16118
16449
|
// the card itself (portfolio arc §2).
|
|
16119
16450
|
const decisions = data.decisions.length === 0
|
|
16120
16451
|
? ""
|
|
16121
|
-
: `<h2>
|
|
16452
|
+
: `<h2>Answer a question</h2><p class="hint">an agent stopped mid-build to ask — nothing proceeds until you answer</p>` +
|
|
16122
16453
|
data.decisions
|
|
16123
16454
|
.map(decision => data.interactive && !data.rollup
|
|
16124
16455
|
? decisionAnswerCard(decision, data.csrf, data.now, false)
|
|
16125
16456
|
: `<a class="decide-card" href="/d/${decision.id}">` +
|
|
16126
16457
|
`<p class="q">${escape(decision.question)}</p>` +
|
|
16127
16458
|
`<span class="mono meta">${escape(decision.taskId)}</span>${chip(decision.repo)}` +
|
|
16128
|
-
`${isOverdue(decision, data.now) ? ` <span class="badge badge-overdue">
|
|
16459
|
+
`${isOverdue(decision, data.now) ? ` <span class="badge badge-overdue">Overdue</span>` : ""}` +
|
|
16129
16460
|
`</a>`)
|
|
16130
16461
|
.join("\n");
|
|
16131
16462
|
const approvals = data.approvals.length === 0
|
|
16132
16463
|
? ""
|
|
16133
|
-
: `<h2>
|
|
16464
|
+
: `<h2>Approve a scope</h2><p class="hint">scopes waiting for your approval — each binds to the exact wording you sign</p>` +
|
|
16134
16465
|
data.approvals
|
|
16135
16466
|
.map(one => `<a class="decide-card" href="${taskHref(one.taskId)}">` +
|
|
16136
16467
|
`<p class="q">${escape(one.title)}</p>` +
|
|
@@ -16140,7 +16471,7 @@ function inboxPage(chrome, data) {
|
|
|
16140
16471
|
.join("\n");
|
|
16141
16472
|
const requeueables = data.requeueables.length === 0
|
|
16142
16473
|
? ""
|
|
16143
|
-
: `<h2>
|
|
16474
|
+
: `<h2>Retry stalled work</h2><p class="hint">failed builds waiting for a person — retry clears the incidents and requeues</p>` +
|
|
16144
16475
|
data.requeueables
|
|
16145
16476
|
.map(one => `<p class="row"><a href="${taskHref(one.taskId)}">${escape(one.taskId)}</a> ${escape(one.title)}${chip(one.repo)}` +
|
|
16146
16477
|
`${one.incidentCount > 0 ? ` <span class="badge badge-failed">${one.incidentCount} incident${one.incidentCount > 1 ? "s" : ""}</span>` : ""}` +
|
|
@@ -16150,11 +16481,11 @@ function inboxPage(chrome, data) {
|
|
|
16150
16481
|
: `<span class="right"><form method="post" action="${taskHref(one.taskId)}/requeue" class="inline">` +
|
|
16151
16482
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
|
|
16152
16483
|
`<input type="hidden" name="return" value="inbox">` +
|
|
16153
|
-
`<button type="submit">
|
|
16484
|
+
`<button type="submit">Retry</button></form></span></p>`))
|
|
16154
16485
|
.join("\n");
|
|
16155
16486
|
const needsVerification = data.needsVerification.length === 0
|
|
16156
16487
|
? ""
|
|
16157
|
-
: `<h2>
|
|
16488
|
+
: `<h2>Needs review</h2><p class="hint">finished, but the evidence needs your attention before accepting</p>` +
|
|
16158
16489
|
data.needsVerification
|
|
16159
16490
|
.map(one => `<p class="row"><a href="${taskHref(one.taskId)}">${escape(one.taskId)}</a> ${escape(one.title)}${chip(one.repo)}` +
|
|
16160
16491
|
` <span class="badge badge-failed">${one.verdict === "refuted" ? "conflicting evidence" : "missing evidence"}</span>${criterionMatrixSummary(one.matrix ?? [])}` +
|
|
@@ -16163,7 +16494,7 @@ function inboxPage(chrome, data) {
|
|
|
16163
16494
|
.join("\n");
|
|
16164
16495
|
const cancelled = data.cancelledBlockers.length === 0
|
|
16165
16496
|
? ""
|
|
16166
|
-
: `<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>` +
|
|
16167
16498
|
data.cancelledBlockers
|
|
16168
16499
|
.map(one => `<p class="row"><a href="${taskHref(one.exampleDependent)}">${escape(one.exampleDependent)}</a>${chip(one.repo)} ` +
|
|
16169
16500
|
`<span class="meta">${one.dependentCount > 1 ? `one of ${one.dependentCount} tasks waiting` : "waiting"} for cancelled task ${escape(one.blockerId)}` +
|
|
@@ -16171,7 +16502,7 @@ function inboxPage(chrome, data) {
|
|
|
16171
16502
|
.join("\n");
|
|
16172
16503
|
const gaps = data.gaps.length === 0
|
|
16173
16504
|
? ""
|
|
16174
|
-
: `<h2>
|
|
16505
|
+
: `<h2>Supply a requirement</h2><p class="hint">approved work is ready except for these — fill one and its tasks start</p>` +
|
|
16175
16506
|
data.gaps
|
|
16176
16507
|
.map(gap => `<p class="row"><a href="/caps">${escape(gap.key)}</a> ` +
|
|
16177
16508
|
`<span class="meta">frees ${gap.unblocks.length} task${gap.unblocks.length > 1 ? "s" : ""}</span>` +
|
|
@@ -16203,8 +16534,8 @@ function inboxPage(chrome, data) {
|
|
|
16203
16534
|
const tab = data.tab ?? "all";
|
|
16204
16535
|
const shows = (one) => tab === "all" || tab === one;
|
|
16205
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");
|
|
16206
|
-
const ready = (data.ready ?? []).length === 0 ? "" : `<h2>
|
|
16207
|
-
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 ?? []);
|
|
16208
16539
|
const asks = { decide: data.decisions.length + data.approvals.length, unblock: data.requeueables.length + data.cancelledBlockers.length + data.gaps.length };
|
|
16209
16540
|
const counts = { ...asks,
|
|
16210
16541
|
"needs-you": asks.decide + asks.unblock,
|
|
@@ -16223,7 +16554,7 @@ function inboxPage(chrome, data) {
|
|
|
16223
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>` : "";
|
|
16224
16555
|
return screen("inbox", [
|
|
16225
16556
|
`<h1>Inbox</h1>`,
|
|
16226
|
-
`<p class="meta">
|
|
16557
|
+
`<p class="meta">Everything that waits on you \u2014 empty means the fleet is working</p>`,
|
|
16227
16558
|
tabs,
|
|
16228
16559
|
noWorker,
|
|
16229
16560
|
wizard,
|
|
@@ -16235,21 +16566,21 @@ function inboxPage(chrome, data) {
|
|
|
16235
16566
|
shows("needs-you") ? askGroup("unblock", counts.unblock, [requeueables, cancelled, gaps]) : "",
|
|
16236
16567
|
shows("running") ? running : "",
|
|
16237
16568
|
data.rollup
|
|
16238
|
-
? `<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>`
|
|
16239
16570
|
: "",
|
|
16240
16571
|
// Quick capture: the shortest path from "I want this done" to the
|
|
16241
16572
|
// approve card — title and goal here, the yes on the next screen. The
|
|
16242
16573
|
// one-shot form posts to the same guarded handler as the full page.
|
|
16243
|
-
data.rollup ? "" : `<h2>
|
|
16574
|
+
data.rollup ? "" : `<h2>Capture new work</h2>`,
|
|
16244
16575
|
data.rollup ? "" : `<form method="post" action="/tasks/add" class="card">`,
|
|
16245
16576
|
...(data.rollup
|
|
16246
16577
|
? []
|
|
16247
16578
|
: [
|
|
16248
16579
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
|
|
16249
16580
|
`<input type="hidden" name="projectRevision" value="${data.revision}">`,
|
|
16250
|
-
`<label>
|
|
16251
|
-
`<label>
|
|
16252
|
-
`<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>`,
|
|
16253
16584
|
`</form>`,
|
|
16254
16585
|
]),
|
|
16255
16586
|
].join("\n"), {
|
|
@@ -16298,7 +16629,7 @@ function systemPage(chrome, data) {
|
|
|
16298
16629
|
const agentLines = data.agents
|
|
16299
16630
|
.map(one => {
|
|
16300
16631
|
if (one.problem !== undefined) {
|
|
16301
|
-
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>`;
|
|
16302
16633
|
}
|
|
16303
16634
|
const who = one.source === "project"
|
|
16304
16635
|
? `chosen for this project${one.setBy === null ? "" : ` by ${escape(one.setBy)}`}`
|
|
@@ -16313,10 +16644,10 @@ function systemPage(chrome, data) {
|
|
|
16313
16644
|
`<p class="meta" style="margin-top:0">${PHASE_SAID[one.phase] ?? ""}</p>`);
|
|
16314
16645
|
})
|
|
16315
16646
|
.join("\n");
|
|
16316
|
-
const agentsCard = `<h2>
|
|
16317
|
-
`<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>` +
|
|
16318
16649
|
`<div class="card">${agentLines}` +
|
|
16319
|
-
`<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>` +
|
|
16320
16651
|
`</div>`;
|
|
16321
16652
|
return screen("system", [
|
|
16322
16653
|
`<h1>System</h1>`,
|
|
@@ -16325,16 +16656,16 @@ function systemPage(chrome, data) {
|
|
|
16325
16656
|
cards.length === 0
|
|
16326
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>`
|
|
16327
16658
|
: `<div class="cards">${cards.join("")}</div>`,
|
|
16328
|
-
data.outboxPending > 0 ? `<p class="meta">
|
|
16659
|
+
data.outboxPending > 0 ? `<p class="meta">Notifications: ${data.outboxPending} pending delivery</p>` : "",
|
|
16329
16660
|
(data.externalWork ?? []).length === 0
|
|
16330
16661
|
? ""
|
|
16331
|
-
: `<h2>
|
|
16662
|
+
: `<h2>External work</h2>` +
|
|
16332
16663
|
(data.externalWork ?? [])
|
|
16333
16664
|
.map(one => `<p class="row"><span class="mono">${escape(one.remoteRepo)}</span> ` +
|
|
16334
16665
|
(one.blocked !== null
|
|
16335
|
-
? `<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>`
|
|
16336
16667
|
: one.openEpisode !== null
|
|
16337
|
-
? `<span class="badge badge-failed">
|
|
16668
|
+
? `<span class="badge badge-failed">Sync failing</span> <span class="meta">${escape(one.openEpisode)}</span>`
|
|
16338
16669
|
: `<span class="meta">syncing normally</span>`) +
|
|
16339
16670
|
`</p>`)
|
|
16340
16671
|
.join("\n"),
|
|
@@ -16437,7 +16768,7 @@ function boardBody(data, ciRed) {
|
|
|
16437
16768
|
queuedHeads.get(`${card.assignedRunner ?? ""}|${card.repo ?? ""}`) === card.taskId
|
|
16438
16769
|
? `<span class="badge">${card.assignedRunner === null ? "next up" : `next for ${escape(card.assignedRunner)}`}</span>`
|
|
16439
16770
|
: "",
|
|
16440
|
-
card.assignedRunner === null ? "" : `<span class="badge">
|
|
16771
|
+
card.assignedRunner === null ? "" : `<span class="badge">Reserved</span>`,
|
|
16441
16772
|
card.routineName === null ? "" : `<span class="badge">${escape(card.routineName)}</span>`,
|
|
16442
16773
|
chip(card.repo).trim(),
|
|
16443
16774
|
]) +
|
|
@@ -16465,7 +16796,7 @@ function boardBody(data, ciRed) {
|
|
|
16465
16796
|
...(claim.branch === null ? [] : [["branch", `${claim.branch}${workspace === null ? "" : ` · ${workspace}`}`]]),
|
|
16466
16797
|
]) +
|
|
16467
16798
|
chips([
|
|
16468
|
-
card.attempt === null ? "" : `<span class="badge">
|
|
16799
|
+
card.attempt === null ? "" : `<span class="badge">Attempt ${card.attempt}</span>`,
|
|
16469
16800
|
chip(card.repo).trim(),
|
|
16470
16801
|
]) +
|
|
16471
16802
|
`</a>`);
|
|
@@ -16487,7 +16818,7 @@ function boardBody(data, ciRed) {
|
|
|
16487
16818
|
["usage", runCostWords({ authMode: row.authMode, costUsd: row.costUsd, tokensIn: null, tokensOut: null })],
|
|
16488
16819
|
]) +
|
|
16489
16820
|
chips([
|
|
16490
|
-
`<span class="badge badge-done">${row.outcome === "no-change" ? "
|
|
16821
|
+
`<span class="badge badge-done">${row.outcome === "no-change" ? "No change" : "Built"}</span>`,
|
|
16491
16822
|
pr.trim(),
|
|
16492
16823
|
data.all && row.repo !== null ? `<span class="badge">${escape(projectName(row.repo))}</span>` : "",
|
|
16493
16824
|
]) +
|
|
@@ -16499,7 +16830,7 @@ function boardBody(data, ciRed) {
|
|
|
16499
16830
|
: `<p class="meta board-scope">` +
|
|
16500
16831
|
(data.all
|
|
16501
16832
|
? (data.project === null
|
|
16502
|
-
? `<strong>
|
|
16833
|
+
? `<strong>All projects</strong>`
|
|
16503
16834
|
: `<a href="/board">${escape(projectName(data.project))}</a> \u00b7 <strong>all projects</strong>`) +
|
|
16504
16835
|
` \u2014 every project this server serves, each card wearing its project`
|
|
16505
16836
|
: `<strong>${escape(projectName(data.project))}</strong> \u00b7 <a href="/board?scope=all">all projects</a>`) +
|
|
@@ -16516,7 +16847,7 @@ function boardBody(data, ciRed) {
|
|
|
16516
16847
|
const ago = (minutes) => minutes < 60 ? `${minutes}m` : minutes < 48 * 60 ? `${Math.round(minutes / 60)}h` : `${Math.round(minutes / (24 * 60))}d`;
|
|
16517
16848
|
const deltaLine = data.delta === null
|
|
16518
16849
|
? ""
|
|
16519
|
-
: `<p class="meta"><strong>
|
|
16850
|
+
: `<p class="meta"><strong>Since you last looked</strong> (${ago(data.delta.agoMinutes)} ago): ` +
|
|
16520
16851
|
[
|
|
16521
16852
|
data.delta.built > 0 ? `<span class="good">${data.delta.built} built</span>` : "",
|
|
16522
16853
|
data.delta.failed > 0 ? `<span class="bad">${data.delta.failed} failed</span>` : "",
|
|
@@ -16527,7 +16858,7 @@ function boardBody(data, ciRed) {
|
|
|
16527
16858
|
`</p>`;
|
|
16528
16859
|
return [
|
|
16529
16860
|
`<h1>Board</h1>`,
|
|
16530
|
-
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>`,
|
|
16531
16862
|
deltaLine,
|
|
16532
16863
|
toggle,
|
|
16533
16864
|
`<div class="board">`,
|
|
@@ -16549,14 +16880,14 @@ function donePage(chrome, rows, ciRed) {
|
|
|
16549
16880
|
const pr = row.prNumber === null
|
|
16550
16881
|
? row.publicationState === null
|
|
16551
16882
|
? ""
|
|
16552
|
-
: ` <span class="badge">${escape(row.publicationState)}</span>`
|
|
16883
|
+
: ` <span class="badge">${escape(sentenceCase(row.publicationState))}</span>`
|
|
16553
16884
|
: ` <a href="${escape(row.prUrl ?? "#")}" class="badge badge-open">PR #${row.prNumber}</a>` +
|
|
16554
16885
|
(ciRed(row.prNumber) ? ` <span class="badge badge-failed">CI failing</span>` : "");
|
|
16555
16886
|
const needsVerification = (row.proofVerdict === "short" || row.proofVerdict === "refuted") && !row.proofAccepted
|
|
16556
16887
|
? ` <span class="badge badge-failed">${row.proofVerdict === "refuted" ? "conflicting evidence" : "missing evidence"}</span>`
|
|
16557
16888
|
: "";
|
|
16558
16889
|
return (`<div class="card"><p><a href="${taskHref(row.taskId)}"><strong>${escape(row.title)}</strong></a>` +
|
|
16559
|
-
`${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>` +
|
|
16560
16891
|
`${row.handoff === null ? "" : `<p class="meta">${escape(row.handoff.length > 200 ? row.handoff.slice(0, 200) + "\u2026" : row.handoff)}</p>`}` +
|
|
16561
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 }))}`}` +
|
|
16562
16893
|
` \u00b7 <a href="${reviewHref(row.taskId)}">review \u2192</a></p></div>`);
|
|
@@ -16622,9 +16953,9 @@ function trackRow(track, all) {
|
|
|
16622
16953
|
`<p>${trackStrip(fires)}` +
|
|
16623
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>` +
|
|
16624
16955
|
(blocker !== null
|
|
16625
|
-
? `<p class="meta">
|
|
16956
|
+
? `<p class="meta">Stopped behind <a href="${taskHref(blocker.taskId)}" class="mono">${escape(blocker.taskId)}</a> (${escape(blocker.state)})</p>`
|
|
16626
16957
|
: latest?.instanceTaskId !== undefined && latest.instanceTaskId !== null
|
|
16627
|
-
? `<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>`
|
|
16628
16959
|
: "") +
|
|
16629
16960
|
`</div>`);
|
|
16630
16961
|
}
|
|
@@ -16741,12 +17072,13 @@ function checkProgressHtml(progress, id = "check-progress") {
|
|
|
16741
17072
|
: Object.values(progress.suites).every(one => one.state === "passed") ? "passed" : "unknown";
|
|
16742
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>`;
|
|
16743
17074
|
}
|
|
16744
|
-
function taskControlDetailsHtml(control, taskId, csrf, surface, inert = false) {
|
|
16745
|
-
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);
|
|
16746
17077
|
return control.kind === "paused" || control.kind === "review-stopped" || control.kind === "stopping"
|
|
16747
17078
|
? `<details id="task-control-details"><summary>${control.kind === "paused" ? "Preserved work and resume" : "Stop details"}</summary>${html}</details>` : html;
|
|
16748
17079
|
}
|
|
16749
|
-
|
|
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) {
|
|
16750
17082
|
if (control.kind === "none")
|
|
16751
17083
|
return "";
|
|
16752
17084
|
const back = `<input type="hidden" name="return" value="${surface}">`;
|
|
@@ -16760,7 +17092,7 @@ function taskControlHtml(control, taskId, csrf, surface, inert = false) {
|
|
|
16760
17092
|
if (control.kind === "stop") {
|
|
16761
17093
|
return (`<section class="card task-control" id="task-control" data-task-control="stop" data-control-run="${control.run}" aria-label="stop this attempt">` +
|
|
16762
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>` +
|
|
16763
|
-
(guarded
|
|
17095
|
+
(stopElsewhere ? "" : guarded
|
|
16764
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>`
|
|
16765
17097
|
: `<button type="button" class="danger task-control-button" disabled>Stop</button>`) +
|
|
16766
17098
|
`</section>`);
|
|
@@ -16800,7 +17132,7 @@ function resumeCeremonyPage(chrome, data) {
|
|
|
16800
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>`,
|
|
16801
17133
|
`<p class="row">Resuming grants no new approval or publishing permission. Scope, budget, agents, risk, fallback, verification, and review limits still apply.</p>`,
|
|
16802
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>`,
|
|
16803
|
-
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>`,
|
|
16804
17136
|
data.gate === null ? "" : `<p class="row"><strong>Before work starts:</strong> ${escape(data.gate.summary)} — ${escape(data.gate.detail)}</p>`,
|
|
16805
17137
|
`</div>`,
|
|
16806
17138
|
`<form method="post" action="${taskHref(data.taskId)}/resume" class="card resume-form">`,
|
|
@@ -16876,12 +17208,23 @@ function taskChatApproval(focus, csrf) {
|
|
|
16876
17208
|
route: focus.route,
|
|
16877
17209
|
coordinator: approval.coordinator,
|
|
16878
17210
|
deliverable: approval.deliverable,
|
|
16879
|
-
|
|
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`,
|
|
16880
17213
|
notNowHref: "/chat",
|
|
16881
17214
|
sticky: false,
|
|
17215
|
+
earlier: { active: focus.assignment?.earlierActive ?? 0, running: focus.assignment?.earlierRunning ?? 0 },
|
|
17216
|
+
edit: null,
|
|
16882
17217
|
}) +
|
|
16883
17218
|
`</section>`);
|
|
16884
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
|
+
}
|
|
16885
17228
|
/** One live, server-derived journey from request to proof. The fragment is
|
|
16886
17229
|
* safe to refresh independently, so an in-progress message is never lost. */
|
|
16887
17230
|
function taskChatLiveRegion(focus, csrf, fragment = false, inert = false, approvalHref = taskChatHref(focus.id) + "#task-chat-action") {
|
|
@@ -16897,7 +17240,7 @@ function taskChatLiveRegion(focus, csrf, fragment = false, inert = false, approv
|
|
|
16897
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 ?? "")}">` +
|
|
16898
17241
|
(focus.guide === true && focus.state !== "cancelled" ? firstTaskJourneyHtml(focus) : "") +
|
|
16899
17242
|
`<div class="task-live-summary">` +
|
|
16900
|
-
(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")) +
|
|
16901
17244
|
checkProgressHtml(focus.checkProgress) +
|
|
16902
17245
|
// The exact-run control (v52): the SAME component the task page
|
|
16903
17246
|
// renders, refreshed with the live region — typed input in the
|
|
@@ -17008,7 +17351,7 @@ function chatFleetOverview(snapshot, projects, csrf, interactive) {
|
|
|
17008
17351
|
return (`<section class="card chat-overview" aria-label="live portfolio overview" data-card-kind="fleet-overview">` +
|
|
17009
17352
|
`<div class="chat-overview-head"><h2>${projects.length} project${projects.length === 1 ? "" : "s"}</h2>` +
|
|
17010
17353
|
(interactive
|
|
17011
|
-
? `<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>`
|
|
17012
17355
|
: `<a href="/board?scope=all" class="chat-overview-link">open board</a>`) +
|
|
17013
17356
|
`</div>` +
|
|
17014
17357
|
`<div class="chat-overview-stats">` +
|
|
@@ -17043,9 +17386,9 @@ function chatProjectRail(projects, csrf, inert) {
|
|
|
17043
17386
|
const ask = `Give me a concise status for ${one.id} (${one.label}) and recommend the next reversible action.`;
|
|
17044
17387
|
return (`<div class="chat-project-card">` +
|
|
17045
17388
|
`<div class="chat-project-name"><span class="mono">${escape(one.id)}</span><strong>${escape(one.label)}</strong>` +
|
|
17046
|
-
`<span class="badge">${escape(statusOf(peek))}</span></div>` +
|
|
17389
|
+
`<span class="badge">${escape(sentenceCase(statusOf(peek)))}</span></div>` +
|
|
17047
17390
|
(peek === null
|
|
17048
|
-
? `<p class="meta">
|
|
17391
|
+
? `<p class="meta">Pulse unavailable</p>`
|
|
17049
17392
|
: `<div class="chat-project-stats">` +
|
|
17050
17393
|
`<span${peek.waiting > 0 ? ' class="hot"' : ""}><b>${peek.waiting}</b> need you</span>` +
|
|
17051
17394
|
`<span><b>${peek.running}</b> live</span><span><b>${peek.queued}</b> queued</span>` +
|
|
@@ -17054,13 +17397,13 @@ function chatProjectRail(projects, csrf, inert) {
|
|
|
17054
17397
|
(inert
|
|
17055
17398
|
? ""
|
|
17056
17399
|
: `<form method="post" action="/chat" class="inline"><input type="hidden" name="csrf" value="${escape(csrf)}">` +
|
|
17057
|
-
`<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>`) +
|
|
17058
17401
|
`<form method="post" action="/projects/open" class="inline"><input type="hidden" name="csrf" value="${escape(csrf)}">` +
|
|
17059
17402
|
`<input type="hidden" name="path" value="${escape(one.path)}"><input type="hidden" name="return" value="/board">` +
|
|
17060
|
-
`<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>`);
|
|
17061
17404
|
}).join("");
|
|
17062
17405
|
return (`<aside class="chat-projects" id="chat-project-panel" aria-label="projects in this conversation">` +
|
|
17063
|
-
`<div class="chat-projects-head"><h2>
|
|
17406
|
+
`<div class="chat-projects-head"><h2>Projects</h2><span class="badge">${projects.length}</span>` +
|
|
17064
17407
|
`<button type="button" class="chat-project-close quiet" aria-label="close projects">×</button></div>` +
|
|
17065
17408
|
`<div class="chat-project-list">${rows}</div></aside>`);
|
|
17066
17409
|
}
|
|
@@ -17073,9 +17416,9 @@ function matePromptStarters(csrf, focus = null) {
|
|
|
17073
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."],
|
|
17074
17417
|
]
|
|
17075
17418
|
: [
|
|
17076
|
-
["
|
|
17077
|
-
["
|
|
17078
|
-
["
|
|
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."],
|
|
17079
17422
|
];
|
|
17080
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)}">` +
|
|
17081
17424
|
(focus === null ? "" : `<input type="hidden" name="task" value="${escape(focus.id)}">`) +
|
|
@@ -17224,23 +17567,23 @@ function leadConfigForm(current, data) {
|
|
|
17224
17567
|
`<form method="post" action="/chat/config" class="card">`,
|
|
17225
17568
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
|
|
17226
17569
|
`<input type="hidden" name="return" value="${escape(data.returnTo)}">`,
|
|
17227
|
-
`<label>
|
|
17570
|
+
`<label>Provider<select name="provider">`,
|
|
17228
17571
|
`<option value="codex-subscription"${current?.provider === "codex-subscription" ? " selected" : ""}>Codex membership (logged-in CLI)</option>`,
|
|
17229
17572
|
`<option value="claude-subscription"${current?.provider === "claude-subscription" ? " selected" : ""}>Anthropic membership (logged-in CLI)</option>`,
|
|
17230
17573
|
`<option value="anthropic-api"${current?.provider === "anthropic-api" ? " selected" : ""}>anthropic-api (direct API)</option>`,
|
|
17231
17574
|
`<option value="openrouter-api"${current?.provider === "openrouter-api" ? " selected" : ""}>openrouter-api (direct API)</option>`,
|
|
17232
17575
|
`</select></label>`,
|
|
17233
|
-
`<label>
|
|
17576
|
+
`<label>Model <span class="meta">(use default for your membership's current model; direct API models need a pinned price)</span>` +
|
|
17234
17577
|
`<input name="model" list="chat-models" value="${escape(current?.model ?? "default")}"><datalist id="chat-models">` +
|
|
17235
17578
|
`${models.map(model => `<option value="${escape(model)}">${escape(labels.get(model) ?? "")}</option>`).join("")}</datalist></label>`,
|
|
17236
17579
|
data.openrouterModels === null
|
|
17237
|
-
? `<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>`
|
|
17238
17581
|
: `<p class="meta">${data.openrouterModels.length} models live from OpenRouter's catalog; saving pins today's price — re-save to re-pin</p>`,
|
|
17239
17582
|
currentSubscription
|
|
17240
|
-
? `<p class="meta"><strong>
|
|
17241
|
-
: `<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>` +
|
|
17242
17585
|
`<input type="text" name="weekly-usd" inputmode="decimal" style="width:8rem" value="${current === null ? "" : (current.weeklyCeilingMicrousd / 1_000_000).toFixed(2)}"></label>`,
|
|
17243
|
-
`<label>
|
|
17586
|
+
`<label>Daily turns <span class="meta">(default 50)</span>` +
|
|
17244
17587
|
`<input type="text" name="daily-turns" inputmode="numeric" style="width:8rem" value="${current === null ? "" : String(current.dailyTurns)}"></label>`,
|
|
17245
17588
|
currentSubscription
|
|
17246
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>`
|
|
@@ -17252,13 +17595,13 @@ function leadConfigForm(current, data) {
|
|
|
17252
17595
|
: `${escape(one.provider)}: stored ${escape(one.tail ?? "")}`)
|
|
17253
17596
|
.join(" · ")})</span>` +
|
|
17254
17597
|
`<input type="password" name="key" placeholder="direct API only — leave empty to keep" autocomplete="off"></label>`,
|
|
17255
|
-
`<label>
|
|
17598
|
+
`<label>Your password <span class="meta">(typed again to change the provider)</span>` +
|
|
17256
17599
|
`<input type="password" name="token" autocomplete="current-password"></label>`,
|
|
17257
17600
|
`<button type="submit">${current === null ? "turn chat on" : "save"}</button>`,
|
|
17258
17601
|
`</form>`,
|
|
17259
17602
|
currentSubscription
|
|
17260
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>`
|
|
17261
|
-
: `<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 (` +
|
|
17262
17605
|
`<span class="mono">ANTHROPIC_API_KEY</span> / <span class="mono">OPENROUTER_API_KEY</span>) always wins when set</p>`,
|
|
17263
17606
|
].join("\n");
|
|
17264
17607
|
}
|
|
@@ -17287,7 +17630,7 @@ function chatPage(chrome, data) {
|
|
|
17287
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>`
|
|
17288
17631
|
: code === "unpriced" || code === "no-key"
|
|
17289
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>`
|
|
17290
|
-
: `<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>`);
|
|
17291
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 } });
|
|
17292
17635
|
}
|
|
17293
17636
|
const config = data.enabled.config;
|
|
@@ -17303,7 +17646,7 @@ function chatPage(chrome, data) {
|
|
|
17303
17646
|
parts.push(data.coordinatorProposals);
|
|
17304
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 } }));
|
|
17305
17648
|
for (const turn of data.latched) {
|
|
17306
|
-
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)} — ` +
|
|
17307
17650
|
`<a href="/chat/ack/${turn.id}">read and acknowledge it</a> to re-enable this credential.</div>`);
|
|
17308
17651
|
}
|
|
17309
17652
|
if (data.pending !== null) {
|
|
@@ -17318,7 +17661,7 @@ function chatPage(chrome, data) {
|
|
|
17318
17661
|
}
|
|
17319
17662
|
else if (last.reply !== null) {
|
|
17320
17663
|
parts.push(`<div class="card" id="latest">${renderChatText(last.reply)}` +
|
|
17321
|
-
(last.proposalsDiscarded ? `<p class="meta">
|
|
17664
|
+
(last.proposalsDiscarded ? `<p class="meta">A draft block in this answer was malformed and was discarded whole</p>` : "") +
|
|
17322
17665
|
`</div>`);
|
|
17323
17666
|
}
|
|
17324
17667
|
}
|
|
@@ -17326,21 +17669,21 @@ function chatPage(chrome, data) {
|
|
|
17326
17669
|
for (const one of candidates) {
|
|
17327
17670
|
const draft = one.draft;
|
|
17328
17671
|
parts.push(`<div class="card">` +
|
|
17329
|
-
`<p><strong>${draft.kind === "task" ? escape(draft.title) : escape(draft.name)}</strong> <span class="badge">
|
|
17330
|
-
`<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>` +
|
|
17331
17674
|
`<p style="white-space:pre-wrap">${escape(draft.goal)}</p>` +
|
|
17332
|
-
(draft.outOfScope === null ? "" : `<p class="meta">
|
|
17333
|
-
(draft.touches.length > 0 ? `<p class="meta">
|
|
17334
|
-
(draft.kind === "routine" ? `<p class="meta">
|
|
17335
|
-
`<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>` +
|
|
17336
17679
|
`<form method="post" action="/chat/file/${escape(one.key)}" class="inline">` +
|
|
17337
17680
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
|
|
17338
17681
|
`<input type="password" name="token" placeholder="your password" autocomplete="current-password">` +
|
|
17339
|
-
`<button type="submit">
|
|
17682
|
+
`<button type="submit">File unapproved</button></form>` +
|
|
17340
17683
|
`</div>`);
|
|
17341
17684
|
}
|
|
17342
17685
|
if (!subscription && data.canManage && data.focusTask === null) {
|
|
17343
|
-
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>`);
|
|
17344
17687
|
}
|
|
17345
17688
|
if (data.canManage) {
|
|
17346
17689
|
parts.push(`<p class="meta" id="chat-settings"><a href="/settings/lead">Lead settings</a> · provider, model and limits</p>`);
|
|
@@ -17363,12 +17706,12 @@ function chatAckPage(chrome, turn, nonce, csrf) {
|
|
|
17363
17706
|
`<p>Turn <span class="mono">#${turn.id}</span> on <span class="mono">${escape(turn.provider)} · ${escape(turn.model)}</span> ` +
|
|
17364
17707
|
`may have started before it failed (${escape(turn.failureReason ?? "crashed")}), and its cost could not be measured.</p>`,
|
|
17365
17708
|
`<p><strong>Acknowledging charges the reserved worst case, ${chatMoney(turn.reservedMicrousd)}, to the ledger and re-enables chat on this credential.</strong></p>`,
|
|
17366
|
-
`<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>`,
|
|
17367
17710
|
`<form method="post" action="/chat/ack/${turn.id}">`,
|
|
17368
17711
|
`<input type="hidden" name="csrf" value="${escape(csrf)}">`,
|
|
17369
17712
|
`<input type="hidden" name="nonce" value="${escape(nonce)}">`,
|
|
17370
|
-
`<label>
|
|
17371
|
-
`<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>`,
|
|
17372
17715
|
`</form>`,
|
|
17373
17716
|
`</div>`,
|
|
17374
17717
|
].join("\n"), { chrome });
|
|
@@ -17575,10 +17918,10 @@ function proposalCardParts(view, csrf, inert, decision, returnTo = null) {
|
|
|
17575
17918
|
const decisionId = typeof payload["decision"] === "number" ? payload["decision"] : 0;
|
|
17576
17919
|
const pick = text("option");
|
|
17577
17920
|
const options = decision === null
|
|
17578
|
-
? `<p class="meta">
|
|
17921
|
+
? `<p class="meta">The decision is gone</p>`
|
|
17579
17922
|
: `<ul class="answer-options">` +
|
|
17580
17923
|
decision.options
|
|
17581
|
-
.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>'}` +
|
|
17582
17925
|
`${one.id === decision.recommendation ? ' <span class="meta">— the builder recommends this</span>' : ""}` +
|
|
17583
17926
|
`${one.id === pick ? ' <span class="meta">— proposed</span>' : ""}` +
|
|
17584
17927
|
`<p class="meta">${escape(one.consequence)}</p></li>`)
|
|
@@ -17608,13 +17951,13 @@ function proposalCardParts(view, csrf, inert, decision, returnTo = null) {
|
|
|
17608
17951
|
: view.kind === "control"
|
|
17609
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>`)
|
|
17610
17953
|
: view.kind === "cancel"
|
|
17611
|
-
? `<p class="meta">
|
|
17612
|
-
`<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>`
|
|
17613
17956
|
: `<div class="acts">` +
|
|
17614
17957
|
`<form method="post" action="${view.actionBase}/${view.id}/confirm" class="inline"><input type="hidden" name="csrf" value="${escape(csrf)}">${returnField}` +
|
|
17615
17958
|
(irreversible ? `<label class="arm"><input type="checkbox" name="confirm" value="yes"> I understand this cannot be undone</label>` : "") +
|
|
17616
17959
|
`<button type="submit">${escape(presentation.action)}</button></form>` +
|
|
17617
|
-
`<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>` +
|
|
17618
17961
|
`</div>`;
|
|
17619
17962
|
}
|
|
17620
17963
|
else if (view.state === "pending") {
|
|
@@ -17645,7 +17988,7 @@ function proposalCardParts(view, csrf, inert, decision, returnTo = null) {
|
|
|
17645
17988
|
const html = (`<article class="card proposal proposal-${escape(view.kind)} ${escape(view.state)}" data-card-kind="${escape(view.kind)}">` +
|
|
17646
17989
|
`<header class="proposal-head"><span class="proposal-icon">${strokeIcon(presentation.icon)}</span>` +
|
|
17647
17990
|
`<span><strong>${escape(presentation.label)}</strong><small>proposed by ${provenance}</small></span>` +
|
|
17648
|
-
`<span class="badge ${stateClass}">${escape(view.state)}</span></header>` +
|
|
17991
|
+
`<span class="badge ${stateClass}">${escape(sentenceCase(view.state))}</span></header>` +
|
|
17649
17992
|
`<div class="proposal-body">${what}</div>` +
|
|
17650
17993
|
`<footer class="proposal-actions">${acts}</footer></article>`);
|
|
17651
17994
|
// The same card as data (chat cards): one act, in the door's own words.
|
|
@@ -17693,7 +18036,7 @@ function relativeAge(iso, now) {
|
|
|
17693
18036
|
function coordinatorProposalsSection(proposals, decisions, csrf, now, heading = true, returnTo = null) {
|
|
17694
18037
|
if (proposals.length === 0)
|
|
17695
18038
|
return "";
|
|
17696
|
-
return ((heading ? `<h2>
|
|
18039
|
+
return ((heading ? `<h2>Proposed by coordinators <span class="meta">${proposals.length}</span></h2>` : "") +
|
|
17697
18040
|
`<div class="coordinator-proposals">` +
|
|
17698
18041
|
proposals.map(one => coordinatorProposalCard(one, csrf, now, decisions.get(typeof one.payload["decision"] === "number" ? one.payload["decision"] : -1) ?? null, returnTo)).join("") +
|
|
17699
18042
|
`</div>`);
|
|
@@ -17817,10 +18160,10 @@ function matePage(chrome, data) {
|
|
|
17817
18160
|
`<span>this week ${chatMoney(data.weeklySpent)} of ${chatMoney(data.config.weeklyCeilingMicrousd)}</span>`) +
|
|
17818
18161
|
`</div>`,
|
|
17819
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>`,
|
|
17820
|
-
`<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>`,
|
|
17821
18164
|
data.recent.length === 0
|
|
17822
18165
|
? ""
|
|
17823
|
-
: `<p class="meta">
|
|
18166
|
+
: `<p class="meta">Recent turns: ${data.recent
|
|
17824
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)}`)
|
|
17825
18168
|
.join(" · ")}</p>`,
|
|
17826
18169
|
`<p class="meta"><a href="/settings/lead">Lead settings</a> · provider, model and limits</p>`,
|
|
@@ -17837,7 +18180,7 @@ function matePage(chrome, data) {
|
|
|
17837
18180
|
if (data.problem !== null)
|
|
17838
18181
|
conversation.push(`<div class="problem">${escape(data.problem)}</div>`);
|
|
17839
18182
|
for (const turn of data.latched) {
|
|
17840
|
-
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)} — ` +
|
|
17841
18184
|
`<a href="/chat/ack/${turn.id}">read and acknowledge it</a> to re-enable this credential.</div>`);
|
|
17842
18185
|
}
|
|
17843
18186
|
const lastMessage = data.messages.at(-1);
|
|
@@ -17847,7 +18190,7 @@ function matePage(chrome, data) {
|
|
|
17847
18190
|
// The New update action (package 2): hidden until a live update lands
|
|
17848
18191
|
// while the reader is above the latest message; a real button, so the
|
|
17849
18192
|
// keyboard reaches it. Only this act moves the reader.
|
|
17850
|
-
`<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>`,
|
|
17851
18194
|
// Concise pass (2026-09-13): the status line speaks only when there is
|
|
17852
18195
|
// a state to report — a reply in progress here, the connection from
|
|
17853
18196
|
// the continuity script — and the composer carries no second intro.
|
|
@@ -17879,25 +18222,25 @@ function routinesPage(chrome, tracks, form) {
|
|
|
17879
18222
|
const capture = chrome.project === null
|
|
17880
18223
|
? ""
|
|
17881
18224
|
: [
|
|
17882
|
-
`<h2>
|
|
18225
|
+
`<h2>File a standing order</h2>`,
|
|
17883
18226
|
form.problem === null ? "" : `<div class="problem">${escape(form.problem)}</div>`,
|
|
17884
18227
|
`<form method="post" action="/routines/add" class="card">`,
|
|
17885
18228
|
`<input type="hidden" name="csrf" value="${escape(form.csrf)}">`,
|
|
17886
18229
|
`<input type="hidden" name="projectRevision" value="${form.revision}">`,
|
|
17887
18230
|
fill === null
|
|
17888
|
-
? `<p class="meta">
|
|
18231
|
+
? `<p class="meta">Start from a template: ${TEMPLATES.filter(one => one.kind === "routine")
|
|
17889
18232
|
.map(one => `<a href="/routines?template=${escape(one.name)}">${escape(one.name)}</a>`)
|
|
17890
18233
|
.join(" · ")}</p>`
|
|
17891
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>`,
|
|
17892
|
-
`<label>
|
|
17893
|
-
`<label>
|
|
17894
|
-
`<label>
|
|
17895
|
-
`<label>
|
|
17896
|
-
`<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>`,
|
|
17897
18240
|
scheduleEditorHtml(fill?.schedule ?? null, form.values),
|
|
17898
|
-
`<label>
|
|
17899
|
-
`<button type="submit">
|
|
17900
|
-
`<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>`,
|
|
17901
18244
|
`</form>`,
|
|
17902
18245
|
].join("\n");
|
|
17903
18246
|
const list = tracks.length === 0
|
|
@@ -17917,19 +18260,19 @@ function routineScreenPage(chrome, data) {
|
|
|
17917
18260
|
const schedule = parseSchedule(routine.schedule);
|
|
17918
18261
|
const scheduleSaid = schedule === null ? routine.schedule : describeSchedule(schedule);
|
|
17919
18262
|
const terms = `<div class="card">` +
|
|
17920
|
-
`<p class="meta">
|
|
17921
|
-
`<p class="meta">
|
|
17922
|
-
`<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>` +
|
|
17923
18266
|
(routine.acceptance.length === 0
|
|
17924
18267
|
? ""
|
|
17925
|
-
: `<p class="meta">
|
|
18268
|
+
: `<p class="meta">Acceptance</p><ul class="recap">${routine.acceptance
|
|
17926
18269
|
.map(c => `<li><code>${escape(c.id)}</code> ${escape(c.statement)} <span class="meta">[requires: ${c.evidence.map(escape).join(", ")}]</span></li>`)
|
|
17927
18270
|
.join("")}</ul>`) +
|
|
17928
|
-
`<p class="meta">
|
|
18271
|
+
`<p class="meta">Needs · ${routine.requirements.length === 0 ? "nothing beyond the repository" : routine.requirements.map(one => escape(one)).join(", ")}</p>` +
|
|
17929
18272
|
routineAgentsHtml(routine, approved) +
|
|
17930
|
-
`<p class="meta">
|
|
17931
|
-
`<p class="meta">
|
|
17932
|
-
`<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>` +
|
|
17933
18276
|
`</div>`;
|
|
17934
18277
|
// THE CONSENT DOOR (v48): the password and the approve act exist only
|
|
17935
18278
|
// where an exact, readable route can be restated — never over agents
|
|
@@ -17961,13 +18304,13 @@ function routineScreenPage(chrome, data) {
|
|
|
17961
18304
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
|
|
17962
18305
|
`<input type="hidden" name="nonce" value="${escape(data.nonce)}">`,
|
|
17963
18306
|
`<input type="hidden" name="digest" value="${escape(routine.digest)}">`,
|
|
17964
|
-
`<p><strong>
|
|
18307
|
+
`<p><strong>Approve this standing order:</strong></p>`,
|
|
17965
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>`,
|
|
17966
18309
|
// The agents the yes freezes (v48): the same concise block every
|
|
17967
18310
|
// approval shows, before the password.
|
|
17968
18311
|
routineAgentsHtml(routine, false, true),
|
|
17969
|
-
`<label>
|
|
17970
|
-
`<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>`,
|
|
17971
18314
|
`</form>`,
|
|
17972
18315
|
].join("\n");
|
|
17973
18316
|
const verb = (name, label, danger = false) => `<form method="post" action="${routineHref(routine.id)}/${name}" class="inline">` +
|
|
@@ -17977,7 +18320,7 @@ function routineScreenPage(chrome, data) {
|
|
|
17977
18320
|
? `<form method="post" action="${routineHref(routine.id)}/run-now" class="inline">` +
|
|
17978
18321
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
|
|
17979
18322
|
`<input type="password" name="token" autocomplete="current-password" class="inline" placeholder="your password" aria-label="password for run now" style="width:11rem"> ` +
|
|
17980
|
-
`<button type="submit">
|
|
18323
|
+
`<button type="submit">Run now</button></form>`
|
|
17981
18324
|
: "";
|
|
17982
18325
|
const acts = `<div class="card">` +
|
|
17983
18326
|
(routine.paused ? verb("resume", "resume") : verb("pause", "pause")) +
|
|
@@ -17991,7 +18334,7 @@ function routineScreenPage(chrome, data) {
|
|
|
17991
18334
|
const said = fire.outcome === "fired"
|
|
17992
18335
|
? fire.instanceTaskId === null
|
|
17993
18336
|
? "fired"
|
|
17994
|
-
: `<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>`}`
|
|
17995
18338
|
: `<span class="meta">skipped — ${escape(fire.reason ?? "")}</span>`;
|
|
17996
18339
|
const slot = fire.scheduledFor.replace(/^manual:/, "");
|
|
17997
18340
|
return `<p class="row">${said}<span class="right meta mono">${fire.reason === "manual" ? "run now · " : ""}${whenTime(slot)}</span></p>`;
|
|
@@ -18006,14 +18349,14 @@ function routineScreenPage(chrome, data) {
|
|
|
18006
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>`
|
|
18007
18350
|
: "",
|
|
18008
18351
|
approved && !routine.paused && routine.nextFireAt !== null
|
|
18009
|
-
? `<p class="meta">
|
|
18352
|
+
? `<p class="meta">Next fire ${whenTime(routine.nextFireAt)}</p>`
|
|
18010
18353
|
: "",
|
|
18011
|
-
"<h2>
|
|
18354
|
+
"<h2>The standing order</h2>",
|
|
18012
18355
|
terms,
|
|
18013
18356
|
approveForm,
|
|
18014
|
-
"<h2>
|
|
18357
|
+
"<h2>Acts</h2>",
|
|
18015
18358
|
acts,
|
|
18016
|
-
"<h2>
|
|
18359
|
+
"<h2>Firings</h2>",
|
|
18017
18360
|
ledger,
|
|
18018
18361
|
].join("\n"), { chrome });
|
|
18019
18362
|
}
|
|
@@ -18032,21 +18375,21 @@ function homePage(chrome, data) {
|
|
|
18032
18375
|
.map(decision => `<a class="decide-card" href="/d/${decision.id}">` +
|
|
18033
18376
|
`<p class="q">${escape(decision.question)}</p>` +
|
|
18034
18377
|
`<span class="mono meta">${escape(decision.taskId)}</span>` +
|
|
18035
|
-
`${isOverdue(decision, data.now) ? ` <span class="badge badge-overdue">
|
|
18378
|
+
`${isOverdue(decision, data.now) ? ` <span class="badge badge-overdue">Overdue</span>` : ""}` +
|
|
18036
18379
|
`</a>`)
|
|
18037
18380
|
.join("\n");
|
|
18038
18381
|
const incidents = data.incidents.length === 0
|
|
18039
18382
|
? ""
|
|
18040
|
-
: `<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>` +
|
|
18041
18384
|
data.incidents
|
|
18042
18385
|
.map(one => `<p class="row"><a href="${taskHref(one.taskId)}">${escape(one.taskId)}</a> — ${escape(incidentWords(one.kind))}` +
|
|
18043
18386
|
`<span class="right"><form method="post" action="/i/${one.id}/resolve" class="inline">` +
|
|
18044
18387
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
|
|
18045
|
-
`<button type="submit">
|
|
18388
|
+
`<button type="submit">Resolve</button></form></span></p>`)
|
|
18046
18389
|
.join("\n");
|
|
18047
18390
|
const stranded = data.stranded.length === 0
|
|
18048
18391
|
? ""
|
|
18049
|
-
: `<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>` +
|
|
18050
18393
|
data.stranded
|
|
18051
18394
|
.map(one => `<p class="row"><a href="${taskHref(one.id)}">${escape(one.id)}</a> waits on ${one.blockedBy
|
|
18052
18395
|
.map(blocker => `<a href="${taskHref(blocker)}">${escape(blocker)}</a>`)
|
|
@@ -18056,7 +18399,7 @@ function homePage(chrome, data) {
|
|
|
18056
18399
|
? ""
|
|
18057
18400
|
: data.gaps.length === 0
|
|
18058
18401
|
? ""
|
|
18059
|
-
: `<h2>
|
|
18402
|
+
: `<h2>Missing requirements</h2><p class="hint">tools or credentials builds need — checked before any money is spent</p>` +
|
|
18060
18403
|
data.gaps
|
|
18061
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>`)
|
|
18062
18405
|
.join("\n") +
|
|
@@ -18068,7 +18411,7 @@ function homePage(chrome, data) {
|
|
|
18068
18411
|
// an operator actually watches this page, so it re-renders itself.
|
|
18069
18412
|
const building = data.building.length === 0
|
|
18070
18413
|
? ""
|
|
18071
|
-
: `<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">` +
|
|
18072
18415
|
data.building
|
|
18073
18416
|
.map(claim => `<a class="stat-card" href="${taskHref(claim.taskId)}" style="text-decoration:none">` +
|
|
18074
18417
|
`<span class="k"><span class="dot dot-ok pulse"></span>${escape(claim.taskId)}</span>` +
|
|
@@ -18104,8 +18447,8 @@ function homePage(chrome, data) {
|
|
|
18104
18447
|
: `last window: ${data.episode.built} built, ${data.episode.broke} broke \u00b7 ended ${whenTime(data.episode.endedAt)}`}</span></div>`;
|
|
18105
18448
|
const fleetCards = [...runnerCards, watchCard, ...worktreeCards].filter(one => one !== "");
|
|
18106
18449
|
const fleet = fleetCards.length === 0
|
|
18107
|
-
? `<h2>
|
|
18108
|
-
: `<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>`;
|
|
18109
18452
|
const startHere = data.taskCount === 0
|
|
18110
18453
|
? [
|
|
18111
18454
|
`<div class="card">`,
|
|
@@ -18125,10 +18468,10 @@ function homePage(chrome, data) {
|
|
|
18125
18468
|
: `<p class="meta"><strong>${escape(projectName(data.repo))}</strong> — the last 24 hours, honestly labeled: a rolling window, whatever your hours are</p>`,
|
|
18126
18469
|
startHere,
|
|
18127
18470
|
ledger,
|
|
18128
|
-
`<p class="meta">
|
|
18129
|
-
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>` : "",
|
|
18130
18473
|
building,
|
|
18131
|
-
`<h2>
|
|
18474
|
+
`<h2>Needs your decision</h2><p class="hint">an agent stopped mid-build to ask — nothing proceeds until you answer</p>`,
|
|
18132
18475
|
decide,
|
|
18133
18476
|
incidents,
|
|
18134
18477
|
stranded,
|
|
@@ -18197,21 +18540,21 @@ function taskComposerHtml(data) {
|
|
|
18197
18540
|
`<details class="task-options"${prefill === null ? "" : " open"}>`,
|
|
18198
18541
|
`<summary><span>Edit details</span><small>optional · defaults are remembered</small></summary>`,
|
|
18199
18542
|
`<div class="task-options-grid">`,
|
|
18200
|
-
`<label class="wide">
|
|
18543
|
+
`<label class="wide">Goal <span class="meta">— provide upfront for automatic approval of an unchanged plan</span>` +
|
|
18201
18544
|
`<textarea name="goal" rows="3" placeholder="What success looks like">${prefill === null ? "" : escape(prefill.goal)}</textarea></label>`,
|
|
18202
|
-
`<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>` +
|
|
18203
18546
|
`<textarea name="acceptance" rows="3" placeholder="Requests over the limit return 429 | check">${prefill === null ? "" : escape(prefill.acceptance)}</textarea></label>`,
|
|
18204
|
-
`<label>
|
|
18205
|
-
`<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>`,
|
|
18206
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>`,
|
|
18207
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>`,
|
|
18208
|
-
`<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>`,
|
|
18209
18552
|
candidates.length === 0
|
|
18210
18553
|
? ""
|
|
18211
|
-
: `<label>
|
|
18554
|
+
: `<label>Starts after <span class="meta">— optional</span><select name="after"><option value="">right away</option>` +
|
|
18212
18555
|
candidates.map(one => `<option value="${escape(one.id)}"${one.id === after ? " selected" : ""}>${escape(one.id)} — ${escape(one.title)}</option>`).join("") +
|
|
18213
18556
|
`</select></label>`,
|
|
18214
|
-
`<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)}` +
|
|
18215
18558
|
`<p class="meta permission-note">Inherited from Settings. You can still change it on the proposed scope before approval.</p></fieldset>`,
|
|
18216
18559
|
`</div>`,
|
|
18217
18560
|
`</details>`,
|
|
@@ -18307,9 +18650,9 @@ function workPage(chrome, data) {
|
|
|
18307
18650
|
project: data.multiProject || row.repo === null ? (row.repo === null ? 'Unplaced' : projectName(row.repo)) : null,
|
|
18308
18651
|
age: relativeAge(row.updatedAt, data.now),
|
|
18309
18652
|
status: { label: row.status.label, tone: row.status.tone, token: row.status.token },
|
|
18310
|
-
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'),
|
|
18311
18654
|
action: actionHref === null || row.primaryAction === null ? null : { label: row.primaryAction.label, href: actionHref },
|
|
18312
|
-
detail: needsYouDetail ? row.status.detail : null,
|
|
18655
|
+
detail: needsYouDetail ? row.status.detail : row.progress ?? null,
|
|
18313
18656
|
problem: row.familyProblem,
|
|
18314
18657
|
notes: (row.status.diagnostics ?? []).map(one => `${one.label} · ${one.detail}`),
|
|
18315
18658
|
};
|
|
@@ -18337,12 +18680,12 @@ function tasksPage(chrome, tasks, state, csrf, problem, repo = null, prefill = n
|
|
|
18337
18680
|
.join("\n");
|
|
18338
18681
|
return screen("tasks", [
|
|
18339
18682
|
"<h1>Tasks</h1>",
|
|
18340
|
-
`<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>`,
|
|
18341
18684
|
repo === null ? "" : `<p class="meta path-words"><span class="mono">${escape(repo)}</span></p>`,
|
|
18342
18685
|
problem === null ? "" : `<div class="problem">${escape(problem)}</div>`,
|
|
18343
|
-
`<p class="meta">
|
|
18686
|
+
`<p class="meta">Filter: <a href="/tasks">all</a> · ${filters}</p>`,
|
|
18344
18687
|
rows,
|
|
18345
|
-
`<h2>
|
|
18688
|
+
`<h2>Add a task</h2>`,
|
|
18346
18689
|
taskComposerHtml({ csrf, project: repo, prefill, permissionDefault, qualityDefault }),
|
|
18347
18690
|
].join("\n"), { chrome });
|
|
18348
18691
|
}
|
|
@@ -18352,21 +18695,21 @@ function browsePage(chrome, data) {
|
|
|
18352
18695
|
`<form method="post" action="/projects/open" class="inline">`,
|
|
18353
18696
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
|
|
18354
18697
|
`<input type="hidden" name="path" value="${escape(path)}">`,
|
|
18355
|
-
`<button type="submit">
|
|
18698
|
+
`<button type="submit">Open</button>`,
|
|
18356
18699
|
`</form>`,
|
|
18357
18700
|
].join("");
|
|
18358
18701
|
return screen("projects", [
|
|
18359
18702
|
`<h1>Choose a folder</h1>`,
|
|
18360
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>`,
|
|
18361
18704
|
data.roots.length > 1
|
|
18362
|
-
? `<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>`
|
|
18363
18706
|
: "",
|
|
18364
18707
|
`<p class="mono meta">${escape(crumb)}</p>`,
|
|
18365
18708
|
data.parent === null
|
|
18366
18709
|
? ""
|
|
18367
18710
|
: `<p class="row"><a href="/projects/browse?at=${encodeURIComponent(data.parent)}">\u2190 up one level</a></p>`,
|
|
18368
18711
|
data.entries.length === 0
|
|
18369
|
-
? `<p class="meta">
|
|
18712
|
+
? `<p class="meta">No folders here</p>`
|
|
18370
18713
|
: data.entries
|
|
18371
18714
|
.map(one => `<p class="row">` +
|
|
18372
18715
|
`<a href="/projects/browse?at=${encodeURIComponent(one.path)}"><strong>${escape(one.name)}</strong></a>` +
|
|
@@ -18384,7 +18727,7 @@ function browsePage(chrome, data) {
|
|
|
18384
18727
|
* switching context must never depend on remembering which repo is open.
|
|
18385
18728
|
*/
|
|
18386
18729
|
function workbenchRail(data) {
|
|
18387
|
-
const workspace = (repo) => `<span class="badge">${repo === null ? "
|
|
18730
|
+
const workspace = (repo) => `<span class="badge">${repo === null ? "Unplaced" : escape(projectName(repo))}</span>`;
|
|
18388
18731
|
const row = (card, reason) => `<a class="wb-row${card.taskId === data.selected ? " wb-selected" : ""}" href="/workbench?t=${encodeURIComponent(card.taskId)}"` +
|
|
18389
18732
|
`${card.taskId === data.selected ? ` aria-current="true"` : ""}>` +
|
|
18390
18733
|
`<span class="wb-title">${escape(card.title)}</span>` +
|
|
@@ -18394,7 +18737,7 @@ function workbenchRail(data) {
|
|
|
18394
18737
|
(cards.length === 0 ? `<p class="meta">${empty}</p>` : cards.slice(0, 100).map(render).join("\n")) +
|
|
18395
18738
|
`</section>`;
|
|
18396
18739
|
const parts = [];
|
|
18397
|
-
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>` +
|
|
18398
18741
|
`<a href="/projects">manage →</a></div>`);
|
|
18399
18742
|
parts.push(group("needs you", data.attention, "Nothing needs your input.", card => row(card, escape(card.reason))));
|
|
18400
18743
|
parts.push(group("in progress", data.building, "No agent is working right now.", card => {
|
|
@@ -18406,23 +18749,23 @@ function workbenchRail(data) {
|
|
|
18406
18749
|
parts.push(group("blocked & waiting", data.waiting, "Nothing is blocked or paused.", card => row(card, escape(card.reason))));
|
|
18407
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)}`}`)));
|
|
18408
18751
|
if (data.done.length > 0) {
|
|
18409
|
-
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>`);
|
|
18410
18753
|
parts.push(data.done
|
|
18411
18754
|
.map(one => `<a class="wb-row${one.taskId === data.selected ? " wb-selected" : ""}" href="/workbench?t=${encodeURIComponent(one.taskId)}">` +
|
|
18412
18755
|
`<span class="wb-title">${escape(one.title)}</span>` +
|
|
18413
18756
|
`<span class="wb-meta"><span class="mono meta">${escape(one.taskId)}</span>${workspace(one.repo)} ` +
|
|
18414
|
-
`<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>`)
|
|
18415
18758
|
.join("\n"));
|
|
18416
18759
|
parts.push(`</section>`);
|
|
18417
18760
|
}
|
|
18418
18761
|
if (data.saturated)
|
|
18419
|
-
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>`);
|
|
18420
18763
|
return parts.join("\n");
|
|
18421
18764
|
}
|
|
18422
18765
|
/** The project chip every all-scope row wears: null is UNPLACED, said so. */
|
|
18423
18766
|
function projectChip(repo) {
|
|
18424
18767
|
return repo === null || repo === undefined
|
|
18425
|
-
? ` <span class="badge">
|
|
18768
|
+
? ` <span class="badge">Unplaced</span>`
|
|
18426
18769
|
: ` <span class="badge">${escape(projectName(repo))}</span>`;
|
|
18427
18770
|
}
|
|
18428
18771
|
/**
|
|
@@ -18440,11 +18783,11 @@ function decisionAnswerCard(decision, csrf, now, chip, returnTo = null) {
|
|
|
18440
18783
|
const options = decision.options
|
|
18441
18784
|
.map(option => {
|
|
18442
18785
|
const recommended = option.id === decision.recommendation
|
|
18443
|
-
? ` <span class="badge">
|
|
18786
|
+
? ` <span class="badge">Recommended</span>`
|
|
18444
18787
|
: "";
|
|
18445
18788
|
if (!option.reversible) {
|
|
18446
18789
|
return (`<p class="decide-option"><a href="/d/${decision.id}${returnTo === null ? "" : `?return=${encodeURIComponent(returnTo)}`}">${escape(option.label)}</a>` +
|
|
18447
|
-
` <span class="badge badge-overdue">
|
|
18790
|
+
` <span class="badge badge-overdue">Irreversible</span>${recommended}` +
|
|
18448
18791
|
` <span class="meta">${escape(option.consequence)}</span></p>`);
|
|
18449
18792
|
}
|
|
18450
18793
|
return (`<form class="decide-option decide-inline" method="post" action="/d/${decision.id}/answer">` +
|
|
@@ -18459,7 +18802,7 @@ function decisionAnswerCard(decision, csrf, now, chip, returnTo = null) {
|
|
|
18459
18802
|
`<p class="q">${escape(decision.question)}</p>` +
|
|
18460
18803
|
`<details class="decision-context"><summary>Context</summary><p class="meta">${escape(decision.recap)}</p><span class="meta mono">${escape(decision.taskId)}</span></details>` +
|
|
18461
18804
|
`<p class="meta">${chip ? projectChip(decision.repo) : ""}` +
|
|
18462
|
-
`${isOverdue(decision, now) ? ` <span class="badge badge-overdue">
|
|
18805
|
+
`${isOverdue(decision, now) ? ` <span class="badge badge-overdue">Overdue</span>` : ""}` +
|
|
18463
18806
|
` <a href="/d/${decision.id}${returnTo === null ? "" : `?return=${encodeURIComponent(returnTo)}`}">View details →</a></p>` +
|
|
18464
18807
|
`<div class="decide-options">${options}</div></div>`);
|
|
18465
18808
|
}
|
|
@@ -18540,10 +18883,10 @@ function portfolioOverview(data) {
|
|
|
18540
18883
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
|
|
18541
18884
|
`<input type="hidden" name="path" value="${escape(one.repo)}">` +
|
|
18542
18885
|
`<input type="hidden" name="return" value="/board">` +
|
|
18543
|
-
`<button type="submit">
|
|
18886
|
+
`<button type="submit">Board →</button></form>`;
|
|
18544
18887
|
return (`<div class="workspace-card${one.attention > 0 ? " hot" : ""}">` +
|
|
18545
18888
|
`<div class="workspace-head"><span class="workspace-name">${escape(workspace(one.repo))}</span>` +
|
|
18546
|
-
`<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>` +
|
|
18547
18890
|
`<div class="workspace-stats">` +
|
|
18548
18891
|
`<span class="pulse-stat${one.attention > 0 ? " hot" : ""}"><b>${one.attention}</b> need you</span>` +
|
|
18549
18892
|
`<span class="pulse-stat"><b>${one.building}</b> live</span>` +
|
|
@@ -18595,7 +18938,7 @@ function portfolioOverview(data) {
|
|
|
18595
18938
|
// ---- running: current live claims, project chips on ----
|
|
18596
18939
|
const liveRows = data.live
|
|
18597
18940
|
.map(one => `<p class="row"><a href="${taskHref(one.taskId)}">${escape(one.taskId)}</a>${projectChip(one.repo)} ` +
|
|
18598
|
-
`<span class="badge badge-running">
|
|
18941
|
+
`<span class="badge badge-running">Running</span> ` +
|
|
18599
18942
|
`<span class="mono meta">${escape(one.runner)}${one.model === null ? "" : ` · ${escape(one.model)}`} · ` +
|
|
18600
18943
|
`<time data-elapsed-since="${escape(one.claimedAt)}"></time></span></p>`)
|
|
18601
18944
|
.join("\n");
|
|
@@ -18603,8 +18946,8 @@ function portfolioOverview(data) {
|
|
|
18603
18946
|
const chipClass = (outcome) => outcome === "built" || outcome === "no-change" ? "badge-done" : outcome === "failed" ? "badge-failed" : "";
|
|
18604
18947
|
const ledgerRows = data.ledger
|
|
18605
18948
|
.map(one => `<p class="row"><a href="/r/${one.runId}">${escape(one.title)}</a>${projectChip(one.repo)} ` +
|
|
18606
|
-
`<span class="badge ${chipClass(one.outcome)}">${escape(one.outcome)}</span> ` +
|
|
18607
|
-
`${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> ` : ""}` +
|
|
18608
18951
|
`<span class="mono meta">${one.provider === null ? "" : escape(one.provider)}${one.model === null ? "" : ` · ${escape(one.model)}`}` +
|
|
18609
18952
|
`${one.ranMinutes === null ? "" : ` · ${one.ranMinutes}m`}` +
|
|
18610
18953
|
` · ${escape(runCostWords({ authMode: one.authMode, costUsd: one.costUsd, tokensIn: null, tokensOut: null }))}` +
|
|
@@ -18618,11 +18961,11 @@ function portfolioOverview(data) {
|
|
|
18618
18961
|
})()}</span></p>`)
|
|
18619
18962
|
.join("\n");
|
|
18620
18963
|
return [
|
|
18621
|
-
`<div class="control-room-head"><div><h1>
|
|
18622
|
-
`<p class="meta">
|
|
18623
|
-
`<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>`,
|
|
18624
18967
|
data.saturated ? `<div class="problem">This overview reached its 200-task display cap; the task list holds the rest.</div>` : "",
|
|
18625
|
-
`<h2>
|
|
18968
|
+
`<h2>Waits on you</h2>`,
|
|
18626
18969
|
waitCount === 0
|
|
18627
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>`
|
|
18628
18971
|
: [
|
|
@@ -18634,17 +18977,17 @@ function portfolioOverview(data) {
|
|
|
18634
18977
|
`<p class="meta"><a href="/next">clear the queue → one thing at a time</a></p>`,
|
|
18635
18978
|
].filter(part => part !== "").join("\n"),
|
|
18636
18979
|
data.gapsProject === null
|
|
18637
|
-
? `<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>`
|
|
18638
18981
|
: "",
|
|
18639
|
-
`<h2>
|
|
18982
|
+
`<h2>Project pulse</h2>`,
|
|
18640
18983
|
`<p class="hint">one row per repository</p>`,
|
|
18641
18984
|
pulseRows.length === 0
|
|
18642
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>`
|
|
18643
18986
|
: `<div class="workspace-pulse">${workspaceRows}</div>`,
|
|
18644
|
-
`<h2>
|
|
18987
|
+
`<h2>The last 24 hours</h2>`,
|
|
18645
18988
|
`<p class="hint">runs started in the last 24 hours</p>`,
|
|
18646
18989
|
data.runs24.length === 0
|
|
18647
|
-
? `<p class="meta">
|
|
18990
|
+
? `<p class="meta">No runs started in the window</p>`
|
|
18648
18991
|
: `<p class="row"><span class="meta">runs started</span> <span class="mono">${data.runs24.length}</span></p>` +
|
|
18649
18992
|
`<p class="row"><span class="meta">outcomes</span> <span class="mono">${escape(outcomeWords)}</span></p>` +
|
|
18650
18993
|
`<p class="row"><span class="meta">spend</span> <span class="mono">${escape(spendLine(summary))}</span></p>` +
|
|
@@ -18654,10 +18997,10 @@ function portfolioOverview(data) {
|
|
|
18654
18997
|
(summary.tokens > 0
|
|
18655
18998
|
? `<p class="row"><span class="meta">tokens</span> <span class="mono">${summary.tokens.toLocaleString()}</span></p>`
|
|
18656
18999
|
: ""),
|
|
18657
|
-
`<h2>
|
|
18658
|
-
data.live.length === 0 ? `<p class="meta">
|
|
18659
|
-
`<h2>
|
|
18660
|
-
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,
|
|
18661
19004
|
].join("\n");
|
|
18662
19005
|
}
|
|
18663
19006
|
/** Dollars for a screen: micro-USD stated as money, unknowables in words. */
|
|
@@ -18753,7 +19096,7 @@ function contestPage(chrome, data) {
|
|
|
18753
19096
|
const glance = summaries.length < 2
|
|
18754
19097
|
? ""
|
|
18755
19098
|
: `<div class="scroll-x"><table class="contest-glance">` +
|
|
18756
|
-
`<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>` +
|
|
18757
19100
|
`<tr><td class="meta">outcome</td>${summaries.map(one => `<td>${escape(one.outcome)}</td>`).join("")}</tr>` +
|
|
18758
19101
|
`<tr><td class="meta">changed</td>${summaries.map(one => `<td>${escape(one.diffWords)}</td>`).join("")}</tr>` +
|
|
18759
19102
|
`<tr><td class="meta">time</td>${summaries.map(one => `<td>${one.minutes === null ? "—" : `${one.minutes} min`}</td>`).join("")}</tr>` +
|
|
@@ -18766,22 +19109,22 @@ function contestPage(chrome, data) {
|
|
|
18766
19109
|
const agent = summary.agent;
|
|
18767
19110
|
const parts = [
|
|
18768
19111
|
`<div class="card${winner ? " picked" : ""}">`,
|
|
18769
|
-
`<p><strong>
|
|
18770
|
-
`${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>`,
|
|
18771
19114
|
`<p class="row">${escape(outcome)}` +
|
|
18772
19115
|
`${minutes === null ? "" : ` <span class="meta">· ${minutes} min</span>`}` +
|
|
18773
19116
|
`${asked > 0 ? ` <span class="meta">· asked ${asked} question${asked > 1 ? "s" : ""}</span>` : ""}` +
|
|
18774
19117
|
`${run === null ? "" : ` <span class="meta">· <a href="/r/${run.id}">the build</a></span>`}</p>`,
|
|
18775
|
-
`<p class="row"><strong>
|
|
18776
|
-
run === null || run.handoff === null ? "" : `<p><strong>
|
|
18777
|
-
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),
|
|
18778
19121
|
];
|
|
18779
19122
|
if (picking) {
|
|
18780
19123
|
if (agent.pickable) {
|
|
18781
|
-
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>`);
|
|
18782
19125
|
}
|
|
18783
19126
|
else {
|
|
18784
|
-
parts.push(`<p class="meta">
|
|
19127
|
+
parts.push(`<p class="meta">Cannot be picked — ${escape(agent.unpickableReason ?? "")}</p>`);
|
|
18785
19128
|
}
|
|
18786
19129
|
}
|
|
18787
19130
|
parts.push(`</div>`);
|
|
@@ -18793,11 +19136,11 @@ function contestPage(chrome, data) {
|
|
|
18793
19136
|
`only one result will be kept as the task's outcome; the rest stay archived with their evidence</p>`,
|
|
18794
19137
|
data.problem === null ? "" : `<div class="problem">${escape(data.problem)}</div>`,
|
|
18795
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>`,
|
|
18796
|
-
contest.pickedBy === null ? "" : `<p class="meta">
|
|
19139
|
+
contest.pickedBy === null ? "" : `<p class="meta">Picked by ${escape(contest.pickedBy)} at ${whenTime(contest.pickedAt ?? "")}</p>`,
|
|
18797
19140
|
contest.kind === "comparison"
|
|
18798
|
-
? `<p class="row"><strong>
|
|
19141
|
+
? `<p class="row"><strong>Spend</strong> ${escape(contestDollars(data.totalMicrousd))} measured on the lanes that report dollars` +
|
|
18799
19142
|
`${data.anyUnknown ? ` <span class="meta">— the rest report tokens only</span>` : ""}</p>`
|
|
18800
|
-
: `<p class="row"><strong>
|
|
19143
|
+
: `<p class="row"><strong>Charged so far</strong> ${escape(contestDollars(data.totalMicrousd))}` +
|
|
18801
19144
|
`${data.anyUnknown ? ` <span class="meta">— includes at least one agent charged its full reservation because the exact figure was unknowable</span>` : ""}</p>`,
|
|
18802
19145
|
glance,
|
|
18803
19146
|
`<div class="contest-compare">${summaries.map(agentCard).join("\n")}</div>`,
|
|
@@ -18807,9 +19150,9 @@ function contestPage(chrome, data) {
|
|
|
18807
19150
|
`<form method="post" action="/contest/${contest.id}/arm" class="inline">`,
|
|
18808
19151
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
|
|
18809
19152
|
`<input type="hidden" name="act" value="abandon">`,
|
|
18810
|
-
`<button type="submit">
|
|
19153
|
+
`<button type="submit">Abandon the ${contestNoun(contest.kind)}…</button>`,
|
|
18811
19154
|
`</form>`,
|
|
18812
|
-
`<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>`,
|
|
18813
19156
|
`</div>`,
|
|
18814
19157
|
].join("\n")
|
|
18815
19158
|
: "",
|
|
@@ -18826,7 +19169,7 @@ function contestCeremonyPage(chrome, data) {
|
|
|
18826
19169
|
const back = `<p class="meta"><a href="/contest/${data.contestId}">back — decide nothing</a></p>`;
|
|
18827
19170
|
if (data.kind === "abandon") {
|
|
18828
19171
|
return screen("tournament", [
|
|
18829
|
-
`<h1>
|
|
19172
|
+
`<h1>Abandon this ${contestNoun(data.contestKind)}?</h1>`,
|
|
18830
19173
|
`<div class="card">`,
|
|
18831
19174
|
`<p class="row">${data.agents} agents ${data.contestKind === "comparison" ? "built independently" : "raced"} on <strong>${escape(data.taskTitle)}</strong>. Abandoning picks nothing:</p>`,
|
|
18832
19175
|
`<p class="row">— the task is marked <strong>failed</strong> and can be re-queued later</p>`,
|
|
@@ -18838,20 +19181,20 @@ function contestCeremonyPage(chrome, data) {
|
|
|
18838
19181
|
`<form method="post" action="/contest/${data.contestId}/abandon" class="card">`,
|
|
18839
19182
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
|
|
18840
19183
|
`<input type="hidden" name="nonce" value="${escape(data.nonceValue)}">`,
|
|
18841
|
-
`<label>
|
|
18842
|
-
`<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>`,
|
|
18843
19186
|
`</form>`,
|
|
18844
19187
|
back,
|
|
18845
19188
|
].join("\n"), { chrome });
|
|
18846
19189
|
}
|
|
18847
19190
|
const agent = data.chosen;
|
|
18848
19191
|
if (agent === undefined || agent.run === null)
|
|
18849
|
-
return screen("tournament", `<p class="meta">
|
|
19192
|
+
return screen("tournament", `<p class="meta">Nothing to confirm</p>`, { chrome });
|
|
18850
19193
|
const run = agent.run;
|
|
18851
19194
|
return screen("tournament", [
|
|
18852
|
-
`<h1>
|
|
19195
|
+
`<h1>Pick agent ${agent.contestant.ordinal}'s result?</h1>`,
|
|
18853
19196
|
`<div class="card">`,
|
|
18854
|
-
`<p class="row"><strong>
|
|
19197
|
+
`<p class="row"><strong>Agent ${agent.contestant.ordinal}</strong> — ${escape(agent.contestant.provider)} · ${escape(agent.contestant.model)}</p>`,
|
|
18855
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>`,
|
|
18856
19199
|
run.outcome === "no-change"
|
|
18857
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>`
|
|
@@ -18861,8 +19204,8 @@ function contestCeremonyPage(chrome, data) {
|
|
|
18861
19204
|
? ""
|
|
18862
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>`,
|
|
18863
19206
|
data.publication === null || data.publication === undefined
|
|
18864
|
-
? `<p class="row"><strong>
|
|
18865
|
-
: `<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 ` +
|
|
18866
19209
|
`<span class="mono">${escape(data.publication.githubRepo)}</span> from <span class="mono">${escape(data.publication.branch)}</span></p>`,
|
|
18867
19210
|
data.contestKind === "comparison"
|
|
18868
19211
|
? `<p class="row">this agent's spend: ${agent.contestant.unknownSpend ? "unmeasured — its harness reports tokens, not dollars" : `${escape(contestDollars(agent.contestant.measuredMicrousd))} measured`}` +
|
|
@@ -18876,8 +19219,8 @@ function contestCeremonyPage(chrome, data) {
|
|
|
18876
19219
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
|
|
18877
19220
|
`<input type="hidden" name="nonce" value="${escape(data.nonceValue)}">`,
|
|
18878
19221
|
`<input type="hidden" name="choice" value="${agent.contestant.id}">`,
|
|
18879
|
-
`<label>
|
|
18880
|
-
`<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>`,
|
|
18881
19224
|
`</form>`,
|
|
18882
19225
|
back,
|
|
18883
19226
|
].filter(one => one !== "").join("\n"), { chrome });
|
|
@@ -18968,18 +19311,18 @@ function githubReposPage(chrome, data) {
|
|
|
18968
19311
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
|
|
18969
19312
|
`<input type="hidden" name="repo" value="${escape(nameWithOwner)}">`,
|
|
18970
19313
|
`<input type="hidden" name="root" value="0">`,
|
|
18971
|
-
`<button type="submit">
|
|
19314
|
+
`<button type="submit">Clone here →</button>`,
|
|
18972
19315
|
`</form>`,
|
|
18973
19316
|
].join("");
|
|
18974
19317
|
const rows = !data.listed.ok
|
|
18975
19318
|
? `<p class="meta">${escape(data.listed.message)}</p>`
|
|
18976
19319
|
: data.listed.repos.length === 0
|
|
18977
|
-
? `<p class="meta">
|
|
19320
|
+
? `<p class="meta">The signed-in GitHub account has no repositories to list</p>`
|
|
18978
19321
|
: data.listed.repos
|
|
18979
19322
|
.map(repo => {
|
|
18980
19323
|
const localPath = data.local.get(repo.nameWithOwner.toLowerCase()) ?? null;
|
|
18981
19324
|
const action = localPath !== null && data.openProject === localPath
|
|
18982
|
-
? `<span class="badge badge-done">
|
|
19325
|
+
? `<span class="badge badge-done">Open now</span>`
|
|
18983
19326
|
: localPath !== null
|
|
18984
19327
|
? openForm(localPath, data.registered.has(localPath) ? "open →" : "add + open →")
|
|
18985
19328
|
: data.cloneReady
|
|
@@ -18987,10 +19330,10 @@ function githubReposPage(chrome, data) {
|
|
|
18987
19330
|
: `<span class="meta">choose a projects folder first</span>`;
|
|
18988
19331
|
return [
|
|
18989
19332
|
`<div class="card project-card">`,
|
|
18990
|
-
`<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>` : ""}`,
|
|
18991
19334
|
`<span class="right">${action}</span></div>`,
|
|
18992
19335
|
localPath === null
|
|
18993
|
-
? `<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>`
|
|
18994
19337
|
: `<p class="meta mono" style="overflow-wrap:anywhere;margin:.2rem 0">${escape(localPath)}</p>`,
|
|
18995
19338
|
repo.description === "" ? "" : `<p class="meta">${escape(repo.description)}</p>`,
|
|
18996
19339
|
`</div>`,
|
|
@@ -18998,8 +19341,8 @@ function githubReposPage(chrome, data) {
|
|
|
18998
19341
|
})
|
|
18999
19342
|
.join("\n");
|
|
19000
19343
|
return screen("projects", [
|
|
19001
|
-
`<h1>
|
|
19002
|
-
`<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>`,
|
|
19003
19346
|
rows,
|
|
19004
19347
|
`<p class="row" style="margin-top:.6rem"><a class="badge" href="/projects">← back to projects</a></p>`,
|
|
19005
19348
|
].join("\n"), { chrome });
|
|
@@ -19014,16 +19357,16 @@ function projectsPage(chrome, recent, candidates, open, csrf, problem, _unscoped
|
|
|
19014
19357
|
? `<p class="meta">${escape(onboard.why)}</p>`
|
|
19015
19358
|
: [
|
|
19016
19359
|
`<details class="project-add-more"${onboard.record === null ? '' : ' open'}><summary>${onboard.record === null ? 'Paste a GitHub link' : 'Review repository'}</summary>`,
|
|
19017
|
-
`<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>`,
|
|
19018
19361
|
onboard.record === null
|
|
19019
19362
|
? [
|
|
19020
19363
|
`<form method="post" action="/projects/onboard-preview" class="card">`,
|
|
19021
19364
|
`<input type="hidden" name="csrf" value="${escape(csrf)}">`,
|
|
19022
|
-
`<label>
|
|
19365
|
+
`<label>Repository <input type="text" name="repo" placeholder="owner/name or https://github.com/owner/name"></label>`,
|
|
19023
19366
|
onboard.roots.length > 1
|
|
19024
|
-
? `<label>
|
|
19025
|
-
: `<input type="hidden" name="root" value="0"><p class="meta">
|
|
19026
|
-
`<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>`,
|
|
19027
19370
|
`</form>`,
|
|
19028
19371
|
].join("\n")
|
|
19029
19372
|
: [
|
|
@@ -19035,8 +19378,8 @@ function projectsPage(chrome, recent, candidates, open, csrf, problem, _unscoped
|
|
|
19035
19378
|
onboard.record[1].large
|
|
19036
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>`
|
|
19037
19380
|
: "",
|
|
19038
|
-
`<label>
|
|
19039
|
-
`<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>`,
|
|
19040
19383
|
`</form>`,
|
|
19041
19384
|
`</div>`,
|
|
19042
19385
|
].join("\n"),
|
|
@@ -19078,7 +19421,7 @@ function projectsPage(chrome, recent, candidates, open, csrf, problem, _unscoped
|
|
|
19078
19421
|
`<div class="row">${open !== null && open === one.path
|
|
19079
19422
|
? `<a class="project-name" href="${escape(returnTo)}"><strong>${escape(one.name)}</strong></a>`
|
|
19080
19423
|
: openForm(one.path, one.name, returnTo, "project-name")}`,
|
|
19081
|
-
`<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>`,
|
|
19082
19425
|
`<p class="meta mono" style="overflow-wrap:anywhere;margin:.2rem 0">${escape(one.path)}</p>`,
|
|
19083
19426
|
`<p class="row" style="gap:.35rem;flex-wrap:wrap">${peekChips(one.path, one.peek)}</p>`,
|
|
19084
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>`,
|
|
@@ -19120,13 +19463,13 @@ function projectsPage(chrome, recent, candidates, open, csrf, problem, _unscoped
|
|
|
19120
19463
|
`<form method="post" action="/projects/open" class="card">`,
|
|
19121
19464
|
`<input type="hidden" name="csrf" value="${escape(csrf)}">`,
|
|
19122
19465
|
`<input type="hidden" name="return" value="${escape(returnTo)}">`,
|
|
19123
|
-
`<label>
|
|
19124
|
-
`<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>`,
|
|
19125
19468
|
`</form></details>`,
|
|
19126
19469
|
].join("\n");
|
|
19127
19470
|
const addCard = [
|
|
19128
19471
|
`<div class="card project-add-card">`,
|
|
19129
|
-
`<h2 class="project-add-title">
|
|
19472
|
+
`<h2 class="project-add-title">Add a project</h2>`,
|
|
19130
19473
|
`<div class="project-add-actions">`,
|
|
19131
19474
|
browsable
|
|
19132
19475
|
? addAction("/projects/browse", FOLDER_PATHS, "Choose a local folder", "Browse the project folders on this machine")
|
|
@@ -19147,8 +19490,8 @@ function projectsPage(chrome, recent, candidates, open, csrf, problem, _unscoped
|
|
|
19147
19490
|
recentItems.length === 0 && candidateItems.length === 0
|
|
19148
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>`
|
|
19149
19492
|
: "",
|
|
19150
|
-
recentItems.length > 0 ? `<h2>
|
|
19151
|
-
candidateItems.length > 0 ? `<h2>
|
|
19493
|
+
recentItems.length > 0 ? `<h2>Recent</h2>${cards(recentItems)}` : "",
|
|
19494
|
+
candidateItems.length > 0 ? `<h2>Available</h2>${cards(candidateItems)}` : "",
|
|
19152
19495
|
addCard,
|
|
19153
19496
|
].join("\n"), { chrome, workspace: { view: {
|
|
19154
19497
|
kind: "projects", choosing, problem, returnTo,
|
|
@@ -19320,25 +19663,25 @@ const NAV_ICONS = {
|
|
|
19320
19663
|
function workToolRows(scoped = false, _offersChat = false, offersCode = false) {
|
|
19321
19664
|
const rows = [
|
|
19322
19665
|
...(offersCode ? [{ key: "code", href: "/code", label: "Coding sessions", hint: "Direct coding sessions and their saved results" }] : []),
|
|
19323
|
-
{ key: "inbox", href: "/inbox", label: "
|
|
19324
|
-
{ key: "board", href: "/board", label: "
|
|
19325
|
-
{ key: "tasks", href: "/tasks", label: "
|
|
19326
|
-
{ key: "recipes", href: "/recipes", label: "
|
|
19327
|
-
{ key: "routines", href: "/routines", label: "
|
|
19328
|
-
{ key: "workbench", href: "/workbench", label: "
|
|
19329
|
-
{ key: "ledger", href: "/ledger", label: "
|
|
19330
|
-
{ 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" },
|
|
19331
19674
|
];
|
|
19332
19675
|
return scoped ? rows.filter(row => row.key !== "workbench" && row.key !== "spend") : rows;
|
|
19333
19676
|
}
|
|
19334
19677
|
function settingsRows(scoped = false, offersSettings = false) {
|
|
19335
19678
|
const rows = [
|
|
19336
|
-
...(offersSettings ? [{ key: "settings", href: "/settings", label: "
|
|
19337
|
-
{ key: "fleet", href: "/fleet", label: "
|
|
19338
|
-
{ key: "caps", href: "/caps", label: "
|
|
19339
|
-
{ key: "people", href: "/people", label: "
|
|
19340
|
-
{ key: "mode", href: "/mode", label: "
|
|
19341
|
-
{ 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" },
|
|
19342
19685
|
];
|
|
19343
19686
|
return scoped ? rows.filter(row => row.key === "people" || row.key === "settings") : rows;
|
|
19344
19687
|
}
|
|
@@ -19368,10 +19711,10 @@ function queueCard(one, csrf, revision, queueRevision, workers, column) {
|
|
|
19368
19711
|
// and the reservation owner. Money is not in this query and is not
|
|
19369
19712
|
// invented here — it stays on the task page, labeled.
|
|
19370
19713
|
const state = one.taken
|
|
19371
|
-
? `<span class="badge">
|
|
19714
|
+
? `<span class="badge">Being taken — keeps its claim</span>`
|
|
19372
19715
|
: column === "anyone"
|
|
19373
|
-
? `<span class="badge">
|
|
19374
|
-
: `<span class="badge">
|
|
19716
|
+
? `<span class="badge">Queued</span>`
|
|
19717
|
+
: `<span class="badge">Reserved for ${escape(column)}</span>`;
|
|
19375
19718
|
const chips = ` ${state}` +
|
|
19376
19719
|
`${one.dispatch === null || one.dispatch === undefined ? "" : ` <a class="badge" href="${taskHref(one.id)}" title="${escape(one.dispatch.summary)}">${escape(dispatchHeadline(one.dispatch))}</a>`}`;
|
|
19377
19720
|
const hidden = `<input type="hidden" name="csrf" value="${escape(csrf)}">` +
|
|
@@ -19388,7 +19731,7 @@ function queueCard(one, csrf, revision, queueRevision, workers, column) {
|
|
|
19388
19731
|
`<select name="column" aria-label="reserve for">` +
|
|
19389
19732
|
`<option value="anyone"${column === "anyone" ? " selected" : ""}>anyone</option>` +
|
|
19390
19733
|
workers.filter(worker => !worker.retired).map(worker => `<option value="${escape(worker.name)}"${column === worker.name ? " selected" : ""}>${escape(worker.name)}</option>`).join("") +
|
|
19391
|
-
`</select><button type="submit">
|
|
19734
|
+
`</select><button type="submit">Move</button></form>`;
|
|
19392
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)}"`}>` +
|
|
19393
19736
|
`<p class="row">${one.taken ? "" : `${GRIP_HANDLE}`}` +
|
|
19394
19737
|
`<a href="${taskHref(one.id)}">${escape(one.title)}</a>${chips}</p>` +
|
|
@@ -19410,22 +19753,22 @@ function queueBody(tasks, workers, csrf, revision, queueRevision) {
|
|
|
19410
19753
|
? `<p class="meta lane-empty">${escape(empty)}</p>`
|
|
19411
19754
|
: rows
|
|
19412
19755
|
.map((one, index) => index === 0 && key === "anyone" && allWorkersBusy && !one.taken
|
|
19413
|
-
? 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">`)
|
|
19414
19757
|
: queueCard(one, csrf, revision, queueRevision, workers, key))
|
|
19415
19758
|
.join("\n")) +
|
|
19416
19759
|
`</section>`;
|
|
19417
19760
|
const noteForm = (worker) => worker.retired
|
|
19418
|
-
? `<p class="meta">
|
|
19761
|
+
? `<p class="meta">This worker is retired — drag these elsewhere, or register the name again</p>`
|
|
19419
19762
|
: `<p class="meta mono">${worker.building} building in this project · unattended capacity ${worker.capacity}</p>` +
|
|
19420
19763
|
`<form method="post" action="/queue/note" class="row">` +
|
|
19421
19764
|
`<input type="hidden" name="csrf" value="${escape(csrf)}">` +
|
|
19422
19765
|
`<input type="hidden" name="projectRevision" value="${revision}">` +
|
|
19423
19766
|
`<input type="hidden" name="runner" value="${escape(worker.name)}">` +
|
|
19424
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">` +
|
|
19425
|
-
`<button type="submit">
|
|
19426
|
-
`<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>`;
|
|
19427
19770
|
return (`<div class="lanes" data-queue-revision="${queueRevision}">` +
|
|
19428
|
-
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") +
|
|
19429
19772
|
workers
|
|
19430
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"))
|
|
19431
19774
|
.join("\n") +
|
|
@@ -19447,7 +19790,7 @@ function fleetBody(queued, building, runners, csrf, queueRevision, visibleRepo)
|
|
|
19447
19790
|
const live = building.filter(one => one.runner === runner.name);
|
|
19448
19791
|
const retired = runner.retiredAt !== null;
|
|
19449
19792
|
const head = retired
|
|
19450
|
-
? `<p class="meta">
|
|
19793
|
+
? `<p class="meta">This worker is retired — drag these elsewhere, or register the name again</p>`
|
|
19451
19794
|
: `<form method="post" action="/queue/note" class="row">` +
|
|
19452
19795
|
`<input type="hidden" name="csrf" value="${escape(csrf)}">` +
|
|
19453
19796
|
`<input type="hidden" name="from" value="fleet">` +
|
|
@@ -19464,9 +19807,9 @@ function fleetBody(queued, building, runners, csrf, queueRevision, visibleRepo)
|
|
|
19464
19807
|
`<p class="row">${one.taken ? "" : `${GRIP_HANDLE}`}` +
|
|
19465
19808
|
`<a href="${taskHref(one.id)}">${escape(one.title)}</a></p>` +
|
|
19466
19809
|
`<p class="row meta"><span class="mono">${escape(one.id)}</span>${chip(one.repo)}` +
|
|
19467
|
-
`${one.approved ? "" : ` <span class="badge">
|
|
19468
|
-
`${one.blockers > 0 ? ` <span class="badge">
|
|
19469
|
-
`${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>`)
|
|
19470
19813
|
.join("\n");
|
|
19471
19814
|
const empty = live.length === 0 && own.length === 0
|
|
19472
19815
|
? `<p class="meta lane-empty">${retired ? "nothing left" : "idle — will take from the shared queue"}</p>`
|
|
@@ -19481,12 +19824,12 @@ function fleetBody(queued, building, runners, csrf, queueRevision, visibleRepo)
|
|
|
19481
19824
|
`<p class="row">${one.taken ? "" : `${GRIP_HANDLE}`}` +
|
|
19482
19825
|
`<a href="${taskHref(one.id)}">${escape(one.title)}</a></p>` +
|
|
19483
19826
|
`<p class="row meta"><span class="mono">${escape(one.id)}</span>${chip(one.repo)}` +
|
|
19484
|
-
`${one.approved ? "" : ` <span class="badge">
|
|
19485
|
-
`${one.blockers > 0 ? ` <span class="badge">
|
|
19486
|
-
`${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>`)
|
|
19487
19830
|
.join("\n");
|
|
19488
|
-
const sharedLane = `<section class="lane queue-column" data-column="anyone"><h2>
|
|
19489
|
-
`<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}` +
|
|
19490
19833
|
(shared.length === 0 ? `<p class="meta lane-empty">nothing waiting — every task is reserved or running</p>` : "") +
|
|
19491
19834
|
`</section>`;
|
|
19492
19835
|
return (`<div class="lanes" data-queue-revision="${queueRevision}">` +
|
|
@@ -19628,13 +19971,15 @@ function newTaskPage(chrome, project, csrf, projectRevision, problem, candidates
|
|
|
19628
19971
|
* brief, but the console renders its known structure as a useful control
|
|
19629
19972
|
* surface. Historical free-form plans keep their safe plain-text fallback. */
|
|
19630
19973
|
const MILESTONE_STATE_WORDS = {
|
|
19631
|
-
pending: "
|
|
19632
|
-
current: "
|
|
19633
|
-
completed: "
|
|
19634
|
-
blocked: "
|
|
19974
|
+
pending: "Pending",
|
|
19975
|
+
current: "In progress",
|
|
19976
|
+
completed: "Completed",
|
|
19977
|
+
blocked: "Blocked",
|
|
19635
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);
|
|
19636
19981
|
function milestoneStateWord(state) {
|
|
19637
|
-
return MILESTONE_STATE_WORDS[state] ?? "
|
|
19982
|
+
return MILESTONE_STATE_WORDS[state] ?? "Pending";
|
|
19638
19983
|
}
|
|
19639
19984
|
/** Adaptive execution plans (v44): the live milestone projection a running
|
|
19640
19985
|
* (or finished) build has reported. A shared render used by BOTH the task
|
|
@@ -19693,9 +20038,9 @@ function planRevisionLedgerHtml(ledger, taskId, csrf) {
|
|
|
19693
20038
|
`</div>`;
|
|
19694
20039
|
const history = ledger.history.length <= 1
|
|
19695
20040
|
? ""
|
|
19696
|
-
: `<details class="plan-revision-history"><summary>
|
|
20041
|
+
: `<details class="plan-revision-history"><summary>Revision history (${ledger.history.length})</summary>` +
|
|
19697
20042
|
ledger.history
|
|
19698
|
-
.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> ` +
|
|
19699
20044
|
`<span class="meta">${escape(one.author)} · ${whenTime(one.createdAt)}</span> — ${escape(one.reason)}</p>`)
|
|
19700
20045
|
.join("\n") +
|
|
19701
20046
|
`</details>`;
|
|
@@ -19814,25 +20159,24 @@ function taskBodyParts(data) {
|
|
|
19814
20159
|
if (liveRunId === null || liveRun === undefined)
|
|
19815
20160
|
return "";
|
|
19816
20161
|
const minutes = Math.max(0, Math.round((data.now.getTime() - new Date(liveRun.startedAt).getTime()) / 60_000));
|
|
19817
|
-
const head = `<p><strong>
|
|
20162
|
+
const head = `<p><strong>Build #${liveRun.id} · ${escape(liveRun.runner)} · running ` +
|
|
19818
20163
|
`<time data-elapsed-since="${escape(liveRun.startedAt)}">${minutes}m</time></strong></p>`;
|
|
19819
20164
|
const door = `<p class="row"><a href="/r/${liveRun.id}">full build view →</a></p>`;
|
|
19820
20165
|
if (data.degraded !== undefined) {
|
|
19821
20166
|
return `<div class="card attempt-live" data-live-run="${liveRun.id}">${head}` +
|
|
19822
|
-
`<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>`;
|
|
19823
20168
|
}
|
|
19824
|
-
|
|
19825
|
-
|
|
19826
|
-
|
|
19827
|
-
|
|
19828
|
-
|
|
19829
|
-
|
|
19830
|
-
|
|
19831
|
-
|
|
19832
|
-
|
|
19833
|
-
|
|
19834
|
-
|
|
19835
|
-
`<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>`;
|
|
19836
20180
|
return `<div class="card attempt-live" data-live-run="${liveRun.id}">${head}${peek}${transcript}${door}</div>`;
|
|
19837
20181
|
})();
|
|
19838
20182
|
// One truthful answer to the first question on a queued task: "will this
|
|
@@ -19935,7 +20279,7 @@ function taskBodyParts(data) {
|
|
|
19935
20279
|
`<p class="meta">Saved assessment history. Current task status and checks are shown above.</p>` +
|
|
19936
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>`) +
|
|
19937
20281
|
(proof.proofReasons.length === 0 ? "" : `<ul>${[...new Set(proof.proofReasons.map(plainReasonWords))].map(reason => `<li>${escape(reason)}</li>`).join("")}</ul>`) +
|
|
19938
|
-
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 }) +
|
|
19939
20283
|
semanticCoverageHtml(proof.proofMatrix, proof.qualityMode ?? "default") + `</div></details>`;
|
|
19940
20284
|
}
|
|
19941
20285
|
// v50: the independent review's own card — attempt counts, what is
|
|
@@ -19987,7 +20331,7 @@ function taskBodyParts(data) {
|
|
|
19987
20331
|
const proofDetails = proof.proofMatrix.length === 0 && machineNote === "" && chainHtml === ""
|
|
19988
20332
|
? ""
|
|
19989
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>` +
|
|
19990
|
-
`<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>`;
|
|
19991
20335
|
// The publication fact is separate from the evidence fact: a PR or
|
|
19992
20336
|
// an observed merge is named from its record; nothing is "deployed".
|
|
19993
20337
|
const publicationWords = (data.publication === null || data.publication === undefined ? "" : ` ${escape(receiptPublicationWords(publicationFactsOf(data.publication)))}`) +
|
|
@@ -20047,26 +20391,30 @@ function taskBodyParts(data) {
|
|
|
20047
20391
|
: `<form method="post" action="${taskHref(task.id)}/steer" class="row">` +
|
|
20048
20392
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
|
|
20049
20393
|
`<input type="text" name="note" placeholder="guidance for the next attempt" aria-label="steering note" style="width:100%;max-width:28rem">` +
|
|
20050
|
-
`<button type="submit">
|
|
20051
|
-
`<p class="meta">
|
|
20052
|
-
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}`;
|
|
20053
20397
|
const holds = data.holds.length === 0
|
|
20054
20398
|
? ""
|
|
20055
|
-
: `<h2>
|
|
20399
|
+
: `<h2>Holds</h2>` +
|
|
20056
20400
|
data.holds
|
|
20057
20401
|
.map(hold => `<p class="row">${escape(holdOwnerWords(hold.ownerKind))} — ${escape(hold.reason)}` +
|
|
20058
20402
|
`${hold.until === null ? "" : ` <span class="meta">until ${whenTime(hold.until)}</span>`}</p>`)
|
|
20059
20403
|
.join("\n") +
|
|
20060
|
-
`<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>`;
|
|
20061
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;
|
|
20062
20410
|
const scopeCard = scope === null
|
|
20063
|
-
? `<p class="meta">
|
|
20411
|
+
? `<p class="meta">No scope proposed — nothing builds this until one is approved</p>`
|
|
20064
20412
|
: [
|
|
20065
20413
|
`<div class="card">`,
|
|
20066
|
-
`<p><strong>
|
|
20067
|
-
scope.outOfScope === null ? "" : `<p><strong>
|
|
20068
|
-
scope.touches.length === 0 ? "" : `<p class="scope-paths"><strong>
|
|
20069
|
-
`<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>`,
|
|
20070
20418
|
// The fallback chain, on the card the yes reads (Layer F): the
|
|
20071
20419
|
// digest binds the WHOLE chain, so every entry — credential
|
|
20072
20420
|
// included — is said before anyone signs.
|
|
@@ -20074,7 +20422,7 @@ function taskBodyParts(data) {
|
|
|
20074
20422
|
const chain = chainFromJson(scope.proposedChainJson ?? null);
|
|
20075
20423
|
if (chain === null || chain.length < 2)
|
|
20076
20424
|
return "";
|
|
20077
|
-
return `<p><strong>
|
|
20425
|
+
return `<p><strong>If its subscription runs out</strong></p><p class="recap">${chain
|
|
20078
20426
|
.slice(1)
|
|
20079
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"}`)
|
|
20080
20428
|
.join("; ")}</p>`;
|
|
@@ -20092,10 +20440,10 @@ function taskBodyParts(data) {
|
|
|
20092
20440
|
// blank — the same rule as the revision brief.
|
|
20093
20441
|
const reportCard = data.report === null || data.report === undefined
|
|
20094
20442
|
? data.deliverable === "report"
|
|
20095
|
-
? `<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>`
|
|
20096
20444
|
: ""
|
|
20097
20445
|
: !data.report.ok
|
|
20098
|
-
? `<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>`
|
|
20099
20447
|
: [
|
|
20100
20448
|
`<div class="card report">`,
|
|
20101
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>`,
|
|
@@ -20104,11 +20452,11 @@ function taskBodyParts(data) {
|
|
|
20104
20452
|
...(data.report.report.followUps.length === 0
|
|
20105
20453
|
? []
|
|
20106
20454
|
: [
|
|
20107
|
-
`<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>`,
|
|
20108
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>` +
|
|
20109
20457
|
(data.csrf === "" || data.repo === null
|
|
20110
20458
|
? `<p class="meta">${data.repo === null ? "this task has no repository — file it by hand" : ""}</p>`
|
|
20111
|
-
: `<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>`) +
|
|
20112
20460
|
`</div>`),
|
|
20113
20461
|
]),
|
|
20114
20462
|
`</div>`,
|
|
@@ -20140,7 +20488,7 @@ function taskBodyParts(data) {
|
|
|
20140
20488
|
? data.revision != null && !("problem" in data.revision)
|
|
20141
20489
|
? `<div class="card planner-status"><span class="planner-orb" aria-hidden="true"></span><p><strong>Updating the plan</strong>` +
|
|
20142
20490
|
`<span class="meta">Adding your notes to the plan. You approve any change before it builds.</span></p></div>`
|
|
20143
|
-
: `<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>` +
|
|
20144
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>`
|
|
20145
20493
|
: ""
|
|
20146
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">`}` +
|
|
@@ -20153,7 +20501,7 @@ function taskBodyParts(data) {
|
|
|
20153
20501
|
? ""
|
|
20154
20502
|
: `<details class="plan-editor" id="plan-edit"><summary>Edit plan</summary><form method="post" action="${taskHref(task.id)}/plan-edit">` +
|
|
20155
20503
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}"><input type="hidden" name="saw-plan" value="${escape(data.planSha)}">` +
|
|
20156
|
-
`<label>
|
|
20504
|
+
`<label>Plan details <span class="meta">Keep the five headings. Approval locks this version for the build.</span>` +
|
|
20157
20505
|
`<textarea name="plan-document" rows="14">${escape(data.planDocument)}</textarea></label>` +
|
|
20158
20506
|
`<button type="submit">Save plan</button></form></details>`) +
|
|
20159
20507
|
`${approval.approved ? `</div></details>` : `</section>`}`;
|
|
@@ -20165,7 +20513,7 @@ function taskBodyParts(data) {
|
|
|
20165
20513
|
const revisionCard = data.revision === null || data.revision === undefined
|
|
20166
20514
|
? ""
|
|
20167
20515
|
: "problem" in data.revision
|
|
20168
|
-
? `<div class="card"><p><strong>
|
|
20516
|
+
? `<div class="card"><p><strong>Revision brief</strong></p><p class="meta">${escape(data.revision.problem)}</p></div>`
|
|
20169
20517
|
: [
|
|
20170
20518
|
`<div class="revision-card" data-revision-feedback>`,
|
|
20171
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>`,
|
|
@@ -20193,7 +20541,7 @@ function taskBodyParts(data) {
|
|
|
20193
20541
|
: scope.profileState === "unresolved"
|
|
20194
20542
|
? `<div class="card approve-form" id="approve"><p><strong>This task is waiting on you: its scope cannot be approved yet.</strong></p>` +
|
|
20195
20543
|
profileWords(scope) +
|
|
20196
|
-
`<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>`
|
|
20197
20545
|
: !consentDoor.open
|
|
20198
20546
|
? consentClosedHtml(task.id, consentDoor, "task")
|
|
20199
20547
|
: approvalSheetHtml({
|
|
@@ -20216,6 +20564,11 @@ function taskBodyParts(data) {
|
|
|
20216
20564
|
editHref: data.csrf !== "" && data.planSha != null && data.planDocument !== null ? "#plan-edit" : "#scope",
|
|
20217
20565
|
notNowHref: "/work",
|
|
20218
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 },
|
|
20219
20572
|
});
|
|
20220
20573
|
// The attended road (Phase 2E): beside the approval, never replacing it.
|
|
20221
20574
|
// The mint button leads to the CONFIRM screen where every term renders
|
|
@@ -20228,12 +20581,12 @@ function taskBodyParts(data) {
|
|
|
20228
20581
|
: attended.open !== null
|
|
20229
20582
|
? [
|
|
20230
20583
|
`<div class="card" data-attended="${escape(attended.open.id)}">`,
|
|
20231
|
-
`<p><strong>
|
|
20584
|
+
`<p><strong>Attended session</strong> <span class="meta">${escape(attended.open.state)}</span></p>`,
|
|
20232
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>`,
|
|
20233
|
-
`<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>`,
|
|
20234
20587
|
`<form method="post" action="${taskHref(task.id)}/attend-revoke" class="inline">`,
|
|
20235
20588
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
|
|
20236
|
-
`<button type="submit">
|
|
20589
|
+
`<button type="submit">Revoke — stop the session</button>`,
|
|
20237
20590
|
`</form>`,
|
|
20238
20591
|
`</div>`,
|
|
20239
20592
|
].join("\n")
|
|
@@ -20241,36 +20594,36 @@ function taskBodyParts(data) {
|
|
|
20241
20594
|
? [
|
|
20242
20595
|
`<form method="post" action="${taskHref(task.id)}/attend-preview" class="card">`,
|
|
20243
20596
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
|
|
20244
|
-
`<p><strong>
|
|
20245
|
-
`<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>`,
|
|
20246
20599
|
attended.mint === undefined || attended.mint.models.length <= 1
|
|
20247
20600
|
? ""
|
|
20248
|
-
: `<label>
|
|
20601
|
+
: `<label>Model <span class="meta">(watched sessions run claude only)</span><select name="model">` +
|
|
20249
20602
|
attended.mint.models
|
|
20250
20603
|
.map(model => `<option value="${escape(model)}"${model === attended.mint?.pinnedModel ? " selected" : ""}>${escape(model)}</option>`)
|
|
20251
20604
|
.join("") +
|
|
20252
20605
|
`</select></label>`,
|
|
20253
20606
|
attended.mint === undefined
|
|
20254
20607
|
? ""
|
|
20255
|
-
: `<label>
|
|
20608
|
+
: `<label>Permissions<select name="posture">` +
|
|
20256
20609
|
`<option value="auto"${attended.mint.posture === "auto" ? " selected" : ""}>safe unattended — routine project commands and edits proceed</option>` +
|
|
20257
20610
|
`<option value="acceptEdits"${attended.mint.posture === "acceptEdits" ? " selected" : ""}>legacy acceptEdits — commands that ask are denied unattended</option>` +
|
|
20258
20611
|
`<option value="bypassPermissions"${attended.mint.posture === "bypassPermissions" ? " selected" : ""}>full permissions — nothing asks</option>` +
|
|
20259
20612
|
`</select></label>`,
|
|
20260
|
-
`<button type="submit">
|
|
20613
|
+
`<button type="submit">Read the terms</button>`,
|
|
20261
20614
|
`</form>`,
|
|
20262
20615
|
].join("\n")
|
|
20263
20616
|
: "";
|
|
20264
20617
|
const scopeForm = [
|
|
20265
|
-
`<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>`,
|
|
20266
20619
|
`<form method="post" action="${taskHref(task.id)}/scope" class="scope-editor">`,
|
|
20267
|
-
|
|
20620
|
+
scopeDraft === undefined || data.problem === null ? "" : `<div class="problem" role="alert" id="scope-error">${escape(data.problem)}</div>`,
|
|
20268
20621
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
|
|
20269
|
-
`<input type="hidden" name="sawDigest" value="${escape(
|
|
20270
|
-
`<label>
|
|
20271
|
-
`<label>
|
|
20272
|
-
`<label>
|
|
20273
|
-
`<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>`,
|
|
20274
20627
|
(() => {
|
|
20275
20628
|
const defaults = data.spendDefaults ?? null;
|
|
20276
20629
|
const budgetPrefill = scope?.budgetMicrousd != null
|
|
@@ -20278,8 +20631,8 @@ function taskBodyParts(data) {
|
|
|
20278
20631
|
: defaults?.buildPerRunMicrousd != null
|
|
20279
20632
|
? (defaults.buildPerRunMicrousd / 1_000_000).toFixed(2)
|
|
20280
20633
|
: "";
|
|
20281
|
-
return `<label>
|
|
20282
|
-
`<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>` +
|
|
20283
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>`;
|
|
20284
20637
|
})(),
|
|
20285
20638
|
(() => {
|
|
@@ -20290,14 +20643,14 @@ function taskBodyParts(data) {
|
|
|
20290
20643
|
: scope?.profile?.provider === "codex" || scope?.profile?.provider === "openrouter"
|
|
20291
20644
|
? scope.profile.sandboxMode === "danger-full-access" ? "bypassPermissions" : "auto"
|
|
20292
20645
|
: null;
|
|
20293
|
-
const selected =
|
|
20294
|
-
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>` +
|
|
20295
20648
|
permissionModeChoices("permission-mode", selected) +
|
|
20296
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>`;
|
|
20297
20650
|
})(),
|
|
20298
20651
|
(() => {
|
|
20299
|
-
const selected =
|
|
20300
|
-
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>` +
|
|
20301
20654
|
qualityModeChoices("quality-mode", selected) +
|
|
20302
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>`;
|
|
20303
20656
|
})(),
|
|
@@ -20308,8 +20661,8 @@ function taskBodyParts(data) {
|
|
|
20308
20661
|
// approval above restates and covers both.
|
|
20309
20662
|
const defaults = data.spendDefaults ?? null;
|
|
20310
20663
|
const terms = data.raceTerms ?? null;
|
|
20311
|
-
const selectedCount =
|
|
20312
|
-
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";
|
|
20313
20666
|
const perPrefill = terms !== null
|
|
20314
20667
|
? (terms.perAgentBudgetMicrousd / 1_000_000).toFixed(2)
|
|
20315
20668
|
: defaults?.racePerAgentMicrousd != null
|
|
@@ -20321,31 +20674,31 @@ function taskBodyParts(data) {
|
|
|
20321
20674
|
? (defaults.raceTotalMicrousd / 1_000_000).toFixed(2)
|
|
20322
20675
|
: "";
|
|
20323
20676
|
return [
|
|
20324
|
-
`<details class="more-agents"${selectedCount > 0 ? " open" : ""}><summary>
|
|
20325
|
-
`<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>` +
|
|
20326
20679
|
`<select name="race-count">` +
|
|
20327
20680
|
`<option value=""${selectedCount === 0 ? " selected" : ""}>one agent — no tournament</option>` +
|
|
20328
20681
|
[2, 3, 4].map(count => `<option value="${count}"${selectedCount === count ? " selected" : ""}>${count} agents</option>`).join("") +
|
|
20329
20682
|
`</select></label>`,
|
|
20330
|
-
`<label>
|
|
20683
|
+
`<label>Competing model <select name="race-model">` +
|
|
20331
20684
|
PRICED_BUILD_MODELS.map(model => `<option value="${escape(model)}"${model === selectedModel ? " selected" : ""}>${escape(model)}</option>`).join("") +
|
|
20332
20685
|
`</select></label>`,
|
|
20333
|
-
`<label>
|
|
20334
|
-
`<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>`,
|
|
20335
20688
|
// The comparison lanes (Phase 3 slice B): 2-4 rows, any registered
|
|
20336
20689
|
// provider, exact model required — no dollar fields exist. Blank
|
|
20337
20690
|
// rows are unused; filling any row files a comparison INSTEAD of a
|
|
20338
20691
|
// tournament, and mixing the two refuses in words.
|
|
20339
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>`,
|
|
20340
|
-
...[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}">` +
|
|
20341
20694
|
`<option value="">—</option>` +
|
|
20342
|
-
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("") +
|
|
20343
20696
|
`</select></label>` +
|
|
20344
|
-
`<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>`),
|
|
20345
20698
|
`</details>`,
|
|
20346
20699
|
].join("\n");
|
|
20347
20700
|
})(),
|
|
20348
|
-
`<button type="submit">
|
|
20701
|
+
`<button type="submit">Save scope</button>`,
|
|
20349
20702
|
`</form></details>`,
|
|
20350
20703
|
].join("\n");
|
|
20351
20704
|
// 41_237 → "41k": token counts read at a glance; exactness lives on the run page.
|
|
@@ -20367,7 +20720,7 @@ function taskBodyParts(data) {
|
|
|
20367
20720
|
// duration · tokens · measured-or-unmeasured cost.
|
|
20368
20721
|
const runs = data.runs.length === 0
|
|
20369
20722
|
? ""
|
|
20370
|
-
: `<h2>
|
|
20723
|
+
: `<h2>Attempts</h2>` +
|
|
20371
20724
|
data.runs
|
|
20372
20725
|
.map(run => {
|
|
20373
20726
|
const bits = [
|
|
@@ -20560,20 +20913,20 @@ function taskBodyParts(data) {
|
|
|
20560
20913
|
})();
|
|
20561
20914
|
const decisions = data.decisions.length === 0
|
|
20562
20915
|
? ""
|
|
20563
|
-
: `<h2>
|
|
20916
|
+
: `<h2>Decisions</h2>` +
|
|
20564
20917
|
data.decisions
|
|
20565
20918
|
.map(decision => `<p class="row"><a href="/d/${decision.id}">${escape(decision.question)}</a> ` +
|
|
20566
20919
|
`<span class="meta">${escape(decision.state)}${isOverdue(decision, data.now) ? " · overdue" : ""}</span></p>`)
|
|
20567
20920
|
.join("\n");
|
|
20568
20921
|
const incidents = data.incidents.length === 0
|
|
20569
20922
|
? ""
|
|
20570
|
-
: `<h2>
|
|
20923
|
+
: `<h2>Incidents</h2>` +
|
|
20571
20924
|
data.incidents
|
|
20572
20925
|
.map(one => one.resolvedAt === null
|
|
20573
20926
|
? `<p class="row">${escape(incidentWords(one.kind))} ` +
|
|
20574
20927
|
`<form method="post" action="/i/${one.id}/resolve" class="inline">` +
|
|
20575
20928
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
|
|
20576
|
-
`<button type="submit">
|
|
20929
|
+
`<button type="submit">Resolve</button></form></p>`
|
|
20577
20930
|
: `<p class="row meta">${escape(incidentWords(one.kind))} — resolved by ${escape(one.resolvedBy ?? "?")}</p>`)
|
|
20578
20931
|
.join("\n");
|
|
20579
20932
|
const stalled = task.state === "failed" || data.incidents.some(one => one.resolvedAt === null);
|
|
@@ -20585,11 +20938,11 @@ function taskBodyParts(data) {
|
|
|
20585
20938
|
const waitsFor = data.waitsFor ?? [];
|
|
20586
20939
|
const candidates = (data.waitCandidates ?? []).filter(one => !waitsFor.some(existing => existing.id === one.id));
|
|
20587
20940
|
const waitRows = waitsFor
|
|
20588
|
-
.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>`}` +
|
|
20589
20942
|
`<form method="post" action="${taskHref(task.id)}/unblock" class="inline">` +
|
|
20590
20943
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
|
|
20591
20944
|
`<input type="hidden" name="on" value="${escape(one.id)}">` +
|
|
20592
|
-
`<button type="submit">
|
|
20945
|
+
`<button type="submit">Don't wait for this</button></form></p>`)
|
|
20593
20946
|
.join("\n");
|
|
20594
20947
|
const waitAdd = data.csrf === "" || candidates.length === 0
|
|
20595
20948
|
? ""
|
|
@@ -20598,11 +20951,11 @@ function taskBodyParts(data) {
|
|
|
20598
20951
|
`<select name="on" aria-label="task to wait for">` +
|
|
20599
20952
|
candidates.map(one => `<option value="${escape(one.id)}">${escape(one.id)} — ${escape(one.title)}</option>`).join("") +
|
|
20600
20953
|
`</select>` +
|
|
20601
|
-
`<button type="submit">
|
|
20954
|
+
`<button type="submit">Wait for this task</button>` +
|
|
20602
20955
|
`<span class="meta"> — this task starts only after it finishes</span></form>`;
|
|
20603
20956
|
const waitsForCard = waitRows === "" && waitAdd === ""
|
|
20604
20957
|
? ""
|
|
20605
|
-
: `<h2>
|
|
20958
|
+
: `<h2>Waits for</h2>${waitRows === "" ? `<p class="meta">Nothing — it starts when a worker is free</p>` : waitRows}${waitAdd}`;
|
|
20606
20959
|
// The acts bar (task page pass): every verb in one row under the title,
|
|
20607
20960
|
// the one that resolves this task's state first and primary — retry on a
|
|
20608
20961
|
// stalled task, build-next in the queue, plan-first with no scope. The
|
|
@@ -20664,9 +21017,10 @@ function taskBodyParts(data) {
|
|
|
20664
21017
|
const section = (title, html, open, count, id = title.replace(/\s+/g, "-")) => {
|
|
20665
21018
|
if (html === "")
|
|
20666
21019
|
return "";
|
|
20667
|
-
const
|
|
20668
|
-
|
|
20669
|
-
|
|
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>`;
|
|
20670
21024
|
};
|
|
20671
21025
|
const receiptLeads = data.assignment == null && task.state === "done" && data.completion?.receipt != null;
|
|
20672
21026
|
// Exact identity stays available in Task options; failures stay in the
|
|
@@ -20678,7 +21032,7 @@ function taskBodyParts(data) {
|
|
|
20678
21032
|
? ` · filed by <span class="mono">${escape(data.coordinator.label)}</span>${data.coordinator.filedAgo === null ? "" : ` ${escape(data.coordinator.filedAgo)}`}`
|
|
20679
21033
|
: data.filedVia === null || data.filedVia === undefined
|
|
20680
21034
|
? ""
|
|
20681
|
-
: ` · 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>`;
|
|
20682
21036
|
// A pull request that can be merged (or opened) owns the one primary action; the result stays in its section.
|
|
20683
21037
|
const pullRequestActs = data.csrf !== "" && (data.pullRequest?.view?.canMerge === true || (data.pullRequest?.view == null && data.pullRequest?.offer != null));
|
|
20684
21038
|
const pullRequestFact = data.pullRequest?.fact != null && data.pullRequest.view?.runId === data.assignment?.receipt?.runId ? data.pullRequest.fact : undefined;
|
|
@@ -20719,12 +21073,12 @@ function taskBodyParts(data) {
|
|
|
20719
21073
|
: "gone from the tracker";
|
|
20720
21074
|
const reopenable = mirror.remoteState === "open" && !mirror.dispatchOk && mirror.closeGeneration !== null &&
|
|
20721
21075
|
mirror.syncGeneration > mirror.closeGeneration && ["cancelled", "failed", "queued"].includes(task.state) && data.csrf !== "";
|
|
20722
|
-
return (`<div class="card"><p><strong>
|
|
21076
|
+
return (`<div class="card"><p><strong>External work</strong> <span class="meta">${link} · ${escape(state)}</span></p>` +
|
|
20723
21077
|
(reopenable
|
|
20724
21078
|
? `<form method="post" action="${taskHref(task.id)}/reopen" class="row">` +
|
|
20725
21079
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
|
|
20726
21080
|
`<input type="password" name="token" placeholder="your password" aria-label="your password" autocomplete="current-password">` +
|
|
20727
|
-
`<button type="submit">
|
|
21081
|
+
`<button type="submit">Reopen — the approved scope stands</button></form>`
|
|
20728
21082
|
: "") +
|
|
20729
21083
|
`</div>`);
|
|
20730
21084
|
})();
|
|
@@ -20738,7 +21092,7 @@ function taskBodyParts(data) {
|
|
|
20738
21092
|
// is as fenced as the builder on a coordinator filing, so "plan
|
|
20739
21093
|
// first" would recommend a road that refuses.
|
|
20740
21094
|
? `<div class="card"><p><strong>This task is waiting on you: an agent filed it, and it has no scope.</strong></p>` +
|
|
20741
|
-
`<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>`
|
|
20742
21096
|
: `<div class="card task-scope-needed"><p><strong>No approved scope yet</strong></p>` +
|
|
20743
21097
|
`<p class="meta"><strong>Plan first</strong> drafts it from the repository, or <a href="#scope">write it yourself</a>.</p></div>`
|
|
20744
21098
|
: "";
|
|
@@ -20773,7 +21127,7 @@ function taskBodyParts(data) {
|
|
|
20773
21127
|
attemptPanel,
|
|
20774
21128
|
data.coordinatorProposals == null || data.coordinatorProposals.rows.length === 0
|
|
20775
21129
|
? ""
|
|
20776
|
-
: section("proposals", `<h2>
|
|
21130
|
+
: section("proposals", `<h2>Proposed by coordinators</h2>` +
|
|
20777
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),
|
|
20778
21132
|
section("decisions", decisions, true, data.decisions.length),
|
|
20779
21133
|
section("incidents", incidents, true, data.incidents.length),
|
|
@@ -20785,13 +21139,13 @@ function taskBodyParts(data) {
|
|
|
20785
21139
|
? ` · CI ${data.publication.lastCheckState} at last observation`
|
|
20786
21140
|
: " · no checks observed"}</span></p>`,
|
|
20787
21141
|
section("report", reportCard, true),
|
|
20788
|
-
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"),
|
|
20789
21143
|
section("usage", spendCard, false),
|
|
20790
21144
|
section("steering", steeringCard, (data.steering ?? []).length > 0, (data.steering ?? []).length),
|
|
20791
21145
|
section("scope",
|
|
20792
21146
|
// The recipe road rides with the scope it reuses (UI polish
|
|
20793
21147
|
// 2026-09-13), off the title-to-action path.
|
|
20794
|
-
["<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))),
|
|
20795
21149
|
dependencyChoiceNeeded ? "" : section("waits for", waitsForCard, (data.waitsFor ?? []).length > 0, (data.waitsFor ?? []).length),
|
|
20796
21150
|
section("holds", holds, true, data.holds.length),
|
|
20797
21151
|
cancelAct,
|
|
@@ -20811,7 +21165,19 @@ function taskBodyParts(data) {
|
|
|
20811
21165
|
thread.push({ key: "filed", at: rootFiled, kind: "filed", who: "person", author: filedBy ?? "", title: "Filed the task",
|
|
20812
21166
|
text: (() => { const goal = data.family?.root.goal ?? scope?.goal ?? null; return goal === null ? null : oneLineOf(goal, 280); })(), link: null, html: "", more: null });
|
|
20813
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));
|
|
20814
21178
|
for (const run of familyRuns) {
|
|
21179
|
+
if (foldedAttempts.has(run.id))
|
|
21180
|
+
continue;
|
|
20815
21181
|
const live = run.id === liveHistoryRunId || (run.outcome === null && run.id === liveRunId);
|
|
20816
21182
|
const noun = runNoun(run);
|
|
20817
21183
|
const href = { label: `${noun[0].toUpperCase()}${noun.slice(1)} #${run.id}`, href: `/r/${run.id}` };
|
|
@@ -20837,7 +21203,7 @@ function taskBodyParts(data) {
|
|
|
20837
21203
|
thread.push({ key: `run-${run.id}`, at: run.finishedAt ?? run.startedAt, kind: "result", who: "agent", author: run.runner,
|
|
20838
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),
|
|
20839
21205
|
link: href, html: "",
|
|
20840
|
-
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 });
|
|
20841
21207
|
}
|
|
20842
21208
|
// What the agent is working through now, and how its plan changed.
|
|
20843
21209
|
const liveAt = liveRun?.startedAt ?? task.updatedAt;
|
|
@@ -20853,7 +21219,7 @@ function taskBodyParts(data) {
|
|
|
20853
21219
|
const feedback = data.revision;
|
|
20854
21220
|
thread.push({ key: "revision", at: task.createdAt, kind: "reply", who: "person", author: feedback.comments[0]?.author ?? "You",
|
|
20855
21221
|
title: feedback.kind === "ci-repair" ? "Asked for a CI repair" : feedback.kind === "criterion-repair" ? "Asked for a repair" : "Asked for changes",
|
|
20856
|
-
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,
|
|
20857
21223
|
link: { label: `From build #${feedback.sourceRun}`, href: `/r/${feedback.sourceRun}` }, html: "", more: null });
|
|
20858
21224
|
}
|
|
20859
21225
|
for (const decision of data.decisions) {
|
|
@@ -20930,9 +21296,6 @@ function taskBodyParts(data) {
|
|
|
20930
21296
|
// needs a person, then facts, then folds; the mechanics under Manage.
|
|
20931
21297
|
const MANAGE = new Set(["steering", "waits-for", "holds"]);
|
|
20932
21298
|
const finished = task.state === "done" || task.state === "cancelled";
|
|
20933
|
-
// Failed: the card says what went wrong in one line, and its one act is Retry itself (the same requeue), never a link to
|
|
20934
|
-
// this page; someone who can't retry here (a viewer, a live claim) gets no act at all.
|
|
20935
|
-
const statusCard = data.assignment != null && assignmentOptions !== null ? assignmentCardOf(data.assignment, assignmentOptions) : null;
|
|
20936
21299
|
const failedCard = statusCard?.status.headline === "Failed";
|
|
20937
21300
|
// A link to this very page, with no section to open, goes nowhere: a card never offers one.
|
|
20938
21301
|
const bareSelfLink = (href) => {
|
|
@@ -20945,7 +21308,33 @@ function taskBodyParts(data) {
|
|
|
20945
21308
|
// Build again, in place, names its form after the card's own act.
|
|
20946
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"
|
|
20947
21310
|
? { action: `${taskHref(data.assignment.rootId)}/requeue` } : null;
|
|
20948
|
-
const retry = failedCard && task.state === "failed" && !data.claimed && !stopControlsActive && data.csrf !== ""
|
|
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
|
+
};
|
|
20949
21338
|
const view = {
|
|
20950
21339
|
kind: "task",
|
|
20951
21340
|
id: task.id,
|
|
@@ -20957,18 +21346,24 @@ function taskBodyParts(data) {
|
|
|
20957
21346
|
{ label: "Ask", href: taskChatHref(data.rootId ?? task.id), active: false },
|
|
20958
21347
|
],
|
|
20959
21348
|
version: data.versionLabel == null ? null : { label: data.versionLabel, current: { label: "Current work", href: taskHref(data.rootId ?? task.id) } },
|
|
20960
|
-
status: statusCard === null ? null : failedCard ? { ...statusCard, action: null, status: { ...statusCard.status, sentence: data.failure?.line ?? statusCard.status.sentence } }
|
|
20961
|
-
:
|
|
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),
|
|
20962
21352
|
statusHtml,
|
|
20963
|
-
failure: failedCard
|
|
21353
|
+
failure: failedCard && data.failure != null ? { line: data.failure.line, evidence: data.failure.evidence, suggestion: data.failure.suggestion, link: data.failure.link } : null,
|
|
20964
21354
|
retry,
|
|
21355
|
+
runChecks,
|
|
21356
|
+
progress,
|
|
21357
|
+
stop,
|
|
21358
|
+
record,
|
|
21359
|
+
earlier,
|
|
20965
21360
|
approval: approvalHtml,
|
|
20966
21361
|
confirmStopped: data.csrf !== "" && data.assignment?.primaryAction?.code === "confirm-stopped" && data.assignment.primaryAction.target.runId !== null
|
|
20967
21362
|
? { action: `${taskHref(data.assignment.rootId)}/confirm-stopped`, run: data.assignment.primaryAction.target.runId, checked: needsCheck(data.assignment) } : null,
|
|
20968
21363
|
rebuild,
|
|
20969
21364
|
// The plan, progress and plan changes are thread entries now; the rest still needs a person here.
|
|
20970
21365
|
lead: [
|
|
20971
|
-
{ 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 },
|
|
20972
21367
|
{ key: "pull-request", html: pullRequestCard },
|
|
20973
21368
|
{ key: "needs-scope", html: needsScopeCard },
|
|
20974
21369
|
{ key: "mirror", html: mirrorCard }, { key: "contest", html: contestCard }, { key: "attempt", html: attemptPanel },
|
|
@@ -20979,7 +21374,7 @@ function taskBodyParts(data) {
|
|
|
20979
21374
|
sections: [
|
|
20980
21375
|
...(previousResult === null || thread.some(one => one.more !== null) ? [] : [{ id: "result", title: previousResult.title, html: previousResult.html, open: false, count: null }]),
|
|
20981
21376
|
// Questions are in the thread; the scope and the ledgers fold here until asked.
|
|
20982
|
-
...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),
|
|
20983
21378
|
],
|
|
20984
21379
|
manage: [
|
|
20985
21380
|
...sectionParts.filter(one => MANAGE.has(one.id)),
|
|
@@ -21425,26 +21820,27 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
|
|
|
21425
21820
|
const intent = view.intent;
|
|
21426
21821
|
let intentView = null;
|
|
21427
21822
|
if (intent === null) {
|
|
21428
|
-
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>`);
|
|
21429
21824
|
}
|
|
21430
21825
|
else {
|
|
21826
|
+
// The time itself goes to the page as a stamp: the one formatter words it in the viewer's zone.
|
|
21431
21827
|
const approvalWords = intent.approval.approved
|
|
21432
|
-
? `
|
|
21828
|
+
? `Approved by ${intent.approvedBy ?? "an operator"}`
|
|
21433
21829
|
: intent.approval.reason === "changed"
|
|
21434
|
-
? "
|
|
21435
|
-
: "
|
|
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.";
|
|
21436
21832
|
const approval = intent.approval.approved
|
|
21437
|
-
? `
|
|
21438
|
-
: approvalWords;
|
|
21439
|
-
const body = `<p class="recap" style="margin-top:.25rem"><strong>
|
|
21440
|
-
(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>`) +
|
|
21441
21837
|
(intent.touches.length === 0
|
|
21442
|
-
? `<p class="meta">
|
|
21443
|
-
: `<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>`) +
|
|
21444
21840
|
(view.plan === null
|
|
21445
21841
|
? ""
|
|
21446
|
-
: `<p class="meta">
|
|
21447
|
-
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 };
|
|
21448
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>` +
|
|
21449
21845
|
`<div class="cockpit-disclosure-body">` + body + `</div></details>`);
|
|
21450
21846
|
}
|
|
@@ -21466,7 +21862,7 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
|
|
|
21466
21862
|
] },
|
|
21467
21863
|
};
|
|
21468
21864
|
if (run === null || view.detail === null) {
|
|
21469
|
-
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>` +
|
|
21470
21866
|
`<p class="meta">This task has no finished build record, so there are no captured changes or checks to review.</p>` +
|
|
21471
21867
|
`<p class="row"><a href="${taskHref(view.taskId)}">Open the task →</a></p></section>`);
|
|
21472
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." } };
|
|
@@ -21499,24 +21895,31 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
|
|
|
21499
21895
|
});
|
|
21500
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>`;
|
|
21501
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;
|
|
21502
21904
|
const complete = assignment?.state === "ready-to-check" && assignment.receipt !== null && canRetryReview && csrf !== ""
|
|
21503
|
-
? { 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;
|
|
21504
21906
|
if (complete !== null)
|
|
21505
|
-
parts.push(completionForm(view.taskId, run.id, complete.receipt, csrf, view.detail?.pullRequestTo ?? null));
|
|
21506
|
-
// The one decision, after the evidence: Accept only when every requirement is met and the checks passed.
|
|
21507
|
-
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;
|
|
21508
21910
|
const matrix = proof === null || proof.proofProblem !== null ? [] : proof.matrix;
|
|
21509
|
-
const
|
|
21510
|
-
const
|
|
21511
|
-
|
|
21512
|
-
|
|
21513
|
-
|
|
21514
|
-
|
|
21515
|
-
|
|
21516
|
-
|
|
21517
|
-
|
|
21518
|
-
|
|
21519
|
-
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 } };
|
|
21520
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.
|
|
21521
21924
|
const followUps = view.detail.followUps ?? null;
|
|
21522
21925
|
const checksRunning = checks?.running != null || (followUps?.checks.some(one => one.state === "waiting" || one.state === "running") ?? false);
|
|
@@ -21526,18 +21929,27 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
|
|
|
21526
21929
|
? { action: `/r/${run.id}/checks`, level: followUps.full ? "full" : "quick", returnTo: `${here}&run=${run.id}&tab=checks` } : null;
|
|
21527
21930
|
const need = panel.panel.need;
|
|
21528
21931
|
const nextKind = selected.next?.kind;
|
|
21529
|
-
|
|
21530
|
-
|
|
21531
|
-
|
|
21532
|
-
|
|
21533
|
-
|
|
21534
|
-
|
|
21535
|
-
|
|
21536
|
-
|
|
21537
|
-
|
|
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);
|
|
21538
21950
|
// A refuted result's card lists every recorded disagreement, each tied to its lines; when the report itself doesn't
|
|
21539
21951
|
// match the changes (not a failed check), that is the headline.
|
|
21540
|
-
const mismatch = proof === null || accepted || proof.verdict !== "refuted" ? null : (() => {
|
|
21952
|
+
const mismatch = failure !== null || proof === null || accepted || proof.verdict !== "refuted" ? null : (() => {
|
|
21541
21953
|
const patch = view.detail?.terminal?.patch ?? null;
|
|
21542
21954
|
const files = patch === null || "problem" in patch ? [] : parseReviewDiff(patch.text).files;
|
|
21543
21955
|
// Each file's first change: the lines it added (else every line it shows), and where that change starts (its link target).
|
|
@@ -21570,25 +21982,54 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
|
|
|
21570
21982
|
};
|
|
21571
21983
|
})();
|
|
21572
21984
|
if (view.notes.length > 0) {
|
|
21573
|
-
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>` +
|
|
21574
21986
|
view.notes.map(one => `<p class="row"><span class="meta">${escape(one.author)} · ${whenTime(one.createdAt)}</span> ${escape(one.note)}</p>`).join("\n") +
|
|
21575
21987
|
`</section>`);
|
|
21576
21988
|
}
|
|
21577
21989
|
return {
|
|
21578
21990
|
html: `<section class="cockpit-detail">${parts.join("\n")}</section>`,
|
|
21579
21991
|
selected: {
|
|
21580
|
-
...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" } }),
|
|
21581
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}` },
|
|
21582
22011
|
},
|
|
21583
22012
|
};
|
|
21584
22013
|
}
|
|
21585
|
-
|
|
21586
|
-
|
|
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)}">`);
|
|
21587
22028
|
if (pullRequestTo !== null) {
|
|
21588
|
-
// With pull requests set up: the PR is the primary road, a bare
|
|
21589
|
-
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>`;
|
|
21590
22031
|
}
|
|
21591
|
-
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>`;
|
|
21592
22033
|
}
|
|
21593
22034
|
/** The pull request, in one card: its state and link, one line of detail, and the one action it needs — Merge
|
|
21594
22035
|
* behind the password when checks passed, or Open a pull request for a result completed without one. */
|
|
@@ -21726,13 +22167,13 @@ function shortDigest(digest) {
|
|
|
21726
22167
|
* orphaned run keeps saying what actually became of it. */
|
|
21727
22168
|
function runOutcomeBadge(run, live) {
|
|
21728
22169
|
if (!live && run.role === "planner" && run.reason === "plan-drafted") {
|
|
21729
|
-
return `<span class="badge">
|
|
22170
|
+
return `<span class="badge">Planned</span>`;
|
|
21730
22171
|
}
|
|
21731
22172
|
if (!live && run.reason === "interrupted")
|
|
21732
|
-
return `<span class="badge">
|
|
22173
|
+
return `<span class="badge">Interrupted</span>`;
|
|
21733
22174
|
return live
|
|
21734
|
-
? `<span class="badge badge-running">
|
|
21735
|
-
: `<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>`;
|
|
21736
22177
|
}
|
|
21737
22178
|
const runNoun = (run) => run.role === "planner" ? "plan" : run.role === "reviewer" ? "review" : run.role === "scout" ? "report" : "build";
|
|
21738
22179
|
function runsPage(chrome, rows, liveIds, nextCursor, verdicts = new Map(), accepted = new Set()) {
|
|
@@ -21747,7 +22188,7 @@ function runsPage(chrome, rows, liveIds, nextCursor, verdicts = new Map(), accep
|
|
|
21747
22188
|
return (`<p class="row"><a href="/r/${run.id}" class="mono">#${run.id}</a> ` +
|
|
21748
22189
|
`<a href="${taskHref(run.taskId)}" class="mono">${escape(run.taskId)}</a> ` +
|
|
21749
22190
|
runOutcomeBadge(run, liveIds.has(run.id)) +
|
|
21750
|
-
`${run.qualityMode === "strict" ? ` <span class="badge">
|
|
22191
|
+
`${run.qualityMode === "strict" ? ` <span class="badge">Strict review</span>` : ""}` +
|
|
21751
22192
|
needsVerification +
|
|
21752
22193
|
criterionMatrixSummary(verdicts.get(run.id)?.matrix ?? []) +
|
|
21753
22194
|
`${run.provider === "claude" ? "" : ` <span class="meta mono">${escape(run.provider)}</span>`}` +
|
|
@@ -21756,7 +22197,7 @@ function runsPage(chrome, rows, liveIds, nextCursor, verdicts = new Map(), accep
|
|
|
21756
22197
|
})
|
|
21757
22198
|
.join("\n");
|
|
21758
22199
|
const older = nextCursor === null ? "" : `<p><a href="/runs?before=${nextCursor}">older →</a></p>`;
|
|
21759
|
-
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 });
|
|
21760
22201
|
}
|
|
21761
22202
|
function structuredHandoffView(artifacts, root) {
|
|
21762
22203
|
const artifact = [...artifacts].reverse().find(one => one.kind === "handoff");
|
|
@@ -22111,7 +22552,7 @@ function reviewDiffHtml(patch, stat, runId, commentable, anchors = new Map()) {
|
|
|
22111
22552
|
const parsed = parseReviewDiff(patch.text);
|
|
22112
22553
|
const structured = parsed.files.some(file => file.hunks.length > 0);
|
|
22113
22554
|
if (!structured) {
|
|
22114
|
-
return (`<details><summary>
|
|
22555
|
+
return (`<details><summary>The patch${patch.truncated ? " (TRUNCATED — the raw record says how much was cut)" : ""}</summary>` +
|
|
22115
22556
|
`<pre class="mono" style="overflow-x:auto">${escape(patch.text)}</pre></details>` +
|
|
22116
22557
|
storedDownloadLink(runId, patch.artifactId, "diff", patch.truncated));
|
|
22117
22558
|
}
|
|
@@ -22185,12 +22626,12 @@ function storedDownloadLink(runId, artifactId, what, truncated) {
|
|
|
22185
22626
|
* page's prefill script reads — a real button, keyboard-reachable, separate
|
|
22186
22627
|
* from the link so the two never fight over one click (finding 4). */
|
|
22187
22628
|
function terminalDiffCard(view, runId, editor = null, commentable = false) {
|
|
22188
|
-
const parts = ["<h2>
|
|
22629
|
+
const parts = ["<h2>What changed</h2>"];
|
|
22189
22630
|
if (view.stat === null) {
|
|
22190
|
-
parts.push(`<p class="meta">
|
|
22631
|
+
parts.push(`<p class="meta">No change summary was captured for this build</p>`);
|
|
22191
22632
|
}
|
|
22192
22633
|
else if ("problem" in view.stat) {
|
|
22193
|
-
parts.push(`<p class="meta">
|
|
22634
|
+
parts.push(`<p class="meta">Stat: ${escape(view.stat.problem)}</p>`);
|
|
22194
22635
|
}
|
|
22195
22636
|
else {
|
|
22196
22637
|
const s = view.stat;
|
|
@@ -22212,23 +22653,23 @@ function terminalDiffCard(view, runId, editor = null, commentable = false) {
|
|
|
22212
22653
|
.slice(0, 40)
|
|
22213
22654
|
.map(file => `<p class="row mono">${fileName(file.path)}${file.renamedFrom === undefined ? "" : ` (was ${escape(file.renamedFrom)})`} ` +
|
|
22214
22655
|
`<span class="meta">${file.additions === null || file.deletions === null ? "binary" : `+${file.additions} −${file.deletions}`}</span>` +
|
|
22215
|
-
`${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>`)
|
|
22216
22657
|
.join("\n") +
|
|
22217
22658
|
(s.files.length > 40 ? `<p class="meta">…and ${s.files.length - 40} more file(s)</p>` : "") +
|
|
22218
22659
|
`</div>`);
|
|
22219
22660
|
if (editor !== null) {
|
|
22220
|
-
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>`);
|
|
22221
22662
|
}
|
|
22222
22663
|
}
|
|
22223
22664
|
}
|
|
22224
22665
|
if (view.patch === null) {
|
|
22225
|
-
parts.push(`<p class="meta">
|
|
22666
|
+
parts.push(`<p class="meta">The final diff was not captured for this build</p>`);
|
|
22226
22667
|
}
|
|
22227
22668
|
else if ("problem" in view.patch) {
|
|
22228
|
-
parts.push(`<p class="meta">
|
|
22669
|
+
parts.push(`<p class="meta">Patch: ${escape(view.patch.problem)}</p>`);
|
|
22229
22670
|
}
|
|
22230
22671
|
else if (view.patch.text.trim() === "") {
|
|
22231
|
-
parts.push(`<p class="meta">
|
|
22672
|
+
parts.push(`<p class="meta">Empty diff — captured successfully, nothing changed</p>`);
|
|
22232
22673
|
}
|
|
22233
22674
|
else {
|
|
22234
22675
|
parts.push(reviewDiffHtml(view.patch, view.stat, runId, commentable));
|
|
@@ -22247,7 +22688,7 @@ function evidenceBundleCard(view, run, publication = null, review = null) {
|
|
|
22247
22688
|
if (view === null)
|
|
22248
22689
|
return "";
|
|
22249
22690
|
const runId = run.id;
|
|
22250
|
-
const parts = ["<h2>
|
|
22691
|
+
const parts = ["<h2>Verification and evidence</h2>"];
|
|
22251
22692
|
if (view.verdict !== null) {
|
|
22252
22693
|
// The same status line every other surface renders for this run
|
|
22253
22694
|
// (workspace package 1), beside the verdict's own explanation.
|
|
@@ -22257,7 +22698,7 @@ function evidenceBundleCard(view, run, publication = null, review = null) {
|
|
|
22257
22698
|
if (view.accepted !== null) {
|
|
22258
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>`);
|
|
22259
22700
|
}
|
|
22260
|
-
parts.push(criterionMatrixHtml(view.matrix, { runId, links: view.matrixLinks }));
|
|
22701
|
+
parts.push(criterionMatrixHtml(view.matrix, { runId, links: view.matrixLinks, verdict: view.verdict }));
|
|
22261
22702
|
parts.push(semanticCoverageHtml(view.matrix, view.qualityMode));
|
|
22262
22703
|
if (reviewConflict(view.matrix, view.machineVerdict, view.verdict)) {
|
|
22263
22704
|
parts.push(`<p class="meta">An independent review found conflicting evidence.</p>`);
|
|
@@ -22268,25 +22709,25 @@ function evidenceBundleCard(view, run, publication = null, review = null) {
|
|
|
22268
22709
|
}
|
|
22269
22710
|
else if (view.proof !== null) {
|
|
22270
22711
|
if (view.proof.criteria.length > 0) {
|
|
22271
|
-
parts.push(`<div class="result-section"><strong>
|
|
22712
|
+
parts.push(`<div class="result-section"><strong>Acceptance criteria</strong><ul>` +
|
|
22272
22713
|
view.proof.criteria
|
|
22273
|
-
.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>`)
|
|
22274
22715
|
.join("") +
|
|
22275
22716
|
`</ul></div>`);
|
|
22276
22717
|
}
|
|
22277
22718
|
if (view.proof.checks.length > 0) {
|
|
22278
|
-
parts.push(`<div class="result-section"><strong>
|
|
22719
|
+
parts.push(`<div class="result-section"><strong>Checks reported by the agent</strong><ul>` +
|
|
22279
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("") +
|
|
22280
22721
|
`</ul></div>`);
|
|
22281
22722
|
}
|
|
22282
22723
|
if (view.proof.caveats.length > 0) {
|
|
22283
|
-
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>`);
|
|
22284
22725
|
}
|
|
22285
22726
|
}
|
|
22286
22727
|
if (view.checkLog !== null)
|
|
22287
22728
|
parts.push(checkLogHtml(view.checkLog, runId, ""));
|
|
22288
22729
|
if (view.screenshots.length > 0) {
|
|
22289
|
-
parts.push(`<div class="result-section"><strong>
|
|
22730
|
+
parts.push(`<div class="result-section"><strong>Screenshots</strong>` +
|
|
22290
22731
|
view.screenshots
|
|
22291
22732
|
.map(shot => `<p class="row"><a href="/r/${runId}/evidence/${shot.artifactId}">` +
|
|
22292
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> ` +
|
|
@@ -22403,7 +22844,7 @@ function completionReceiptCard(view, taskId, place, standing = receiptStatusOf(v
|
|
|
22403
22844
|
(view.publication?.state === "failed" ? `<p class="receipt-note">${statusIconSvg("note")} Pull request couldn't open. The commit is safe locally.</p>` : "") +
|
|
22404
22845
|
caveats +
|
|
22405
22846
|
coverage +
|
|
22406
|
-
`<details class="receipt-details"><summary
|
|
22847
|
+
`<details class="receipt-details"><summary>${STATUS_MORE}</summary>` +
|
|
22407
22848
|
(view.summary !== null && view.summary !== conciseOutcomeOf(view.summary) ? `<p class="recap">${escape(view.summary)}</p>` : "") +
|
|
22408
22849
|
`<p class="receipt-publication" data-receipt-publication="${escape(facts.publicationState)}">${escape(facts.publicationWords)}</p>` +
|
|
22409
22850
|
// A review in flight is the receipt's primary status too (its chip
|
|
@@ -22534,9 +22975,10 @@ function resultPanelParts(detail, o) {
|
|
|
22534
22975
|
items: personCheckItems(proof, patchOk ? patch.text : null, shown, runId),
|
|
22535
22976
|
accept: acceptable && need?.accept == null ? { action: `${taskHref(detail.taskId)}/accept-proof`, run: run.id, returnTo: o.returnTo } : null,
|
|
22536
22977
|
};
|
|
22978
|
+
const youCheckForm = youCheck?.accept != null && o.finishes !== true ? youCheck.accept : null;
|
|
22537
22979
|
const youCheckHtml = youCheck === null ? "" :
|
|
22538
22980
|
`<div class="result-you-check" data-result-you-check="${youCheck.lines.length}"><ul>${youCheck.lines.map(one => `<li>${escape(one)}</li>`).join("")}</ul>` +
|
|
22539
|
-
(
|
|
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>`) +
|
|
22540
22982
|
`</div>`;
|
|
22541
22983
|
const attentionHtml = attention.length === 0
|
|
22542
22984
|
? ""
|
|
@@ -22663,9 +23105,9 @@ function resultPanelParts(detail, o) {
|
|
|
22663
23105
|
detail.files
|
|
22664
23106
|
.map(file => {
|
|
22665
23107
|
const counts = file.additions === null || file.deletions === null ? "binary" : `+${file.additions} −${file.deletions}`;
|
|
22666
|
-
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>` : "");
|
|
22667
23109
|
const why = file.priority.reasons.length === 0 ? "" : ` <span class="meta">— ${file.priority.reasons.map(escape).join("; ")}</span>`;
|
|
22668
|
-
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>`;
|
|
22669
23111
|
})
|
|
22670
23112
|
.join("") +
|
|
22671
23113
|
`</ol>` +
|
|
@@ -22706,14 +23148,14 @@ function resultPanelParts(detail, o) {
|
|
|
22706
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>`);
|
|
22707
23149
|
}
|
|
22708
23150
|
else {
|
|
22709
|
-
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 }));
|
|
22710
23152
|
}
|
|
22711
23153
|
checkParts.push(semanticCoverageHtml(proof.matrix, proof.qualityMode));
|
|
22712
23154
|
if (reviewConflict(proof.matrix, proof.machineVerdict, proof.verdict))
|
|
22713
23155
|
checkParts.push(`<p class="meta">An independent review found conflicting evidence.</p>`);
|
|
22714
23156
|
checkParts.push(repairChainHtml(proof.repairChain));
|
|
22715
23157
|
if (detail.reviewerFindings.length > 0) {
|
|
22716
|
-
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>` +
|
|
22717
23159
|
reviewerNotes +
|
|
22718
23160
|
`</ul></div>`);
|
|
22719
23161
|
}
|
|
@@ -22742,7 +23184,7 @@ function resultPanelParts(detail, o) {
|
|
|
22742
23184
|
// The handoff's own account of its checks — the agent's words, labeled
|
|
22743
23185
|
// as such, whether or not a proof exists.
|
|
22744
23186
|
if (!directAssessment && handoff !== null && handoff.verification.length > 0)
|
|
22745
|
-
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>`);
|
|
22746
23188
|
if (o.extraChecks !== undefined)
|
|
22747
23189
|
checkParts.push(o.extraChecks);
|
|
22748
23190
|
// ---- request changes: both feedback styles, one sealed road ------------
|
|
@@ -22787,8 +23229,8 @@ function resultPanelParts(detail, o) {
|
|
|
22787
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>` +
|
|
22788
23230
|
`<span class="meta diff-comment-limit" id="comment-note-limit">up to ${LIMITS.note} characters</span>` +
|
|
22789
23231
|
`<details class="result-pin"><summary>Attach to a file or line</summary>` +
|
|
22790
|
-
`<div class="diff-comment-target"><label>
|
|
22791
|
-
`<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>` +
|
|
22792
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>`);
|
|
22793
23235
|
}
|
|
22794
23236
|
// ---- the panel ---------------------------------------------------------
|
|
@@ -22830,6 +23272,8 @@ function resultPanelParts(detail, o) {
|
|
|
22830
23272
|
reviewHistory: REVIEW_TOKENS.has(status.token) ? status.detail : null,
|
|
22831
23273
|
attention: attention.filter(one => !shortenedMaterialReason(one)).map(plainReasonWords),
|
|
22832
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),
|
|
22833
23277
|
limits: attention.filter(one => shortenedMaterialReason(one)),
|
|
22834
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 })),
|
|
22835
23279
|
views: [{ key: "summary", html: summaryParts.join("\n") }, { key: "changes", html: changeParts.join("\n") }, { key: "checks", html: checkParts.join("\n") }],
|
|
@@ -23024,9 +23468,9 @@ function runPage(chrome, run, taskId, artifacts, terminal = null, notes = [], cs
|
|
|
23024
23468
|
: " · the agent is working on this";
|
|
23025
23469
|
const conversation = heldTurns === null
|
|
23026
23470
|
? ""
|
|
23027
|
-
: `<h2>
|
|
23471
|
+
: `<h2>Conversation</h2>` +
|
|
23028
23472
|
(heldTurns.turns.length === 0
|
|
23029
|
-
? `<p class="meta">
|
|
23473
|
+
? `<p class="meta">Nothing said yet</p>`
|
|
23030
23474
|
: heldTurns.turns
|
|
23031
23475
|
.map(turn => `<p class="row"><span class="meta">${turn.sourceKind === "operator"
|
|
23032
23476
|
? escape(turn.author ?? "operator")
|
|
@@ -23040,11 +23484,11 @@ function runPage(chrome, run, taskId, artifacts, terminal = null, notes = [], cs
|
|
|
23040
23484
|
? `<form method="post" action="/r/${run.id}/turn" class="row">` +
|
|
23041
23485
|
`<input type="hidden" name="csrf" value="${escape(csrf)}">` +
|
|
23042
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">` +
|
|
23043
|
-
`<button type="submit">
|
|
23487
|
+
`<button type="submit">Send</button></form>` +
|
|
23044
23488
|
`<p class="meta">${escape(heldTurns.state)} · ${heldTurns.turns.length}/${heldTurns.cap} messages · a waiting question must be answered before free-form messages</p>`
|
|
23045
23489
|
: heldTurns.open
|
|
23046
23490
|
? ""
|
|
23047
|
-
: `<p class="meta">
|
|
23491
|
+
: `<p class="meta">The session has ended — the record above is complete</p>`);
|
|
23048
23492
|
// The live peek region (A2): the poller fills it only on a serve that
|
|
23049
23493
|
// asserted its runner. Without the assertion the section still appears
|
|
23050
23494
|
// for a running build and says honestly why it is empty \u2014 a page that
|
|
@@ -23053,24 +23497,24 @@ function runPage(chrome, run, taskId, artifacts, terminal = null, notes = [], cs
|
|
|
23053
23497
|
const peek = !running
|
|
23054
23498
|
? ""
|
|
23055
23499
|
: peekable
|
|
23056
|
-
? `<h2>
|
|
23057
|
-
`<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>` +
|
|
23058
23502
|
`<p class="meta" id="run-peek-stamp"></p>`
|
|
23059
|
-
: `<h2>
|
|
23060
|
-
`<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>`;
|
|
23061
23505
|
// The live transcript (arc 1): the agent's own words, streamed to a file
|
|
23062
23506
|
// beside the run and polled as raw text. Honesty stated on the surface:
|
|
23063
23507
|
// this is display only, and the machine running the agent could alter it.
|
|
23064
23508
|
const transcript = !running
|
|
23065
23509
|
? ""
|
|
23066
23510
|
: !peekable
|
|
23067
|
-
? `<h2>
|
|
23068
|
-
`<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>`
|
|
23069
23513
|
: run.provider !== "claude"
|
|
23070
|
-
? `<h2>
|
|
23071
|
-
`<p class="meta">
|
|
23072
|
-
: `<h2>
|
|
23073
|
-
`<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>` +
|
|
23074
23518
|
`<pre id="live-transcript" class="mono" style="max-height:24rem;overflow:auto;white-space:pre-wrap"></pre>` +
|
|
23075
23519
|
`<p class="meta" id="live-transcript-state"></p>`;
|
|
23076
23520
|
const resultSummary = structuredHandoff?.conclusion ?? run.handoff;
|
|
@@ -23079,7 +23523,7 @@ function runPage(chrome, run, taskId, artifacts, terminal = null, notes = [], cs
|
|
|
23079
23523
|
: `<div class="result-section"><strong>${escape(label)}</strong><ul>${items.map(one => `<li>${escape(one)}</li>`).join("")}</ul></div>`;
|
|
23080
23524
|
const handoff = resultSummary === null
|
|
23081
23525
|
? ""
|
|
23082
|
-
: `<section class="card result-card"><h2>
|
|
23526
|
+
: `<section class="card result-card"><h2>Result</h2><p class="recap">${escape(resultSummary)}</p>` +
|
|
23083
23527
|
(structuredHandoff === null
|
|
23084
23528
|
? ""
|
|
23085
23529
|
: resultList("completed", structuredHandoff.changes) +
|
|
@@ -23115,9 +23559,9 @@ function runPage(chrome, run, taskId, artifacts, terminal = null, notes = [], cs
|
|
|
23115
23559
|
? ""
|
|
23116
23560
|
: `<form method="post" action="/r/${run.id}/comment" class="card diff-comment-form" id="comment-form">` +
|
|
23117
23561
|
`<input type="hidden" name="csrf" value="${escape(csrf)}">` +
|
|
23118
|
-
`<div class="diff-comment-target"><label>
|
|
23119
|
-
`<label>
|
|
23120
|
-
`<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>` +
|
|
23121
23565
|
`<span class="meta diff-comment-limit" id="comment-note-limit">up to ${LIMITS.note} characters</span>` +
|
|
23122
23566
|
`<button type="submit">Add annotation</button></form>`;
|
|
23123
23567
|
// The device-side half of the editor-link activation (arc 6, finding 1):
|
|
@@ -23145,7 +23589,7 @@ function runPage(chrome, run, taskId, artifacts, terminal = null, notes = [], cs
|
|
|
23145
23589
|
: `<div class="card"><p><strong>CI is failing on PR #${ciRepair.pr}</strong> <span class="meta">observed by the episode watcher</span></p>` +
|
|
23146
23590
|
`<form method="post" action="/r/${run.id}/draft-repair">` +
|
|
23147
23591
|
`<input type="hidden" name="csrf" value="${escape(csrf)}">` +
|
|
23148
|
-
`<button type="submit">
|
|
23592
|
+
`<button type="submit">Draft a repair task</button>` +
|
|
23149
23593
|
`<span class="meta"> — one unapproved task; you approve its scope before anything builds</span></form></div>`;
|
|
23150
23594
|
const reviewCard = (commentRows === "" && commentForm === "" ? "" : `<h2 id="review">Review and revise</h2>` +
|
|
23151
23595
|
`<p class="meta">Annotate the diff above. When the batch is ready, create one scoped revision task from it.</p>` +
|
|
@@ -23161,8 +23605,8 @@ function runPage(chrome, run, taskId, artifacts, terminal = null, notes = [], cs
|
|
|
23161
23605
|
: `<form method="post" action="/r/${run.id}/note" class="row">` +
|
|
23162
23606
|
`<input type="hidden" name="csrf" value="${escape(csrf)}">` +
|
|
23163
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">` +
|
|
23164
|
-
`<button type="submit">
|
|
23165
|
-
const notesCard = noteRows === "" && noteForm === "" ? "" : `<h2>
|
|
23608
|
+
`<button type="submit">Add note</button></form>`;
|
|
23609
|
+
const notesCard = noteRows === "" && noteForm === "" ? "" : `<h2>Operator notes</h2>${noteRows}${noteForm}`;
|
|
23166
23610
|
// Continuation (Phase 2E, A4): a finished attempt offers a watched
|
|
23167
23611
|
// follow-up — the text you type here enters the SIGNED terms on the
|
|
23168
23612
|
// confirm screen; nothing runs until your password agrees to exactly it.
|
|
@@ -23302,7 +23746,7 @@ function capsPage(chrome, caps, gaps, repo, now) {
|
|
|
23302
23746
|
if (caps === null) {
|
|
23303
23747
|
return screen("requirements", [
|
|
23304
23748
|
`<h1>Requirements</h1>`,
|
|
23305
|
-
`<p class="meta">
|
|
23749
|
+
`<p class="meta">Open a project to see its requirements — <a href="/projects">projects</a></p>`,
|
|
23306
23750
|
].join("\n"), { chrome });
|
|
23307
23751
|
}
|
|
23308
23752
|
const list = caps.length === 0
|
|
@@ -23312,7 +23756,7 @@ function capsPage(chrome, caps, gaps, repo, now) {
|
|
|
23312
23756
|
`${escape(describeCapability(capability, now ?? new Date()))}</p>`)
|
|
23313
23757
|
.join("\n");
|
|
23314
23758
|
const blocked = gaps.length === 0
|
|
23315
|
-
? `<p class="meta">
|
|
23759
|
+
? `<p class="meta">No gaps — everything recorded is verified</p>`
|
|
23316
23760
|
: gaps
|
|
23317
23761
|
.map(gap => `<div class="card"><p>${escape(gap.key)} — ${escape(gap.state)}</p>` +
|
|
23318
23762
|
`<p class="meta">${gap.unblocks.length > 0
|
|
@@ -23320,16 +23764,16 @@ function capsPage(chrome, caps, gaps, repo, now) {
|
|
|
23320
23764
|
: gap.alsoBlocks.length > 0
|
|
23321
23765
|
? `part of what holds: ${gap.alsoBlocks.map(one => escape(one)).join(", ")}`
|
|
23322
23766
|
: "nothing queued needs it yet"}</p>` +
|
|
23323
|
-
`<p class="meta">
|
|
23767
|
+
`<p class="meta">Verify: ${escape(gap.verify)}</p>` +
|
|
23324
23768
|
`<p class="meta">${escape(gap.instructions)}</p></div>`)
|
|
23325
23769
|
.join("\n");
|
|
23326
23770
|
return screen("requirements", [
|
|
23327
23771
|
`<h1>Requirements</h1>`,
|
|
23328
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>`,
|
|
23329
23773
|
list,
|
|
23330
|
-
`<h2>
|
|
23774
|
+
`<h2>Missing, ranked by what filling them frees</h2>`,
|
|
23331
23775
|
blocked,
|
|
23332
|
-
`<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>`,
|
|
23333
23777
|
].join("\n"), { chrome });
|
|
23334
23778
|
}
|
|
23335
23779
|
/** v98: how the bot's messages reach Toolroll, in words: pushed to the public address, or asked for. */
|
|
@@ -23358,7 +23802,7 @@ function settingsPage(chrome, existing, hasEnv, csrf, problem, messaging = null,
|
|
|
23358
23802
|
`<input type="hidden" name="csrf" value="${escape(csrf)}">` +
|
|
23359
23803
|
permissionModeChoices("permission-mode", permissionDefault.mode) +
|
|
23360
23804
|
`<button type="submit">Save default</button></form>`
|
|
23361
|
-
: `<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>`,
|
|
23362
23806
|
permissionDefault.updatedAt === null
|
|
23363
23807
|
? ""
|
|
23364
23808
|
: `<p class="meta settings-changed">Changed ${whenTime(permissionDefault.updatedAt)}${permissionDefault.updatedBy === null ? "" : ` by ${escape(permissionDefault.updatedBy)}`}</p>`,
|
|
@@ -23372,7 +23816,7 @@ function settingsPage(chrome, existing, hasEnv, csrf, problem, messaging = null,
|
|
|
23372
23816
|
`<input type="hidden" name="csrf" value="${escape(csrf)}">` +
|
|
23373
23817
|
qualityModeChoices("quality-mode", qualityDefault.mode) +
|
|
23374
23818
|
`<button type="submit">Save default</button></form>`
|
|
23375
|
-
: `<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>`,
|
|
23376
23820
|
qualityDefault.updatedAt === null
|
|
23377
23821
|
? ""
|
|
23378
23822
|
: `<p class="meta settings-changed">Changed ${whenTime(qualityDefault.updatedAt)}${qualityDefault.updatedBy === null ? "" : ` by ${escape(qualityDefault.updatedBy)}`}</p>`,
|
|
@@ -23762,18 +24206,18 @@ function nextPage(chrome, data) {
|
|
|
23762
24206
|
`<p>${escape(item.approval.title)}</p>` +
|
|
23763
24207
|
(data.planDocument === null
|
|
23764
24208
|
? ""
|
|
23765
|
-
: `<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>`) +
|
|
23766
24210
|
`<form method="post" action="${taskHref(item.approval.taskId)}/approve" class="card approve-form">` +
|
|
23767
24211
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
|
|
23768
24212
|
`<input type="hidden" name="nonce" value="${escape(data.nonce)}">` +
|
|
23769
24213
|
`<input type="hidden" name="digest" value="${escape(data.approvalDigest ?? item.approval.digest)}">` +
|
|
23770
24214
|
`<input type="hidden" name="return" value="next">` +
|
|
23771
|
-
`<p><strong>
|
|
23772
|
-
(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>` : "") +
|
|
23773
24217
|
(scope === null || scope.profileState !== "unresolved" ? "" : profileWords(scope)) +
|
|
23774
|
-
`<p class="meta">
|
|
23775
|
-
`<p class="meta">
|
|
23776
|
-
`<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>` +
|
|
23777
24221
|
(scope === null ? "" : acceptanceCeremonyHtml(scope.acceptance)) +
|
|
23778
24222
|
(data.raceTerms === null
|
|
23779
24223
|
? ""
|
|
@@ -23783,8 +24227,8 @@ function nextPage(chrome, data) {
|
|
|
23783
24227
|
// tap away, never in the way.
|
|
23784
24228
|
agentsCeremonyHtml(data.route) +
|
|
23785
24229
|
(scope === null ? "" : runtimeDetailsHtml(scope)) +
|
|
23786
|
-
`<label>
|
|
23787
|
-
`<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>` +
|
|
23788
24232
|
`</form>` +
|
|
23789
24233
|
`<p class="meta"><a href="${taskHref(item.approval.taskId)}">open the full task</a> to edit the scope first</p>`;
|
|
23790
24234
|
}
|
|
@@ -23792,22 +24236,22 @@ function nextPage(chrome, data) {
|
|
|
23792
24236
|
card =
|
|
23793
24237
|
`<h1>${escape(item.stalled.taskId)}</h1>` +
|
|
23794
24238
|
`<p>${escape(item.stalled.title)}</p>` +
|
|
23795
|
-
`<p class="meta">
|
|
24239
|
+
`<p class="meta">Stopped — ${item.stalled.incidentCount} incident(s)${item.stalled.strikes > 0 ? ` after ${item.stalled.strikes} attempt(s)` : ""}</p>` +
|
|
23796
24240
|
`<form method="post" action="${taskHref(item.stalled.taskId)}/requeue" class="card">` +
|
|
23797
24241
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
|
|
23798
24242
|
`<input type="hidden" name="return" value="next">` +
|
|
23799
|
-
`<p class="meta">
|
|
23800
|
-
`<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>` +
|
|
23801
24245
|
`</form>` +
|
|
23802
24246
|
`<p class="meta"><a href="${taskHref(item.stalled.taskId)}">open the full task</a> to read the runs first</p>`;
|
|
23803
24247
|
}
|
|
23804
24248
|
else {
|
|
23805
24249
|
const { gap } = item;
|
|
23806
24250
|
card =
|
|
23807
|
-
`<h1>
|
|
24251
|
+
`<h1>Supply ${escape(gap.key)}</h1>` +
|
|
23808
24252
|
`<p class="meta">${escape(gap.state)}</p>` +
|
|
23809
24253
|
`<p>Filling this starts ${gap.unblocks.length} task(s): ${gap.unblocks.map(one => `<span class="mono">${escape(one)}</span>`).join(", ")}</p>` +
|
|
23810
|
-
`<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>`;
|
|
23811
24255
|
}
|
|
23812
24256
|
return screen("next", [header, card].join("\n"), { chrome });
|
|
23813
24257
|
}
|
|
@@ -23828,9 +24272,9 @@ function decisionOptionForms(decision, csrf, returnTo) {
|
|
|
23828
24272
|
`<input type="hidden" name="choice" value="${escape(option.id)}">`,
|
|
23829
24273
|
...(returnTo === null ? [] : [`<input type="hidden" name="return" value="${escape(returnTo)}">`]),
|
|
23830
24274
|
...(option.reversible ? [] : [`<input type="hidden" name="confirm" value="yes">`]),
|
|
23831
|
-
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>` : "",
|
|
23832
24276
|
`<p class="consequence">${escape(option.consequence)}</p>`,
|
|
23833
|
-
`<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>`,
|
|
23834
24278
|
`<input type="text" name="note" placeholder="optional note — travels with this answer" aria-label="optional note">`,
|
|
23835
24279
|
`</form>`,
|
|
23836
24280
|
].join("\n");
|
|
@@ -23848,14 +24292,14 @@ function decisionPage(chrome, decision, taskId, artifacts, who, now, returnTo =
|
|
|
23848
24292
|
: "";
|
|
23849
24293
|
const evidence = artifacts.length === 0
|
|
23850
24294
|
? ""
|
|
23851
|
-
: `<div class="evidence"><strong>
|
|
24295
|
+
: `<div class="evidence"><strong>Evidence</strong>` +
|
|
23852
24296
|
artifacts
|
|
23853
24297
|
.map(artifact => `<a href="/d/${decision.id}/evidence/${artifact.id}">${escape(evidenceWords(artifact.kind))}` +
|
|
23854
24298
|
`${artifact.truncated ? " (truncated)" : ""} · ${artifact.bytesStored} bytes</a>`)
|
|
23855
24299
|
.join("\n") +
|
|
23856
24300
|
"</div>";
|
|
23857
24301
|
return screen(`decide \u00b7 ${taskId}`, [
|
|
23858
|
-
`<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>`,
|
|
23859
24303
|
`<div class="recap">${escape(decision.recap)}</div>`,
|
|
23860
24304
|
`<div class="question">${escape(decision.question)}</div>`,
|
|
23861
24305
|
decision.state === "answered" ? answered : options,
|