toolroll 0.9.14 → 0.9.16
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 +34 -0
- package/dist/assignment-brief.d.ts +10 -0
- package/dist/assignment-brief.js +49 -26
- package/dist/assignment-ui.d.ts +1 -0
- package/dist/assignment-ui.js +6 -4
- package/dist/assignment.d.ts +34 -1
- package/dist/assignment.js +86 -7
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +38 -38
- package/dist/browser-crew.d.ts +2 -1
- package/dist/browser-crew.js +3 -1
- package/dist/browser-workspace.d.ts +93 -18
- package/dist/chat-actions.d.ts +1 -1
- package/dist/chat-actions.js +6 -4
- package/dist/chat-ask.d.ts +28 -0
- package/dist/chat-ask.js +77 -0
- package/dist/chat-channel.js +6 -1
- package/dist/chat-controls.d.ts +8 -0
- package/dist/chat-controls.js +3 -0
- package/dist/chat-delivery-state.d.ts +7 -2
- package/dist/chat-delivery-state.js +15 -2
- package/dist/chat-delivery.js +20 -3
- package/dist/chat-quiet.js +4 -2
- package/dist/chat-voice.d.ts +6 -9
- package/dist/chat-voice.js +11 -5
- package/dist/demo.js +82 -2
- package/dist/disclosure.d.ts +6 -0
- package/dist/disclosure.js +13 -0
- package/dist/discord-chat.js +10 -0
- package/dist/lead-commitments.d.ts +84 -0
- package/dist/lead-commitments.js +208 -0
- package/dist/lead-context.d.ts +32 -3
- package/dist/lead-context.js +133 -19
- package/dist/lead-follow.js +3 -0
- package/dist/lead-identity.d.ts +23 -0
- package/dist/lead-identity.js +33 -0
- package/dist/lead-status.js +2 -2
- package/dist/lead-voice.d.ts +12 -9
- package/dist/lead-voice.js +25 -16
- package/dist/ledger-view.js +3 -2
- package/dist/mate-cli.d.ts +1 -1
- package/dist/mate-cli.js +8 -2
- package/dist/mate-contract.d.ts +1 -1
- package/dist/mate-contract.js +11 -4
- package/dist/mate-progress.js +5 -0
- package/dist/mate-tools.d.ts +18 -1
- package/dist/mate-tools.js +250 -6
- package/dist/mate.d.ts +9 -0
- package/dist/mate.js +34 -38
- package/dist/needs-you.d.ts +48 -0
- package/dist/needs-you.js +93 -1
- package/dist/operate.js +2 -1
- package/dist/project-delete.js +3 -1
- package/dist/result-acts.d.ts +63 -5
- package/dist/result-acts.js +53 -3
- package/dist/result-review.d.ts +4 -3
- package/dist/result-review.js +7 -3
- package/dist/retention.js +6 -0
- package/dist/serve.d.ts +3 -0
- package/dist/serve.js +1220 -694
- package/dist/slack-chat.js +11 -1
- package/dist/store.d.ts +51 -0
- package/dist/store.js +115 -1
- package/dist/surface.js +2 -2
- package/dist/task-status.d.ts +26 -19
- package/dist/task-status.js +59 -21
- package/dist/team-runtime.js +2 -1
- package/dist/teams-chat.js +4 -1
- package/dist/telegram-mate.d.ts +4 -1
- package/dist/telegram-mate.js +17 -1
- package/dist/telegram-progress.js +3 -2
- package/dist/telegram.js +43 -3
- 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 +24 -2
- package/dist/when-html.js +98 -11
- package/dist/work-index.d.ts +5 -1
- package/dist/work-index.js +22 -9
- package/dist/work-summary.d.ts +1 -1
- package/dist/workspace-motion.js +3 -3
- package/dist/workspace-ui.d.ts +44 -20
- package/dist/workspace-ui.js +63 -37
- package/package.json +1 -1
package/dist/serve.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { checkLeadIdentity, leadIdentityOf, leadNameOf, LEAD_NAME_MAX, LEAD_PERSONA_MAX } from "./lead-identity.js";
|
|
1
2
|
import { withActor } from "./actor.js";
|
|
2
3
|
import { repositoryContext, repositoryContextRead } from './repository-context.js';
|
|
3
4
|
import { ADD_TESTS_ACTION, followUpChecksOf, requestFollowUpChecks, fileAddTestsTask, resultCheckLevel } from './result-follow-ups.js';
|
|
@@ -6,6 +7,7 @@ import { repositoryContextHtml } from './repository-context-ui.js';
|
|
|
6
7
|
import { browserAssetsAvailable, browserWorkspaceDocument, serveBrowserAsset } from './browser-shell.js';
|
|
7
8
|
import { browserCrewOf, browserCrewFromIndex, browserWorkActionHref, browserProjectsOf, browserNavigationOf } from './browser-workspace.js';
|
|
8
9
|
import { configureLeadFollow, leadFollowStatus, runLeadFollowPass } from './lead-follow.js';
|
|
10
|
+
import { cancelCommitment, conditionWords, openCommitments } from './lead-commitments.js';
|
|
9
11
|
import { startMaintenance } from './maintenance.js';
|
|
10
12
|
import { codingHandoffPreview, createCodingHandoff } from './coding-handoff.js';
|
|
11
13
|
import { codingShippingHtml, CODING_SHIPPING_CSS } from './coding-shipping-ui.js';
|
|
@@ -56,7 +58,7 @@ import { knowledgeHtml, knowledgeContextHtml, KNOWLEDGE_CSS, decisionsHtml, memo
|
|
|
56
58
|
import { listDecisions, recordDecision, retireDecision, searchMemory } from "./project-memory.js";
|
|
57
59
|
import { decideProposal, listProposals, memoryStatus } from "./memory-pass.js";
|
|
58
60
|
import { learningHtml } from "./workspace-ui.js";
|
|
59
|
-
import { resultActsOf } from "./result-acts.js";
|
|
61
|
+
import { acceptWithChecksOf, resultActsOf } from "./result-acts.js";
|
|
60
62
|
import { createSessionEndpoint } from './session-server.js';
|
|
61
63
|
import { handleTeamHttp } from './team-http.js';
|
|
62
64
|
import { teamWorkspaceHtml } from './team-ui.js';
|
|
@@ -65,7 +67,7 @@ import { prepareWorkspaceRevision, WorkspaceValidatorCache } from "./workspace-r
|
|
|
65
67
|
import { workIndexPage, workCountsByProject, WorkIndexCursorError, WORK_INDEX_MAX_LIMIT } from "./work-index.js";
|
|
66
68
|
import { leadActivity } from "./lead-voice.js";
|
|
67
69
|
import { openWorkDecisionOf } from "./work-summary.js";
|
|
68
|
-
import { assignmentOf, checkAssignmentAsOperator } from './assignment.js';
|
|
70
|
+
import { acceptAndCompleteAsOperator, assignmentOf, checkAssignmentAsOperator, owedAcceptance } from './assignment.js';
|
|
69
71
|
import { demoChatHtml, demoThreadHtml, DEMO_CHAT_SCRIPT } from "./demo-chat.js";
|
|
70
72
|
import { pullRequestSettingsHtml, PULL_REQUEST_SETTINGS_CSS } from './pull-request-ui.js';
|
|
71
73
|
import { checkSettingsHtml, CHECK_SETTINGS_CSS } from './check-levels-ui.js';
|
|
@@ -74,8 +76,8 @@ import { checkPublishing, completeAndOpenPullRequest, mergeAsPerson, pullRequest
|
|
|
74
76
|
import { leadBriefHtml, LEAD_CONTEXT_CSS } from './lead-context.js';
|
|
75
77
|
import { assignmentCatchUp } from './assignment-brief.js';
|
|
76
78
|
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';
|
|
79
|
+
import { assignmentStageOf, demoChecksOf, requirementWordOf, STATUS_MORE, pullRequestFactOf, requirementsOf, stageOfCode, stageOfDispatch, statusDetailsHtml, statusIconSvg, statusWhyHtml, taskStatusOf, TASK_STATUS_CSS } from './task-status.js';
|
|
80
|
+
import { ASKS, ASK_LABEL, GENERAL_SUGGESTION, NEEDS, NO_REASON_RECORDED, failedAttemptSentence, retryNoteOf, failingCheckSuggestion, isInternalErrorReason, latestFinishedAttempt, missedRequirementLine, missedRequirementSuggestion, runReasonWords, stopSuggestionOf } from './needs-you.js';
|
|
79
81
|
import { assignmentPresentationOf, historicalAssessmentReason, shortenedMaterialReason } from './assignment-presentation.js';
|
|
80
82
|
import { ledgerBody } from "./ledger-view.js";
|
|
81
83
|
import { EVIDENCE_PACK_CSS, evidencePack, evidencePackHtml, exportDay, ledgerExportChunks } from "./evidence-pack.js";
|
|
@@ -86,6 +88,7 @@ import { createResultRevision, requestResultChanges } from "./result-actions.js"
|
|
|
86
88
|
import { CHAT_CONTROLS, chatControlHref, chatResultHref as sharedResultHref, isChatControl } from "./chat-controls.js";
|
|
87
89
|
import { CHAT_TASK_ACTIONS, isChatTaskAction } from "./chat-task-actions.js";
|
|
88
90
|
import { WORKSPACE_MOTION_CSS, WORKSPACE_MOTION_SCRIPT } from "./workspace-motion.js";
|
|
91
|
+
import { DISCLOSURE_CSS } from "./disclosure.js";
|
|
89
92
|
import { styleAsset } from "./style-asset.js";
|
|
90
93
|
import { MOBILE_VIEWPORT_SCRIPT } from "./mobile-viewport.js";
|
|
91
94
|
import { authorizePlanUnderMode, applyModeToNewFiling, planAutoPending } from "./plan-auto.js";
|
|
@@ -164,7 +167,7 @@ import { starterRecipes, savedRecipes, findRecipe, importRecipe, exportRecipe, c
|
|
|
164
167
|
import { recipeFromForm, recipeLibraryHtml, recipeEditorHtml, workflowPreviewHtml, recipeScript, RECIPE_CSS, recipeDefinitionPreviewHtml, recipeRunHtml, recipeAnswersFromForm } from "./recipe-ui.js";
|
|
165
168
|
import { EVIDENCE_CAPS, readVerifiedArtifact, readVerifiedReport, readVerifiedProofForRun, storeEvidence, writeEvidenceFile, scanForSecrets } from "./evidence.js";
|
|
166
169
|
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";
|
|
170
|
+
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
171
|
import { PRICED_BUILD_MODELS } from "./pricing.js";
|
|
169
172
|
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
173
|
import { parseReport } from "./scout-report.js";
|
|
@@ -184,7 +187,7 @@ import { dirname } from "node:path";
|
|
|
184
187
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
185
188
|
import { loadOrCreateVapidKeys, validatePushEndpoint } from "./push.js";
|
|
186
189
|
import { parseGithubRepo, previewGithubRepo, cloneGithubRepo, listGithubRepos, isLargeRepo } from "./onboard.js";
|
|
187
|
-
import { verifiedAuthor, LEAD_THREAD, isDigestTime } from "./store.js";
|
|
190
|
+
import { verifiedAuthor, LEAD_THREAD, isDigestTime, MATE_ASK_OTHER } from "./store.js";
|
|
188
191
|
import { digestTimes } from "./digest-times.js";
|
|
189
192
|
import { updateRepos, addRepos, removeRepos } from "./repos.js";
|
|
190
193
|
import { run as execRun } from "./exec.js";
|
|
@@ -240,7 +243,7 @@ import { parseProjectConcurrency, PROJECT_CONCURRENCY_DEFAULT, projectConcurrenc
|
|
|
240
243
|
import { buildReviewOf, findingWords } from "./review-switch.js";
|
|
241
244
|
import { deleteProject, holdingsWords, projectHoldings, projectRunning } from "./project-delete.js";
|
|
242
245
|
import { POLICY_CSS, policyHtml } from "./policy-ui.js";
|
|
243
|
-
import { checkPolicy, parseList, policyParts } from "./policy.js";
|
|
246
|
+
import { checkPolicy, levelOfProfile, parseList, policyParts } from "./policy.js";
|
|
244
247
|
import { LIMITS_CSS, limitsHtml, limitsView } from "./limits-ui.js";
|
|
245
248
|
import { budgetHoldWords, budgetLabel, budgetStates, monthNamed, monthOf, spendItems, teammateNames as teammateNamesOf, usd as spendUsd } from "./spend.js";
|
|
246
249
|
import { targetOf } from "./monitoring.js";
|
|
@@ -2104,6 +2107,30 @@ export function createDecisionServer(options) {
|
|
|
2104
2107
|
return refuse(response, who, 400, 'This task page has expired. Open the first page.', '/work');
|
|
2105
2108
|
throw error;
|
|
2106
2109
|
}
|
|
2110
|
+
// A failed row says what its latest attempt missed, as its task page does (a stop reason already reads so), from
|
|
2111
|
+
// 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
|
|
2112
|
+
// stuck on, from that live attempt's own progress only, never a stopped attempt's; the step's words come from its
|
|
2113
|
+
// current plan (one saved file, for live rows only), and without it the row names the step by number.
|
|
2114
|
+
work = { ...work, items: work.items.map((item) => {
|
|
2115
|
+
if (item.liveRunId !== null) {
|
|
2116
|
+
const ref = store.lookupRef(item.activeTaskId);
|
|
2117
|
+
const recorded = ref === null ? null : store.latestCheckpointForRun(item.liveRunId);
|
|
2118
|
+
if (ref === null || recorded === null || recorded.taskRef !== ref.id)
|
|
2119
|
+
return item;
|
|
2120
|
+
const plan = store.currentPlanRevision(ref.id);
|
|
2121
|
+
const parsed = plan === null ? null : (() => { const doc = revisionDocOf(plan); return doc === null ? null : parseExecutionPlanDocument(doc.document); })();
|
|
2122
|
+
const words = new Map(parsed?.ok === true ? milestonesOf(parsed.document).map(one => [one.id, one.description]) : []);
|
|
2123
|
+
const steps = buildProgressOf(recorded.snapshot.milestones.map(one => ({ description: words.get(one.id) ?? null, state: one.state, note: one.note ?? null })));
|
|
2124
|
+
return steps === null ? item : { ...item, progress: steps.line };
|
|
2125
|
+
}
|
|
2126
|
+
if (item.state !== "failed")
|
|
2127
|
+
return item;
|
|
2128
|
+
const family = familyOf(item.activeTaskId);
|
|
2129
|
+
if (family === null || family.problem !== null)
|
|
2130
|
+
return item;
|
|
2131
|
+
const failure = failureOf(family.versions.flatMap(version => store.runsFor(version.refId)), null, false);
|
|
2132
|
+
return failure.kind === "reason" ? item : { ...item, status: { ...item.status, detail: failure.line } };
|
|
2133
|
+
}) };
|
|
2107
2134
|
const page = workPage(chromeFor(project, "work", undefined, rollup ? "all" : undefined), {
|
|
2108
2135
|
view,
|
|
2109
2136
|
...(url.searchParams.has('project') && project !== null ? { projectFilter: project } : {}),
|
|
@@ -2234,7 +2261,7 @@ export function createDecisionServer(options) {
|
|
|
2234
2261
|
if (selected !== null) {
|
|
2235
2262
|
const view = taskViewData(selected, who, null);
|
|
2236
2263
|
detail = view === null
|
|
2237
|
-
? `<p class="workbench-mobile-back"><a href="/workbench">← all work</a></p><div class="card"><p class="meta">
|
|
2264
|
+
? `<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
2265
|
: `<p class="workbench-mobile-back"><a href="/workbench">← all work</a></p>${taskBody({ ...view, degraded: "pane" })}`;
|
|
2239
2266
|
}
|
|
2240
2267
|
return sendScreen(response, 200, screen("portfolio", detail, {
|
|
@@ -2389,10 +2416,15 @@ export function createDecisionServer(options) {
|
|
|
2389
2416
|
// The ranked queue itself stays bounded and says so.
|
|
2390
2417
|
const beyond = wantedId === null || inQueue !== null ? null : completedRowFor(wantedId, project);
|
|
2391
2418
|
const beyondRow = beyond === null ? null : resultRow(beyond);
|
|
2392
|
-
const
|
|
2419
|
+
const completedChoice = inQueue ?? (beyondRow === null ? null : { ...beyondRow, priority: reviewPriorityOf(beyondRow) });
|
|
2420
|
+
const expectedRun = url.searchParams.get("run");
|
|
2421
|
+
const namedRun = expectedRun !== null && /^[1-9]\d*$/.test(expectedRun) ? Number(expectedRun) : null;
|
|
2422
|
+
// A build that failed (or stopped) has its own result page too: the run named, or a failed task's latest attempt.
|
|
2423
|
+
const attempt = wantedId === null || (completedChoice !== null && (namedRun === null || completedChoice.runId === namedRun)) ? null : attemptRowFor(wantedId, namedRun, project);
|
|
2424
|
+
const attemptRow = attempt === null ? null : resultRow(attempt);
|
|
2425
|
+
const chosen = attemptRow !== null ? { ...attemptRow, priority: reviewPriorityOf(attemptRow) } : completedChoice;
|
|
2393
2426
|
const selectedRow = wantedId === null ? ranked[0] ?? null : chosen;
|
|
2394
2427
|
const csrf = who.via === "cookie" ? who.session.csrf : "";
|
|
2395
|
-
const expectedRun = url.searchParams.get("run");
|
|
2396
2428
|
// An exact link to a result this person can't see (or that never existed) reads the same either way.
|
|
2397
2429
|
if (expectedRun !== null && wantedId !== null && chosen === null)
|
|
2398
2430
|
return refuse(response, who, 404, "No such result in your projects.", "/work");
|
|
@@ -2400,11 +2432,34 @@ export function createDecisionServer(options) {
|
|
|
2400
2432
|
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
2433
|
}
|
|
2402
2434
|
const selected = selectedRow === null ? null : reviewCockpitViewOf(selectedRow, who, now);
|
|
2435
|
+
// A build that delivered nothing reads Failed here, and so does a failed task's delivered result: what went wrong
|
|
2436
|
+
// is the card and Retry the ink act. That result may still be accepted, but only in outline, as Accept anyway, with
|
|
2437
|
+
// a reason.
|
|
2438
|
+
const delivered = selected?.run?.outcome === "built" || selected?.run?.outcome === "no-change";
|
|
2439
|
+
const failedTask = selected === null ? null : store.getTask(selected.taskId);
|
|
2440
|
+
if (selected !== null && attemptRow !== null && selected.run !== null && (!delivered || failedTask?.state === "failed")) {
|
|
2441
|
+
const attemptRun = store.getRun(selected.run.id);
|
|
2442
|
+
const runs = attemptRun === null ? [] : store.runsFor(attemptRun.taskRef);
|
|
2443
|
+
const latest = latestFinishedAttempt(runs);
|
|
2444
|
+
// Retry from here only when the task failed, nothing holds it, and this is its latest attempt or a result it delivered.
|
|
2445
|
+
const retry = failedTask?.state === "failed" && (latest?.id === selected.run.id || delivered) && csrf !== "" && who.role === "approver" && store.currentLiveLease(attemptRun.taskRef, now) === null
|
|
2446
|
+
? { action: `${taskHref(selected.taskId)}/requeue` } : null;
|
|
2447
|
+
// Accept anyway: the same acceptance this task's latest result takes, never for an older one or one already accepted.
|
|
2448
|
+
const family = delivered ? familyOf(selected.taskId) : null;
|
|
2449
|
+
const acceptAnyway = delivered && csrf !== "" && who.role === "approver" && family?.current.id === selected.taskId && family.problem === null &&
|
|
2450
|
+
runs.find(runIsTaskResult)?.id === selected.run.id && store.proofAcceptance(selected.run.id) === null
|
|
2451
|
+
? { action: `${taskHref(selected.taskId)}/accept-proof`, run: selected.run.id } : null;
|
|
2452
|
+
if (attemptRun !== null) {
|
|
2453
|
+
// A delivered result didn't fail itself: the task's failure is its latest attempt's.
|
|
2454
|
+
const failure = delivered ? failureOf(runs, null) : explainAttempt(attemptRun, null);
|
|
2455
|
+
selected.failure = { ...failure, retry: retry === null ? null : { ...retry, note: retryNoteOf(failure.suggestion) }, ...(acceptAnyway === null ? {} : { acceptAnyway }) };
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2403
2458
|
const reviewPage = reviewCockpitPage(chromeFor(wantedId === null ? project : chosenProject, "runs"), {
|
|
2404
2459
|
queue: ranked,
|
|
2405
2460
|
queueCap: REVIEW_QUEUE_CAP,
|
|
2406
2461
|
selected,
|
|
2407
|
-
beyondQueue: beyond !== null,
|
|
2462
|
+
beyondQueue: beyond !== null && attemptRow === null,
|
|
2408
2463
|
// A deep link to a result this console cannot show — not done,
|
|
2409
2464
|
// not admitted, or never existed — says so in one sentence and
|
|
2410
2465
|
// shows the top of the queue; the three cases read identically.
|
|
@@ -2412,6 +2467,7 @@ export function createDecisionServer(options) {
|
|
|
2412
2467
|
csrf,
|
|
2413
2468
|
canRetryReview: who.via === "cookie" && who.role === "approver",
|
|
2414
2469
|
noted: url.searchParams.get("noted") !== null,
|
|
2470
|
+
refusal: Object.hasOwn(RESULT_REFUSALS, url.searchParams.get("refused") ?? "") ? RESULT_REFUSALS[url.searchParams.get("refused")] : null,
|
|
2415
2471
|
tab: parseResultTab(url.searchParams.get("tab")),
|
|
2416
2472
|
user: who.name,
|
|
2417
2473
|
now,
|
|
@@ -2547,7 +2603,7 @@ export function createDecisionServer(options) {
|
|
|
2547
2603
|
const csrf = who.via === "cookie" ? who.session.csrf : "";
|
|
2548
2604
|
const hasGrant = store.hasMergeCapableGrant(project, now);
|
|
2549
2605
|
const current = live === null || liveTerms === null
|
|
2550
|
-
? `<div class="card"><h2>
|
|
2606
|
+
? `<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
2607
|
: [
|
|
2552
2608
|
`<div class="card">`,
|
|
2553
2609
|
`<h2 style="margin-top:0">${escape(live.name)} <span class="meta">signed by ${personChip(live.signedBy)}</span></h2>`,
|
|
@@ -2557,7 +2613,7 @@ export function createDecisionServer(options) {
|
|
|
2557
2613
|
who.role === "approver"
|
|
2558
2614
|
? `<form method="post" action="/mode/revoke" class="row">` +
|
|
2559
2615
|
`<input type="hidden" name="csrf" value="${escape(csrf)}">` +
|
|
2560
|
-
`<button type="submit">
|
|
2616
|
+
`<button type="submit">End this mode now</button>` +
|
|
2561
2617
|
`<span class="meta">one click — every act it covered falls back to its own ceremony</span></form>`
|
|
2562
2618
|
: "",
|
|
2563
2619
|
`</div>`,
|
|
@@ -2569,31 +2625,31 @@ export function createDecisionServer(options) {
|
|
|
2569
2625
|
`<h2 style="margin-top:0">${live === null ? "sign a mode" : "replace it — a renewal is a new signature"}</h2>`,
|
|
2570
2626
|
`<form method="post" action="/mode/confirm">`,
|
|
2571
2627
|
`<input type="hidden" name="csrf" value="${escape(csrf)}">`,
|
|
2572
|
-
`<label>
|
|
2573
|
-
`<label>
|
|
2574
|
-
`<label>
|
|
2628
|
+
`<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>`,
|
|
2629
|
+
`<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>`,
|
|
2630
|
+
`<label>Merges<select name="publication">` +
|
|
2575
2631
|
`<option value="notify">wait for me — even under a merge grant</option>` +
|
|
2576
2632
|
(hasGrant ? `<option value="automerge">merge themselves when CI is green on the exact commit</option>` : "") +
|
|
2577
2633
|
`</select></label>` +
|
|
2578
2634
|
(hasGrant ? "" : `<span class="meta">self-merging needs a merge-capable publication grant first</span>`),
|
|
2579
|
-
`<label>
|
|
2635
|
+
`<label>My filings<select name="auto-approve">` +
|
|
2580
2636
|
`<option value="">the preset's default (standard: wait for approval; hands-off: auto-approve)</option>` +
|
|
2581
2637
|
`<option value="1">approve the moment I file them</option>` +
|
|
2582
2638
|
`<option value="0">wait for their own approval</option>` +
|
|
2583
2639
|
`</select></label>`,
|
|
2584
|
-
`<label>
|
|
2640
|
+
`<label>Planner approval<select name="plan-auto">` +
|
|
2585
2641
|
`<option value="0">wait for my approval</option>` +
|
|
2586
2642
|
`<option value="1">auto-approve plans that preserve my filed contract</option>` +
|
|
2587
2643
|
`</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>
|
|
2644
|
+
`<label>If a subscription runs out<select name="allow-paid-fallback">` +
|
|
2589
2645
|
`<option value="">never switch to a paid API key on its own (the default, every preset)</option>` +
|
|
2590
2646
|
`<option value="1">allow the approved fallback — spend moves to that account</option>` +
|
|
2591
2647
|
`</select></label>`,
|
|
2592
|
-
`<button type="submit">
|
|
2648
|
+
`<button type="submit">Read the full terms</button>`,
|
|
2593
2649
|
`</form>`,
|
|
2594
2650
|
`</div>`,
|
|
2595
2651
|
].join("\n");
|
|
2596
|
-
return sendScreen(response, 200, screen("mode", [`<h1>
|
|
2652
|
+
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
2653
|
}
|
|
2598
2654
|
if (url.pathname === "/people") {
|
|
2599
2655
|
// Approvers see everyone (D7's ceiling: every fact already passed
|
|
@@ -2635,13 +2691,13 @@ export function createDecisionServer(options) {
|
|
|
2635
2691
|
? ""
|
|
2636
2692
|
: `<p class="row">watching now: ${attended.map(session => `<a href="/t/${escape(session.taskId)}">${escape(session.taskId)}</a>`).join(", ")}</p>`,
|
|
2637
2693
|
acts.length === 0
|
|
2638
|
-
? `<p class="meta">
|
|
2694
|
+
? `<p class="meta">No recorded acts yet</p>`
|
|
2639
2695
|
: 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
2696
|
approverView && one.revokedAt === null && one.name !== who.name
|
|
2641
2697
|
? `<form method="post" action="/people/revoke" class="row">` +
|
|
2642
2698
|
`<input type="hidden" name="csrf" value="${escape(csrf)}"><input type="hidden" name="name" value="${escape(one.name)}">` +
|
|
2643
|
-
`<label>
|
|
2644
|
-
`<button type="submit">
|
|
2699
|
+
`<label>Your password<input type="password" name="token" autocomplete="current-password"></label>` +
|
|
2700
|
+
`<button type="submit">Remove ${escape(one.name)}'s sign-in</button>` +
|
|
2645
2701
|
`<span class="meta">ends their access and everything it signed \u2014 history stays</span></form>`
|
|
2646
2702
|
: "",
|
|
2647
2703
|
`</div>`,
|
|
@@ -2651,20 +2707,20 @@ export function createDecisionServer(options) {
|
|
|
2651
2707
|
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
2708
|
` <form method="post" action="/people/invite-revoke" style="display:inline">` +
|
|
2653
2709
|
`<input type="hidden" name="csrf" value="${escape(csrf)}"><input type="hidden" name="id" value="${one.id}">` +
|
|
2654
|
-
`<label>
|
|
2655
|
-
`<button type="submit">
|
|
2710
|
+
`<label>Password <input type="password" name="token" autocomplete="current-password" style="width:8rem"></label>` +
|
|
2711
|
+
`<button type="submit">Cancel it</button></form></p>`);
|
|
2656
2712
|
const inviteCard = !approverView
|
|
2657
2713
|
? ""
|
|
2658
2714
|
: [
|
|
2659
2715
|
`<div class="card">`,
|
|
2660
|
-
`<h2 style="margin-top:0">
|
|
2661
|
-
inviteRows.length === 0 ? `<p class="meta">
|
|
2716
|
+
`<h2 style="margin-top:0">Invite someone</h2>`,
|
|
2717
|
+
inviteRows.length === 0 ? `<p class="meta">No open invites</p>` : inviteRows.join("\n"),
|
|
2662
2718
|
`<form method="post" action="/people/invite" class="row">`,
|
|
2663
2719
|
`<input type="hidden" name="csrf" value="${escape(csrf)}">`,
|
|
2664
|
-
`<label>
|
|
2720
|
+
`<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
2721
|
accessFields(project === null ? [] : [project]),
|
|
2666
|
-
`<label>
|
|
2667
|
-
`<button type="submit">
|
|
2722
|
+
`<label>Your password<input type="password" name="token" autocomplete="current-password"></label>`,
|
|
2723
|
+
`<button type="submit">Make an invite link</button>`,
|
|
2668
2724
|
`<span class="meta">single-use, expires in 72 hours</span>`,
|
|
2669
2725
|
`</form>`,
|
|
2670
2726
|
`</div>`,
|
|
@@ -2786,7 +2842,7 @@ export function createDecisionServer(options) {
|
|
|
2786
2842
|
`<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
2843
|
`<span class="right meta mono">${escape(run.runner)} \u00b7 ${escape(run.role)} \u00b7 ${escape(stage)} \u00b7 ${escape(relativeAge(run.startedAt, now))}</span></p>`,
|
|
2788
2844
|
options.localRunner === undefined
|
|
2789
|
-
? `<p class="meta">
|
|
2845
|
+
? `<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
2846
|
: `<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
2847
|
`</section>`,
|
|
2792
2848
|
].join("\n");
|
|
@@ -2799,7 +2855,7 @@ export function createDecisionServer(options) {
|
|
|
2799
2855
|
return sendScreen(response, 200, screen("peek", [
|
|
2800
2856
|
`<h1>Peek</h1>`,
|
|
2801
2857
|
panes.length === 0
|
|
2802
|
-
? `<p class="meta">
|
|
2858
|
+
? `<p class="meta">No agent is working right now \u2014 this page follows them the moment one starts</p>`
|
|
2803
2859
|
: panes.join("\n"),
|
|
2804
2860
|
].join("\n"), { chrome: chromeFor(project, "runs"), functional: { script, fetches: true } }));
|
|
2805
2861
|
}
|
|
@@ -2823,19 +2879,19 @@ export function createDecisionServer(options) {
|
|
|
2823
2879
|
...(said === null ? [] : [`<p class="meta">${escape(said)}</p>`]),
|
|
2824
2880
|
`<div id="fleet-region">${body}</div>`,
|
|
2825
2881
|
`<p class="meta" id="fleet-region-stamp"></p>`,
|
|
2826
|
-
`<h2>
|
|
2882
|
+
`<h2>Register a worker</h2>`,
|
|
2827
2883
|
`<form method="post" action="/fleet/runner/register" class="card">` +
|
|
2828
2884
|
`<input type="hidden" name="csrf" value="${escape(csrf)}">` +
|
|
2829
|
-
`<label>
|
|
2830
|
-
`<label>
|
|
2831
|
-
`<label>
|
|
2832
|
-
`<button type="submit">
|
|
2833
|
-
`<details class="arm-danger"><summary>
|
|
2885
|
+
`<label>Name<input type="text" name="name" placeholder="builder-2" maxlength="60" required></label>` +
|
|
2886
|
+
`<label>Capacity<input type="text" name="capacity" inputmode="numeric" value="1" aria-label="capacity"></label>` +
|
|
2887
|
+
`<label>Your password, typed again<input type="password" name="token" autocomplete="current-password" required></label>` +
|
|
2888
|
+
`<button type="submit">Register — its token is shown once</button></form>`,
|
|
2889
|
+
`<details class="arm-danger"><summary>Retire a worker</summary>` +
|
|
2834
2890
|
`<form method="post" action="/fleet/runner/retire" class="card">` +
|
|
2835
2891
|
`<input type="hidden" name="csrf" value="${escape(csrf)}">` +
|
|
2836
|
-
`<label>
|
|
2837
|
-
`<label>
|
|
2838
|
-
`<button type="submit" class="danger">
|
|
2892
|
+
`<label>Which worker<input type="text" name="name" placeholder="builder-1" required></label>` +
|
|
2893
|
+
`<label>Your password, typed again<input type="password" name="token" autocomplete="current-password" required></label>` +
|
|
2894
|
+
`<button type="submit" class="danger">Retire it</button></form></details>`,
|
|
2839
2895
|
].join("\n"),
|
|
2840
2896
|
// The password fields make this screen sensitive: sendScreen strips
|
|
2841
2897
|
// the chrome additions and keeps the reorder poller — the named
|
|
@@ -2861,9 +2917,10 @@ export function createDecisionServer(options) {
|
|
|
2861
2917
|
const version = url.searchParams.get("version");
|
|
2862
2918
|
if (version !== null && !family.versions.some(one => one.id === version))
|
|
2863
2919
|
return refuse(response, who, 404, "That version is not available for this task.", taskHref(family.root.id));
|
|
2920
|
+
// A version's own address lands on its family's page, still asking for the plan editor when it did (Chat's Edit plan).
|
|
2864
2921
|
if (task.taskId !== family.root.id)
|
|
2865
|
-
return redirect(response, `${taskHref(family.root.id)}?version=${encodeURIComponent(task.taskId)}`);
|
|
2866
|
-
return taskScreen(response, who, version ?? family.current.id, null, 200);
|
|
2922
|
+
return redirect(response, `${taskHref(family.root.id)}?version=${encodeURIComponent(task.taskId)}${url.searchParams.get("edit") === "plan" ? "&edit=plan" : ""}`);
|
|
2923
|
+
return taskScreen(response, who, version ?? family.current.id, null, 200, undefined, undefined, url.searchParams.get("edit") === "plan");
|
|
2867
2924
|
}
|
|
2868
2925
|
// v103: a task's evidence pack, as a printable page or JSON (the whole family, sealed ledger entries included).
|
|
2869
2926
|
const evidence = matchTaskPath(url.pathname, "/evidence$");
|
|
@@ -3034,6 +3091,11 @@ export function createDecisionServer(options) {
|
|
|
3034
3091
|
const tab = parseResultTab(url.searchParams.get("tab"));
|
|
3035
3092
|
return redirect(response, `${reviewHref(taskId, found.id)}${tab === "summary" ? "" : `&tab=${tab}`}`);
|
|
3036
3093
|
}
|
|
3094
|
+
// A build that failed or stopped opens on its own result page too: its changes, its checks and what went wrong.
|
|
3095
|
+
if (url.searchParams.get("record") === null && !running && (found.role === "builder" || found.role === "scout") && attemptRowFor(taskId, found.id, null) !== null) {
|
|
3096
|
+
const tab = parseResultTab(url.searchParams.get("tab"));
|
|
3097
|
+
return redirect(response, `${reviewHref(taskId, found.id)}${tab === "summary" ? "" : `&tab=${tab}`}`);
|
|
3098
|
+
}
|
|
3037
3099
|
// Pollers exist only for a LIVE run — an orphaned null-outcome run
|
|
3038
3100
|
// would otherwise be refetched forever (finding 15). The nonce is
|
|
3039
3101
|
// sendScreen's business now.
|
|
@@ -3424,7 +3486,7 @@ export function createDecisionServer(options) {
|
|
|
3424
3486
|
const fragments = known === version
|
|
3425
3487
|
? null
|
|
3426
3488
|
: {
|
|
3427
|
-
thread: mateThreadHtml({ ...rows, focusTask, csrf, now, problem: takeMateNote(csrf, session.id) }),
|
|
3489
|
+
thread: mateThreadHtml({ ...rows, focusTask, csrf, now, problem: takeMateNote(csrf, session.id), chatProject }),
|
|
3428
3490
|
after: mateAfterComposerHtml({ messages: rows.messages, pending: rows.pending, focusTask, csrf }),
|
|
3429
3491
|
live: focusTask === null ? null : taskChatLiveRegion(focusTask, csrf, true, rows.pending !== null),
|
|
3430
3492
|
};
|
|
@@ -3533,6 +3595,8 @@ export function createDecisionServer(options) {
|
|
|
3533
3595
|
})();
|
|
3534
3596
|
const roomId = url.searchParams.get('conversation');
|
|
3535
3597
|
const resultLink = (href) => roomId ? href + '&conversation=' + encodeURIComponent(roomId) : href;
|
|
3598
|
+
// Accept and finish is here: the one act that accepts the person's own checks and finishes the task.
|
|
3599
|
+
const finishes = resultRun !== null && who.role === 'approver' && focusTask?.assignment?.state === 'ready-to-check' && focusTask.assignment.receipt?.runId === resultRun.id;
|
|
3536
3600
|
const resultPanel = resultRun === null
|
|
3537
3601
|
? null
|
|
3538
3602
|
: resultPanelHtml(resultDetailOf(resultRun, who, now), {
|
|
@@ -3545,9 +3609,9 @@ export function createDecisionServer(options) {
|
|
|
3545
3609
|
hrefFor: one => resultLink(chatResultHref(focusTask?.id ?? "", resultRun.id, one)),
|
|
3546
3610
|
returnTo: resultLink(chatResultHref(focusTask?.id ?? "", resultRun.id)),
|
|
3547
3611
|
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)) : '');
|
|
3612
|
+
finishes,
|
|
3613
|
+
}) + (finishes && focusTask?.assignment?.receipt != null
|
|
3614
|
+
? completionForm(focusTask.assignment.receipt.taskId, resultRun.id, focusTask.assignment.receipt.digest, who.session.csrf, pullRequestTargetOf(resultRun.id), owedAcceptanceOf(focusTask.assignment.receipt)) : '');
|
|
3551
3615
|
const focusProblem = requestedTask !== null && focusTask === null
|
|
3552
3616
|
? "That task is not available in this workspace."
|
|
3553
3617
|
: requestedResult !== null && focusTask !== null && resultRun === null
|
|
@@ -4072,8 +4136,9 @@ export function createDecisionServer(options) {
|
|
|
4072
4136
|
returnTo: "/settings/lead",
|
|
4073
4137
|
};
|
|
4074
4138
|
const signedIn = localSignIn?.states.claude === "connected" ? "Claude Code" : localSignIn?.states.codex === "connected" ? "Codex" : null;
|
|
4139
|
+
const promises = openCommitments(store, who.name, 50).map(one => ({ id: one.id, what: one.what, when: conditionWords(store, one.condition), until: one.expiresAt }));
|
|
4075
4140
|
return sendScreen(response, 200, screen("Lead", leadSettingsHtml({ config, facts, words: leadWords(), signedIn, command: agentSignInCommand(),
|
|
4076
|
-
said: url.searchParams.get("said") }), { chrome: chromeFor(project, "settings") }));
|
|
4141
|
+
said: url.searchParams.get("said"), saved: url.searchParams.get("saved") === "1", identity: leadIdentityOf(store, who.name), promises }), { chrome: chromeFor(project, "settings") }));
|
|
4077
4142
|
}
|
|
4078
4143
|
if (url.pathname === "/settings/telegram") {
|
|
4079
4144
|
// Any approver pairs their OWN phone here; the bot token stays on /settings.
|
|
@@ -4372,8 +4437,16 @@ export function createDecisionServer(options) {
|
|
|
4372
4437
|
// One reply runs at a time per person; this thread shows it only when
|
|
4373
4438
|
// the reply is its own.
|
|
4374
4439
|
const live = store.liveMateTurnFor(who.name);
|
|
4440
|
+
const messages = store.listMateMessages(opened.thread.id, 40);
|
|
4441
|
+
// The lead's question with buttons, while its reply is the last word in the thread.
|
|
4442
|
+
const last = messages.at(-1);
|
|
4443
|
+
const ask = last?.role === "assistant" && last.turn !== null && store.getMateTurn(last.turn)?.approver === who.name ? store.mateAskOpen(last.turn, now) : null;
|
|
4444
|
+
// Answered questions stay readable above the answer; only their buttons go.
|
|
4445
|
+
const asks = new Map(messages.flatMap(one => { const asked = one.role === "assistant" && one.turn !== null ? store.mateAsk(one.turn) : null; return asked === null ? [] : [[asked.turn, asked]]; }));
|
|
4375
4446
|
return {
|
|
4376
|
-
messages
|
|
4447
|
+
messages,
|
|
4448
|
+
ask,
|
|
4449
|
+
asks,
|
|
4377
4450
|
proposals,
|
|
4378
4451
|
decisions: decisionsFor(store, [...proposals, ...coordinatorProposals]),
|
|
4379
4452
|
coordinatorProposals,
|
|
@@ -4448,7 +4521,7 @@ export function createDecisionServer(options) {
|
|
|
4448
4521
|
const rows = mateConversationRows(who, principal, focusTask, now, chatProject);
|
|
4449
4522
|
return {
|
|
4450
4523
|
sessionId: session.id, user: session.approver, version: mateChatVersion({ ...rows, focusTask }),
|
|
4451
|
-
messages: mateBrowserMessages(rows, who.session.csrf, back),
|
|
4524
|
+
messages: mateBrowserMessages(rows, who.session.csrf, back, { task: focusTask?.id ?? null, project: chatProject }),
|
|
4452
4525
|
pendingTurnId: rows.pending?.id ?? null, requestId: randomBytes(16).toString("hex"), maxChars: MATE_MESSAGE_MAX_CHARS,
|
|
4453
4526
|
taskId: focusTask?.id ?? null, resultRunId, project: focusTask === null ? chatProject : null,
|
|
4454
4527
|
};
|
|
@@ -4916,7 +4989,8 @@ export function createDecisionServer(options) {
|
|
|
4916
4989
|
try {
|
|
4917
4990
|
const project = s.chrome.active === 'chat' ? null : s.chrome.project;
|
|
4918
4991
|
crew = extras.team?.tasks ? browserCrewFromIndex(extras.team.tasks, extras.team.selected?.id) : requestFacts.workCrew?.project === project ? browserCrewFromIndex(requestFacts.workCrew.page)
|
|
4919
|
-
|
|
4992
|
+
// The reader's own lead's claims read "<name> is on it" here too.
|
|
4993
|
+
: browserCrewOf(store, clock(), { principal: 'operator', repos: managedRepos(), includeUnplaced: false, viewer: requestFacts.actor ?? null }, { evidenceRoot, project });
|
|
4920
4994
|
}
|
|
4921
4995
|
catch {
|
|
4922
4996
|
notices.push('Crew updates are unavailable. Open Tasks to inspect saved work.');
|
|
@@ -4958,6 +5032,7 @@ export function createDecisionServer(options) {
|
|
|
4958
5032
|
const request = requestFacts.workspaceRequest;
|
|
4959
5033
|
const workspace = {
|
|
4960
5034
|
version: 1, path: currentPath, title: s.title, user: requestFacts.actor ?? '', csrf: requestFacts.csrf, sensitive,
|
|
5035
|
+
...(requestFacts.actor ? { leadName: leadNameOf(store, requestFacts.actor) } : {}),
|
|
4961
5036
|
refreshUrl: path.pathname + path.search,
|
|
4962
5037
|
receipt: conversation !== null && request && REQUEST_TOKEN.test(request)
|
|
4963
5038
|
? { request, received: store.mateRequestReceipt(conversation.sessionId, request) !== null } : null,
|
|
@@ -5063,7 +5138,7 @@ export function createDecisionServer(options) {
|
|
|
5063
5138
|
// One line: what this person's lead is doing now and when it last acted.
|
|
5064
5139
|
const activity = leadActivity(store, who.name);
|
|
5065
5140
|
const leadTask = activity?.taskId == null ? null : store.lookupRef(activity.taskId);
|
|
5066
|
-
const lead = activity === null ? null : { doing: activity.doing, at: activity.at,
|
|
5141
|
+
const lead = activity === null ? null : { name: activity.name, doing: activity.doing, at: activity.at,
|
|
5067
5142
|
href: leadTask == null || !admitted(leadTask.repo) ? null : taskHref(familyOf(activity.taskId)?.root.id ?? activity.taskId) };
|
|
5068
5143
|
return {
|
|
5069
5144
|
agents, counts, catchUp, allHref: "/work", ...(lead === null ? {} : { lead }),
|
|
@@ -5164,7 +5239,7 @@ export function createDecisionServer(options) {
|
|
|
5164
5239
|
};
|
|
5165
5240
|
}
|
|
5166
5241
|
function workAccess() {
|
|
5167
|
-
// The reader's own lead's claims read "
|
|
5242
|
+
// The reader's own lead's claims read "<name> is on it" (lead-voice.ts); nobody else's.
|
|
5168
5243
|
return { principal: 'operator', repos: admissionList(), includeUnplaced: visible(null), viewer: requestContext.getStore()?.actor ?? null };
|
|
5169
5244
|
}
|
|
5170
5245
|
// All project counts are read together. No task artifacts or processes are
|
|
@@ -5317,7 +5392,7 @@ export function createDecisionServer(options) {
|
|
|
5317
5392
|
.map(task => `<a class="item${task.id === currentId ? " current" : ""}" href="${taskHref(task.id)}">` +
|
|
5318
5393
|
`<span class="t">${escape(task.title)}</span></a>`)
|
|
5319
5394
|
.join("\n");
|
|
5320
|
-
return `<h2>
|
|
5395
|
+
return `<h2>Tasks</h2>\n${items === "" ? `<p class="meta">None yet</p>` : items}`;
|
|
5321
5396
|
}
|
|
5322
5397
|
/**
|
|
5323
5398
|
* The one liveness fact (round-4 findings 14/15): a run is being built
|
|
@@ -5350,7 +5425,7 @@ export function createDecisionServer(options) {
|
|
|
5350
5425
|
`<span class="m">${runOutcomeBadge(run, live.has(run.id))}` +
|
|
5351
5426
|
`<span class="mono">${whenTime(run.startedAt)}</span></span></a>`)
|
|
5352
5427
|
.join("\n");
|
|
5353
|
-
return `<h2>
|
|
5428
|
+
return `<h2>Builds</h2>\n${items === "" ? `<p class="meta">None yet</p>` : items}`;
|
|
5354
5429
|
}
|
|
5355
5430
|
/**
|
|
5356
5431
|
* The opener: every project inside the ceiling, most recently opened
|
|
@@ -5834,6 +5909,8 @@ export function createDecisionServer(options) {
|
|
|
5834
5909
|
// available, computed fresh only when this call has no `who`.
|
|
5835
5910
|
planRevisions,
|
|
5836
5911
|
milestoneProgress: view?.milestoneProgress ?? progressOf(ref.id, planRevisions?.current?.document ?? null),
|
|
5912
|
+
// What a failed task missed, said as the task page says it.
|
|
5913
|
+
failure: task.state === "failed" ? failureOf(family.versions.flatMap(version => store.runsFor(version.refId)), view?.assignment ?? assignmentOf(store, task.id, now, workAccess(), evidenceRoot)) : null,
|
|
5837
5914
|
claimed: view?.claimed ?? store.hasLiveClaim(ref.id, now),
|
|
5838
5915
|
liveRun: live === null ? null : { id: live.id, runner: live.runner, startedAt: live.startedAt, phase: live.phase },
|
|
5839
5916
|
checkProgress: view?.checkProgress ?? (live === null && latest === null ? null : store.checkProgress((live ?? latest).id)),
|
|
@@ -5895,34 +5972,69 @@ export function createDecisionServer(options) {
|
|
|
5895
5972
|
return null;
|
|
5896
5973
|
return { taskId: rootId, view: null, offer: { taskId: receipt.taskId, runId: receipt.runId, digest: receipt.digest }, target };
|
|
5897
5974
|
}
|
|
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) {
|
|
5975
|
+
/** What went wrong with the latest finished attempt (whatever its outcome, never an older failure). */
|
|
5976
|
+
function failureOf(runs, assignment, readLog = true) {
|
|
5903
5977
|
const last = latestFinishedAttempt(runs);
|
|
5904
5978
|
if (last === null)
|
|
5905
|
-
return { line: NO_REASON_RECORDED, link: null };
|
|
5906
|
-
const
|
|
5907
|
-
|
|
5908
|
-
|
|
5979
|
+
return { kind: "reason", line: NO_REASON_RECORDED, evidence: null, suggestion: GENERAL_SUGGESTION, link: null };
|
|
5980
|
+
const full = store.getRun(last.id);
|
|
5981
|
+
return full === null ? { kind: "reason", line: failedAttemptSentence(last.reason), evidence: null, suggestion: stopSuggestionOf(last.reason), link: null }
|
|
5982
|
+
: explainAttempt(full, assignment?.receipt?.runId === last.id ? assignment.receipt : null, readLog);
|
|
5983
|
+
}
|
|
5984
|
+
/** One finished attempt's failure in plain words: a failing check's last error line (linked to that line in its
|
|
5985
|
+
* saved log), else the first signed requirement it missed with the evidence line behind it, else its recorded reason
|
|
5986
|
+
* in plain words (or that none was recorded). Each comes with one suggestion of what to change, and a link to the
|
|
5987
|
+
* attempt's own result page, where its changes, checks and failure are; machine output reads as an internal error,
|
|
5988
|
+
* its detail behind a link to that line of the attempt's record. `readLog` false (a list's rows) reads no saved
|
|
5989
|
+
* check log: the requirement or the reason, from the database alone. */
|
|
5990
|
+
function explainAttempt(run, receipt, readLog = true) {
|
|
5991
|
+
const taskId = store.externalIdFor(run.taskRef) ?? "";
|
|
5992
|
+
const page = `${reviewHref(taskId)}&run=${run.id}`;
|
|
5993
|
+
const see = taskId === "" ? null : { label: `See build #${run.id}`, href: page };
|
|
5994
|
+
const artifacts = store.artifactsFor(run.id);
|
|
5995
|
+
const logArtifact = !readLog ? null : receipt?.checks.status === "failed" && receipt.checks.logArtifactId !== null
|
|
5996
|
+
? artifacts.find(one => one.id === receipt.checks.logArtifactId) ?? null
|
|
5997
|
+
: [...artifacts].reverse().find(one => one.kind === "check-log") ?? null;
|
|
5998
|
+
if (logArtifact !== null) {
|
|
5909
5999
|
let read = null;
|
|
5910
6000
|
try {
|
|
5911
|
-
read =
|
|
6001
|
+
read = readVerifiedArtifact(evidenceRoot, logArtifact);
|
|
5912
6002
|
}
|
|
5913
6003
|
catch {
|
|
5914
6004
|
read = null;
|
|
5915
6005
|
}
|
|
5916
|
-
const
|
|
6006
|
+
const text = read !== null && read.ok ? read.content.toString("utf8") : null;
|
|
6007
|
+
const exit = text === null ? null : /^\(exit (-?[0-9]+)\)$/m.exec(text);
|
|
6008
|
+
const failedCheck = receipt?.checks.status === "failed" || (exit !== null && exit !== undefined && Number(exit[1]) !== 0);
|
|
6009
|
+
const found = text !== null && failedCheck ? lastErrorLineOf(text) : null;
|
|
5917
6010
|
if (found !== null) {
|
|
5918
|
-
|
|
5919
|
-
|
|
6011
|
+
const line = oneLineOf(found.text, 140);
|
|
6012
|
+
return { kind: "check", line: `The check failed: ${line}`, evidence: null, suggestion: failingCheckSuggestion(line),
|
|
6013
|
+
link: taskId === "" ? null : { label: `Check output, line ${found.line}`, href: `${page}&tab=checks#check-log-L${found.line}` } };
|
|
5920
6014
|
}
|
|
5921
6015
|
}
|
|
5922
|
-
|
|
5923
|
-
|
|
6016
|
+
const verdict = store.proofVerdictFor(run.id);
|
|
6017
|
+
const missed = verdict === null ? null : missedRequirementOf(verdict.matrix);
|
|
6018
|
+
if (missed !== null) {
|
|
6019
|
+
return { kind: "requirement", line: missedRequirementLine(missed.statement), evidence: missed.evidence === null ? null : oneLineOf(missed.evidence, 240),
|
|
6020
|
+
suggestion: missedRequirementSuggestion(missed.statement), link: see };
|
|
6021
|
+
}
|
|
6022
|
+
const internal = isInternalErrorReason(run.reason);
|
|
6023
|
+
return { kind: "reason", line: failedAttemptSentence(run.reason), evidence: null, suggestion: stopSuggestionOf(run.reason),
|
|
6024
|
+
link: internal ? { label: "The recorded error", href: `/r/${run.id}?record=1#run-reason-detail` } : see };
|
|
6025
|
+
}
|
|
6026
|
+
/** Run checks in place on a result's commit: an approver's browser session, outside the demo, when the project has a
|
|
6027
|
+
* check and none is waiting or running on it. Comes back to `returnTo`. */
|
|
6028
|
+
function runChecksHere(runId, who, returnTo) {
|
|
6029
|
+
if (runId === null || who.via !== "cookie" || who.role !== "approver" || store.isDemo())
|
|
6030
|
+
return null;
|
|
6031
|
+
const run = store.getRun(runId);
|
|
6032
|
+
const followUps = run === null ? null : followUpsFor(store, evidenceRoot, run, clock());
|
|
6033
|
+
if (followUps === null || !(followUps.quick || followUps.full) || followUps.checks.some(one => one.state === "waiting" || one.state === "running"))
|
|
6034
|
+
return null;
|
|
6035
|
+
return { action: `/r/${runId}/checks`, level: followUps.full ? "full" : "quick", returnTo };
|
|
5924
6036
|
}
|
|
5925
|
-
function taskScreen(response, who, taskId, problem, status, scopeDraft, cancelDraft) {
|
|
6037
|
+
function taskScreen(response, who, taskId, problem, status, scopeDraft, cancelDraft, editPlan = false) {
|
|
5926
6038
|
const admittedFamily = familyOf(taskId);
|
|
5927
6039
|
if (admittedFamily?.problem != null)
|
|
5928
6040
|
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 +6049,8 @@ export function createDecisionServer(options) {
|
|
|
5937
6049
|
presentedData.scopeDraft = scopeDraft;
|
|
5938
6050
|
if (cancelDraft !== undefined)
|
|
5939
6051
|
presentedData.cancelDraft = cancelDraft;
|
|
6052
|
+
if (editPlan)
|
|
6053
|
+
presentedData.editPlan = true;
|
|
5940
6054
|
presentedData.pullRequest = taskPullRequestOf(taskId, who);
|
|
5941
6055
|
// The thread reads the whole family: the original, its revisions, and every attempt across them.
|
|
5942
6056
|
if (family !== null && family.problem === null) {
|
|
@@ -5946,6 +6060,8 @@ export function createDecisionServer(options) {
|
|
|
5946
6060
|
}
|
|
5947
6061
|
// What went wrong reads the family's latest finished attempt, as the Tasks row does.
|
|
5948
6062
|
presentedData.failure = failureOf(presentedData.family?.runs ?? data.runs, data.assignment ?? null);
|
|
6063
|
+
presentedData.runChecks = runChecksHere(data.assignment?.receipt?.runId ?? null, who, taskHref(taskId));
|
|
6064
|
+
presentedData.demo = store.isDemo();
|
|
5949
6065
|
const paneProject = restricted() ? store.lookupRef(taskId)?.repo ?? null : who.via === "cookie" ? who.session.project : null;
|
|
5950
6066
|
const page = taskPage(paneProject === null && !unscopedMode
|
|
5951
6067
|
? chromeFor(paneProject, "tasks")
|
|
@@ -6126,7 +6242,7 @@ export function createDecisionServer(options) {
|
|
|
6126
6242
|
return peekSay(seen.reason);
|
|
6127
6243
|
const stamp = clock().toISOString().slice(11, 19);
|
|
6128
6244
|
const parts = [
|
|
6129
|
-
`<p class="meta">
|
|
6245
|
+
`<p class="meta">Best-effort look at ${escape(stamp)} UTC — files can change mid-read</p>`,
|
|
6130
6246
|
];
|
|
6131
6247
|
const changed = seen.rows.filter(one => one.kind === "changed");
|
|
6132
6248
|
const deleted = seen.rows.filter(one => one.kind === "deleted");
|
|
@@ -6151,7 +6267,7 @@ export function createDecisionServer(options) {
|
|
|
6151
6267
|
for (const row of deleted)
|
|
6152
6268
|
parts.push(line(row, "−"));
|
|
6153
6269
|
if (fresh.total > 0) {
|
|
6154
|
-
parts.push(`<p class="meta">
|
|
6270
|
+
parts.push(`<p class="meta">New files · ${fresh.total}</p>`);
|
|
6155
6271
|
for (const row of fresh.rows) {
|
|
6156
6272
|
parts.push(row.collapsed
|
|
6157
6273
|
? `<p class="row mono">+ ${peekName(row.label)} <span class="meta">collapsed names — ${row.count} files</span></p>`
|
|
@@ -6162,7 +6278,7 @@ export function createDecisionServer(options) {
|
|
|
6162
6278
|
}
|
|
6163
6279
|
}
|
|
6164
6280
|
if (unchecked.length > 0) {
|
|
6165
|
-
parts.push(`<p class="meta">
|
|
6281
|
+
parts.push(`<p class="meta">Not verified this look — absence above does not mean unchanged:</p>`);
|
|
6166
6282
|
for (const row of unchecked)
|
|
6167
6283
|
parts.push(line(row, "?"));
|
|
6168
6284
|
}
|
|
@@ -6173,7 +6289,7 @@ export function createDecisionServer(options) {
|
|
|
6173
6289
|
// The byte cap is enforced AFTER escaping (finding 32): an oversize
|
|
6174
6290
|
// rendering is replaced whole by its exact counts.
|
|
6175
6291
|
fragment =
|
|
6176
|
-
`<p class="meta">
|
|
6292
|
+
`<p class="meta">Best-effort look at ${escape(stamp)} UTC</p>` +
|
|
6177
6293
|
`<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
6294
|
}
|
|
6179
6295
|
peekCache.set(key, { fragment, at: Date.now() });
|
|
@@ -8604,7 +8720,7 @@ export function createDecisionServer(options) {
|
|
|
8604
8720
|
// THE CEREMONY (M1): every resolved term in words, and the password
|
|
8605
8721
|
// signs exactly this digest — a drifted form refuses at /mode/sign.
|
|
8606
8722
|
const nonce = mintApprovalNonce(who.name, "mode-sign", `${project}:${digest}`);
|
|
8607
|
-
const bodyHtml = `<h1 style="overflow-wrap:anywhere">
|
|
8723
|
+
const bodyHtml = `<h1 style="overflow-wrap:anywhere">Sign the ${escape(name)} mode for ${escape(project)}</h1>` +
|
|
8608
8724
|
`<form method="post" action="/mode/sign" class="card approve-form">` +
|
|
8609
8725
|
`<input type="hidden" name="csrf" value="${escape(who.session.csrf)}">` +
|
|
8610
8726
|
`<input type="hidden" name="nonce" value="${escape(nonce)}">` +
|
|
@@ -8613,12 +8729,12 @@ export function createDecisionServer(options) {
|
|
|
8613
8729
|
.map(field => `<input type="hidden" name="${field}" value="${escape(body.get(field) ?? "")}">`)
|
|
8614
8730
|
.join("") +
|
|
8615
8731
|
`<input type="hidden" name="expiry" value="${escape(expiry)}">` +
|
|
8616
|
-
`<p><strong>
|
|
8732
|
+
`<p><strong>Your password signs exactly this:</strong></p>` +
|
|
8617
8733
|
modeWords(terms)
|
|
8618
8734
|
.map(words => `<p class="recap" style="margin-top:.4rem">${escape(words)}</p>`)
|
|
8619
8735
|
.join("") +
|
|
8620
|
-
`<label>
|
|
8621
|
-
`<div class="sticky-actions"><button type="submit">
|
|
8736
|
+
`<label>Your password, typed again<input type="password" name="token" autocomplete="current-password"></label>` +
|
|
8737
|
+
`<div class="sticky-actions"><button type="submit">Sign it</button></div>` +
|
|
8622
8738
|
`</form>` +
|
|
8623
8739
|
`<p class="meta"><a href="/mode">back — sign nothing</a></p>`;
|
|
8624
8740
|
return sendScreen(response, 200, screen("mode", bodyHtml, { chrome: chromeFor(project, "mode") }));
|
|
@@ -9105,6 +9221,25 @@ export function createDecisionServer(options) {
|
|
|
9105
9221
|
}
|
|
9106
9222
|
// Turn the lead on with the agent signed in on this computer: its membership spends no dollars, so like starting a
|
|
9107
9223
|
// conversation it needs no second password; the full form (Advanced) still asks for one.
|
|
9224
|
+
// Settings → Lead: the name and persona this person's lead speaks with.
|
|
9225
|
+
if (url.pathname === "/settings/lead/identity") {
|
|
9226
|
+
if (who.via !== "cookie" || who.role !== "approver")
|
|
9227
|
+
return refuse(response, who, 403, "An approver names their lead.", "/settings/lead");
|
|
9228
|
+
const checked = checkLeadIdentity(body.get("name") ?? "", body.get("persona") ?? "");
|
|
9229
|
+
if (!checked.ok)
|
|
9230
|
+
return redirect(response, chatReturnWithSaid("/settings/lead", checked.message));
|
|
9231
|
+
store.setLeadConfig(who.name, checked.identity.name, checked.identity.persona, now);
|
|
9232
|
+
return redirect(response, "/settings/lead?saved=1");
|
|
9233
|
+
}
|
|
9234
|
+
// Settings → Lead: stop the lead following up on one of your promises.
|
|
9235
|
+
if (url.pathname === "/settings/lead/promise/cancel") {
|
|
9236
|
+
if (who.via !== "cookie" || who.role !== "approver")
|
|
9237
|
+
return refuse(response, who, 403, "An approver manages the lead's promises.", "/settings/lead");
|
|
9238
|
+
const id = Number(body.get("promise"));
|
|
9239
|
+
const done = Number.isSafeInteger(id) && cancelCommitment(store, who.name, id, who.name, "Cancelled in Settings.", now);
|
|
9240
|
+
// The promise leaving the list says it was cancelled; only a refusal needs words.
|
|
9241
|
+
return redirect(response, done ? "/settings/lead" : chatReturnWithSaid("/settings/lead", "That promise was already closed."));
|
|
9242
|
+
}
|
|
9108
9243
|
if (url.pathname === "/settings/lead/on") {
|
|
9109
9244
|
if (who.via !== "cookie" || who.role !== "approver")
|
|
9110
9245
|
return refuse(response, who, 403, "An approver turns the lead on.", "/settings/lead");
|
|
@@ -9418,7 +9553,7 @@ export function createDecisionServer(options) {
|
|
|
9418
9553
|
}
|
|
9419
9554
|
const opened = store.openMateThread(who.name, principal.ceilingDigest, now, chatScopeOf(focusTask, chatProject));
|
|
9420
9555
|
const onProgress = beginLiveTurn(opened.thread.id);
|
|
9421
|
-
void runMateTurn({ store, who: principal, session: mateSession, thread: opened.thread, config: enabled.config, key: enabled.key, message, onProgress, ...(requestId === null ? {} : { requestId }), ...(focusTask === null && chatProject !== null ? { context: `Current project: ${projectName(chatProject)} (${chatProject}). Keep this conversation about that project unless the operator explicitly asks to broaden it; use it as the repo for project tools.` } : {}), ...(focusTask === null ? {} : { context: `Current task: ${focusTask.id}. Read it with get_task before answering or proposing changes. Read its currentExecution next and bind new actions to that exact execution. Never replace the target of a prior proposal with a newer revision. Keep this turn about that task unless the operator explicitly asks to broaden it.${resultContext}${modeContext}` }), fetcher: chatFetcher, ...(options.subscriptionChatRunner === undefined ? {} : { subscriptionRunner: options.subscriptionChatRunner }), clock, evidenceRoot })
|
|
9556
|
+
void runMateTurn({ store, who: principal, session: mateSession, thread: opened.thread, config: enabled.config, key: enabled.key, message, onProgress, channel: "console", ...(requestId === null ? {} : { requestId }), ...(focusTask === null && chatProject !== null ? { context: `Current project: ${projectName(chatProject)} (${chatProject}). Keep this conversation about that project unless the operator explicitly asks to broaden it; use it as the repo for project tools.` } : {}), ...(focusTask === null ? {} : { context: `Current task: ${focusTask.id}. Read it with get_task before answering or proposing changes. Read its currentExecution next and bind new actions to that exact execution. Never replace the target of a prior proposal with a newer revision. Keep this turn about that task unless the operator explicitly asks to broaden it.${resultContext}${modeContext}` }), fetcher: chatFetcher, ...(options.subscriptionChatRunner === undefined ? {} : { subscriptionRunner: options.subscriptionChatRunner }), clock, evidenceRoot })
|
|
9422
9557
|
.then(outcome => {
|
|
9423
9558
|
if (!outcome.ok)
|
|
9424
9559
|
noteMate(who.session.csrf, "turn" in outcome ? outcome.turn : null, outcome.message);
|
|
@@ -9936,7 +10071,9 @@ export function createDecisionServer(options) {
|
|
|
9936
10071
|
const found = store.getRun(id);
|
|
9937
10072
|
if (found === null || !visible(taskRepoOf(found.taskRef)))
|
|
9938
10073
|
return refuse(response, who, 404, "no such run");
|
|
9939
|
-
|
|
10074
|
+
// The task page that offered Run checks in place is a return target too: this run's own task, or its family's root.
|
|
10075
|
+
const runTask = store.externalIdFor(found.taskRef);
|
|
10076
|
+
const back = resultReturnTarget(body.get("return"), id, runTask === null ? [] : [runTask, familyOf(runTask)?.root.id ?? runTask]);
|
|
9940
10077
|
if (who.via !== "cookie")
|
|
9941
10078
|
return refuse(response, who, 403, "Sign in with a browser session to do this.", back);
|
|
9942
10079
|
if (followUp[2] === "checks") {
|
|
@@ -9948,7 +10085,8 @@ export function createDecisionServer(options) {
|
|
|
9948
10085
|
const asked = requestFollowUpChecks(store, { runId: id, level, actor: who.name }, now);
|
|
9949
10086
|
if (!asked.ok)
|
|
9950
10087
|
return refuse(response, who, 409, asked.message, back);
|
|
9951
|
-
|
|
10088
|
+
// Back where it was asked: the task page's status row says the checks are running; a result page shows them under #follow-ups.
|
|
10089
|
+
return redirect(response, back.startsWith("/t/") ? back : `${back.split("#")[0]}#follow-ups`);
|
|
9952
10090
|
}
|
|
9953
10091
|
const filed = fileAddTestsTask(store, evidenceRoot, { runId: id, actor: who.name, filedVia: "console", ...(admissionList() === null ? {} : { admittedRepos: admissionList() }) }, now);
|
|
9954
10092
|
if (!filed.ok)
|
|
@@ -10162,7 +10300,7 @@ export function createDecisionServer(options) {
|
|
|
10162
10300
|
const quickMode = quickRef?.repo == null ? null : store.activeMode(quickRef.repo, clock());
|
|
10163
10301
|
const quickTerms = quickMode === null ? null : modeTermsFromJson(quickMode.termsJson);
|
|
10164
10302
|
const quick = quickTerms?.quickMint === true && quickMode !== null && quickMode.signedBy === who.name;
|
|
10165
|
-
const body = `<h1>
|
|
10303
|
+
const body = `<h1>Run ${escape(terms.taskId)} once, while you watch</h1>` +
|
|
10166
10304
|
`<form method="post" action="${taskHref(terms.taskId)}/attend" class="card approve-form">` +
|
|
10167
10305
|
`<input type="hidden" name="csrf" value="${escape(csrf)}">` +
|
|
10168
10306
|
`<input type="hidden" name="nonce" value="${escape(nonce)}">` +
|
|
@@ -10175,10 +10313,10 @@ export function createDecisionServer(options) {
|
|
|
10175
10313
|
(inputs.posture === undefined ? "" : `<input type="hidden" name="posture" value="${escape(inputs.posture)}">`) +
|
|
10176
10314
|
(terms.parentRun == null ? "" : `<input type="hidden" name="parent" value="${terms.parentRun}">`) +
|
|
10177
10315
|
(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">
|
|
10316
|
+
`<p><strong>Your password signs exactly this:</strong></p>` +
|
|
10317
|
+
`<p class="meta">Goal</p><p class="recap" style="margin-top:0">${escape(scope?.goal ?? "")}</p>` +
|
|
10318
|
+
`<p class="meta">Not this</p><p class="recap" style="margin-top:0">${scope?.outOfScope == null ? "<em>no exclusions</em>" : escape(scope.outOfScope)}</p>` +
|
|
10319
|
+
`<p class="meta">Touches · ${scope === null || scope.touches.length === 0 ? "anything" : scope.touches.map(one => escape(one)).join(", ")}</p>` +
|
|
10182
10320
|
(() => {
|
|
10183
10321
|
const profile = JSON.parse(terms.profileJson).profile;
|
|
10184
10322
|
const posture = profile?.permissionArgv === "bypassPermissions"
|
|
@@ -10186,21 +10324,21 @@ export function createDecisionServer(options) {
|
|
|
10186
10324
|
: profile?.permissionArgv === "auto"
|
|
10187
10325
|
? "safe unattended permissions — routine project commands and edits proceed; risky acts stop"
|
|
10188
10326
|
: "legacy acceptEdits — edits proceed, commands that ask are denied unattended";
|
|
10189
|
-
return `<p class="meta">
|
|
10327
|
+
return `<p class="meta">Runs on</p><p class="recap" style="margin-top:0">claude · ${escape(String(profile?.model ?? ""))} — ${posture}</p>`;
|
|
10190
10328
|
})() +
|
|
10191
10329
|
(terms.parentRun == null
|
|
10192
10330
|
? ""
|
|
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">
|
|
10331
|
+
: `<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>` +
|
|
10332
|
+
`<p class="meta">The follow-up — this is the instruction</p><p class="recap" style="margin-top:0">${escape(terms.followup ?? "")}</p>`) +
|
|
10333
|
+
`<p class="meta">Repository · head</p><p class="recap mono" style="margin-top:0">${escape(terms.repo)} @ ${escape(terms.head.slice(0, 12))}</p>` +
|
|
10334
|
+
`<p class="meta">Worker</p><p class="recap" style="margin-top:0">${escape(terms.runner)} (this machine)</p>` +
|
|
10335
|
+
`<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>` +
|
|
10336
|
+
`<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>` +
|
|
10337
|
+
`<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
10338
|
(quick
|
|
10201
|
-
? `<p class="meta">
|
|
10202
|
-
: `<label>
|
|
10203
|
-
`<div class="sticky-actions"><button type="submit">
|
|
10339
|
+
? `<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>`
|
|
10340
|
+
: `<label>Your password, typed again — a signed-in session alone cannot authorize work<input type="password" name="token" autocomplete="current-password"></label>`) +
|
|
10341
|
+
`<div class="sticky-actions"><button type="submit">Run it while I watch</button></div>` +
|
|
10204
10342
|
`</form>` +
|
|
10205
10343
|
`<p class="meta"><a href="${taskHref(terms.taskId)}">back to the task</a></p>`;
|
|
10206
10344
|
// The chrome binds to the AUTHORITY repository (surfaces round 1,
|
|
@@ -10748,6 +10886,13 @@ export function createDecisionServer(options) {
|
|
|
10748
10886
|
if (budgetUsd !== null && (!Number.isFinite(budgetUsd) || budgetUsd <= 0)) {
|
|
10749
10887
|
return taskScreen(response, who, taskId, "the dollar cap is a positive amount", 400);
|
|
10750
10888
|
}
|
|
10889
|
+
// The approval sheet's in-place editor carries the cap it was shown
|
|
10890
|
+
// exactly, in millionths, and "none" for no limit (never a default).
|
|
10891
|
+
const exactBudgetGiven = body.get("budget-microusd");
|
|
10892
|
+
const exactBudget = exactBudgetGiven === null ? undefined : exactBudgetGiven === "none" ? null : /^[1-9][0-9]{0,14}$/.test(exactBudgetGiven) ? Number(exactBudgetGiven) : NaN;
|
|
10893
|
+
if (Number.isNaN(exactBudget))
|
|
10894
|
+
return taskScreen(response, who, taskId, "the dollar cap is a positive amount", 400);
|
|
10895
|
+
const inPlace = body.has("requirement-new");
|
|
10751
10896
|
// The tournament controls (operator request): a count of 2–4 files
|
|
10752
10897
|
// race terms BESIDE the scope — validated and priced BEFORE anything
|
|
10753
10898
|
// saves, so a bad tournament never half-lands on a good scope.
|
|
@@ -10802,94 +10947,120 @@ export function createDecisionServer(options) {
|
|
|
10802
10947
|
if (plannedRace !== null && store.mirrorByTask(taskId) !== null) {
|
|
10803
10948
|
return taskScreen(response, who, taskId, "external work races in a follow-up release — file the tournament on a local task", 409);
|
|
10804
10949
|
}
|
|
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
|
-
|
|
10950
|
+
let saved;
|
|
10951
|
+
try {
|
|
10952
|
+
saved = store.transact(() => {
|
|
10953
|
+
if ((plannedRace !== null || plannedComparison !== null) && store.openAuthorizationFor(ref.id) !== null) {
|
|
10954
|
+
return { ok: false, status: 409, message: "an attended authorization is open on this task — revoke it before filing a tournament or comparison" };
|
|
10955
|
+
}
|
|
10956
|
+
// C1/M3: the signer's own credentialed filing auto-approves —
|
|
10957
|
+
// coverage is asked INSIDE this transaction, the escalated
|
|
10958
|
+
// default and budget ride the filing, and the seal commits
|
|
10959
|
+
// atomically with it. Plain scopes only: a tournament or
|
|
10960
|
+
// comparison keeps its own human ceremony.
|
|
10961
|
+
// COOKIE ONLY (C1's channel table; surfaces round 1, finding 1):
|
|
10962
|
+
// bearer credentials are for machines, and a machine road must
|
|
10963
|
+
// never spend the signer's mode.
|
|
10964
|
+
const coverage = who.via === "cookie" && plannedRace === null && plannedComparison === null
|
|
10965
|
+
? modeFilingCoverage(store, ref.repo, who.name, now)
|
|
10966
|
+
: null;
|
|
10967
|
+
const before = store.getScope(taskId);
|
|
10968
|
+
const proposed = proposeGuarded(store, {
|
|
10969
|
+
taskId, author: who.name,
|
|
10970
|
+
goal: body.has("goal-brief") ? `${(body.get("goal") ?? "").trim()}${body.get("goal-brief")}` : body.get("goal") ?? "",
|
|
10971
|
+
outOfScope: body.get("not") ?? null,
|
|
10972
|
+
touches: (body.get("touches") ?? "").split(/[\n,]/),
|
|
10973
|
+
// The approval sheet's in-place editor sends Done when line by line.
|
|
10974
|
+
acceptance: inPlace
|
|
10975
|
+
? requirementsFromEditor(body, before?.acceptance ?? [])
|
|
10976
|
+
: acceptanceLinesToInput((body.get("acceptance") ?? "").split("\n")),
|
|
10977
|
+
permissionMode,
|
|
10978
|
+
qualityMode,
|
|
10979
|
+
...(exactBudget !== undefined
|
|
10980
|
+
? exactBudget === null ? {} : { budgetMicrousd: exactBudget }
|
|
10981
|
+
: budgetUsd !== null
|
|
10982
|
+
? { budgetMicrousd: Math.round(budgetUsd * 1_000_000) }
|
|
10983
|
+
: coverage?.defaultBudgetMicrousd != null
|
|
10984
|
+
? { budgetMicrousd: coverage.defaultBudgetMicrousd }
|
|
10985
|
+
: {}),
|
|
10986
|
+
...(coverage?.escalated === true ? { posture: "escalated" } : {}),
|
|
10987
|
+
sawDigest: sawDigest === null || sawDigest === "" ? null : sawDigest,
|
|
10988
|
+
taskRef: ref.id,
|
|
10989
|
+
now,
|
|
10990
|
+
});
|
|
10991
|
+
if (!proposed.ok) {
|
|
10992
|
+
return {
|
|
10993
|
+
ok: false,
|
|
10994
|
+
status: proposed.reason === "changed" || proposed.reason === "claimed" ? 409 : 400,
|
|
10995
|
+
message: proposed.message ?? (proposed.reason === "acceptance-required" ? "Not saved: add at least one requirement under Done when." : `scope not saved: ${proposed.reason}`),
|
|
10996
|
+
};
|
|
10997
|
+
}
|
|
10998
|
+
// Editing the plan in place never changes what the agent may do:
|
|
10999
|
+
// a save that would (a legacy setting, a newer policy or mode)
|
|
11000
|
+
// rolls back whole.
|
|
11001
|
+
if (inPlace && before?.profile != null && proposed.scope.profile != null && levelOfProfile(before.profile) !== levelOfProfile(proposed.scope.profile)) {
|
|
11002
|
+
throw new PermissionsWouldChange();
|
|
11003
|
+
}
|
|
11004
|
+
if (coverage !== null && ref.plan === "requested") {
|
|
11005
|
+
authorizePlanUnderMode(store, taskId, who.name, now);
|
|
11006
|
+
}
|
|
11007
|
+
else if (coverage !== null) {
|
|
11008
|
+
const filedScope = store.getScope(taskId);
|
|
11009
|
+
if (filedScope?.profileState === "resolved") {
|
|
11010
|
+
const sealedUnderMode = store.sealScopeApproval(taskId, who.name, now, {}, { kind: "mode", modeDigest: coverage.digest });
|
|
11011
|
+
// The quarantine speaks at the caller (review finding 7).
|
|
11012
|
+
if (!sealedUnderMode) {
|
|
11013
|
+
return { ok: false, status: 200, message: "scope saved — coordinator-filed: mode coverage cannot admit it; sign the scope" };
|
|
11014
|
+
}
|
|
10855
11015
|
}
|
|
10856
11016
|
}
|
|
10857
|
-
|
|
10858
|
-
|
|
11017
|
+
if (plannedComparison !== null && plannedComparison.ok) {
|
|
11018
|
+
store.fileTournamentTerms({
|
|
11019
|
+
taskRef: ref.id,
|
|
11020
|
+
kind: "comparison",
|
|
11021
|
+
raceDigest: plannedComparison.plan.comparisonDigest,
|
|
11022
|
+
agents: plannedComparison.plan.agents,
|
|
11023
|
+
perAgentBudgetMicrousd: 0,
|
|
11024
|
+
overrunReserveMicrousd: 0,
|
|
11025
|
+
totalBudgetMicrousd: 0,
|
|
11026
|
+
priceVersion: 0,
|
|
11027
|
+
publicationPolicy: plannedComparison.plan.publicationPolicy,
|
|
11028
|
+
}, now);
|
|
11029
|
+
return { ok: true };
|
|
11030
|
+
}
|
|
11031
|
+
if (plannedRace === null) {
|
|
11032
|
+
// Switching back to "one agent" withdraws a standing race OR
|
|
11033
|
+
// comparison — the deactivated row survives as history, and
|
|
11034
|
+
// the approval card returns to the scope alone.
|
|
11035
|
+
store.retractTournamentTerms(ref.id);
|
|
11036
|
+
return { ok: true };
|
|
11037
|
+
}
|
|
10859
11038
|
store.fileTournamentTerms({
|
|
10860
11039
|
taskRef: ref.id,
|
|
10861
|
-
|
|
10862
|
-
|
|
10863
|
-
|
|
10864
|
-
|
|
10865
|
-
|
|
10866
|
-
|
|
10867
|
-
|
|
10868
|
-
publicationPolicy: plannedComparison.plan.publicationPolicy,
|
|
11040
|
+
raceDigest: plannedRace.raceDigest,
|
|
11041
|
+
agents: plannedRace.agents,
|
|
11042
|
+
perAgentBudgetMicrousd: plannedRace.perAgentBudgetMicrousd,
|
|
11043
|
+
overrunReserveMicrousd: plannedRace.overrunReserveMicrousd,
|
|
11044
|
+
totalBudgetMicrousd: plannedRace.totalBudgetMicrousd,
|
|
11045
|
+
priceVersion: plannedRace.priceVersion,
|
|
11046
|
+
publicationPolicy: plannedRace.publicationPolicy,
|
|
10869
11047
|
}, now);
|
|
10870
11048
|
return { ok: true };
|
|
10871
|
-
}
|
|
10872
|
-
|
|
10873
|
-
|
|
10874
|
-
|
|
10875
|
-
|
|
10876
|
-
|
|
10877
|
-
|
|
10878
|
-
|
|
10879
|
-
|
|
10880
|
-
|
|
10881
|
-
|
|
10882
|
-
|
|
10883
|
-
|
|
10884
|
-
|
|
10885
|
-
|
|
10886
|
-
priceVersion: plannedRace.priceVersion,
|
|
10887
|
-
publicationPolicy: plannedRace.publicationPolicy,
|
|
10888
|
-
}, now);
|
|
10889
|
-
return { ok: true };
|
|
10890
|
-
});
|
|
10891
|
-
if (!saved.ok)
|
|
10892
|
-
return taskScreen(response, who, taskId, saved.message, saved.status, body);
|
|
11049
|
+
});
|
|
11050
|
+
}
|
|
11051
|
+
catch (error) {
|
|
11052
|
+
if (!(error instanceof PermissionsWouldChange))
|
|
11053
|
+
throw error;
|
|
11054
|
+
saved = { ok: false, status: 409, message: "Not saved: this would change what the agent may do. Change permissions in Details, under edit the scope." };
|
|
11055
|
+
}
|
|
11056
|
+
if (!saved.ok) {
|
|
11057
|
+
// Refused because the plan moved on: the draft comes back with the version now on file, so saving it
|
|
11058
|
+
// again (having read the reason) can succeed instead of being refused the same way.
|
|
11059
|
+
const draft = new URLSearchParams(body);
|
|
11060
|
+
if (saved.status === 409)
|
|
11061
|
+
draft.set("sawDigest", store.getScope(taskId)?.digest ?? "");
|
|
11062
|
+
return taskScreen(response, who, taskId, saved.message, saved.status, draft);
|
|
11063
|
+
}
|
|
10893
11064
|
return redirect(response, taskHref(taskId));
|
|
10894
11065
|
}
|
|
10895
11066
|
case "approve": {
|
|
@@ -11003,6 +11174,14 @@ export function createDecisionServer(options) {
|
|
|
11003
11174
|
}
|
|
11004
11175
|
note = validated.note;
|
|
11005
11176
|
}
|
|
11177
|
+
// A failed task's result is accepted only anyway, on purpose: with a reason.
|
|
11178
|
+
if (note === null && store.getTask(taskId)?.state === "failed") {
|
|
11179
|
+
// Posted from the result page: back there, where the reason field is, with the refusal said on it.
|
|
11180
|
+
const back = body.get("return") === null ? null : safeReturn(body.get("return"));
|
|
11181
|
+
if (back !== null && back.startsWith("/review?"))
|
|
11182
|
+
return redirect(response, withRefusal(back, "reason"));
|
|
11183
|
+
return taskScreen(response, who, taskId, ACCEPT_ANYWAY_NEEDS_REASON, 400);
|
|
11184
|
+
}
|
|
11006
11185
|
store.acceptProof(latest.id, verifiedAuthor(who.name), note, now);
|
|
11007
11186
|
return redirect(response, body.get("return") === null ? taskHref(taskId) : safeReturn(body.get("return")));
|
|
11008
11187
|
}
|
|
@@ -11018,6 +11197,23 @@ export function createDecisionServer(options) {
|
|
|
11018
11197
|
if (current === null || current.receipt?.taskId !== taskId || !/^[0-9]{1,15}$/.test(namedRun) || current.receipt.runId !== Number(namedRun)) {
|
|
11019
11198
|
return taskScreen(response, who, taskId, "This result changed. Open the current result before marking it complete.", 409);
|
|
11020
11199
|
}
|
|
11200
|
+
if (body.get("accept") === "1") {
|
|
11201
|
+
// Accept and finish: the person's acceptance and the completion of what it accepted, in one request
|
|
11202
|
+
// and one transaction. The receipt named is the one read, before accepting.
|
|
11203
|
+
const rawNote = (body.get("note") ?? "").trim();
|
|
11204
|
+
let note = null;
|
|
11205
|
+
if (rawNote !== "") {
|
|
11206
|
+
const validated = validateNote(rawNote);
|
|
11207
|
+
if (!validated.ok)
|
|
11208
|
+
return taskScreen(response, who, taskId, validated.problem, 400);
|
|
11209
|
+
note = validated.note;
|
|
11210
|
+
}
|
|
11211
|
+
const publish = body.get("publish") === "1";
|
|
11212
|
+
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);
|
|
11213
|
+
if (!finished.ok)
|
|
11214
|
+
return taskScreen(response, who, taskId, finished.message, finished.reason === "needs-reason" ? 400 : 409);
|
|
11215
|
+
return redirect(response, publish ? `${taskHref(familyOf(taskId)?.root.id ?? taskId)}#merge` : body.get("return") === null ? reviewHref(taskId, Number(namedRun)) : safeReturn(body.get("return")));
|
|
11216
|
+
}
|
|
11021
11217
|
if (body.get("publish") === "1") {
|
|
11022
11218
|
// "Complete and open a pull request": the same exact-receipt completion, plus the owed PR for this
|
|
11023
11219
|
// exact commit in one transaction. The watch process pushes and opens it; the task shows its progress.
|
|
@@ -11438,6 +11634,36 @@ export function createDecisionServer(options) {
|
|
|
11438
11634
|
const run = store.runsFor(ref.id).find(one => (one.outcome === "built" || one.outcome === "no-change") &&
|
|
11439
11635
|
one.finishedAt !== null &&
|
|
11440
11636
|
(one.role === "builder" || one.role === "scout")) ?? null;
|
|
11637
|
+
return resultRowOf(task, ref.repo, family.problem, run);
|
|
11638
|
+
}
|
|
11639
|
+
/**
|
|
11640
|
+
* A build attempt that didn't deliver a result (failed, refused, stopped,
|
|
11641
|
+
* never finished) still has a result page: its changes, its checks and what
|
|
11642
|
+
* went wrong. The row for one: the run named by `runId` when it is an ended
|
|
11643
|
+
* builder or scout attempt of this task that isn't a delivered result, or a
|
|
11644
|
+
* failed task's latest attempt; with no run named, that latest attempt. The
|
|
11645
|
+
* same admission as a completed row: in this project (or none) and inside
|
|
11646
|
+
* the ceiling. Null otherwise, which reads like a task that never existed.
|
|
11647
|
+
*/
|
|
11648
|
+
function attemptRowFor(taskId, runId, project) {
|
|
11649
|
+
const family = familyOf(taskId);
|
|
11650
|
+
const task = store.getTask(taskId);
|
|
11651
|
+
const ref = store.lookupRef(taskId);
|
|
11652
|
+
if (family === null || task === null || ref === null)
|
|
11653
|
+
return null;
|
|
11654
|
+
if (!(project === null || ref.repo === null || ref.repo === project) || !visible(ref.repo))
|
|
11655
|
+
return null;
|
|
11656
|
+
const attempts = store.runsFor(ref.id).filter(one => (one.role === "builder" || one.role === "scout") && (one.finishedAt !== null || one.outcome !== null));
|
|
11657
|
+
const latest = task.state === "failed" ? latestFinishedAttempt(attempts) : null;
|
|
11658
|
+
const run = runId !== null ? attempts.find(one => one.id === runId) ?? null : latest;
|
|
11659
|
+
// A delivered result has its completed row; a failed task's latest attempt is what failed, whatever it ended as.
|
|
11660
|
+
if (run === null || runIsLive(run) || ((run.outcome === "built" || run.outcome === "no-change") && run.id !== latest?.id))
|
|
11661
|
+
return null;
|
|
11662
|
+
return resultRowOf(task, ref.repo, family.problem, run);
|
|
11663
|
+
}
|
|
11664
|
+
function resultRowOf(task, repo, historyProblem, run) {
|
|
11665
|
+
const ref = { repo };
|
|
11666
|
+
const family = { problem: historyProblem };
|
|
11441
11667
|
const publication = run === null ? null : store.publicationForRun(run.id);
|
|
11442
11668
|
const verdict = run === null ? null : store.proofVerdictFor(run.id);
|
|
11443
11669
|
return {
|
|
@@ -11628,6 +11854,7 @@ export function createDecisionServer(options) {
|
|
|
11628
11854
|
intent,
|
|
11629
11855
|
plan,
|
|
11630
11856
|
contest: contestOf(),
|
|
11857
|
+
...(store.isDemo() ? { demo: true } : {}),
|
|
11631
11858
|
};
|
|
11632
11859
|
if (run === null) {
|
|
11633
11860
|
return { ...base, run: null, detail: null, reviewRetry: null, review: null, notes: [] };
|
|
@@ -11949,8 +12176,8 @@ export function editorFileHref(worktree, path, line) {
|
|
|
11949
12176
|
function matrixStateBadge(state) {
|
|
11950
12177
|
// Red belongs to a Failed headline alone (task-status.ts); an unmet requirement is an amber note.
|
|
11951
12178
|
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(
|
|
12179
|
+
// The same words as the Checks tab and the card's Requirements row (task-status.ts).
|
|
12180
|
+
return `<span class="badge ${cls}" data-matrix-state="${escape(state)}">${escape(requirementWordOf({ state }))}</span>`;
|
|
11954
12181
|
}
|
|
11955
12182
|
/** v40: the bounded repair chain's own line — presented as the user-facing
|
|
11956
12183
|
* recovery it represents, without exposing the internal workflow name.
|
|
@@ -12051,8 +12278,9 @@ function criterionMatrixHtml(matrix, options = {}) {
|
|
|
12051
12278
|
const awaitingAssessment = row.assessment?.evidenceState === "pass" && row.review === null;
|
|
12052
12279
|
const confirmed = row.assessment !== undefined && state === "pass";
|
|
12053
12280
|
// Plain words (task-status brief): the retired assessment step is not a state a person acts on.
|
|
12054
|
-
|
|
12055
|
-
const
|
|
12281
|
+
// The card's Requirements row reads the same words from the same source (task-status.ts), so they agree.
|
|
12282
|
+
const label = requirementWordOf(row, options.verdict);
|
|
12283
|
+
const cls = label === "Met" ? "badge-done" : label === "You check" ? "badge-manual-review" : "badge-note";
|
|
12056
12284
|
const warnings = [];
|
|
12057
12285
|
// Only replace the known boilerplate. Other recorded failure details
|
|
12058
12286
|
// remain verbatim, so concision cannot hide a different problem.
|
|
@@ -12122,10 +12350,10 @@ function planContractHtml(view, mode) {
|
|
|
12122
12350
|
if ("problem" in view) {
|
|
12123
12351
|
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
12352
|
}
|
|
12125
|
-
const stale = view.current ? "" : `<p class="meta">
|
|
12353
|
+
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
12354
|
if (!view.filed) {
|
|
12127
12355
|
return mode === "full"
|
|
12128
|
-
? `<div class="contract-panel contract-drafted"><p class="approval-label">filed contract</p><p class="meta">
|
|
12356
|
+
? `<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
12357
|
: `<p class="meta contract-note">no scope was filed before planning — every term above is the planner's proposal</p>`;
|
|
12130
12358
|
}
|
|
12131
12359
|
if (view.revision === true) {
|
|
@@ -12135,19 +12363,19 @@ function planContractHtml(view, mode) {
|
|
|
12135
12363
|
}
|
|
12136
12364
|
return (`<div class="contract-panel contract-amended"${mode === "full" ? ` id="contract-amendment"` : ""}><p class="approval-label">updated plan</p>` +
|
|
12137
12365
|
`<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>
|
|
12366
|
+
(view.amendment === null ? "" : `<p class="recap contract-reason"><strong>Why:</strong> ${escape(view.amendment)}</p>`) +
|
|
12139
12367
|
`<ul class="recap contract-changes">${view.changes.map(contractChangeHtml).join("")}</ul>` +
|
|
12140
12368
|
stale +
|
|
12141
12369
|
`</div>`);
|
|
12142
12370
|
}
|
|
12143
12371
|
if (view.changes.length === 0) {
|
|
12144
|
-
return `<div class="contract-panel contract-preserved"><p class="approval-label">filed contract</p><p><strong>
|
|
12372
|
+
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
12373
|
}
|
|
12146
12374
|
return (`<div class="contract-panel contract-amended"${mode === "full" ? ` id="contract-amendment"` : ""}><p class="approval-label">filed contract · amendment proposed</p>` +
|
|
12147
12375
|
`<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
12376
|
(view.amendment === null
|
|
12149
|
-
? `<p class="meta">
|
|
12150
|
-
: `<p class="recap contract-reason"><strong>
|
|
12377
|
+
? `<p class="meta">The planner stated no reason for the amendment</p>`
|
|
12378
|
+
: `<p class="recap contract-reason"><strong>Why:</strong> ${escape(view.amendment)}</p>`) +
|
|
12151
12379
|
`<ul class="recap contract-changes">${view.changes.map(contractChangeHtml).join("")}</ul>` +
|
|
12152
12380
|
stale +
|
|
12153
12381
|
`</div>`);
|
|
@@ -12176,7 +12404,7 @@ function approvalPathsHtml(touches) {
|
|
|
12176
12404
|
function acceptanceCeremonyHtml(criteria) {
|
|
12177
12405
|
if (criteria.length === 0)
|
|
12178
12406
|
return "";
|
|
12179
|
-
return (`<p class="meta">
|
|
12407
|
+
return (`<p class="meta">Acceptance</p><ul class="recap acceptance-rubric">` +
|
|
12180
12408
|
criteria
|
|
12181
12409
|
.map(c => `<li><code>${escape(c.id)}</code> ${escape(c.statement)} <span class="meta">[requires: ${c.evidence.map(escape).join(", ")}]</span></li>`)
|
|
12182
12410
|
.join("") +
|
|
@@ -12294,10 +12522,20 @@ function approvalSheetHtml(input) {
|
|
|
12294
12522
|
const planner = legOf("plan");
|
|
12295
12523
|
if (planner !== undefined)
|
|
12296
12524
|
who.push(`Planner ${agentNameWords(planner.provider, planner.model)}`);
|
|
12297
|
-
|
|
12298
|
-
|
|
12299
|
-
|
|
12300
|
-
|
|
12525
|
+
// What the yes allows, in plain words, right above it: the agent's
|
|
12526
|
+
// permissions, the per-attempt limit, and an earlier version still running.
|
|
12527
|
+
const allowing = [];
|
|
12528
|
+
const allowed = permissionPlainWordsOf(scope.profile);
|
|
12529
|
+
if (allowed !== null)
|
|
12530
|
+
allowing.push(allowed);
|
|
12531
|
+
if (raceTerms === null)
|
|
12532
|
+
allowing.push(scope.budgetMicrousd === null ? "no attempt limit" : `up to ${money(scope.budgetMicrousd)} per attempt`);
|
|
12533
|
+
const earlier = earlierVersionsWords(input.earlier.active, input.earlier.running);
|
|
12534
|
+
if (earlier !== null)
|
|
12535
|
+
allowing.push(earlier);
|
|
12536
|
+
const yoursToCheck = scope.acceptance.filter(one => one.evidence.includes("manual-review")).map(one => one.statement.trim().replace(/[.\s]+$/, ""));
|
|
12537
|
+
const consent = (allowing.length === 0 ? "" : `<p class="approval-allowing" data-approval-allowing>You’re allowing: ${allowing.map(escape).join(" · ")}</p>`) +
|
|
12538
|
+
(yoursToCheck.length === 0 ? "" : `<p class="approval-you-check" data-approval-you-check>You’ll check: ${yoursToCheck.map(escape).join("; ")}</p>`);
|
|
12301
12539
|
const race = raceTerms === null
|
|
12302
12540
|
? ""
|
|
12303
12541
|
: `<p class="approval-race">${raceTerms.n} agents build this separately: ${raceTerms.agents.map(one => escape(agentNameWords(one.provider, one.model))).join(" and ")}. ` +
|
|
@@ -12324,7 +12562,7 @@ function approvalSheetHtml(input) {
|
|
|
12324
12562
|
: !contract.filed && contract.revision !== true ? `<p class="meta">Nothing was filed before planning, so every term is the planner's proposal.</p>`
|
|
12325
12563
|
: `<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
12564
|
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">
|
|
12565
|
+
: `<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
12566
|
const projection = route?.projection ?? null;
|
|
12329
12567
|
const agentsDetail = route === null || route === undefined ? ""
|
|
12330
12568
|
: projection === null
|
|
@@ -12338,7 +12576,7 @@ function approvalSheetHtml(input) {
|
|
|
12338
12576
|
const limits = profileWords(scope) +
|
|
12339
12577
|
`<p class="meta">Checks level: ${escape(qualityModeTitle(scope.qualityMode ?? "default"))}${permission === null ? "" : ` · ${escape(permission)}`}</p>` +
|
|
12340
12578
|
(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>
|
|
12579
|
+
const details = `<details class="approval-details"><summary>Plan details</summary>` +
|
|
12342
12580
|
revisionDetail +
|
|
12343
12581
|
(repairText === null ? "" : detail("Signed goal", `<p>${escape(scope.goal)}</p>`)) +
|
|
12344
12582
|
detail("Plan record", contractDetail) +
|
|
@@ -12348,6 +12586,53 @@ function approvalSheetHtml(input) {
|
|
|
12348
12586
|
(scope.candidate ? detail("Saved commit", `<p><code class="approval-commit">${escape(scope.candidate)}</code></p>`) : "") +
|
|
12349
12587
|
detail("Seal", `<p class="meta">Approval binds to this exact wording. <span class="seal mono">signs ${shortDigest(scope.digest)}</span></p>`) +
|
|
12350
12588
|
`</details>`;
|
|
12589
|
+
const passwordNote = input.surface === "task" ? "approval-password-note" : "chat-approval-password-note";
|
|
12590
|
+
// Edit plan, in place (task page): the rows become fields and save
|
|
12591
|
+
// through the scope's own route. The fields belong to a form after this
|
|
12592
|
+
// one (forms never nest), so Approve never carries them.
|
|
12593
|
+
const edit = input.edit;
|
|
12594
|
+
const draft = edit?.draft ?? null;
|
|
12595
|
+
const editForm = "plan-editor-form";
|
|
12596
|
+
// Each field opens tall enough for what it holds (up to ten lines).
|
|
12597
|
+
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>`}` +
|
|
12598
|
+
`<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>`;
|
|
12599
|
+
// Done when, one line per requirement in the signed order; the server
|
|
12600
|
+
// matches them back by position (the digest guard keeps that order).
|
|
12601
|
+
const requirement = (name, value, label, placeholder = "") => `<li><input type="text" name="${name}" value="${escape(value)}" aria-label="${label}"${placeholder === "" ? "" : ` placeholder="${placeholder}"`} form="${editForm}"></li>`;
|
|
12602
|
+
const requirements = draft?.getAll("requirement") ?? scope.acceptance.map(one => one.statement);
|
|
12603
|
+
const added = draft?.get("requirement-new") ?? "";
|
|
12604
|
+
const editor = edit === null ? null :
|
|
12605
|
+
`<details class="approval-edit" id="plan-editor"${draft === null && !edit.open ? "" : " open"}>` +
|
|
12606
|
+
`<summary class="approval-link"><span class="approval-edit-open">Edit plan</span><span class="approval-edit-close">Cancel</span></summary>` +
|
|
12607
|
+
`<div class="approval-editor">` +
|
|
12608
|
+
(edit.problem === null ? "" : `<p class="problem" role="alert">${escape(edit.problem)}</p>`) +
|
|
12609
|
+
field("goal", draft?.get("goal") ?? goalShown, 3, "Goal") +
|
|
12610
|
+
field("touches", draft?.get("touches") ?? scope.touches.join("\n"), 2, "Changes", "Only in these paths, one per line. Leave empty for any file.") +
|
|
12611
|
+
field("not", draft?.get("not") ?? scope.outOfScope ?? "", 2, "Won’t touch") +
|
|
12612
|
+
`<fieldset class="approval-field"><legend class="approval-field-label">Done when</legend>` +
|
|
12613
|
+
`<span class="approval-field-hint">Clear a line to drop it.</span><ul class="approval-requirements">` +
|
|
12614
|
+
requirements.map((one, index) => requirement("requirement", one, `Requirement ${index + 1}`)).join("") +
|
|
12615
|
+
requirement("requirement-new", added, "Add a requirement", "Add a requirement") +
|
|
12616
|
+
`</ul></fieldset>` +
|
|
12617
|
+
`<div class="approval-edit-act"><button type="submit" form="${editForm}">Save plan</button>` +
|
|
12618
|
+
// A plan with written steps keeps its own editor for them.
|
|
12619
|
+
(edit.stepsHref === null ? "" : `<a class="approval-link" href="${escape(edit.stepsHref)}">Edit steps</a>`) + `</div>` +
|
|
12620
|
+
`</div></details>`;
|
|
12621
|
+
const keptMode = permissionModeOfProfile(scope.profile);
|
|
12622
|
+
const editorForm = edit === null ? "" :
|
|
12623
|
+
`<form method="post" action="${edit.action}" id="${editForm}" class="approval-editor-form">` +
|
|
12624
|
+
`<input type="hidden" name="csrf" value="${escape(input.csrf)}">` +
|
|
12625
|
+
// A refused save keeps the version its draft was edited from, so a
|
|
12626
|
+
// plan that changed meanwhile is never silently overwritten.
|
|
12627
|
+
`<input type="hidden" name="sawDigest" value="${escape(draft?.get("sawDigest") ?? scope.digest)}">` +
|
|
12628
|
+
// The terms the editor doesn't show ride along unchanged: a repair's
|
|
12629
|
+
// brief after its goal (it stays in Details), permissions, checks, cap.
|
|
12630
|
+
(repairText === null ? "" : `<input type="hidden" name="goal-brief" value="${escape(repairText[0])}">`) +
|
|
12631
|
+
(keptMode === null ? "" : `<input type="hidden" name="permission-mode" value="${keptMode}">`) +
|
|
12632
|
+
`<input type="hidden" name="quality-mode" value="${escape(scope.qualityMode ?? "default")}">` +
|
|
12633
|
+
// The cap rides in exact millionths ("none" keeps no limit).
|
|
12634
|
+
`<input type="hidden" name="budget-microusd" value="${scope.budgetMicrousd === null ? "none" : String(scope.budgetMicrousd)}">` +
|
|
12635
|
+
`</form>`;
|
|
12351
12636
|
return (`<form method="post" action="${input.action}" class="approve-form approval-sheet"${input.surface === "task" ? ` id="approve"` : ""}${input.sticky ? " data-sticky" : ""}>` +
|
|
12352
12637
|
`<input type="hidden" name="csrf" value="${escape(input.csrf)}">` +
|
|
12353
12638
|
`<input type="hidden" name="nonce" value="${escape(input.nonce)}">` +
|
|
@@ -12367,13 +12652,75 @@ function approvalSheetHtml(input) {
|
|
|
12367
12652
|
approvalAmendmentHtml(contract) +
|
|
12368
12653
|
race +
|
|
12369
12654
|
(who.length === 0 ? "" : `<p class="approval-who">${who.map(escape).join(" · ")}</p>`) +
|
|
12655
|
+
consent +
|
|
12370
12656
|
`<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>` +
|
|
12657
|
+
`<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>` +
|
|
12658
|
+
`<p class="approval-password-note" id="${passwordNote}">Your password signs this approval.</p>` +
|
|
12372
12659
|
`<button type="submit" data-primary-action>${submit}</button></div>` +
|
|
12373
12660
|
`<p class="approval-after">${after}</p>` +
|
|
12374
|
-
`<
|
|
12661
|
+
`<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
12662
|
details +
|
|
12376
|
-
`</form>`
|
|
12663
|
+
`</form>` +
|
|
12664
|
+
editorForm);
|
|
12665
|
+
}
|
|
12666
|
+
/** Earlier versions of a task still in flight, in plain words: queued
|
|
12667
|
+
* while they only wait, running once one has started; null when none. */
|
|
12668
|
+
export function earlierVersionsWords(active, running) {
|
|
12669
|
+
if (active <= 0)
|
|
12670
|
+
return null;
|
|
12671
|
+
const still = running >= active ? "running" : running <= 0 ? "queued" : "queued or running";
|
|
12672
|
+
return active === 1 ? `an earlier version is still ${still}` : `${active} earlier versions are still ${still}`;
|
|
12673
|
+
}
|
|
12674
|
+
/** An in-place plan edit that would change the agent's permissions: thrown to roll the save back. */
|
|
12675
|
+
class PermissionsWouldChange extends Error {
|
|
12676
|
+
}
|
|
12677
|
+
/** Evidence kinds in plain words, for the signed criteria in Details. */
|
|
12678
|
+
const EVIDENCE_PLAIN = { check: "the project check", screenshot: "screenshots", "changed-path": "changed files", "manual-review": "your own check" };
|
|
12679
|
+
/** What an agent may do on its own, in a few plain words. */
|
|
12680
|
+
function permissionPlainWordsOf(profile) {
|
|
12681
|
+
if (profile === null || profile === undefined)
|
|
12682
|
+
return null;
|
|
12683
|
+
const full = "full access, nothing asks first";
|
|
12684
|
+
if (profile.provider === "claude") {
|
|
12685
|
+
return profile.permissionArgv === "bypassPermissions" ? full
|
|
12686
|
+
: profile.permissionArgv === "auto" ? "file edits and routine commands; anything risky stops"
|
|
12687
|
+
: "file edits only; commands are refused";
|
|
12688
|
+
}
|
|
12689
|
+
if (profile.provider === "gemini")
|
|
12690
|
+
return profile.approvalArgv === "yolo" ? full : "file edits only; other tools are refused";
|
|
12691
|
+
return profile.sandboxMode === "danger-full-access" ? full : "file edits and commands inside the project only";
|
|
12692
|
+
}
|
|
12693
|
+
/** The permission choice a sealed profile carries, as the scope form names it. */
|
|
12694
|
+
function permissionModeOfProfile(profile) {
|
|
12695
|
+
if (profile === null || profile === undefined)
|
|
12696
|
+
return null;
|
|
12697
|
+
return profile.provider === "claude"
|
|
12698
|
+
? profile.permissionArgv === "bypassPermissions" ? "bypassPermissions" : "auto"
|
|
12699
|
+
: profile.provider === "gemini"
|
|
12700
|
+
? profile.approvalArgv === "yolo" ? "bypassPermissions" : "auto"
|
|
12701
|
+
: profile.sandboxMode === "danger-full-access" ? "bypassPermissions" : "auto";
|
|
12702
|
+
}
|
|
12703
|
+
/** Done when, from the in-place editor: each line keeps its requirement's
|
|
12704
|
+
* id by position, and its evidence and guidance while its words stand; a
|
|
12705
|
+
* rewritten one is yours to check, like an added one; an emptied one is
|
|
12706
|
+
* dropped. */
|
|
12707
|
+
function requirementsFromEditor(body, current) {
|
|
12708
|
+
const used = new Set(current.map(one => one.id));
|
|
12709
|
+
let next = 1;
|
|
12710
|
+
while (used.has(`c${next}`))
|
|
12711
|
+
next++;
|
|
12712
|
+
const plain = (raw) => raw.replace(/\s+/g, " ").trim();
|
|
12713
|
+
const kept = body.getAll("requirement").slice(0, current.length).flatMap((raw, index) => {
|
|
12714
|
+
const statement = plain(raw);
|
|
12715
|
+
const criterion = current[index];
|
|
12716
|
+
if (statement === "")
|
|
12717
|
+
return [];
|
|
12718
|
+
return statement === plain(criterion.statement)
|
|
12719
|
+
? [{ id: criterion.id, statement: criterion.statement, evidence: [...criterion.evidence], how: criterion.how }]
|
|
12720
|
+
: [{ id: criterion.id, statement, evidence: ["manual-review"], how: null }];
|
|
12721
|
+
});
|
|
12722
|
+
const added = plain(body.get("requirement-new") ?? "");
|
|
12723
|
+
return added === "" ? kept : [...kept, { id: `c${next}`, statement: added, evidence: ["manual-review"], how: null }];
|
|
12377
12724
|
}
|
|
12378
12725
|
function consentDoorOf(scope, route) {
|
|
12379
12726
|
if (scope === null)
|
|
@@ -12426,31 +12773,31 @@ function consentClosedHtml(taskId, door, surface) {
|
|
|
12426
12773
|
}
|
|
12427
12774
|
function profileWords(scope) {
|
|
12428
12775
|
if (scope.profileState === "unresolved") {
|
|
12429
|
-
return `<p class="meta"><strong>
|
|
12776
|
+
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
12777
|
}
|
|
12431
12778
|
const profile = scope.profile ?? null;
|
|
12432
12779
|
if (profile === null) {
|
|
12433
12780
|
return (scope.digestVersion ?? 1) < 2
|
|
12434
|
-
? `<p class="meta">
|
|
12781
|
+
? `<p class="meta">Approved before routing was bound — pinned at upgrade to the configuration of that day</p>`
|
|
12435
12782
|
: "";
|
|
12436
12783
|
}
|
|
12437
12784
|
const repair = profile.repairModel === "inherit" ? "same model" : profile.repairModel;
|
|
12438
12785
|
const base = profile.provider === "claude"
|
|
12439
|
-
? `<p class="meta">
|
|
12786
|
+
? `<p class="meta">Runs on <span class="mono">claude · ${escape(profile.model)}</span> — ${profile.permissionArgv === "bypassPermissions"
|
|
12440
12787
|
? "FULL permissions; claude runs with --dangerously-skip-permissions and nothing asks"
|
|
12441
12788
|
: profile.permissionArgv === "auto"
|
|
12442
12789
|
? "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}
|
|
12790
|
+
: "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
12791
|
: profile.provider === "gemini"
|
|
12445
|
-
? `<p class="meta">
|
|
12446
|
-
: `<p class="meta">
|
|
12792
|
+
? `<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>`
|
|
12793
|
+
: `<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
12794
|
// The fallback chain rides EVERY surface these words sign (F+G review,
|
|
12448
12795
|
// finding 2): the digest binds the whole chain, so the password form —
|
|
12449
12796
|
// task page and /next alike — states every entry, credential included.
|
|
12450
12797
|
const chain = chainFromJson(scope.proposedChainJson ?? null);
|
|
12451
12798
|
const chainLine = chain === null || chain.length < 2
|
|
12452
12799
|
? ""
|
|
12453
|
-
: `<p class="meta">
|
|
12800
|
+
: `<p class="meta">If its subscription runs out: ${chain
|
|
12454
12801
|
.slice(1)
|
|
12455
12802
|
.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
12803
|
.join("; ")}</p>`;
|
|
@@ -12479,9 +12826,9 @@ function agentsSummaryWords(view) {
|
|
|
12479
12826
|
/** Neutral metadata badges: risk, posture, and standing. */
|
|
12480
12827
|
function agentsBadgesHtml(view) {
|
|
12481
12828
|
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>`);
|
|
12829
|
+
return (`<span class="badge">${escape(sentenceCase(riskTitle(view.riskLevel)))}</span>` +
|
|
12830
|
+
(p === null ? "" : `<span class="badge">${escape(sentenceCase(p.postureWords))}</span>`) +
|
|
12831
|
+
`<span class="badge">${escape(sentenceCase(agentsStandingWords(view)))}</span>`);
|
|
12485
12832
|
}
|
|
12486
12833
|
/** Volatile availability, per provider on the route — shown beside the
|
|
12487
12834
|
* agents, never inside the approval's terms. */
|
|
@@ -12506,7 +12853,7 @@ function agentsWhyHtml(projection, summaryLabel = "Why these agents") {
|
|
|
12506
12853
|
.map(leg => {
|
|
12507
12854
|
const chosen = chosenWords(leg);
|
|
12508
12855
|
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>` +
|
|
12856
|
+
`<dd><span class="mono">${escape(leg.provider)} · ${escape(leg.model)}</span> <span class="badge">${escape(sentenceCase(chosen))}</span>` +
|
|
12510
12857
|
`<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
12858
|
})
|
|
12512
12859
|
.join("") +
|
|
@@ -12527,7 +12874,7 @@ function agentsCeremonyHtml(view) {
|
|
|
12527
12874
|
}
|
|
12528
12875
|
return (`<div class="agents-ceremony"><p class="approval-label">agents</p>` +
|
|
12529
12876
|
`<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>` +
|
|
12877
|
+
`<div class="agents-badges"><span class="badge">${escape(sentenceCase(riskTitle(view.riskLevel)))}</span><span class="badge">${escape(sentenceCase(view.projection.postureWords))}</span></div>` +
|
|
12531
12878
|
`<p class="meta agents-risk-line">${escape(riskTitle(view.riskLevel))}: ${escape(riskConsequence(view.riskLevel))}.</p>` +
|
|
12532
12879
|
agentsWhyHtml(view.projection) +
|
|
12533
12880
|
`<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 +12901,7 @@ function routineAgentsHtml(routine, approved, ceremony = false) {
|
|
|
12554
12901
|
const projection = projectRoute(route, () => null);
|
|
12555
12902
|
return (`<div class="agents-ceremony"><p class="approval-label">agents</p>` +
|
|
12556
12903
|
`<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>` +
|
|
12904
|
+
`<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
12905
|
(ceremony ? agentsWhyHtml(projection) : "") +
|
|
12559
12906
|
`<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
12907
|
}
|
|
@@ -12584,7 +12931,7 @@ function agentsCardHtml(taskId, view, csrf, canEdit) {
|
|
|
12584
12931
|
? ""
|
|
12585
12932
|
: `<ul class="agents-overrides">${view.overrides
|
|
12586
12933
|
.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">
|
|
12934
|
+
(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
12935
|
`</li>`)
|
|
12589
12936
|
.join("")}</ul>`;
|
|
12590
12937
|
// The controls (v48): a risk choice that says what each level does, and
|
|
@@ -12614,7 +12961,7 @@ function agentsCardHtml(taskId, view, csrf, canEdit) {
|
|
|
12614
12961
|
? `<p class="meta">${escape(view.editableWhy ?? "the agents cannot change right now")}</p>`
|
|
12615
12962
|
: `<details class="agents-change"><summary>Change agents</summary>` +
|
|
12616
12963
|
`<form method="post" action="${taskHref(taskId)}/route" class="agents-form-risk">${hidden}` +
|
|
12617
|
-
`<label>
|
|
12964
|
+
`<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
12965
|
`<button type="submit" class="secondary">Set risk</button></form>` +
|
|
12619
12966
|
riskGuide +
|
|
12620
12967
|
`<div class="agents-role-forms">${roleForms.join("")}</div>` +
|
|
@@ -13937,9 +14284,34 @@ ${THEME_DARK}
|
|
|
13937
14284
|
.approval-act input[type=password] { margin: 0; font-family: var(--font-sans); }
|
|
13938
14285
|
.approval-act button { margin: 0; min-height: 2.125rem; white-space: nowrap; }
|
|
13939
14286
|
.approval-after { max-width: 75ch; color: var(--muted-foreground); font-size: 13px; line-height: 1.5; }
|
|
13940
|
-
.approval-
|
|
14287
|
+
.approval-allowing, .approval-you-check { max-width: 75ch; font-size: 14px; line-height: 1.55; overflow-wrap: anywhere; }
|
|
14288
|
+
.approval-sheet .approval-allowing + .approval-you-check { margin-top: -10px; }
|
|
14289
|
+
.approval-act > button { grid-column: 2; grid-row: 1; }
|
|
14290
|
+
.approval-password-note { grid-column: 1; grid-row: 2; margin: 0; color: var(--muted-foreground); font-size: 13px; line-height: 1.5; }
|
|
14291
|
+
.approval-secondary { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
|
|
14292
|
+
details.approval-edit, details.approval-edit[open] { margin: 0; padding: 0; border: 0; border-radius: 0; background: none; }
|
|
14293
|
+
/* Edit plan, in place: the rows become fields where they stood; the
|
|
14294
|
+
approval act and Details step aside until it is saved or cancelled. */
|
|
14295
|
+
.approval-sheet { position: relative; }
|
|
14296
|
+
.approval-edit > summary { list-style: none; cursor: pointer; }
|
|
14297
|
+
.approval-edit > summary::-webkit-details-marker { display: none; }
|
|
14298
|
+
.approval-edit-close, .approval-edit[open] .approval-edit-open { display: none; }
|
|
14299
|
+
.approval-edit[open] .approval-edit-close { display: inline; }
|
|
14300
|
+
.approval-edit[open] > summary { position: absolute; top: 14px; right: 20px; }
|
|
14301
|
+
.approval-sheet:has(.approval-edit[open]) > :not(.approval-sheet-title, .approval-secondary) { display: none; }
|
|
14302
|
+
.approval-secondary:has(> .approval-edit[open]) { display: block; }
|
|
14303
|
+
.approval-secondary:has(> .approval-edit[open]) > a { display: none; }
|
|
14304
|
+
.approval-editor { display: grid; gap: 14px; }
|
|
14305
|
+
.approval-editor .problem { margin: 0; }
|
|
14306
|
+
.approval-field { display: grid; gap: 4px; min-width: 0; margin: 0; padding: 0; border: 0; }
|
|
14307
|
+
.approval-field-label { padding: 0; color: var(--muted-foreground); font-size: 13px; font-weight: 500; line-height: 1.6; }
|
|
14308
|
+
.approval-field-hint { color: var(--muted-foreground); font-size: 12px; line-height: 1.5; }
|
|
14309
|
+
.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; }
|
|
14310
|
+
.approval-requirements { display: grid; gap: 6px; margin: 2px 0 0; padding: 0; list-style: none; }
|
|
14311
|
+
.approval-edit-act { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
|
|
14312
|
+
.approval-edit-act button { margin: 0; min-height: 2.125rem; white-space: nowrap; }
|
|
13941
14313
|
.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-
|
|
14314
|
+
.approval-secondary > .approval-link:not(:first-child) { border-color: transparent; background: none; color: var(--muted-foreground); }
|
|
13943
14315
|
@media (hover: hover) and (pointer: fine) { .approval-link:hover { background: var(--muted); color: var(--foreground); } }
|
|
13944
14316
|
details.approval-details { margin: 0; padding: 0; border: 0; border-top: 1px solid var(--border); border-radius: 0; background: none; }
|
|
13945
14317
|
details.approval-details[open] { padding-bottom: 0; }
|
|
@@ -13959,6 +14331,7 @@ ${THEME_DARK}
|
|
|
13959
14331
|
.approval-sheet { gap: 12px; padding: 14px; }
|
|
13960
14332
|
.approval-row { grid-template-columns: 1fr; gap: 2px; }
|
|
13961
14333
|
.approval-act { grid-template-columns: 1fr; }
|
|
14334
|
+
.approval-act > button, .approval-password-note { grid-column: auto; grid-row: auto; }
|
|
13962
14335
|
.approval-act input[type=password], .approval-act button { min-height: 44px; font-size: 16px; }
|
|
13963
14336
|
.approval-act button { width: 100%; }
|
|
13964
14337
|
/* The one act stays under the thumb: sticky at the bottom, clear of the home indicator. */
|
|
@@ -13967,6 +14340,9 @@ ${THEME_DARK}
|
|
|
13967
14340
|
border-top: 1px solid var(--border); background: var(--card);
|
|
13968
14341
|
}
|
|
13969
14342
|
.approval-link { min-height: 44px; padding: 0 16px; }
|
|
14343
|
+
.approval-edit[open] > summary { top: 6px; right: 6px; }
|
|
14344
|
+
.approval-field textarea, .approval-field input[type=text] { font-size: 16px; }
|
|
14345
|
+
.approval-edit-act button { width: 100%; min-height: 44px; font-size: 16px; }
|
|
13970
14346
|
.approval-details > summary { min-height: 44px; }
|
|
13971
14347
|
.approval-roles > div { grid-template-columns: 1fr; gap: 2px; }
|
|
13972
14348
|
}
|
|
@@ -15482,7 +15858,7 @@ const INBOX_TABS_CSS = '.inbox-tabs{display:inline-flex;gap:2px;max-width:100%;o
|
|
|
15482
15858
|
'.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
15859
|
'.inbox-unread{display:none;position:absolute;top:4px;right:3px;width:6px;height:6px;border-radius:50%;background:var(--so-signal)}' +
|
|
15484
15860
|
'@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}');
|
|
15861
|
+
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
15862
|
/**
|
|
15487
15863
|
* The board's liveness: fetch this page's own fragment on a timer and swap
|
|
15488
15864
|
* it in place — no flicker, no scroll reset, no long-lived stream to manage,
|
|
@@ -15737,7 +16113,7 @@ FINISH: unreviewed and undocumented is unfinished; this build ends with the fini
|
|
|
15737
16113
|
* absent from sensitive pages. Navigation help in plain words — no key
|
|
15738
16114
|
* ever posts. */
|
|
15739
16115
|
const KBD_HELP = `<div class="kbd-help" hidden role="dialog" aria-label="keyboard shortcuts" tabindex="-1">` +
|
|
15740
|
-
`<h2>
|
|
16116
|
+
`<h2>Keyboard shortcuts</h2><table>` +
|
|
15741
16117
|
`<tr><td><kbd>/</kbd></td><td>jump to a page or an open task</td></tr>` +
|
|
15742
16118
|
`<tr><td><kbd>g</kbd> then <kbd>i</kbd></td><td>go to the inbox</td></tr>` +
|
|
15743
16119
|
`<tr><td><kbd>g</kbd> then <kbd>b</kbd></td><td>go to the board</td></tr>` +
|
|
@@ -15812,7 +16188,7 @@ function shell(title, body, options = {}) {
|
|
|
15812
16188
|
: `<span class="scope-status">` +
|
|
15813
16189
|
(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
16190
|
`<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)) : "
|
|
16191
|
+
const scopeName = effectiveScope === "project" && chrome.project !== null ? escape(projectName(chrome.project)) : "All projects";
|
|
15816
16192
|
// The switcher (board pass): the scope's name opens a menu of every
|
|
15817
16193
|
// enrolled project — one tap to open one, or to widen to all — as plain
|
|
15818
16194
|
// POST forms carrying the session's csrf, returning to this screen.
|
|
@@ -15827,7 +16203,7 @@ function shell(title, body, options = {}) {
|
|
|
15827
16203
|
const allCurrent = effectiveScope !== "project";
|
|
15828
16204
|
const rows = [
|
|
15829
16205
|
`<form method="post" action="/projects/select">${hidden}<input type="hidden" name="path" value="">` +
|
|
15830
|
-
`<button type="submit"${allCurrent ? ' class="current" aria-current="true"' : ""}>
|
|
16206
|
+
`<button type="submit"${allCurrent ? ' class="current" aria-current="true"' : ""}>All projects</button></form>`,
|
|
15831
16207
|
...(chrome.projects ?? []).map(one => `<form method="post" action="/projects/open">${hidden}<input type="hidden" name="path" value="${escape(one.path)}">` +
|
|
15832
16208
|
`<button type="submit"${!allCurrent && chrome.project === one.path ? ' class="current" aria-current="true"' : ""}>${escape(one.name)}</button></form>`),
|
|
15833
16209
|
];
|
|
@@ -15871,10 +16247,10 @@ function shell(title, body, options = {}) {
|
|
|
15871
16247
|
primaryItem("flows", "/flows", "Flows"),
|
|
15872
16248
|
primaryItem("projects", "/projects", "Projects"),
|
|
15873
16249
|
`</nav>`,
|
|
15874
|
-
...(chrome.active === "code" ? [] : [`<a class="new-task" href="/tasks/new" aria-label="
|
|
16250
|
+
...(chrome.active === "code" ? [] : [`<a class="new-task" href="/tasks/new" aria-label="New task">+ New task</a>`]),
|
|
15875
16251
|
`<nav class="nav-groups">`,
|
|
15876
|
-
navGroup("tools", "
|
|
15877
|
-
navGroup("settings", "
|
|
16252
|
+
navGroup("tools", "Work tools", workToolRows(chrome.projectScoped, chrome.chat, chrome.code), TOOL_KEYS.has(chrome.active)),
|
|
16253
|
+
navGroup("settings", "Settings", settingsRows(chrome.projectScoped, chrome.settings), SETTINGS_KEYS.has(chrome.active)),
|
|
15878
16254
|
`</nav>`,
|
|
15879
16255
|
`<span class="grow"></span>`,
|
|
15880
16256
|
`</aside>`,
|
|
@@ -15883,11 +16259,11 @@ function shell(title, body, options = {}) {
|
|
|
15883
16259
|
// must not pass as production (adoption review, finding 8; the FENCE is
|
|
15884
16260
|
// the refuseDemo gate in operate.ts, this is the honest label).
|
|
15885
16261
|
const demoBanner = (chrome.demo === true
|
|
15886
|
-
? `<div class="banner"><span class="badge">
|
|
16262
|
+
? `<div class="banner"><span class="badge">Demo</span>${escape(DEMO_BANNER.replace(/^Demo: /, ""))}</div>`
|
|
15887
16263
|
: "") +
|
|
15888
16264
|
(chrome.modeBanner === undefined
|
|
15889
16265
|
? ""
|
|
15890
|
-
: `<div class="banner"><span class="badge badge-running">
|
|
16266
|
+
: `<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
16267
|
(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
16268
|
`<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
16269
|
(chrome.updateWaiting === undefined
|
|
@@ -16109,7 +16485,7 @@ function inboxPage(chrome, data) {
|
|
|
16109
16485
|
/** The row's project, worn openly in the roll-up — null is UNPLACED,
|
|
16110
16486
|
* said as such, never a silent missing chip (finding 13). */
|
|
16111
16487
|
const chip = (repo) => !data.rollup ? "" : repo === null || repo === undefined
|
|
16112
|
-
? ` <span class="badge">
|
|
16488
|
+
? ` <span class="badge">Unplaced</span>`
|
|
16113
16489
|
: ` <span class="badge">${escape(projectName(repo))}</span>`;
|
|
16114
16490
|
const empty = data.decisions.length + data.approvals.length + data.requeueables.length +
|
|
16115
16491
|
data.cancelledBlockers.length + data.gaps.length + data.needsVerification.length === 0;
|
|
@@ -16118,19 +16494,19 @@ function inboxPage(chrome, data) {
|
|
|
16118
16494
|
// the card itself (portfolio arc §2).
|
|
16119
16495
|
const decisions = data.decisions.length === 0
|
|
16120
16496
|
? ""
|
|
16121
|
-
: `<h2>
|
|
16497
|
+
: `<h2>Answer a question</h2><p class="hint">an agent stopped mid-build to ask — nothing proceeds until you answer</p>` +
|
|
16122
16498
|
data.decisions
|
|
16123
16499
|
.map(decision => data.interactive && !data.rollup
|
|
16124
16500
|
? decisionAnswerCard(decision, data.csrf, data.now, false)
|
|
16125
16501
|
: `<a class="decide-card" href="/d/${decision.id}">` +
|
|
16126
16502
|
`<p class="q">${escape(decision.question)}</p>` +
|
|
16127
16503
|
`<span class="mono meta">${escape(decision.taskId)}</span>${chip(decision.repo)}` +
|
|
16128
|
-
`${isOverdue(decision, data.now) ? ` <span class="badge badge-overdue">
|
|
16504
|
+
`${isOverdue(decision, data.now) ? ` <span class="badge badge-overdue">Overdue</span>` : ""}` +
|
|
16129
16505
|
`</a>`)
|
|
16130
16506
|
.join("\n");
|
|
16131
16507
|
const approvals = data.approvals.length === 0
|
|
16132
16508
|
? ""
|
|
16133
|
-
: `<h2>
|
|
16509
|
+
: `<h2>Approve a scope</h2><p class="hint">scopes waiting for your approval — each binds to the exact wording you sign</p>` +
|
|
16134
16510
|
data.approvals
|
|
16135
16511
|
.map(one => `<a class="decide-card" href="${taskHref(one.taskId)}">` +
|
|
16136
16512
|
`<p class="q">${escape(one.title)}</p>` +
|
|
@@ -16140,7 +16516,7 @@ function inboxPage(chrome, data) {
|
|
|
16140
16516
|
.join("\n");
|
|
16141
16517
|
const requeueables = data.requeueables.length === 0
|
|
16142
16518
|
? ""
|
|
16143
|
-
: `<h2>
|
|
16519
|
+
: `<h2>Retry stalled work</h2><p class="hint">failed builds waiting for a person — retry clears the incidents and requeues</p>` +
|
|
16144
16520
|
data.requeueables
|
|
16145
16521
|
.map(one => `<p class="row"><a href="${taskHref(one.taskId)}">${escape(one.taskId)}</a> ${escape(one.title)}${chip(one.repo)}` +
|
|
16146
16522
|
`${one.incidentCount > 0 ? ` <span class="badge badge-failed">${one.incidentCount} incident${one.incidentCount > 1 ? "s" : ""}</span>` : ""}` +
|
|
@@ -16150,11 +16526,11 @@ function inboxPage(chrome, data) {
|
|
|
16150
16526
|
: `<span class="right"><form method="post" action="${taskHref(one.taskId)}/requeue" class="inline">` +
|
|
16151
16527
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
|
|
16152
16528
|
`<input type="hidden" name="return" value="inbox">` +
|
|
16153
|
-
`<button type="submit">
|
|
16529
|
+
`<button type="submit">Retry</button></form></span></p>`))
|
|
16154
16530
|
.join("\n");
|
|
16155
16531
|
const needsVerification = data.needsVerification.length === 0
|
|
16156
16532
|
? ""
|
|
16157
|
-
: `<h2>
|
|
16533
|
+
: `<h2>Needs review</h2><p class="hint">finished, but the evidence needs your attention before accepting</p>` +
|
|
16158
16534
|
data.needsVerification
|
|
16159
16535
|
.map(one => `<p class="row"><a href="${taskHref(one.taskId)}">${escape(one.taskId)}</a> ${escape(one.title)}${chip(one.repo)}` +
|
|
16160
16536
|
` <span class="badge badge-failed">${one.verdict === "refuted" ? "conflicting evidence" : "missing evidence"}</span>${criterionMatrixSummary(one.matrix ?? [])}` +
|
|
@@ -16163,7 +16539,7 @@ function inboxPage(chrome, data) {
|
|
|
16163
16539
|
.join("\n");
|
|
16164
16540
|
const cancelled = data.cancelledBlockers.length === 0
|
|
16165
16541
|
? ""
|
|
16166
|
-
: `<h2>
|
|
16542
|
+
: `<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
16543
|
data.cancelledBlockers
|
|
16168
16544
|
.map(one => `<p class="row"><a href="${taskHref(one.exampleDependent)}">${escape(one.exampleDependent)}</a>${chip(one.repo)} ` +
|
|
16169
16545
|
`<span class="meta">${one.dependentCount > 1 ? `one of ${one.dependentCount} tasks waiting` : "waiting"} for cancelled task ${escape(one.blockerId)}` +
|
|
@@ -16171,7 +16547,7 @@ function inboxPage(chrome, data) {
|
|
|
16171
16547
|
.join("\n");
|
|
16172
16548
|
const gaps = data.gaps.length === 0
|
|
16173
16549
|
? ""
|
|
16174
|
-
: `<h2>
|
|
16550
|
+
: `<h2>Supply a requirement</h2><p class="hint">approved work is ready except for these — fill one and its tasks start</p>` +
|
|
16175
16551
|
data.gaps
|
|
16176
16552
|
.map(gap => `<p class="row"><a href="/caps">${escape(gap.key)}</a> ` +
|
|
16177
16553
|
`<span class="meta">frees ${gap.unblocks.length} task${gap.unblocks.length > 1 ? "s" : ""}</span>` +
|
|
@@ -16203,8 +16579,8 @@ function inboxPage(chrome, data) {
|
|
|
16203
16579
|
const tab = data.tab ?? "all";
|
|
16204
16580
|
const shows = (one) => tab === "all" || tab === one;
|
|
16205
16581
|
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>
|
|
16582
|
+
const ready = (data.ready ?? []).length === 0 ? "" : `<h2>Ready to review</h2>` + listRows(data.ready ?? []);
|
|
16583
|
+
const running = (data.running ?? []).length === 0 ? "" : `<h2>Running now</h2>` + listRows(data.running ?? []);
|
|
16208
16584
|
const asks = { decide: data.decisions.length + data.approvals.length, unblock: data.requeueables.length + data.cancelledBlockers.length + data.gaps.length };
|
|
16209
16585
|
const counts = { ...asks,
|
|
16210
16586
|
"needs-you": asks.decide + asks.unblock,
|
|
@@ -16223,7 +16599,7 @@ function inboxPage(chrome, data) {
|
|
|
16223
16599
|
? `<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
16600
|
return screen("inbox", [
|
|
16225
16601
|
`<h1>Inbox</h1>`,
|
|
16226
|
-
`<p class="meta">
|
|
16602
|
+
`<p class="meta">Everything that waits on you \u2014 empty means the fleet is working</p>`,
|
|
16227
16603
|
tabs,
|
|
16228
16604
|
noWorker,
|
|
16229
16605
|
wizard,
|
|
@@ -16235,21 +16611,21 @@ function inboxPage(chrome, data) {
|
|
|
16235
16611
|
shows("needs-you") ? askGroup("unblock", counts.unblock, [requeueables, cancelled, gaps]) : "",
|
|
16236
16612
|
shows("running") ? running : "",
|
|
16237
16613
|
data.rollup
|
|
16238
|
-
? `<p class="meta">
|
|
16614
|
+
? `<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
16615
|
: "",
|
|
16240
16616
|
// Quick capture: the shortest path from "I want this done" to the
|
|
16241
16617
|
// approve card — title and goal here, the yes on the next screen. The
|
|
16242
16618
|
// one-shot form posts to the same guarded handler as the full page.
|
|
16243
|
-
data.rollup ? "" : `<h2>
|
|
16619
|
+
data.rollup ? "" : `<h2>Capture new work</h2>`,
|
|
16244
16620
|
data.rollup ? "" : `<form method="post" action="/tasks/add" class="card">`,
|
|
16245
16621
|
...(data.rollup
|
|
16246
16622
|
? []
|
|
16247
16623
|
: [
|
|
16248
16624
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
|
|
16249
16625
|
`<input type="hidden" name="projectRevision" value="${data.revision}">`,
|
|
16250
|
-
`<label>
|
|
16251
|
-
`<label>
|
|
16252
|
-
`<button type="submit">
|
|
16626
|
+
`<label>What should get done<input type="text" name="title" placeholder="task title" maxlength="200"></label>`,
|
|
16627
|
+
`<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>`,
|
|
16628
|
+
`<button type="submit">Queue it \u2192 approve its scope next</button>`,
|
|
16253
16629
|
`</form>`,
|
|
16254
16630
|
]),
|
|
16255
16631
|
].join("\n"), {
|
|
@@ -16298,7 +16674,7 @@ function systemPage(chrome, data) {
|
|
|
16298
16674
|
const agentLines = data.agents
|
|
16299
16675
|
.map(one => {
|
|
16300
16676
|
if (one.problem !== undefined) {
|
|
16301
|
-
return `<p class="row"><span class="mono">${escape(one.phase)}</span> <span class="badge badge-failed">
|
|
16677
|
+
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
16678
|
}
|
|
16303
16679
|
const who = one.source === "project"
|
|
16304
16680
|
? `chosen for this project${one.setBy === null ? "" : ` by ${escape(one.setBy)}`}`
|
|
@@ -16313,10 +16689,10 @@ function systemPage(chrome, data) {
|
|
|
16313
16689
|
`<p class="meta" style="margin-top:0">${PHASE_SAID[one.phase] ?? ""}</p>`);
|
|
16314
16690
|
})
|
|
16315
16691
|
.join("\n");
|
|
16316
|
-
const agentsCard = `<h2>
|
|
16317
|
-
`<p class="meta">
|
|
16692
|
+
const agentsCard = `<h2>Agents</h2>` +
|
|
16693
|
+
`<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
16694
|
`<div class="card">${agentLines}` +
|
|
16319
|
-
`<p class="meta">
|
|
16695
|
+
`<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
16696
|
`</div>`;
|
|
16321
16697
|
return screen("system", [
|
|
16322
16698
|
`<h1>System</h1>`,
|
|
@@ -16325,16 +16701,16 @@ function systemPage(chrome, data) {
|
|
|
16325
16701
|
cards.length === 0
|
|
16326
16702
|
? `<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
16703
|
: `<div class="cards">${cards.join("")}</div>`,
|
|
16328
|
-
data.outboxPending > 0 ? `<p class="meta">
|
|
16704
|
+
data.outboxPending > 0 ? `<p class="meta">Notifications: ${data.outboxPending} pending delivery</p>` : "",
|
|
16329
16705
|
(data.externalWork ?? []).length === 0
|
|
16330
16706
|
? ""
|
|
16331
|
-
: `<h2>
|
|
16707
|
+
: `<h2>External work</h2>` +
|
|
16332
16708
|
(data.externalWork ?? [])
|
|
16333
16709
|
.map(one => `<p class="row"><span class="mono">${escape(one.remoteRepo)}</span> ` +
|
|
16334
16710
|
(one.blocked !== null
|
|
16335
|
-
? `<span class="badge badge-failed">
|
|
16711
|
+
? `<span class="badge badge-failed">Dispatch blocked</span> <span class="meta">the tracker connection needs repair — \`toolroll sync\` says why</span>`
|
|
16336
16712
|
: one.openEpisode !== null
|
|
16337
|
-
? `<span class="badge badge-failed">
|
|
16713
|
+
? `<span class="badge badge-failed">Sync failing</span> <span class="meta">${escape(one.openEpisode)}</span>`
|
|
16338
16714
|
: `<span class="meta">syncing normally</span>`) +
|
|
16339
16715
|
`</p>`)
|
|
16340
16716
|
.join("\n"),
|
|
@@ -16437,7 +16813,7 @@ function boardBody(data, ciRed) {
|
|
|
16437
16813
|
queuedHeads.get(`${card.assignedRunner ?? ""}|${card.repo ?? ""}`) === card.taskId
|
|
16438
16814
|
? `<span class="badge">${card.assignedRunner === null ? "next up" : `next for ${escape(card.assignedRunner)}`}</span>`
|
|
16439
16815
|
: "",
|
|
16440
|
-
card.assignedRunner === null ? "" : `<span class="badge">
|
|
16816
|
+
card.assignedRunner === null ? "" : `<span class="badge">Reserved</span>`,
|
|
16441
16817
|
card.routineName === null ? "" : `<span class="badge">${escape(card.routineName)}</span>`,
|
|
16442
16818
|
chip(card.repo).trim(),
|
|
16443
16819
|
]) +
|
|
@@ -16465,7 +16841,7 @@ function boardBody(data, ciRed) {
|
|
|
16465
16841
|
...(claim.branch === null ? [] : [["branch", `${claim.branch}${workspace === null ? "" : ` · ${workspace}`}`]]),
|
|
16466
16842
|
]) +
|
|
16467
16843
|
chips([
|
|
16468
|
-
card.attempt === null ? "" : `<span class="badge">
|
|
16844
|
+
card.attempt === null ? "" : `<span class="badge">Attempt ${card.attempt}</span>`,
|
|
16469
16845
|
chip(card.repo).trim(),
|
|
16470
16846
|
]) +
|
|
16471
16847
|
`</a>`);
|
|
@@ -16487,7 +16863,7 @@ function boardBody(data, ciRed) {
|
|
|
16487
16863
|
["usage", runCostWords({ authMode: row.authMode, costUsd: row.costUsd, tokensIn: null, tokensOut: null })],
|
|
16488
16864
|
]) +
|
|
16489
16865
|
chips([
|
|
16490
|
-
`<span class="badge badge-done">${row.outcome === "no-change" ? "
|
|
16866
|
+
`<span class="badge badge-done">${row.outcome === "no-change" ? "No change" : "Built"}</span>`,
|
|
16491
16867
|
pr.trim(),
|
|
16492
16868
|
data.all && row.repo !== null ? `<span class="badge">${escape(projectName(row.repo))}</span>` : "",
|
|
16493
16869
|
]) +
|
|
@@ -16499,7 +16875,7 @@ function boardBody(data, ciRed) {
|
|
|
16499
16875
|
: `<p class="meta board-scope">` +
|
|
16500
16876
|
(data.all
|
|
16501
16877
|
? (data.project === null
|
|
16502
|
-
? `<strong>
|
|
16878
|
+
? `<strong>All projects</strong>`
|
|
16503
16879
|
: `<a href="/board">${escape(projectName(data.project))}</a> \u00b7 <strong>all projects</strong>`) +
|
|
16504
16880
|
` \u2014 every project this server serves, each card wearing its project`
|
|
16505
16881
|
: `<strong>${escape(projectName(data.project))}</strong> \u00b7 <a href="/board?scope=all">all projects</a>`) +
|
|
@@ -16516,7 +16892,7 @@ function boardBody(data, ciRed) {
|
|
|
16516
16892
|
const ago = (minutes) => minutes < 60 ? `${minutes}m` : minutes < 48 * 60 ? `${Math.round(minutes / 60)}h` : `${Math.round(minutes / (24 * 60))}d`;
|
|
16517
16893
|
const deltaLine = data.delta === null
|
|
16518
16894
|
? ""
|
|
16519
|
-
: `<p class="meta"><strong>
|
|
16895
|
+
: `<p class="meta"><strong>Since you last looked</strong> (${ago(data.delta.agoMinutes)} ago): ` +
|
|
16520
16896
|
[
|
|
16521
16897
|
data.delta.built > 0 ? `<span class="good">${data.delta.built} built</span>` : "",
|
|
16522
16898
|
data.delta.failed > 0 ? `<span class="bad">${data.delta.failed} failed</span>` : "",
|
|
@@ -16527,7 +16903,7 @@ function boardBody(data, ciRed) {
|
|
|
16527
16903
|
`</p>`;
|
|
16528
16904
|
return [
|
|
16529
16905
|
`<h1>Board</h1>`,
|
|
16530
|
-
data.all ? "" : `<p class="meta board-view"><strong>
|
|
16906
|
+
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
16907
|
deltaLine,
|
|
16532
16908
|
toggle,
|
|
16533
16909
|
`<div class="board">`,
|
|
@@ -16549,14 +16925,14 @@ function donePage(chrome, rows, ciRed) {
|
|
|
16549
16925
|
const pr = row.prNumber === null
|
|
16550
16926
|
? row.publicationState === null
|
|
16551
16927
|
? ""
|
|
16552
|
-
: ` <span class="badge">${escape(row.publicationState)}</span>`
|
|
16928
|
+
: ` <span class="badge">${escape(sentenceCase(row.publicationState))}</span>`
|
|
16553
16929
|
: ` <a href="${escape(row.prUrl ?? "#")}" class="badge badge-open">PR #${row.prNumber}</a>` +
|
|
16554
16930
|
(ciRed(row.prNumber) ? ` <span class="badge badge-failed">CI failing</span>` : "");
|
|
16555
16931
|
const needsVerification = (row.proofVerdict === "short" || row.proofVerdict === "refuted") && !row.proofAccepted
|
|
16556
16932
|
? ` <span class="badge badge-failed">${row.proofVerdict === "refuted" ? "conflicting evidence" : "missing evidence"}</span>`
|
|
16557
16933
|
: "";
|
|
16558
16934
|
return (`<div class="card"><p><a href="${taskHref(row.taskId)}"><strong>${escape(row.title)}</strong></a>` +
|
|
16559
|
-
`${row.outcome === "no-change" ? ` <span class="badge">
|
|
16935
|
+
`${row.outcome === "no-change" ? ` <span class="badge">No change needed</span>` : ""}${pr}${needsVerification}${criterionMatrixSummary(row.proofMatrix)}</p>` +
|
|
16560
16936
|
`${row.handoff === null ? "" : `<p class="meta">${escape(row.handoff.length > 200 ? row.handoff.slice(0, 200) + "\u2026" : row.handoff)}</p>`}` +
|
|
16561
16937
|
`<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
16938
|
` \u00b7 <a href="${reviewHref(row.taskId)}">review \u2192</a></p></div>`);
|
|
@@ -16622,9 +16998,9 @@ function trackRow(track, all) {
|
|
|
16622
16998
|
`<p>${trackStrip(fires)}` +
|
|
16623
16999
|
`<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
17000
|
(blocker !== null
|
|
16625
|
-
? `<p class="meta">
|
|
17001
|
+
? `<p class="meta">Stopped behind <a href="${taskHref(blocker.taskId)}" class="mono">${escape(blocker.taskId)}</a> (${escape(blocker.state)})</p>`
|
|
16626
17002
|
: latest?.instanceTaskId !== undefined && latest.instanceTaskId !== null
|
|
16627
|
-
? `<p class="meta">
|
|
17003
|
+
? `<p class="meta">Latest: <a href="${taskHref(latest.instanceTaskId)}" class="mono">${escape(latest.instanceTaskId)}</a>${latest.instanceState === null ? "" : ` (${escape(latest.instanceState)})`}</p>`
|
|
16628
17004
|
: "") +
|
|
16629
17005
|
`</div>`);
|
|
16630
17006
|
}
|
|
@@ -16741,12 +17117,13 @@ function checkProgressHtml(progress, id = "check-progress") {
|
|
|
16741
17117
|
: Object.values(progress.suites).every(one => one.state === "passed") ? "passed" : "unknown";
|
|
16742
17118
|
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
17119
|
}
|
|
16744
|
-
function taskControlDetailsHtml(control, taskId, csrf, surface, inert = false) {
|
|
16745
|
-
const html = taskControlHtml(control, taskId, csrf, surface, inert);
|
|
17120
|
+
function taskControlDetailsHtml(control, taskId, csrf, surface, inert = false, stopElsewhere = false) {
|
|
17121
|
+
const html = taskControlHtml(control, taskId, csrf, surface, inert, stopElsewhere);
|
|
16746
17122
|
return control.kind === "paused" || control.kind === "review-stopped" || control.kind === "stopping"
|
|
16747
17123
|
? `<details id="task-control-details"><summary>${control.kind === "paused" ? "Preserved work and resume" : "Stop details"}</summary>${html}</details>` : html;
|
|
16748
17124
|
}
|
|
16749
|
-
|
|
17125
|
+
/** `stopElsewhere`: the Building card already carries this exact run's Stop, so the control card keeps only its details. */
|
|
17126
|
+
function taskControlHtml(control, taskId, csrf, surface, inert = false, stopElsewhere = false) {
|
|
16750
17127
|
if (control.kind === "none")
|
|
16751
17128
|
return "";
|
|
16752
17129
|
const back = `<input type="hidden" name="return" value="${surface}">`;
|
|
@@ -16760,7 +17137,7 @@ function taskControlHtml(control, taskId, csrf, surface, inert = false) {
|
|
|
16760
17137
|
if (control.kind === "stop") {
|
|
16761
17138
|
return (`<section class="card task-control" id="task-control" data-task-control="stop" data-control-run="${control.run}" aria-label="stop this attempt">` +
|
|
16762
17139
|
`<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
|
|
17140
|
+
(stopElsewhere ? "" : guarded
|
|
16764
17141
|
? `<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
17142
|
: `<button type="button" class="danger task-control-button" disabled>Stop</button>`) +
|
|
16766
17143
|
`</section>`);
|
|
@@ -16800,7 +17177,7 @@ function resumeCeremonyPage(chrome, data) {
|
|
|
16800
17177
|
`<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
17178
|
`<p class="row">Resuming grants no new approval or publishing permission. Scope, budget, agents, risk, fallback, verification, and review limits still apply.</p>`,
|
|
16802
17179
|
`<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>
|
|
17180
|
+
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
17181
|
data.gate === null ? "" : `<p class="row"><strong>Before work starts:</strong> ${escape(data.gate.summary)} — ${escape(data.gate.detail)}</p>`,
|
|
16805
17182
|
`</div>`,
|
|
16806
17183
|
`<form method="post" action="${taskHref(data.taskId)}/resume" class="card resume-form">`,
|
|
@@ -16876,12 +17253,23 @@ function taskChatApproval(focus, csrf) {
|
|
|
16876
17253
|
route: focus.route,
|
|
16877
17254
|
coordinator: approval.coordinator,
|
|
16878
17255
|
deliverable: approval.deliverable,
|
|
16879
|
-
|
|
17256
|
+
// Edit plan opens the task page's sheet with its fields open for editing.
|
|
17257
|
+
editHref: approval.raceTerms !== null ? `${taskHref(focus.id)}#${approval.planDocument === null ? "scope" : "plan-edit"}` : `${taskHref(focus.id)}?edit=plan#plan-editor`,
|
|
16880
17258
|
notNowHref: "/chat",
|
|
16881
17259
|
sticky: false,
|
|
17260
|
+
earlier: { active: focus.assignment?.earlierActive ?? 0, running: focus.assignment?.earlierRunning ?? 0 },
|
|
17261
|
+
edit: null,
|
|
16882
17262
|
}) +
|
|
16883
17263
|
`</section>`);
|
|
16884
17264
|
}
|
|
17265
|
+
/** The chat's status sentence, worded as the task page words it: what a failed attempt missed, or which step a live
|
|
17266
|
+
* build is on (and the step it is stuck on). Undefined keeps the shared status sentence. */
|
|
17267
|
+
function chatStatusSentence(focus) {
|
|
17268
|
+
if (focus.state === "failed" && focus.failure != null)
|
|
17269
|
+
return [focus.failure.line, focus.failure.evidence].filter(Boolean).join(" ");
|
|
17270
|
+
const steps = focus.liveRun === null ? null : buildProgressOf(focus.milestoneProgress);
|
|
17271
|
+
return steps === null ? undefined : steps.line;
|
|
17272
|
+
}
|
|
16885
17273
|
/** One live, server-derived journey from request to proof. The fragment is
|
|
16886
17274
|
* safe to refresh independently, so an in-progress message is never lost. */
|
|
16887
17275
|
function taskChatLiveRegion(focus, csrf, fragment = false, inert = false, approvalHref = taskChatHref(focus.id) + "#task-chat-action") {
|
|
@@ -16897,7 +17285,7 @@ function taskChatLiveRegion(focus, csrf, fragment = false, inert = false, approv
|
|
|
16897
17285
|
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
17286
|
(focus.guide === true && focus.state !== "cancelled" ? firstTaskJourneyHtml(focus) : "") +
|
|
16899
17287
|
`<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")) +
|
|
17288
|
+
(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
17289
|
checkProgressHtml(focus.checkProgress) +
|
|
16902
17290
|
// The exact-run control (v52): the SAME component the task page
|
|
16903
17291
|
// renders, refreshed with the live region — typed input in the
|
|
@@ -17008,7 +17396,7 @@ function chatFleetOverview(snapshot, projects, csrf, interactive) {
|
|
|
17008
17396
|
return (`<section class="card chat-overview" aria-label="live portfolio overview" data-card-kind="fleet-overview">` +
|
|
17009
17397
|
`<div class="chat-overview-head"><h2>${projects.length} project${projects.length === 1 ? "" : "s"}</h2>` +
|
|
17010
17398
|
(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">
|
|
17399
|
+
? `<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
17400
|
: `<a href="/board?scope=all" class="chat-overview-link">open board</a>`) +
|
|
17013
17401
|
`</div>` +
|
|
17014
17402
|
`<div class="chat-overview-stats">` +
|
|
@@ -17043,9 +17431,9 @@ function chatProjectRail(projects, csrf, inert) {
|
|
|
17043
17431
|
const ask = `Give me a concise status for ${one.id} (${one.label}) and recommend the next reversible action.`;
|
|
17044
17432
|
return (`<div class="chat-project-card">` +
|
|
17045
17433
|
`<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>` +
|
|
17434
|
+
`<span class="badge">${escape(sentenceCase(statusOf(peek)))}</span></div>` +
|
|
17047
17435
|
(peek === null
|
|
17048
|
-
? `<p class="meta">
|
|
17436
|
+
? `<p class="meta">Pulse unavailable</p>`
|
|
17049
17437
|
: `<div class="chat-project-stats">` +
|
|
17050
17438
|
`<span${peek.waiting > 0 ? ' class="hot"' : ""}><b>${peek.waiting}</b> need you</span>` +
|
|
17051
17439
|
`<span><b>${peek.running}</b> live</span><span><b>${peek.queued}</b> queued</span>` +
|
|
@@ -17054,13 +17442,13 @@ function chatProjectRail(projects, csrf, inert) {
|
|
|
17054
17442
|
(inert
|
|
17055
17443
|
? ""
|
|
17056
17444
|
: `<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)}">
|
|
17445
|
+
`<button type="submit" name="message" value="${escape(ask)}" class="quiet" aria-label="ask about ${escape(one.label)}">Ask</button></form>`) +
|
|
17058
17446
|
`<form method="post" action="/projects/open" class="inline"><input type="hidden" name="csrf" value="${escape(csrf)}">` +
|
|
17059
17447
|
`<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">
|
|
17448
|
+
`<button type="submit" class="quiet" aria-label="open ${escape(one.label)} board">Board</button></form></div></div>`);
|
|
17061
17449
|
}).join("");
|
|
17062
17450
|
return (`<aside class="chat-projects" id="chat-project-panel" aria-label="projects in this conversation">` +
|
|
17063
|
-
`<div class="chat-projects-head"><h2>
|
|
17451
|
+
`<div class="chat-projects-head"><h2>Projects</h2><span class="badge">${projects.length}</span>` +
|
|
17064
17452
|
`<button type="button" class="chat-project-close quiet" aria-label="close projects">×</button></div>` +
|
|
17065
17453
|
`<div class="chat-project-list">${rows}</div></aside>`);
|
|
17066
17454
|
}
|
|
@@ -17073,9 +17461,9 @@ function matePromptStarters(csrf, focus = null) {
|
|
|
17073
17461
|
["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
17462
|
]
|
|
17075
17463
|
: [
|
|
17076
|
-
["
|
|
17077
|
-
["
|
|
17078
|
-
["
|
|
17464
|
+
["What’s happening", "Read this task and explain its current status, what is blocking it, and what should happen next."],
|
|
17465
|
+
["Review results", "Review the evidence recorded for this task and tell me what is proven and what is still unverified."],
|
|
17466
|
+
["Adjust the plan", "Read this task and propose a tighter scope if that would improve the outcome. Explain why before I confirm anything."],
|
|
17079
17467
|
];
|
|
17080
17468
|
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
17469
|
(focus === null ? "" : `<input type="hidden" name="task" value="${escape(focus.id)}">`) +
|
|
@@ -17205,6 +17593,14 @@ function leadSettingsHtml(data) {
|
|
|
17205
17593
|
`<p><a href="/settings">Settings</a></p><h1>Lead</h1>`,
|
|
17206
17594
|
data.said === null ? "" : `<p class="problem" role="status">${escape(data.said)}</p>`,
|
|
17207
17595
|
`<section class="card lead-settings" data-lead-settings>${summary}</section>`,
|
|
17596
|
+
data.identity === undefined ? "" : `<form method="post" action="/settings/lead/identity" class="card lead-identity" data-lead-identity>` +
|
|
17597
|
+
`<input type="hidden" name="csrf" value="${escape(facts.csrf)}">` +
|
|
17598
|
+
`<label>Name your lead<input name="name" value="${escape(data.identity.name)}" maxlength="${LEAD_NAME_MAX}" autocomplete="off" required></label>` +
|
|
17599
|
+
`<label>Persona<textarea name="persona" rows="4" maxlength="${LEAD_PERSONA_MAX}">${escape(data.identity.persona)}</textarea></label>` +
|
|
17600
|
+
`<button type="submit">Save</button>${data.saved === true ? ` <span class="meta" role="status">Saved.</span>` : ""}</form>`,
|
|
17601
|
+
// What the lead promised to follow up on; it reports each once, here in chat, and drops it after 7 days.
|
|
17602
|
+
(data.promises ?? []).length === 0 ? "" : `<section class="card lead-promises" data-lead-promises><h2>Promises</h2><ul class="lead-promise-list">${data.promises.map(one => `<li data-promise="${one.id}"><p>${escape(one.what)}</p><p class="meta">${escape(one.when.charAt(0).toUpperCase() + one.when.slice(1))} · <span class="nowrap">until ${escape(new Date(one.until).toLocaleDateString("en-US", { month: "short", day: "numeric", timeZone: "UTC" }))}</span></p>` +
|
|
17603
|
+
`<form method="post" action="/settings/lead/promise/cancel" class="inline">${hidden}<input type="hidden" name="promise" value="${one.id}"><button type="submit" class="secondary">Cancel</button></form></li>`).join("")}</ul></section>`,
|
|
17208
17604
|
`<details class="lead-advanced" data-lead-advanced><summary>Advanced</summary>`,
|
|
17209
17605
|
leadConfigForm(config, facts),
|
|
17210
17606
|
config === null ? "" : `<form method="post" action="/chat/config" class="inline">${hidden}<input type="hidden" name="off" value="1">` +
|
|
@@ -17224,23 +17620,23 @@ function leadConfigForm(current, data) {
|
|
|
17224
17620
|
`<form method="post" action="/chat/config" class="card">`,
|
|
17225
17621
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
|
|
17226
17622
|
`<input type="hidden" name="return" value="${escape(data.returnTo)}">`,
|
|
17227
|
-
`<label>
|
|
17623
|
+
`<label>Provider<select name="provider">`,
|
|
17228
17624
|
`<option value="codex-subscription"${current?.provider === "codex-subscription" ? " selected" : ""}>Codex membership (logged-in CLI)</option>`,
|
|
17229
17625
|
`<option value="claude-subscription"${current?.provider === "claude-subscription" ? " selected" : ""}>Anthropic membership (logged-in CLI)</option>`,
|
|
17230
17626
|
`<option value="anthropic-api"${current?.provider === "anthropic-api" ? " selected" : ""}>anthropic-api (direct API)</option>`,
|
|
17231
17627
|
`<option value="openrouter-api"${current?.provider === "openrouter-api" ? " selected" : ""}>openrouter-api (direct API)</option>`,
|
|
17232
17628
|
`</select></label>`,
|
|
17233
|
-
`<label>
|
|
17629
|
+
`<label>Model <span class="meta">(use default for your membership's current model; direct API models need a pinned price)</span>` +
|
|
17234
17630
|
`<input name="model" list="chat-models" value="${escape(current?.model ?? "default")}"><datalist id="chat-models">` +
|
|
17235
17631
|
`${models.map(model => `<option value="${escape(model)}">${escape(labels.get(model) ?? "")}</option>`).join("")}</datalist></label>`,
|
|
17236
17632
|
data.openrouterModels === null
|
|
17237
|
-
? `<p class="meta">
|
|
17633
|
+
? `<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
17634
|
: `<p class="meta">${data.openrouterModels.length} models live from OpenRouter's catalog; saving pins today's price — re-save to re-pin</p>`,
|
|
17239
17635
|
currentSubscription
|
|
17240
|
-
? `<p class="meta"><strong>
|
|
17241
|
-
: `<label>
|
|
17636
|
+
? `<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>`
|
|
17637
|
+
: `<label>Weekly ceiling <span class="meta">(direct API only; leave blank when choosing a membership)</span>` +
|
|
17242
17638
|
`<input type="text" name="weekly-usd" inputmode="decimal" style="width:8rem" value="${current === null ? "" : (current.weeklyCeilingMicrousd / 1_000_000).toFixed(2)}"></label>`,
|
|
17243
|
-
`<label>
|
|
17639
|
+
`<label>Daily turns <span class="meta">(default 50)</span>` +
|
|
17244
17640
|
`<input type="text" name="daily-turns" inputmode="numeric" style="width:8rem" value="${current === null ? "" : String(current.dailyTurns)}"></label>`,
|
|
17245
17641
|
currentSubscription
|
|
17246
17642
|
? `<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 +17648,13 @@ function leadConfigForm(current, data) {
|
|
|
17252
17648
|
: `${escape(one.provider)}: stored ${escape(one.tail ?? "")}`)
|
|
17253
17649
|
.join(" · ")})</span>` +
|
|
17254
17650
|
`<input type="password" name="key" placeholder="direct API only — leave empty to keep" autocomplete="off"></label>`,
|
|
17255
|
-
`<label>
|
|
17651
|
+
`<label>Your password <span class="meta">(typed again to change the provider)</span>` +
|
|
17256
17652
|
`<input type="password" name="token" autocomplete="current-password"></label>`,
|
|
17257
17653
|
`<button type="submit">${current === null ? "turn chat on" : "save"}</button>`,
|
|
17258
17654
|
`</form>`,
|
|
17259
17655
|
currentSubscription
|
|
17260
17656
|
? `<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">
|
|
17657
|
+
: `<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
17658
|
`<span class="mono">ANTHROPIC_API_KEY</span> / <span class="mono">OPENROUTER_API_KEY</span>) always wins when set</p>`,
|
|
17263
17659
|
].join("\n");
|
|
17264
17660
|
}
|
|
@@ -17287,7 +17683,7 @@ function chatPage(chrome, data) {
|
|
|
17287
17683
|
? `<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
17684
|
: code === "unpriced" || code === "no-key"
|
|
17289
17685
|
? `<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>
|
|
17686
|
+
: `<div class="card" id="latest"><p><strong>Chat is off.</strong></p><p class="meta">${escape(data.enabled.why)}</p></div>`);
|
|
17291
17687
|
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
17688
|
}
|
|
17293
17689
|
const config = data.enabled.config;
|
|
@@ -17303,7 +17699,7 @@ function chatPage(chrome, data) {
|
|
|
17303
17699
|
parts.push(data.coordinatorProposals);
|
|
17304
17700
|
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
17701
|
for (const turn of data.latched) {
|
|
17306
|
-
parts.push(`<div class="problem"><strong>
|
|
17702
|
+
parts.push(`<div class="problem"><strong>Unknown spend blocks chat.</strong> turn #${turn.id} may have cost up to ${chatMoney(turn.reservedMicrousd)} — ` +
|
|
17307
17703
|
`<a href="/chat/ack/${turn.id}">read and acknowledge it</a> to re-enable this credential.</div>`);
|
|
17308
17704
|
}
|
|
17309
17705
|
if (data.pending !== null) {
|
|
@@ -17318,7 +17714,7 @@ function chatPage(chrome, data) {
|
|
|
17318
17714
|
}
|
|
17319
17715
|
else if (last.reply !== null) {
|
|
17320
17716
|
parts.push(`<div class="card" id="latest">${renderChatText(last.reply)}` +
|
|
17321
|
-
(last.proposalsDiscarded ? `<p class="meta">
|
|
17717
|
+
(last.proposalsDiscarded ? `<p class="meta">A draft block in this answer was malformed and was discarded whole</p>` : "") +
|
|
17322
17718
|
`</div>`);
|
|
17323
17719
|
}
|
|
17324
17720
|
}
|
|
@@ -17326,21 +17722,21 @@ function chatPage(chrome, data) {
|
|
|
17326
17722
|
for (const one of candidates) {
|
|
17327
17723
|
const draft = one.draft;
|
|
17328
17724
|
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">
|
|
17725
|
+
`<p><strong>${draft.kind === "task" ? escape(draft.title) : escape(draft.name)}</strong> <span class="badge">Draft ${escape(draft.kind)}</span></p>` +
|
|
17726
|
+
`<p class="meta">Drafted by the model from fleet context — nothing is filed; drafts do not survive a restart</p>` +
|
|
17331
17727
|
`<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">
|
|
17728
|
+
(draft.outOfScope === null ? "" : `<p class="meta">Not: ${escape(draft.outOfScope)}</p>`) +
|
|
17729
|
+
(draft.touches.length > 0 ? `<p class="meta">Touches: ${escape(draft.touches.join(", "))}</p>` : "") +
|
|
17730
|
+
(draft.kind === "routine" ? `<p class="meta">Schedule: ${escape(draft.schedule)}</p>` : "") +
|
|
17731
|
+
`<p class="meta">Repo: <span class="mono">${escape(projectName(one.repoPath))}</span></p>` +
|
|
17336
17732
|
`<form method="post" action="/chat/file/${escape(one.key)}" class="inline">` +
|
|
17337
17733
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
|
|
17338
17734
|
`<input type="password" name="token" placeholder="your password" autocomplete="current-password">` +
|
|
17339
|
-
`<button type="submit">
|
|
17735
|
+
`<button type="submit">File unapproved</button></form>` +
|
|
17340
17736
|
`</div>`);
|
|
17341
17737
|
}
|
|
17342
17738
|
if (!subscription && data.canManage && data.focusTask === null) {
|
|
17343
|
-
parts.push(`<h2>
|
|
17739
|
+
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
17740
|
}
|
|
17345
17741
|
if (data.canManage) {
|
|
17346
17742
|
parts.push(`<p class="meta" id="chat-settings"><a href="/settings/lead">Lead settings</a> · provider, model and limits</p>`);
|
|
@@ -17363,12 +17759,12 @@ function chatAckPage(chrome, turn, nonce, csrf) {
|
|
|
17363
17759
|
`<p>Turn <span class="mono">#${turn.id}</span> on <span class="mono">${escape(turn.provider)} · ${escape(turn.model)}</span> ` +
|
|
17364
17760
|
`may have started before it failed (${escape(turn.failureReason ?? "crashed")}), and its cost could not be measured.</p>`,
|
|
17365
17761
|
`<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">
|
|
17762
|
+
`<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
17763
|
`<form method="post" action="/chat/ack/${turn.id}">`,
|
|
17368
17764
|
`<input type="hidden" name="csrf" value="${escape(csrf)}">`,
|
|
17369
17765
|
`<input type="hidden" name="nonce" value="${escape(nonce)}">`,
|
|
17370
|
-
`<label>
|
|
17371
|
-
`<button type="submit">
|
|
17766
|
+
`<label>Your password<input type="password" name="token" autocomplete="current-password"></label>`,
|
|
17767
|
+
`<button type="submit">Accept the charge — re-enable chat</button>`,
|
|
17372
17768
|
`</form>`,
|
|
17373
17769
|
`</div>`,
|
|
17374
17770
|
].join("\n"), { chrome });
|
|
@@ -17575,10 +17971,10 @@ function proposalCardParts(view, csrf, inert, decision, returnTo = null) {
|
|
|
17575
17971
|
const decisionId = typeof payload["decision"] === "number" ? payload["decision"] : 0;
|
|
17576
17972
|
const pick = text("option");
|
|
17577
17973
|
const options = decision === null
|
|
17578
|
-
? `<p class="meta">
|
|
17974
|
+
? `<p class="meta">The decision is gone</p>`
|
|
17579
17975
|
: `<ul class="answer-options">` +
|
|
17580
17976
|
decision.options
|
|
17581
|
-
.map(one => `<li${one.id === pick ? ' class="picked"' : ""}><strong>${escape(one.label)}</strong>${one.reversible ? "" : ' <span class="badge">
|
|
17977
|
+
.map(one => `<li${one.id === pick ? ' class="picked"' : ""}><strong>${escape(one.label)}</strong>${one.reversible ? "" : ' <span class="badge">Irreversible</span>'}` +
|
|
17582
17978
|
`${one.id === decision.recommendation ? ' <span class="meta">— the builder recommends this</span>' : ""}` +
|
|
17583
17979
|
`${one.id === pick ? ' <span class="meta">— proposed</span>' : ""}` +
|
|
17584
17980
|
`<p class="meta">${escape(one.consequence)}</p></li>`)
|
|
@@ -17608,13 +18004,13 @@ function proposalCardParts(view, csrf, inert, decision, returnTo = null) {
|
|
|
17608
18004
|
: view.kind === "control"
|
|
17609
18005
|
? (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
18006
|
: 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">
|
|
18007
|
+
? `<p class="meta">Cancelling is armed on the task itself — <a href="${taskHref(task)}">open ${escape(task)}</a></p>` +
|
|
18008
|
+
`<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
18009
|
: `<div class="acts">` +
|
|
17614
18010
|
`<form method="post" action="${view.actionBase}/${view.id}/confirm" class="inline"><input type="hidden" name="csrf" value="${escape(csrf)}">${returnField}` +
|
|
17615
18011
|
(irreversible ? `<label class="arm"><input type="checkbox" name="confirm" value="yes"> I understand this cannot be undone</label>` : "") +
|
|
17616
18012
|
`<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">
|
|
18013
|
+
`<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
18014
|
`</div>`;
|
|
17619
18015
|
}
|
|
17620
18016
|
else if (view.state === "pending") {
|
|
@@ -17645,7 +18041,7 @@ function proposalCardParts(view, csrf, inert, decision, returnTo = null) {
|
|
|
17645
18041
|
const html = (`<article class="card proposal proposal-${escape(view.kind)} ${escape(view.state)}" data-card-kind="${escape(view.kind)}">` +
|
|
17646
18042
|
`<header class="proposal-head"><span class="proposal-icon">${strokeIcon(presentation.icon)}</span>` +
|
|
17647
18043
|
`<span><strong>${escape(presentation.label)}</strong><small>proposed by ${provenance}</small></span>` +
|
|
17648
|
-
`<span class="badge ${stateClass}">${escape(view.state)}</span></header>` +
|
|
18044
|
+
`<span class="badge ${stateClass}">${escape(sentenceCase(view.state))}</span></header>` +
|
|
17649
18045
|
`<div class="proposal-body">${what}</div>` +
|
|
17650
18046
|
`<footer class="proposal-actions">${acts}</footer></article>`);
|
|
17651
18047
|
// The same card as data (chat cards): one act, in the door's own words.
|
|
@@ -17693,18 +18089,35 @@ function relativeAge(iso, now) {
|
|
|
17693
18089
|
function coordinatorProposalsSection(proposals, decisions, csrf, now, heading = true, returnTo = null) {
|
|
17694
18090
|
if (proposals.length === 0)
|
|
17695
18091
|
return "";
|
|
17696
|
-
return ((heading ? `<h2>
|
|
18092
|
+
return ((heading ? `<h2>Proposed by coordinators <span class="meta">${proposals.length}</span></h2>` : "") +
|
|
17697
18093
|
`<div class="coordinator-proposals">` +
|
|
17698
18094
|
proposals.map(one => coordinatorProposalCard(one, csrf, now, decisions.get(typeof one.payload["decision"] === "number" ? one.payload["decision"] : -1) ?? null, returnTo)).join("") +
|
|
17699
18095
|
`</div>`);
|
|
17700
18096
|
}
|
|
18097
|
+
/** The lead's question as buttons: each option sends itself as the next message (the same POST as a typed one);
|
|
18098
|
+
* "Something else" moves to the composer. Nothing shows while a reply is running. */
|
|
18099
|
+
function mateAskHtml(ask, csrf, target, composer, open = true) {
|
|
18100
|
+
if (!open)
|
|
18101
|
+
return `<div class="so-owner-ask" data-ask="${ask.turn}"><p class="so-owner-ask-question"><strong>${escape(ask.question)}</strong></p></div>`;
|
|
18102
|
+
const hidden = `<input type="hidden" name="csrf" value="${escape(csrf)}">` +
|
|
18103
|
+
(target.task !== null ? `<input type="hidden" name="task" value="${escape(target.task)}">` : target.project !== null ? `<input type="hidden" name="project" value="${escape(target.project)}">` : "");
|
|
18104
|
+
return `<div class="so-owner-ask" data-ask="${ask.turn}"><p class="so-owner-ask-question"><strong>${escape(ask.question)}</strong></p>` +
|
|
18105
|
+
`<div class="so-owner-ask-options" role="group" aria-label="Answer options">` +
|
|
18106
|
+
ask.options.map(option => `<form method="post" action="/chat" class="inline">${hidden}<button type="submit" name="message" value="${escape(option)}" class="so-suggestion quiet">${escape(option)}</button></form>`).join("") +
|
|
18107
|
+
`<label for="${escape(composer)}" class="so-suggestion so-owner-ask-other">${escape(MATE_ASK_OTHER)}</label></div></div>`;
|
|
18108
|
+
}
|
|
17701
18109
|
/** The thread's messages as the React conversation renders them, each
|
|
17702
18110
|
* with the proposal cards its turn produced; `back` is where a card's
|
|
17703
18111
|
* confirm or dismiss returns. */
|
|
17704
|
-
function mateBrowserMessages(rows, csrf, back) {
|
|
18112
|
+
function mateBrowserMessages(rows, csrf, back, target = { task: null, project: null }) {
|
|
18113
|
+
const open = rows.pending === null ? rows.ask?.turn ?? null : null;
|
|
18114
|
+
const asked = (turn) => {
|
|
18115
|
+
const ask = turn === null ? undefined : rows.asks?.get(turn);
|
|
18116
|
+
return ask === undefined ? "" : mateAskHtml(ask, csrf, target, target.task === null ? "lead-message" : "task-message", ask.turn === open);
|
|
18117
|
+
};
|
|
17705
18118
|
return rows.messages.map(message => ({
|
|
17706
18119
|
id: message.id, role: message.role, text: message.text,
|
|
17707
|
-
html: message.role === 'operator' ? `<p>${escape(message.text)}</p>` : renderChatText(message.text),
|
|
18120
|
+
html: message.role === 'operator' ? `<p>${escape(message.text)}</p>` : renderChatText(message.text) + asked(message.turn),
|
|
17708
18121
|
activity: message.activity, createdAt: message.createdAt,
|
|
17709
18122
|
...(() => {
|
|
17710
18123
|
const parts = message.turn === null ? [] : rows.proposals.filter(one => one.turn === message.turn)
|
|
@@ -17727,6 +18140,7 @@ function mateChatVersion(rows) {
|
|
|
17727
18140
|
rows.coordinatorProposals.map(one => [one.id, one.state, one.outcome === null ? null : JSON.stringify(one.outcome)]),
|
|
17728
18141
|
[...rows.decisions.values()].map(one => [one.id, one.state, one.choice ?? null]),
|
|
17729
18142
|
rows.pending === null ? null : [rows.pending.id, rows.pending.steps],
|
|
18143
|
+
rows.ask?.turn ?? null,
|
|
17730
18144
|
rows.recent[0] === undefined ? null : [rows.recent[0].id, rows.recent[0].state, rows.recent[0].failureReason],
|
|
17731
18145
|
focus === null
|
|
17732
18146
|
? null
|
|
@@ -17782,6 +18196,10 @@ function mateThreadHtml(data) {
|
|
|
17782
18196
|
const cards = message.turn === null ? [] : (byTurn.get(message.turn) ?? []);
|
|
17783
18197
|
parts.push(`<div class="msg mate" data-message-role="assistant" data-key="m${message.id}"${message.id === latestReply ? ' id="latest"' : ""}>` +
|
|
17784
18198
|
renderChatText(message.text) +
|
|
18199
|
+
(() => {
|
|
18200
|
+
const ask = message.turn === null ? undefined : data.asks?.get(message.turn);
|
|
18201
|
+
return ask === undefined ? "" : mateAskHtml(ask, data.csrf, { task: data.focusTask?.id ?? null, project: data.chatProject ?? null }, "chat-message", data.pending === null && data.ask?.turn === ask.turn);
|
|
18202
|
+
})() +
|
|
17785
18203
|
cards.map(one => mateProposalCard(one, data.csrf, inert, data.decisions.get(typeof one.payload["decision"] === "number" ? one.payload["decision"] : -1) ?? null, data.focusTask === null ? null : returnTo)).join("") +
|
|
17786
18204
|
`<div class="chat-message-foot">${chatActivity(message.activity)}<time datetime="${escape(message.createdAt)}">${escape(relativeAge(message.createdAt, data.now))}</time></div>` +
|
|
17787
18205
|
`</div>`);
|
|
@@ -17817,10 +18235,10 @@ function matePage(chrome, data) {
|
|
|
17817
18235
|
`<span>this week ${chatMoney(data.weeklySpent)} of ${chatMoney(data.config.weeklyCeilingMicrousd)}</span>`) +
|
|
17818
18236
|
`</div>`,
|
|
17819
18237
|
`<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">
|
|
18238
|
+
`<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
18239
|
data.recent.length === 0
|
|
17822
18240
|
? ""
|
|
17823
|
-
: `<p class="meta">
|
|
18241
|
+
: `<p class="meta">Recent turns: ${data.recent
|
|
17824
18242
|
.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
18243
|
.join(" · ")}</p>`,
|
|
17826
18244
|
`<p class="meta"><a href="/settings/lead">Lead settings</a> · provider, model and limits</p>`,
|
|
@@ -17837,17 +18255,17 @@ function matePage(chrome, data) {
|
|
|
17837
18255
|
if (data.problem !== null)
|
|
17838
18256
|
conversation.push(`<div class="problem">${escape(data.problem)}</div>`);
|
|
17839
18257
|
for (const turn of data.latched) {
|
|
17840
|
-
conversation.push(`<div class="problem"><strong>
|
|
18258
|
+
conversation.push(`<div class="problem"><strong>Unknown spend blocks chat.</strong> turn #${turn.id} may have cost up to ${chatMoney(turn.reservedMicrousd)} — ` +
|
|
17841
18259
|
`<a href="/chat/ack/${turn.id}">read and acknowledge it</a> to re-enable this credential.</div>`);
|
|
17842
18260
|
}
|
|
17843
18261
|
const lastMessage = data.messages.at(-1);
|
|
17844
18262
|
const latestReply = data.pending === null && lastMessage?.role === "assistant" ? lastMessage.id : null;
|
|
17845
|
-
conversation.push(mateThreadHtml({ ...data, problem: null }));
|
|
18263
|
+
conversation.push(mateThreadHtml({ ...data, problem: null, chatProject }));
|
|
17846
18264
|
conversation.push(
|
|
17847
18265
|
// The New update action (package 2): hidden until a live update lands
|
|
17848
18266
|
// while the reader is above the latest message; a real button, so the
|
|
17849
18267
|
// 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>
|
|
18268
|
+
`<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 id="chat-message" 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
18269
|
// Concise pass (2026-09-13): the status line speaks only when there is
|
|
17852
18270
|
// a state to report — a reply in progress here, the connection from
|
|
17853
18271
|
// the continuity script — and the composer carries no second intro.
|
|
@@ -17859,7 +18277,7 @@ function matePage(chrome, data) {
|
|
|
17859
18277
|
workspace: {
|
|
17860
18278
|
conversation: {
|
|
17861
18279
|
sessionId: data.session.id, user: data.session.approver, version: mateChatVersion(data),
|
|
17862
|
-
messages: mateBrowserMessages(data, data.csrf, data.focusTask === null && chatProject === null ? null : returnTo),
|
|
18280
|
+
messages: mateBrowserMessages(data, data.csrf, data.focusTask === null && chatProject === null ? null : returnTo, { task: data.focusTask?.id ?? null, project: chatProject }),
|
|
17863
18281
|
pendingTurnId: data.pending?.id ?? null, requestId: randomBytes(16).toString('hex'), maxChars: MATE_MESSAGE_MAX_CHARS,
|
|
17864
18282
|
taskId: data.focusTask?.id ?? null, resultRunId: data.resultRunId ?? null, project: chatProject,
|
|
17865
18283
|
},
|
|
@@ -17879,25 +18297,25 @@ function routinesPage(chrome, tracks, form) {
|
|
|
17879
18297
|
const capture = chrome.project === null
|
|
17880
18298
|
? ""
|
|
17881
18299
|
: [
|
|
17882
|
-
`<h2>
|
|
18300
|
+
`<h2>File a standing order</h2>`,
|
|
17883
18301
|
form.problem === null ? "" : `<div class="problem">${escape(form.problem)}</div>`,
|
|
17884
18302
|
`<form method="post" action="/routines/add" class="card">`,
|
|
17885
18303
|
`<input type="hidden" name="csrf" value="${escape(form.csrf)}">`,
|
|
17886
18304
|
`<input type="hidden" name="projectRevision" value="${form.revision}">`,
|
|
17887
18305
|
fill === null
|
|
17888
|
-
? `<p class="meta">
|
|
18306
|
+
? `<p class="meta">Start from a template: ${TEMPLATES.filter(one => one.kind === "routine")
|
|
17889
18307
|
.map(one => `<a href="/routines?template=${escape(one.name)}">${escape(one.name)}</a>`)
|
|
17890
18308
|
.join(" · ")}</p>`
|
|
17891
18309
|
: `<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>
|
|
18310
|
+
`<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>`,
|
|
18311
|
+
`<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>`,
|
|
18312
|
+
`<label>Not this <span class="meta">(optional)</span><input type="text" name="not" value="${fill === null ? "" : escape(fill.not)}"></label>`,
|
|
18313
|
+
`<label>Touches <span class="meta">(paths, comma-separated, optional)</span><input type="text" name="touches" value="${fill === null ? "" : escape(fill.touches)}"></label>`,
|
|
18314
|
+
`<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
18315
|
scheduleEditorHtml(fill?.schedule ?? null, form.values),
|
|
17898
|
-
`<label>
|
|
17899
|
-
`<button type="submit">
|
|
17900
|
-
`<p class="meta">
|
|
18316
|
+
`<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>`,
|
|
18317
|
+
`<button type="submit">File it \u2192 approve the standing order next</button>`,
|
|
18318
|
+
`<p class="meta">Filing is cheap — nothing fires until you approve the template on the next screen, password and all</p>`,
|
|
17901
18319
|
`</form>`,
|
|
17902
18320
|
].join("\n");
|
|
17903
18321
|
const list = tracks.length === 0
|
|
@@ -17917,19 +18335,19 @@ function routineScreenPage(chrome, data) {
|
|
|
17917
18335
|
const schedule = parseSchedule(routine.schedule);
|
|
17918
18336
|
const scheduleSaid = schedule === null ? routine.schedule : describeSchedule(schedule);
|
|
17919
18337
|
const terms = `<div class="card">` +
|
|
17920
|
-
`<p class="meta">
|
|
17921
|
-
`<p class="meta">
|
|
17922
|
-
`<p class="meta">
|
|
18338
|
+
`<p class="meta">Goal</p><p class="recap" style="margin-top:0">${escape(routine.goal)}</p>` +
|
|
18339
|
+
`<p class="meta">Not this</p><p class="recap" style="margin-top:0">${routine.outOfScope === null ? "<em>no exclusions</em>" : escape(routine.outOfScope)}</p>` +
|
|
18340
|
+
`<p class="meta">Touches · ${routine.touches.length === 0 ? "anything" : routine.touches.map(one => escape(one)).join(", ")}</p>` +
|
|
17923
18341
|
(routine.acceptance.length === 0
|
|
17924
18342
|
? ""
|
|
17925
|
-
: `<p class="meta">
|
|
18343
|
+
: `<p class="meta">Acceptance</p><ul class="recap">${routine.acceptance
|
|
17926
18344
|
.map(c => `<li><code>${escape(c.id)}</code> ${escape(c.statement)} <span class="meta">[requires: ${c.evidence.map(escape).join(", ")}]</span></li>`)
|
|
17927
18345
|
.join("")}</ul>`) +
|
|
17928
|
-
`<p class="meta">
|
|
18346
|
+
`<p class="meta">Needs · ${routine.requirements.length === 0 ? "nothing beyond the repository" : routine.requirements.map(one => escape(one)).join(", ")}</p>` +
|
|
17929
18347
|
routineAgentsHtml(routine, approved) +
|
|
17930
|
-
`<p class="meta">
|
|
17931
|
-
`<p class="meta">
|
|
17932
|
-
`<p class="meta">
|
|
18348
|
+
`<p class="meta">Schedule · ${escape(scheduleSaid)}</p>` +
|
|
18349
|
+
`<p class="meta">Budget · ${routine.costCeilingUsd === null ? "no ceiling" : `$${routine.costCeilingUsd.toFixed(2)} per rolling 7 days`}</p>` +
|
|
18350
|
+
`<p class="meta">One at a time — a firing skips while the previous instance is unfinished</p>` +
|
|
17933
18351
|
`</div>`;
|
|
17934
18352
|
// THE CONSENT DOOR (v48): the password and the approve act exist only
|
|
17935
18353
|
// where an exact, readable route can be restated — never over agents
|
|
@@ -17961,13 +18379,13 @@ function routineScreenPage(chrome, data) {
|
|
|
17961
18379
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
|
|
17962
18380
|
`<input type="hidden" name="nonce" value="${escape(data.nonce)}">`,
|
|
17963
18381
|
`<input type="hidden" name="digest" value="${escape(routine.digest)}">`,
|
|
17964
|
-
`<p><strong>
|
|
18382
|
+
`<p><strong>Approve this standing order:</strong></p>`,
|
|
17965
18383
|
`<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
18384
|
// The agents the yes freezes (v48): the same concise block every
|
|
17967
18385
|
// approval shows, before the password.
|
|
17968
18386
|
routineAgentsHtml(routine, false, true),
|
|
17969
|
-
`<label>
|
|
17970
|
-
`<button type="submit">
|
|
18387
|
+
`<label>Your password, typed again — a signed-in session alone cannot agree to standing work<input type="password" name="token" autocomplete="current-password"></label>` +
|
|
18388
|
+
`<button type="submit">Approve this routine</button>`,
|
|
17971
18389
|
`</form>`,
|
|
17972
18390
|
].join("\n");
|
|
17973
18391
|
const verb = (name, label, danger = false) => `<form method="post" action="${routineHref(routine.id)}/${name}" class="inline">` +
|
|
@@ -17977,7 +18395,7 @@ function routineScreenPage(chrome, data) {
|
|
|
17977
18395
|
? `<form method="post" action="${routineHref(routine.id)}/run-now" class="inline">` +
|
|
17978
18396
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
|
|
17979
18397
|
`<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">
|
|
18398
|
+
`<button type="submit">Run now</button></form>`
|
|
17981
18399
|
: "";
|
|
17982
18400
|
const acts = `<div class="card">` +
|
|
17983
18401
|
(routine.paused ? verb("resume", "resume") : verb("pause", "pause")) +
|
|
@@ -17991,7 +18409,7 @@ function routineScreenPage(chrome, data) {
|
|
|
17991
18409
|
const said = fire.outcome === "fired"
|
|
17992
18410
|
? fire.instanceTaskId === null
|
|
17993
18411
|
? "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>`}`
|
|
18412
|
+
: `<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
18413
|
: `<span class="meta">skipped — ${escape(fire.reason ?? "")}</span>`;
|
|
17996
18414
|
const slot = fire.scheduledFor.replace(/^manual:/, "");
|
|
17997
18415
|
return `<p class="row">${said}<span class="right meta mono">${fire.reason === "manual" ? "run now · " : ""}${whenTime(slot)}</span></p>`;
|
|
@@ -18006,14 +18424,14 @@ function routineScreenPage(chrome, data) {
|
|
|
18006
18424
|
? `<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
18425
|
: "",
|
|
18008
18426
|
approved && !routine.paused && routine.nextFireAt !== null
|
|
18009
|
-
? `<p class="meta">
|
|
18427
|
+
? `<p class="meta">Next fire ${whenTime(routine.nextFireAt)}</p>`
|
|
18010
18428
|
: "",
|
|
18011
|
-
"<h2>
|
|
18429
|
+
"<h2>The standing order</h2>",
|
|
18012
18430
|
terms,
|
|
18013
18431
|
approveForm,
|
|
18014
|
-
"<h2>
|
|
18432
|
+
"<h2>Acts</h2>",
|
|
18015
18433
|
acts,
|
|
18016
|
-
"<h2>
|
|
18434
|
+
"<h2>Firings</h2>",
|
|
18017
18435
|
ledger,
|
|
18018
18436
|
].join("\n"), { chrome });
|
|
18019
18437
|
}
|
|
@@ -18032,21 +18450,21 @@ function homePage(chrome, data) {
|
|
|
18032
18450
|
.map(decision => `<a class="decide-card" href="/d/${decision.id}">` +
|
|
18033
18451
|
`<p class="q">${escape(decision.question)}</p>` +
|
|
18034
18452
|
`<span class="mono meta">${escape(decision.taskId)}</span>` +
|
|
18035
|
-
`${isOverdue(decision, data.now) ? ` <span class="badge badge-overdue">
|
|
18453
|
+
`${isOverdue(decision, data.now) ? ` <span class="badge badge-overdue">Overdue</span>` : ""}` +
|
|
18036
18454
|
`</a>`)
|
|
18037
18455
|
.join("\n");
|
|
18038
18456
|
const incidents = data.incidents.length === 0
|
|
18039
18457
|
? ""
|
|
18040
|
-
: `<h2>
|
|
18458
|
+
: `<h2>Incidents</h2><p class="hint">builds that stopped and need a person — resolve here, or open the task to retry it</p>` +
|
|
18041
18459
|
data.incidents
|
|
18042
18460
|
.map(one => `<p class="row"><a href="${taskHref(one.taskId)}">${escape(one.taskId)}</a> — ${escape(incidentWords(one.kind))}` +
|
|
18043
18461
|
`<span class="right"><form method="post" action="/i/${one.id}/resolve" class="inline">` +
|
|
18044
18462
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
|
|
18045
|
-
`<button type="submit">
|
|
18463
|
+
`<button type="submit">Resolve</button></form></span></p>`)
|
|
18046
18464
|
.join("\n");
|
|
18047
18465
|
const stranded = data.stranded.length === 0
|
|
18048
18466
|
? ""
|
|
18049
|
-
: `<h2>
|
|
18467
|
+
: `<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
18468
|
data.stranded
|
|
18051
18469
|
.map(one => `<p class="row"><a href="${taskHref(one.id)}">${escape(one.id)}</a> waits on ${one.blockedBy
|
|
18052
18470
|
.map(blocker => `<a href="${taskHref(blocker)}">${escape(blocker)}</a>`)
|
|
@@ -18056,7 +18474,7 @@ function homePage(chrome, data) {
|
|
|
18056
18474
|
? ""
|
|
18057
18475
|
: data.gaps.length === 0
|
|
18058
18476
|
? ""
|
|
18059
|
-
: `<h2>
|
|
18477
|
+
: `<h2>Missing requirements</h2><p class="hint">tools or credentials builds need — checked before any money is spent</p>` +
|
|
18060
18478
|
data.gaps
|
|
18061
18479
|
.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
18480
|
.join("\n") +
|
|
@@ -18068,7 +18486,7 @@ function homePage(chrome, data) {
|
|
|
18068
18486
|
// an operator actually watches this page, so it re-renders itself.
|
|
18069
18487
|
const building = data.building.length === 0
|
|
18070
18488
|
? ""
|
|
18071
|
-
: `<h2>
|
|
18489
|
+
: `<h2>Building now</h2><p class="hint">live builds — this page refreshes itself every 10 seconds while anything runs</p><div class="cards">` +
|
|
18072
18490
|
data.building
|
|
18073
18491
|
.map(claim => `<a class="stat-card" href="${taskHref(claim.taskId)}" style="text-decoration:none">` +
|
|
18074
18492
|
`<span class="k"><span class="dot dot-ok pulse"></span>${escape(claim.taskId)}</span>` +
|
|
@@ -18104,8 +18522,8 @@ function homePage(chrome, data) {
|
|
|
18104
18522
|
: `last window: ${data.episode.built} built, ${data.episode.broke} broke \u00b7 ended ${whenTime(data.episode.endedAt)}`}</span></div>`;
|
|
18105
18523
|
const fleetCards = [...runnerCards, watchCard, ...worktreeCards].filter(one => one !== "");
|
|
18106
18524
|
const fleet = fleetCards.length === 0
|
|
18107
|
-
? `<h2>
|
|
18108
|
-
: `<h2>
|
|
18525
|
+
? `<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>`
|
|
18526
|
+
: `<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
18527
|
const startHere = data.taskCount === 0
|
|
18110
18528
|
? [
|
|
18111
18529
|
`<div class="card">`,
|
|
@@ -18125,10 +18543,10 @@ function homePage(chrome, data) {
|
|
|
18125
18543
|
: `<p class="meta"><strong>${escape(projectName(data.repo))}</strong> — the last 24 hours, honestly labeled: a rolling window, whatever your hours are</p>`,
|
|
18126
18544
|
startHere,
|
|
18127
18545
|
ledger,
|
|
18128
|
-
`<p class="meta">
|
|
18129
|
-
data.outboxPending > 0 ? `<p class="meta">
|
|
18546
|
+
`<p class="meta">Spend: ${escape(consoleSpend(summary))}</p>`,
|
|
18547
|
+
data.outboxPending > 0 ? `<p class="meta">Notifications: ${data.outboxPending} pending delivery</p>` : "",
|
|
18130
18548
|
building,
|
|
18131
|
-
`<h2>
|
|
18549
|
+
`<h2>Needs your decision</h2><p class="hint">an agent stopped mid-build to ask — nothing proceeds until you answer</p>`,
|
|
18132
18550
|
decide,
|
|
18133
18551
|
incidents,
|
|
18134
18552
|
stranded,
|
|
@@ -18197,21 +18615,21 @@ function taskComposerHtml(data) {
|
|
|
18197
18615
|
`<details class="task-options"${prefill === null ? "" : " open"}>`,
|
|
18198
18616
|
`<summary><span>Edit details</span><small>optional · defaults are remembered</small></summary>`,
|
|
18199
18617
|
`<div class="task-options-grid">`,
|
|
18200
|
-
`<label class="wide">
|
|
18618
|
+
`<label class="wide">Goal <span class="meta">— provide upfront for automatic approval of an unchanged plan</span>` +
|
|
18201
18619
|
`<textarea name="goal" rows="3" placeholder="What success looks like">${prefill === null ? "" : escape(prefill.goal)}</textarea></label>`,
|
|
18202
|
-
`<label class="wide">
|
|
18620
|
+
`<label class="wide">Acceptance <span class="meta">— required with a goal; one per line: <code>statement | evidence,kinds | how</code></span>` +
|
|
18203
18621
|
`<textarea name="acceptance" rows="3" placeholder="Requests over the limit return 429 | check">${prefill === null ? "" : escape(prefill.acceptance)}</textarea></label>`,
|
|
18204
|
-
`<label>
|
|
18205
|
-
`<label>
|
|
18622
|
+
`<label>Not this <span class="meta">— optional boundary</span><input type="text" name="not" value="${prefill === null ? "" : escape(prefill.not)}"></label>`,
|
|
18623
|
+
`<label>Likely touches <span class="meta">— paths, comma-separated</span><input type="text" name="touches" value="${prefill === null ? "" : escape(prefill.touches)}"></label>`,
|
|
18206
18624
|
`<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
18625
|
`<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>
|
|
18626
|
+
`<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
18627
|
candidates.length === 0
|
|
18210
18628
|
? ""
|
|
18211
|
-
: `<label>
|
|
18629
|
+
: `<label>Starts after <span class="meta">— optional</span><select name="after"><option value="">right away</option>` +
|
|
18212
18630
|
candidates.map(one => `<option value="${escape(one.id)}"${one.id === after ? " selected" : ""}>${escape(one.id)} — ${escape(one.title)}</option>`).join("") +
|
|
18213
18631
|
`</select></label>`,
|
|
18214
|
-
`<fieldset class="permission-field"><legend>
|
|
18632
|
+
`<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
18633
|
`<p class="meta permission-note">Inherited from Settings. You can still change it on the proposed scope before approval.</p></fieldset>`,
|
|
18216
18634
|
`</div>`,
|
|
18217
18635
|
`</details>`,
|
|
@@ -18307,9 +18725,9 @@ function workPage(chrome, data) {
|
|
|
18307
18725
|
project: data.multiProject || row.repo === null ? (row.repo === null ? 'Unplaced' : projectName(row.repo)) : null,
|
|
18308
18726
|
age: relativeAge(row.updatedAt, data.now),
|
|
18309
18727
|
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'),
|
|
18728
|
+
ask: row.ask, chip: row.chip, group: row.ask ?? (row.status.rank === 1 ? 'building' : 'rest'),
|
|
18311
18729
|
action: actionHref === null || row.primaryAction === null ? null : { label: row.primaryAction.label, href: actionHref },
|
|
18312
|
-
detail: needsYouDetail ? row.status.detail : null,
|
|
18730
|
+
detail: needsYouDetail ? row.status.detail : row.progress ?? null,
|
|
18313
18731
|
problem: row.familyProblem,
|
|
18314
18732
|
notes: (row.status.diagnostics ?? []).map(one => `${one.label} · ${one.detail}`),
|
|
18315
18733
|
};
|
|
@@ -18337,12 +18755,12 @@ function tasksPage(chrome, tasks, state, csrf, problem, repo = null, prefill = n
|
|
|
18337
18755
|
.join("\n");
|
|
18338
18756
|
return screen("tasks", [
|
|
18339
18757
|
"<h1>Tasks</h1>",
|
|
18340
|
-
`<p class="meta">
|
|
18758
|
+
`<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
18759
|
repo === null ? "" : `<p class="meta path-words"><span class="mono">${escape(repo)}</span></p>`,
|
|
18342
18760
|
problem === null ? "" : `<div class="problem">${escape(problem)}</div>`,
|
|
18343
|
-
`<p class="meta">
|
|
18761
|
+
`<p class="meta">Filter: <a href="/tasks">all</a> · ${filters}</p>`,
|
|
18344
18762
|
rows,
|
|
18345
|
-
`<h2>
|
|
18763
|
+
`<h2>Add a task</h2>`,
|
|
18346
18764
|
taskComposerHtml({ csrf, project: repo, prefill, permissionDefault, qualityDefault }),
|
|
18347
18765
|
].join("\n"), { chrome });
|
|
18348
18766
|
}
|
|
@@ -18352,21 +18770,21 @@ function browsePage(chrome, data) {
|
|
|
18352
18770
|
`<form method="post" action="/projects/open" class="inline">`,
|
|
18353
18771
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
|
|
18354
18772
|
`<input type="hidden" name="path" value="${escape(path)}">`,
|
|
18355
|
-
`<button type="submit">
|
|
18773
|
+
`<button type="submit">Open</button>`,
|
|
18356
18774
|
`</form>`,
|
|
18357
18775
|
].join("");
|
|
18358
18776
|
return screen("projects", [
|
|
18359
18777
|
`<h1>Choose a folder</h1>`,
|
|
18360
18778
|
`<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
18779
|
data.roots.length > 1
|
|
18362
|
-
? `<p class="meta">
|
|
18780
|
+
? `<p class="meta">Roots: ${data.roots.map(one => `<a href="/projects/browse?at=${encodeURIComponent(one)}" class="mono">${escape(projectName(one))}</a>`).join(" · ")}</p>`
|
|
18363
18781
|
: "",
|
|
18364
18782
|
`<p class="mono meta">${escape(crumb)}</p>`,
|
|
18365
18783
|
data.parent === null
|
|
18366
18784
|
? ""
|
|
18367
18785
|
: `<p class="row"><a href="/projects/browse?at=${encodeURIComponent(data.parent)}">\u2190 up one level</a></p>`,
|
|
18368
18786
|
data.entries.length === 0
|
|
18369
|
-
? `<p class="meta">
|
|
18787
|
+
? `<p class="meta">No folders here</p>`
|
|
18370
18788
|
: data.entries
|
|
18371
18789
|
.map(one => `<p class="row">` +
|
|
18372
18790
|
`<a href="/projects/browse?at=${encodeURIComponent(one.path)}"><strong>${escape(one.name)}</strong></a>` +
|
|
@@ -18384,7 +18802,7 @@ function browsePage(chrome, data) {
|
|
|
18384
18802
|
* switching context must never depend on remembering which repo is open.
|
|
18385
18803
|
*/
|
|
18386
18804
|
function workbenchRail(data) {
|
|
18387
|
-
const workspace = (repo) => `<span class="badge">${repo === null ? "
|
|
18805
|
+
const workspace = (repo) => `<span class="badge">${repo === null ? "Unplaced" : escape(projectName(repo))}</span>`;
|
|
18388
18806
|
const row = (card, reason) => `<a class="wb-row${card.taskId === data.selected ? " wb-selected" : ""}" href="/workbench?t=${encodeURIComponent(card.taskId)}"` +
|
|
18389
18807
|
`${card.taskId === data.selected ? ` aria-current="true"` : ""}>` +
|
|
18390
18808
|
`<span class="wb-title">${escape(card.title)}</span>` +
|
|
@@ -18394,7 +18812,7 @@ function workbenchRail(data) {
|
|
|
18394
18812
|
(cards.length === 0 ? `<p class="meta">${empty}</p>` : cards.slice(0, 100).map(render).join("\n")) +
|
|
18395
18813
|
`</section>`;
|
|
18396
18814
|
const parts = [];
|
|
18397
|
-
parts.push(`<div class="wb-rail-head"><div><span class="eyebrow">portfolio</span><h2>
|
|
18815
|
+
parts.push(`<div class="wb-rail-head"><div><span class="eyebrow">portfolio</span><h2>All projects</h2></div>` +
|
|
18398
18816
|
`<a href="/projects">manage →</a></div>`);
|
|
18399
18817
|
parts.push(group("needs you", data.attention, "Nothing needs your input.", card => row(card, escape(card.reason))));
|
|
18400
18818
|
parts.push(group("in progress", data.building, "No agent is working right now.", card => {
|
|
@@ -18406,23 +18824,23 @@ function workbenchRail(data) {
|
|
|
18406
18824
|
parts.push(group("blocked & waiting", data.waiting, "Nothing is blocked or paused.", card => row(card, escape(card.reason))));
|
|
18407
18825
|
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
18826
|
if (data.done.length > 0) {
|
|
18409
|
-
parts.push(`<section class="wb-group"><h2>
|
|
18827
|
+
parts.push(`<section class="wb-group"><h2>Just finished <span class="lane-count">${data.done.length}</span></h2>`);
|
|
18410
18828
|
parts.push(data.done
|
|
18411
18829
|
.map(one => `<a class="wb-row${one.taskId === data.selected ? " wb-selected" : ""}" href="/workbench?t=${encodeURIComponent(one.taskId)}">` +
|
|
18412
18830
|
`<span class="wb-title">${escape(one.title)}</span>` +
|
|
18413
18831
|
`<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>`)
|
|
18832
|
+
`<span class="badge badge-${one.outcome === "built" || one.outcome === "no-change" ? "done" : "failed"}">${escape(sentenceCase(one.outcome ?? "?"))}</span></span></a>`)
|
|
18415
18833
|
.join("\n"));
|
|
18416
18834
|
parts.push(`</section>`);
|
|
18417
18835
|
}
|
|
18418
18836
|
if (data.saturated)
|
|
18419
|
-
parts.push(`<p class="meta">
|
|
18837
|
+
parts.push(`<p class="meta">More exists — this rail is capped; the <a href="/board?scope=all">board</a> holds the rest</p>`);
|
|
18420
18838
|
return parts.join("\n");
|
|
18421
18839
|
}
|
|
18422
18840
|
/** The project chip every all-scope row wears: null is UNPLACED, said so. */
|
|
18423
18841
|
function projectChip(repo) {
|
|
18424
18842
|
return repo === null || repo === undefined
|
|
18425
|
-
? ` <span class="badge">
|
|
18843
|
+
? ` <span class="badge">Unplaced</span>`
|
|
18426
18844
|
: ` <span class="badge">${escape(projectName(repo))}</span>`;
|
|
18427
18845
|
}
|
|
18428
18846
|
/**
|
|
@@ -18440,11 +18858,11 @@ function decisionAnswerCard(decision, csrf, now, chip, returnTo = null) {
|
|
|
18440
18858
|
const options = decision.options
|
|
18441
18859
|
.map(option => {
|
|
18442
18860
|
const recommended = option.id === decision.recommendation
|
|
18443
|
-
? ` <span class="badge">
|
|
18861
|
+
? ` <span class="badge">Recommended</span>`
|
|
18444
18862
|
: "";
|
|
18445
18863
|
if (!option.reversible) {
|
|
18446
18864
|
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">
|
|
18865
|
+
` <span class="badge badge-overdue">Irreversible</span>${recommended}` +
|
|
18448
18866
|
` <span class="meta">${escape(option.consequence)}</span></p>`);
|
|
18449
18867
|
}
|
|
18450
18868
|
return (`<form class="decide-option decide-inline" method="post" action="/d/${decision.id}/answer">` +
|
|
@@ -18459,7 +18877,7 @@ function decisionAnswerCard(decision, csrf, now, chip, returnTo = null) {
|
|
|
18459
18877
|
`<p class="q">${escape(decision.question)}</p>` +
|
|
18460
18878
|
`<details class="decision-context"><summary>Context</summary><p class="meta">${escape(decision.recap)}</p><span class="meta mono">${escape(decision.taskId)}</span></details>` +
|
|
18461
18879
|
`<p class="meta">${chip ? projectChip(decision.repo) : ""}` +
|
|
18462
|
-
`${isOverdue(decision, now) ? ` <span class="badge badge-overdue">
|
|
18880
|
+
`${isOverdue(decision, now) ? ` <span class="badge badge-overdue">Overdue</span>` : ""}` +
|
|
18463
18881
|
` <a href="/d/${decision.id}${returnTo === null ? "" : `?return=${encodeURIComponent(returnTo)}`}">View details →</a></p>` +
|
|
18464
18882
|
`<div class="decide-options">${options}</div></div>`);
|
|
18465
18883
|
}
|
|
@@ -18540,10 +18958,10 @@ function portfolioOverview(data) {
|
|
|
18540
18958
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
|
|
18541
18959
|
`<input type="hidden" name="path" value="${escape(one.repo)}">` +
|
|
18542
18960
|
`<input type="hidden" name="return" value="/board">` +
|
|
18543
|
-
`<button type="submit">
|
|
18961
|
+
`<button type="submit">Board →</button></form>`;
|
|
18544
18962
|
return (`<div class="workspace-card${one.attention > 0 ? " hot" : ""}">` +
|
|
18545
18963
|
`<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>
|
|
18964
|
+
`<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
18965
|
`<div class="workspace-stats">` +
|
|
18548
18966
|
`<span class="pulse-stat${one.attention > 0 ? " hot" : ""}"><b>${one.attention}</b> need you</span>` +
|
|
18549
18967
|
`<span class="pulse-stat"><b>${one.building}</b> live</span>` +
|
|
@@ -18595,7 +19013,7 @@ function portfolioOverview(data) {
|
|
|
18595
19013
|
// ---- running: current live claims, project chips on ----
|
|
18596
19014
|
const liveRows = data.live
|
|
18597
19015
|
.map(one => `<p class="row"><a href="${taskHref(one.taskId)}">${escape(one.taskId)}</a>${projectChip(one.repo)} ` +
|
|
18598
|
-
`<span class="badge badge-running">
|
|
19016
|
+
`<span class="badge badge-running">Running</span> ` +
|
|
18599
19017
|
`<span class="mono meta">${escape(one.runner)}${one.model === null ? "" : ` · ${escape(one.model)}`} · ` +
|
|
18600
19018
|
`<time data-elapsed-since="${escape(one.claimedAt)}"></time></span></p>`)
|
|
18601
19019
|
.join("\n");
|
|
@@ -18603,8 +19021,8 @@ function portfolioOverview(data) {
|
|
|
18603
19021
|
const chipClass = (outcome) => outcome === "built" || outcome === "no-change" ? "badge-done" : outcome === "failed" ? "badge-failed" : "";
|
|
18604
19022
|
const ledgerRows = data.ledger
|
|
18605
19023
|
.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">
|
|
19024
|
+
`<span class="badge ${chipClass(one.outcome)}">${escape(sentenceCase(one.outcome))}</span> ` +
|
|
19025
|
+
`${one.role === "scout" ? `<a class="badge" href="${taskHref(one.taskId)}#report">Report</a> ` : ""}` +
|
|
18608
19026
|
`<span class="mono meta">${one.provider === null ? "" : escape(one.provider)}${one.model === null ? "" : ` · ${escape(one.model)}`}` +
|
|
18609
19027
|
`${one.ranMinutes === null ? "" : ` · ${one.ranMinutes}m`}` +
|
|
18610
19028
|
` · ${escape(runCostWords({ authMode: one.authMode, costUsd: one.costUsd, tokensIn: null, tokensOut: null }))}` +
|
|
@@ -18618,11 +19036,11 @@ function portfolioOverview(data) {
|
|
|
18618
19036
|
})()}</span></p>`)
|
|
18619
19037
|
.join("\n");
|
|
18620
19038
|
return [
|
|
18621
|
-
`<div class="control-room-head"><div><h1>
|
|
18622
|
-
`<p class="meta">
|
|
18623
|
-
`<div class="actions"><a class="badge" href="/tasks/new">+
|
|
19039
|
+
`<div class="control-room-head"><div><h1>Portfolio</h1>` +
|
|
19040
|
+
`<p class="meta">Every project and live build in one place</p></div>` +
|
|
19041
|
+
`<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
19042
|
data.saturated ? `<div class="problem">This overview reached its 200-task display cap; the task list holds the rest.</div>` : "",
|
|
18625
|
-
`<h2>
|
|
19043
|
+
`<h2>Waits on you</h2>`,
|
|
18626
19044
|
waitCount === 0
|
|
18627
19045
|
? `<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
19046
|
: [
|
|
@@ -18634,17 +19052,17 @@ function portfolioOverview(data) {
|
|
|
18634
19052
|
`<p class="meta"><a href="/next">clear the queue → one thing at a time</a></p>`,
|
|
18635
19053
|
].filter(part => part !== "").join("\n"),
|
|
18636
19054
|
data.gapsProject === null
|
|
18637
|
-
? `<p class="meta">
|
|
19055
|
+
? `<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
19056
|
: "",
|
|
18639
|
-
`<h2>
|
|
19057
|
+
`<h2>Project pulse</h2>`,
|
|
18640
19058
|
`<p class="hint">one row per repository</p>`,
|
|
18641
19059
|
pulseRows.length === 0
|
|
18642
19060
|
? `<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
19061
|
: `<div class="workspace-pulse">${workspaceRows}</div>`,
|
|
18644
|
-
`<h2>
|
|
19062
|
+
`<h2>The last 24 hours</h2>`,
|
|
18645
19063
|
`<p class="hint">runs started in the last 24 hours</p>`,
|
|
18646
19064
|
data.runs24.length === 0
|
|
18647
|
-
? `<p class="meta">
|
|
19065
|
+
? `<p class="meta">No runs started in the window</p>`
|
|
18648
19066
|
: `<p class="row"><span class="meta">runs started</span> <span class="mono">${data.runs24.length}</span></p>` +
|
|
18649
19067
|
`<p class="row"><span class="meta">outcomes</span> <span class="mono">${escape(outcomeWords)}</span></p>` +
|
|
18650
19068
|
`<p class="row"><span class="meta">spend</span> <span class="mono">${escape(spendLine(summary))}</span></p>` +
|
|
@@ -18654,10 +19072,10 @@ function portfolioOverview(data) {
|
|
|
18654
19072
|
(summary.tokens > 0
|
|
18655
19073
|
? `<p class="row"><span class="meta">tokens</span> <span class="mono">${summary.tokens.toLocaleString()}</span></p>`
|
|
18656
19074
|
: ""),
|
|
18657
|
-
`<h2>
|
|
18658
|
-
data.live.length === 0 ? `<p class="meta">
|
|
18659
|
-
`<h2>
|
|
18660
|
-
data.ledger.length === 0 ? `<p class="meta">
|
|
19075
|
+
`<h2>Running</h2>`,
|
|
19076
|
+
data.live.length === 0 ? `<p class="meta">No agent is working right now</p>` : liveRows,
|
|
19077
|
+
`<h2>Terminal runs started in the last 24 hours</h2>`,
|
|
19078
|
+
data.ledger.length === 0 ? `<p class="meta">None yet</p>` : ledgerRows,
|
|
18661
19079
|
].join("\n");
|
|
18662
19080
|
}
|
|
18663
19081
|
/** Dollars for a screen: micro-USD stated as money, unknowables in words. */
|
|
@@ -18753,7 +19171,7 @@ function contestPage(chrome, data) {
|
|
|
18753
19171
|
const glance = summaries.length < 2
|
|
18754
19172
|
? ""
|
|
18755
19173
|
: `<div class="scroll-x"><table class="contest-glance">` +
|
|
18756
|
-
`<tr><td></td>${summaries.map(one => `<th>
|
|
19174
|
+
`<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
19175
|
`<tr><td class="meta">outcome</td>${summaries.map(one => `<td>${escape(one.outcome)}</td>`).join("")}</tr>` +
|
|
18758
19176
|
`<tr><td class="meta">changed</td>${summaries.map(one => `<td>${escape(one.diffWords)}</td>`).join("")}</tr>` +
|
|
18759
19177
|
`<tr><td class="meta">time</td>${summaries.map(one => `<td>${one.minutes === null ? "—" : `${one.minutes} min`}</td>`).join("")}</tr>` +
|
|
@@ -18766,22 +19184,22 @@ function contestPage(chrome, data) {
|
|
|
18766
19184
|
const agent = summary.agent;
|
|
18767
19185
|
const parts = [
|
|
18768
19186
|
`<div class="card${winner ? " picked" : ""}">`,
|
|
18769
|
-
`<p><strong>
|
|
18770
|
-
`${winner ? ` <span class="badge badge-done">
|
|
19187
|
+
`<p><strong>Agent ${contestant.ordinal}</strong> <span class="meta">${escape(contestant.provider)} · ${escape(contestant.model)}</span>` +
|
|
19188
|
+
`${winner ? ` <span class="badge badge-done">Picked</span>` : ""}</p>`,
|
|
18771
19189
|
`<p class="row">${escape(outcome)}` +
|
|
18772
19190
|
`${minutes === null ? "" : ` <span class="meta">· ${minutes} min</span>`}` +
|
|
18773
19191
|
`${asked > 0 ? ` <span class="meta">· asked ${asked} question${asked > 1 ? "s" : ""}</span>` : ""}` +
|
|
18774
19192
|
`${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">
|
|
19193
|
+
`<p class="row"><strong>Cost</strong> ${escape(cost)}</p>`,
|
|
19194
|
+
run === null || run.handoff === null ? "" : `<p><strong>Its own conclusion</strong></p><p class="recap">${escape(run.handoff)}</p>`,
|
|
19195
|
+
diff === null ? `<p class="meta">No diff was captured</p>` : terminalDiffCard(diff, run === null ? 0 : run.id),
|
|
18778
19196
|
];
|
|
18779
19197
|
if (picking) {
|
|
18780
19198
|
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">
|
|
19199
|
+
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
19200
|
}
|
|
18783
19201
|
else {
|
|
18784
|
-
parts.push(`<p class="meta">
|
|
19202
|
+
parts.push(`<p class="meta">Cannot be picked — ${escape(agent.unpickableReason ?? "")}</p>`);
|
|
18785
19203
|
}
|
|
18786
19204
|
}
|
|
18787
19205
|
parts.push(`</div>`);
|
|
@@ -18793,11 +19211,11 @@ function contestPage(chrome, data) {
|
|
|
18793
19211
|
`only one result will be kept as the task's outcome; the rest stay archived with their evidence</p>`,
|
|
18794
19212
|
data.problem === null ? "" : `<div class="problem">${escape(data.problem)}</div>`,
|
|
18795
19213
|
`<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">
|
|
19214
|
+
contest.pickedBy === null ? "" : `<p class="meta">Picked by ${escape(contest.pickedBy)} at ${whenTime(contest.pickedAt ?? "")}</p>`,
|
|
18797
19215
|
contest.kind === "comparison"
|
|
18798
|
-
? `<p class="row"><strong>
|
|
19216
|
+
? `<p class="row"><strong>Spend</strong> ${escape(contestDollars(data.totalMicrousd))} measured on the lanes that report dollars` +
|
|
18799
19217
|
`${data.anyUnknown ? ` <span class="meta">— the rest report tokens only</span>` : ""}</p>`
|
|
18800
|
-
: `<p class="row"><strong>
|
|
19218
|
+
: `<p class="row"><strong>Charged so far</strong> ${escape(contestDollars(data.totalMicrousd))}` +
|
|
18801
19219
|
`${data.anyUnknown ? ` <span class="meta">— includes at least one agent charged its full reservation because the exact figure was unknowable</span>` : ""}</p>`,
|
|
18802
19220
|
glance,
|
|
18803
19221
|
`<div class="contest-compare">${summaries.map(agentCard).join("\n")}</div>`,
|
|
@@ -18807,9 +19225,9 @@ function contestPage(chrome, data) {
|
|
|
18807
19225
|
`<form method="post" action="/contest/${contest.id}/arm" class="inline">`,
|
|
18808
19226
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
|
|
18809
19227
|
`<input type="hidden" name="act" value="abandon">`,
|
|
18810
|
-
`<button type="submit">
|
|
19228
|
+
`<button type="submit">Abandon the ${contestNoun(contest.kind)}…</button>`,
|
|
18811
19229
|
`</form>`,
|
|
18812
|
-
`<p class="meta">
|
|
19230
|
+
`<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
19231
|
`</div>`,
|
|
18814
19232
|
].join("\n")
|
|
18815
19233
|
: "",
|
|
@@ -18826,7 +19244,7 @@ function contestCeremonyPage(chrome, data) {
|
|
|
18826
19244
|
const back = `<p class="meta"><a href="/contest/${data.contestId}">back — decide nothing</a></p>`;
|
|
18827
19245
|
if (data.kind === "abandon") {
|
|
18828
19246
|
return screen("tournament", [
|
|
18829
|
-
`<h1>
|
|
19247
|
+
`<h1>Abandon this ${contestNoun(data.contestKind)}?</h1>`,
|
|
18830
19248
|
`<div class="card">`,
|
|
18831
19249
|
`<p class="row">${data.agents} agents ${data.contestKind === "comparison" ? "built independently" : "raced"} on <strong>${escape(data.taskTitle)}</strong>. Abandoning picks nothing:</p>`,
|
|
18832
19250
|
`<p class="row">— the task is marked <strong>failed</strong> and can be re-queued later</p>`,
|
|
@@ -18838,20 +19256,20 @@ function contestCeremonyPage(chrome, data) {
|
|
|
18838
19256
|
`<form method="post" action="/contest/${data.contestId}/abandon" class="card">`,
|
|
18839
19257
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
|
|
18840
19258
|
`<input type="hidden" name="nonce" value="${escape(data.nonceValue)}">`,
|
|
18841
|
-
`<label>
|
|
18842
|
-
`<button type="submit" class="danger">
|
|
19259
|
+
`<label>Your password, typed again<input type="password" name="token" autocomplete="current-password"></label>`,
|
|
19260
|
+
`<button type="submit" class="danger">Abandon the ${contestNoun(data.contestKind)}</button>`,
|
|
18843
19261
|
`</form>`,
|
|
18844
19262
|
back,
|
|
18845
19263
|
].join("\n"), { chrome });
|
|
18846
19264
|
}
|
|
18847
19265
|
const agent = data.chosen;
|
|
18848
19266
|
if (agent === undefined || agent.run === null)
|
|
18849
|
-
return screen("tournament", `<p class="meta">
|
|
19267
|
+
return screen("tournament", `<p class="meta">Nothing to confirm</p>`, { chrome });
|
|
18850
19268
|
const run = agent.run;
|
|
18851
19269
|
return screen("tournament", [
|
|
18852
|
-
`<h1>
|
|
19270
|
+
`<h1>Pick agent ${agent.contestant.ordinal}'s result?</h1>`,
|
|
18853
19271
|
`<div class="card">`,
|
|
18854
|
-
`<p class="row"><strong>
|
|
19272
|
+
`<p class="row"><strong>Agent ${agent.contestant.ordinal}</strong> — ${escape(agent.contestant.provider)} · ${escape(agent.contestant.model)}</p>`,
|
|
18855
19273
|
`<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
19274
|
run.outcome === "no-change"
|
|
18857
19275
|
? `<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 +19279,8 @@ function contestCeremonyPage(chrome, data) {
|
|
|
18861
19279
|
? ""
|
|
18862
19280
|
: `<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
19281
|
data.publication === null || data.publication === undefined
|
|
18864
|
-
? `<p class="row"><strong>
|
|
18865
|
-
: `<p class="row"><strong>
|
|
19282
|
+
? `<p class="row"><strong>Nothing is published</strong> — the branch stays local to this machine</p>`
|
|
19283
|
+
: `<p class="row"><strong>A ${data.publication.draft ? "draft " : ""}pull request will be opened</strong> on ` +
|
|
18866
19284
|
`<span class="mono">${escape(data.publication.githubRepo)}</span> from <span class="mono">${escape(data.publication.branch)}</span></p>`,
|
|
18867
19285
|
data.contestKind === "comparison"
|
|
18868
19286
|
? `<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 +19294,8 @@ function contestCeremonyPage(chrome, data) {
|
|
|
18876
19294
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
|
|
18877
19295
|
`<input type="hidden" name="nonce" value="${escape(data.nonceValue)}">`,
|
|
18878
19296
|
`<input type="hidden" name="choice" value="${agent.contestant.id}">`,
|
|
18879
|
-
`<label>
|
|
18880
|
-
`<div class="sticky-actions"><button type="submit">
|
|
19297
|
+
`<label>Your password, typed again<input type="password" name="token" autocomplete="current-password"></label>`,
|
|
19298
|
+
`<div class="sticky-actions"><button type="submit">Yes — pick this result</button></div>`,
|
|
18881
19299
|
`</form>`,
|
|
18882
19300
|
back,
|
|
18883
19301
|
].filter(one => one !== "").join("\n"), { chrome });
|
|
@@ -18968,18 +19386,18 @@ function githubReposPage(chrome, data) {
|
|
|
18968
19386
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
|
|
18969
19387
|
`<input type="hidden" name="repo" value="${escape(nameWithOwner)}">`,
|
|
18970
19388
|
`<input type="hidden" name="root" value="0">`,
|
|
18971
|
-
`<button type="submit">
|
|
19389
|
+
`<button type="submit">Clone here →</button>`,
|
|
18972
19390
|
`</form>`,
|
|
18973
19391
|
].join("");
|
|
18974
19392
|
const rows = !data.listed.ok
|
|
18975
19393
|
? `<p class="meta">${escape(data.listed.message)}</p>`
|
|
18976
19394
|
: data.listed.repos.length === 0
|
|
18977
|
-
? `<p class="meta">
|
|
19395
|
+
? `<p class="meta">The signed-in GitHub account has no repositories to list</p>`
|
|
18978
19396
|
: data.listed.repos
|
|
18979
19397
|
.map(repo => {
|
|
18980
19398
|
const localPath = data.local.get(repo.nameWithOwner.toLowerCase()) ?? null;
|
|
18981
19399
|
const action = localPath !== null && data.openProject === localPath
|
|
18982
|
-
? `<span class="badge badge-done">
|
|
19400
|
+
? `<span class="badge badge-done">Open now</span>`
|
|
18983
19401
|
: localPath !== null
|
|
18984
19402
|
? openForm(localPath, data.registered.has(localPath) ? "open →" : "add + open →")
|
|
18985
19403
|
: data.cloneReady
|
|
@@ -18987,10 +19405,10 @@ function githubReposPage(chrome, data) {
|
|
|
18987
19405
|
: `<span class="meta">choose a projects folder first</span>`;
|
|
18988
19406
|
return [
|
|
18989
19407
|
`<div class="card project-card">`,
|
|
18990
|
-
`<div class="row"><strong>${escape(repo.nameWithOwner)}</strong>${repo.isPrivate ? ` <span class="badge">
|
|
19408
|
+
`<div class="row"><strong>${escape(repo.nameWithOwner)}</strong>${repo.isPrivate ? ` <span class="badge">Private</span>` : ""}`,
|
|
18991
19409
|
`<span class="right">${action}</span></div>`,
|
|
18992
19410
|
localPath === null
|
|
18993
|
-
? `<p class="meta">
|
|
19411
|
+
? `<p class="meta">Not on this machine yet${/^\d{4}-\d{2}-\d{2}T/.test(repo.updatedAt) ? ` · pushed ${whenTime(repo.updatedAt)}` : ""}</p>`
|
|
18994
19412
|
: `<p class="meta mono" style="overflow-wrap:anywhere;margin:.2rem 0">${escape(localPath)}</p>`,
|
|
18995
19413
|
repo.description === "" ? "" : `<p class="meta">${escape(repo.description)}</p>`,
|
|
18996
19414
|
`</div>`,
|
|
@@ -18998,8 +19416,8 @@ function githubReposPage(chrome, data) {
|
|
|
18998
19416
|
})
|
|
18999
19417
|
.join("\n");
|
|
19000
19418
|
return screen("projects", [
|
|
19001
|
-
`<h1>
|
|
19002
|
-
`<p class="meta">
|
|
19419
|
+
`<h1>Your GitHub repositories</h1>`,
|
|
19420
|
+
`<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
19421
|
rows,
|
|
19004
19422
|
`<p class="row" style="margin-top:.6rem"><a class="badge" href="/projects">← back to projects</a></p>`,
|
|
19005
19423
|
].join("\n"), { chrome });
|
|
@@ -19014,16 +19432,16 @@ function projectsPage(chrome, recent, candidates, open, csrf, problem, _unscoped
|
|
|
19014
19432
|
? `<p class="meta">${escape(onboard.why)}</p>`
|
|
19015
19433
|
: [
|
|
19016
19434
|
`<details class="project-add-more"${onboard.record === null ? '' : ' open'}><summary>${onboard.record === null ? 'Paste a GitHub link' : 'Review repository'}</summary>`,
|
|
19017
|
-
`<p class="meta">
|
|
19435
|
+
`<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
19436
|
onboard.record === null
|
|
19019
19437
|
? [
|
|
19020
19438
|
`<form method="post" action="/projects/onboard-preview" class="card">`,
|
|
19021
19439
|
`<input type="hidden" name="csrf" value="${escape(csrf)}">`,
|
|
19022
|
-
`<label>
|
|
19440
|
+
`<label>Repository <input type="text" name="repo" placeholder="owner/name or https://github.com/owner/name"></label>`,
|
|
19023
19441
|
onboard.roots.length > 1
|
|
19024
|
-
? `<label>
|
|
19025
|
-
: `<input type="hidden" name="root" value="0"><p class="meta">
|
|
19026
|
-
`<button type="submit">
|
|
19442
|
+
? `<label>Into <select name="root">${onboard.roots.map((one, index) => `<option value="${index}">${escape(one)}</option>`).join("")}</select></label>`
|
|
19443
|
+
: `<input type="hidden" name="root" value="0"><p class="meta">Into ${escape(onboard.roots[0] ?? "")}</p>`,
|
|
19444
|
+
`<button type="submit">Preview</button>`,
|
|
19027
19445
|
`</form>`,
|
|
19028
19446
|
].join("\n")
|
|
19029
19447
|
: [
|
|
@@ -19035,8 +19453,8 @@ function projectsPage(chrome, recent, candidates, open, csrf, problem, _unscoped
|
|
|
19035
19453
|
onboard.record[1].large
|
|
19036
19454
|
? `<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
19455
|
: "",
|
|
19038
|
-
`<label>
|
|
19039
|
-
`<div class="sticky-actions"><button type="submit">
|
|
19456
|
+
`<label>Your password, typed again <input type="password" name="token" autocomplete="current-password"></label>`,
|
|
19457
|
+
`<div class="sticky-actions"><button type="submit">Clone and open</button></div>`,
|
|
19040
19458
|
`</form>`,
|
|
19041
19459
|
`</div>`,
|
|
19042
19460
|
].join("\n"),
|
|
@@ -19078,7 +19496,7 @@ function projectsPage(chrome, recent, candidates, open, csrf, problem, _unscoped
|
|
|
19078
19496
|
`<div class="row">${open !== null && open === one.path
|
|
19079
19497
|
? `<a class="project-name" href="${escape(returnTo)}"><strong>${escape(one.name)}</strong></a>`
|
|
19080
19498
|
: openForm(one.path, one.name, returnTo, "project-name")}`,
|
|
19081
|
-
`<span class="right">${open !== null && open === one.path ? `<span class="badge badge-done">
|
|
19499
|
+
`<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
19500
|
`<p class="meta mono" style="overflow-wrap:anywhere;margin:.2rem 0">${escape(one.path)}</p>`,
|
|
19083
19501
|
`<p class="row" style="gap:.35rem;flex-wrap:wrap">${peekChips(one.path, one.peek)}</p>`,
|
|
19084
19502
|
`<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 +19538,13 @@ function projectsPage(chrome, recent, candidates, open, csrf, problem, _unscoped
|
|
|
19120
19538
|
`<form method="post" action="/projects/open" class="card">`,
|
|
19121
19539
|
`<input type="hidden" name="csrf" value="${escape(csrf)}">`,
|
|
19122
19540
|
`<input type="hidden" name="return" value="${escape(returnTo)}">`,
|
|
19123
|
-
`<label>
|
|
19124
|
-
`<button type="submit">
|
|
19541
|
+
`<label>Path on this server<input type="text" name="path" placeholder="/Users/you/code/your-repo"></label>`,
|
|
19542
|
+
`<button type="submit">Open project</button>`,
|
|
19125
19543
|
`</form></details>`,
|
|
19126
19544
|
].join("\n");
|
|
19127
19545
|
const addCard = [
|
|
19128
19546
|
`<div class="card project-add-card">`,
|
|
19129
|
-
`<h2 class="project-add-title">
|
|
19547
|
+
`<h2 class="project-add-title">Add a project</h2>`,
|
|
19130
19548
|
`<div class="project-add-actions">`,
|
|
19131
19549
|
browsable
|
|
19132
19550
|
? addAction("/projects/browse", FOLDER_PATHS, "Choose a local folder", "Browse the project folders on this machine")
|
|
@@ -19147,8 +19565,8 @@ function projectsPage(chrome, recent, candidates, open, csrf, problem, _unscoped
|
|
|
19147
19565
|
recentItems.length === 0 && candidateItems.length === 0
|
|
19148
19566
|
? `<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
19567
|
: "",
|
|
19150
|
-
recentItems.length > 0 ? `<h2>
|
|
19151
|
-
candidateItems.length > 0 ? `<h2>
|
|
19568
|
+
recentItems.length > 0 ? `<h2>Recent</h2>${cards(recentItems)}` : "",
|
|
19569
|
+
candidateItems.length > 0 ? `<h2>Available</h2>${cards(candidateItems)}` : "",
|
|
19152
19570
|
addCard,
|
|
19153
19571
|
].join("\n"), { chrome, workspace: { view: {
|
|
19154
19572
|
kind: "projects", choosing, problem, returnTo,
|
|
@@ -19186,6 +19604,15 @@ function accentHead() {
|
|
|
19186
19604
|
}
|
|
19187
19605
|
const requestContext = new AsyncLocalStorage();
|
|
19188
19606
|
/** A same-site path or "/": never a scheme, a host, or a protocol-relative road. */
|
|
19607
|
+
/** The words a result page shows for a refusal its own form led to, by the fixed code a redirect carries. */
|
|
19608
|
+
const ACCEPT_ANYWAY_NEEDS_REASON = "Accepting anyway needs a reason.";
|
|
19609
|
+
const RESULT_REFUSALS = { reason: ACCEPT_ANYWAY_NEEDS_REASON };
|
|
19610
|
+
/** A same-site page address with a refusal code added, before any fragment. */
|
|
19611
|
+
function withRefusal(href, code) {
|
|
19612
|
+
const at = href.indexOf("#");
|
|
19613
|
+
const [path, hash] = at === -1 ? [href, ""] : [href.slice(0, at), href.slice(at)];
|
|
19614
|
+
return `${path}${path.includes("?") ? "&" : "?"}refused=${code}${hash}`;
|
|
19615
|
+
}
|
|
19189
19616
|
function safeReturn(raw) {
|
|
19190
19617
|
if (raw === null || raw === undefined)
|
|
19191
19618
|
return "/";
|
|
@@ -19320,25 +19747,25 @@ const NAV_ICONS = {
|
|
|
19320
19747
|
function workToolRows(scoped = false, _offersChat = false, offersCode = false) {
|
|
19321
19748
|
const rows = [
|
|
19322
19749
|
...(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: "
|
|
19750
|
+
{ key: "inbox", href: "/inbox", label: "Inbox", hint: "questions, approvals, and repairs to act on" },
|
|
19751
|
+
{ key: "board", href: "/board", label: "Board", hint: "lanes by state, with the order view" },
|
|
19752
|
+
{ key: "tasks", href: "/tasks", label: "Task list", hint: "everything, filterable by state" },
|
|
19753
|
+
{ key: "recipes", href: "/recipes", label: "Recipes", hint: "choose, customize, and reuse a workflow" },
|
|
19754
|
+
{ key: "routines", href: "/routines", label: "Routines", hint: "scheduled tracks and their firings" },
|
|
19755
|
+
{ key: "workbench", href: "/workbench", label: "Portfolio", hint: "every project and live build in one place" },
|
|
19756
|
+
{ key: "ledger", href: "/ledger", label: "Action ledger", hint: "who acted, what happened, and the result" },
|
|
19757
|
+
{ key: "spend", href: "/spend", label: "Spend", hint: "what agent work cost, and monthly budgets" },
|
|
19331
19758
|
];
|
|
19332
19759
|
return scoped ? rows.filter(row => row.key !== "workbench" && row.key !== "spend") : rows;
|
|
19333
19760
|
}
|
|
19334
19761
|
function settingsRows(scoped = false, offersSettings = false) {
|
|
19335
19762
|
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: "
|
|
19763
|
+
...(offersSettings ? [{ key: "settings", href: "/settings", label: "Settings", hint: "agent defaults, alerts, credentials" }] : []),
|
|
19764
|
+
{ key: "fleet", href: "/fleet", label: "Fleet", hint: "who is working, and on what" },
|
|
19765
|
+
{ key: "caps", href: "/caps", label: "Requirements", hint: "tools and credentials builds need" },
|
|
19766
|
+
{ key: "people", href: "/people", label: "People", hint: "who can sign in, and what they have done" },
|
|
19767
|
+
{ key: "mode", href: "/mode", label: "Operating mode", hint: "the signed posture this repository runs under" },
|
|
19768
|
+
{ key: "system", href: "/system", label: "System", hint: "workers, providers, and grants" },
|
|
19342
19769
|
];
|
|
19343
19770
|
return scoped ? rows.filter(row => row.key === "people" || row.key === "settings") : rows;
|
|
19344
19771
|
}
|
|
@@ -19368,10 +19795,10 @@ function queueCard(one, csrf, revision, queueRevision, workers, column) {
|
|
|
19368
19795
|
// and the reservation owner. Money is not in this query and is not
|
|
19369
19796
|
// invented here — it stays on the task page, labeled.
|
|
19370
19797
|
const state = one.taken
|
|
19371
|
-
? `<span class="badge">
|
|
19798
|
+
? `<span class="badge">Being taken — keeps its claim</span>`
|
|
19372
19799
|
: column === "anyone"
|
|
19373
|
-
? `<span class="badge">
|
|
19374
|
-
: `<span class="badge">
|
|
19800
|
+
? `<span class="badge">Queued</span>`
|
|
19801
|
+
: `<span class="badge">Reserved for ${escape(column)}</span>`;
|
|
19375
19802
|
const chips = ` ${state}` +
|
|
19376
19803
|
`${one.dispatch === null || one.dispatch === undefined ? "" : ` <a class="badge" href="${taskHref(one.id)}" title="${escape(one.dispatch.summary)}">${escape(dispatchHeadline(one.dispatch))}</a>`}`;
|
|
19377
19804
|
const hidden = `<input type="hidden" name="csrf" value="${escape(csrf)}">` +
|
|
@@ -19388,7 +19815,7 @@ function queueCard(one, csrf, revision, queueRevision, workers, column) {
|
|
|
19388
19815
|
`<select name="column" aria-label="reserve for">` +
|
|
19389
19816
|
`<option value="anyone"${column === "anyone" ? " selected" : ""}>anyone</option>` +
|
|
19390
19817
|
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">
|
|
19818
|
+
`</select><button type="submit">Move</button></form>`;
|
|
19392
19819
|
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
19820
|
`<p class="row">${one.taken ? "" : `${GRIP_HANDLE}`}` +
|
|
19394
19821
|
`<a href="${taskHref(one.id)}">${escape(one.title)}</a>${chips}</p>` +
|
|
@@ -19410,22 +19837,22 @@ function queueBody(tasks, workers, csrf, revision, queueRevision) {
|
|
|
19410
19837
|
? `<p class="meta lane-empty">${escape(empty)}</p>`
|
|
19411
19838
|
: rows
|
|
19412
19839
|
.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">
|
|
19840
|
+
? 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
19841
|
: queueCard(one, csrf, revision, queueRevision, workers, key))
|
|
19415
19842
|
.join("\n")) +
|
|
19416
19843
|
`</section>`;
|
|
19417
19844
|
const noteForm = (worker) => worker.retired
|
|
19418
|
-
? `<p class="meta">
|
|
19845
|
+
? `<p class="meta">This worker is retired — drag these elsewhere, or register the name again</p>`
|
|
19419
19846
|
: `<p class="meta mono">${worker.building} building in this project · unattended capacity ${worker.capacity}</p>` +
|
|
19420
19847
|
`<form method="post" action="/queue/note" class="row">` +
|
|
19421
19848
|
`<input type="hidden" name="csrf" value="${escape(csrf)}">` +
|
|
19422
19849
|
`<input type="hidden" name="projectRevision" value="${revision}">` +
|
|
19423
19850
|
`<input type="hidden" name="runner" value="${escape(worker.name)}">` +
|
|
19424
19851
|
`<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">
|
|
19852
|
+
`<button type="submit">Save</button></form>` +
|
|
19853
|
+
`<p class="meta">Takes from the shared queue when this column is empty</p>`;
|
|
19427
19854
|
return (`<div class="lanes" data-queue-revision="${queueRevision}">` +
|
|
19428
|
-
column("shared queue", "anyone", `<p class="meta">
|
|
19855
|
+
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
19856
|
workers
|
|
19430
19857
|
.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
19858
|
.join("\n") +
|
|
@@ -19447,7 +19874,7 @@ function fleetBody(queued, building, runners, csrf, queueRevision, visibleRepo)
|
|
|
19447
19874
|
const live = building.filter(one => one.runner === runner.name);
|
|
19448
19875
|
const retired = runner.retiredAt !== null;
|
|
19449
19876
|
const head = retired
|
|
19450
|
-
? `<p class="meta">
|
|
19877
|
+
? `<p class="meta">This worker is retired — drag these elsewhere, or register the name again</p>`
|
|
19451
19878
|
: `<form method="post" action="/queue/note" class="row">` +
|
|
19452
19879
|
`<input type="hidden" name="csrf" value="${escape(csrf)}">` +
|
|
19453
19880
|
`<input type="hidden" name="from" value="fleet">` +
|
|
@@ -19464,9 +19891,9 @@ function fleetBody(queued, building, runners, csrf, queueRevision, visibleRepo)
|
|
|
19464
19891
|
`<p class="row">${one.taken ? "" : `${GRIP_HANDLE}`}` +
|
|
19465
19892
|
`<a href="${taskHref(one.id)}">${escape(one.title)}</a></p>` +
|
|
19466
19893
|
`<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">
|
|
19894
|
+
`${one.approved ? "" : ` <span class="badge">Unapproved scope</span>`}` +
|
|
19895
|
+
`${one.blockers > 0 ? ` <span class="badge">Waits for ${one.blockers}</span>` : ""}` +
|
|
19896
|
+
`${one.taken ? ` <span class="badge">Being taken</span>` : ""}</p></div>`)
|
|
19470
19897
|
.join("\n");
|
|
19471
19898
|
const empty = live.length === 0 && own.length === 0
|
|
19472
19899
|
? `<p class="meta lane-empty">${retired ? "nothing left" : "idle — will take from the shared queue"}</p>`
|
|
@@ -19481,12 +19908,12 @@ function fleetBody(queued, building, runners, csrf, queueRevision, visibleRepo)
|
|
|
19481
19908
|
`<p class="row">${one.taken ? "" : `${GRIP_HANDLE}`}` +
|
|
19482
19909
|
`<a href="${taskHref(one.id)}">${escape(one.title)}</a></p>` +
|
|
19483
19910
|
`<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">
|
|
19911
|
+
`${one.approved ? "" : ` <span class="badge">Unapproved scope</span>`}` +
|
|
19912
|
+
`${one.blockers > 0 ? ` <span class="badge">Waits for ${one.blockers}</span>` : ""}` +
|
|
19913
|
+
`${one.taken ? ` <span class="badge">Being taken</span>` : ""}</p></div>`)
|
|
19487
19914
|
.join("\n");
|
|
19488
|
-
const sharedLane = `<section class="lane queue-column" data-column="anyone"><h2>
|
|
19489
|
-
`<p class="meta">
|
|
19915
|
+
const sharedLane = `<section class="lane queue-column" data-column="anyone"><h2>Shared queue</h2>` +
|
|
19916
|
+
`<p class="meta">Any free worker takes from here, top first</p>${sharedCards}` +
|
|
19490
19917
|
(shared.length === 0 ? `<p class="meta lane-empty">nothing waiting — every task is reserved or running</p>` : "") +
|
|
19491
19918
|
`</section>`;
|
|
19492
19919
|
return (`<div class="lanes" data-queue-revision="${queueRevision}">` +
|
|
@@ -19628,13 +20055,15 @@ function newTaskPage(chrome, project, csrf, projectRevision, problem, candidates
|
|
|
19628
20055
|
* brief, but the console renders its known structure as a useful control
|
|
19629
20056
|
* surface. Historical free-form plans keep their safe plain-text fallback. */
|
|
19630
20057
|
const MILESTONE_STATE_WORDS = {
|
|
19631
|
-
pending: "
|
|
19632
|
-
current: "
|
|
19633
|
-
completed: "
|
|
19634
|
-
blocked: "
|
|
20058
|
+
pending: "Pending",
|
|
20059
|
+
current: "In progress",
|
|
20060
|
+
completed: "Completed",
|
|
20061
|
+
blocked: "Blocked",
|
|
19635
20062
|
};
|
|
20063
|
+
/** Badges, labels and buttons read in sentence case, whatever word a record stores. */
|
|
20064
|
+
const sentenceCase = (word) => word.charAt(0).toUpperCase() + word.slice(1);
|
|
19636
20065
|
function milestoneStateWord(state) {
|
|
19637
|
-
return MILESTONE_STATE_WORDS[state] ?? "
|
|
20066
|
+
return MILESTONE_STATE_WORDS[state] ?? "Pending";
|
|
19638
20067
|
}
|
|
19639
20068
|
/** Adaptive execution plans (v44): the live milestone projection a running
|
|
19640
20069
|
* (or finished) build has reported. A shared render used by BOTH the task
|
|
@@ -19693,9 +20122,9 @@ function planRevisionLedgerHtml(ledger, taskId, csrf) {
|
|
|
19693
20122
|
`</div>`;
|
|
19694
20123
|
const history = ledger.history.length <= 1
|
|
19695
20124
|
? ""
|
|
19696
|
-
: `<details class="plan-revision-history"><summary>
|
|
20125
|
+
: `<details class="plan-revision-history"><summary>Revision history (${ledger.history.length})</summary>` +
|
|
19697
20126
|
ledger.history
|
|
19698
|
-
.map(one => `<p class="row"><span class="mono">rev ${one.revision}</span> <span class="badge">${escape(one.status)}</span> ` +
|
|
20127
|
+
.map(one => `<p class="row"><span class="mono">rev ${one.revision}</span> <span class="badge">${escape(sentenceCase(one.status))}</span> ` +
|
|
19699
20128
|
`<span class="meta">${escape(one.author)} · ${whenTime(one.createdAt)}</span> — ${escape(one.reason)}</p>`)
|
|
19700
20129
|
.join("\n") +
|
|
19701
20130
|
`</details>`;
|
|
@@ -19814,25 +20243,24 @@ function taskBodyParts(data) {
|
|
|
19814
20243
|
if (liveRunId === null || liveRun === undefined)
|
|
19815
20244
|
return "";
|
|
19816
20245
|
const minutes = Math.max(0, Math.round((data.now.getTime() - new Date(liveRun.startedAt).getTime()) / 60_000));
|
|
19817
|
-
const head = `<p><strong>
|
|
20246
|
+
const head = `<p><strong>Build #${liveRun.id} · ${escape(liveRun.runner)} · running ` +
|
|
19818
20247
|
`<time data-elapsed-since="${escape(liveRun.startedAt)}">${minutes}m</time></strong></p>`;
|
|
19819
20248
|
const door = `<p class="row"><a href="/r/${liveRun.id}">full build view →</a></p>`;
|
|
19820
20249
|
if (data.degraded !== undefined) {
|
|
19821
20250
|
return `<div class="card attempt-live" data-live-run="${liveRun.id}">${head}` +
|
|
19822
|
-
`<p class="meta">${data.degraded === "sensitive" ? "
|
|
20251
|
+
`<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
20252
|
}
|
|
19824
|
-
|
|
19825
|
-
|
|
19826
|
-
|
|
19827
|
-
|
|
19828
|
-
|
|
19829
|
-
|
|
19830
|
-
|
|
19831
|
-
|
|
19832
|
-
|
|
19833
|
-
|
|
19834
|
-
|
|
19835
|
-
`<p class="meta" id="live-transcript-state"></p>`;
|
|
20253
|
+
// With the live file view off there is nothing live to show here: no panel (the build's own page says why).
|
|
20254
|
+
if (data.peekable !== true)
|
|
20255
|
+
return "";
|
|
20256
|
+
const peek = `<p class="meta">What is changing right now</p>` +
|
|
20257
|
+
`<div id="run-peek"><p class="meta">Watching\u2026 the first look lands within 15 seconds</p></div>` +
|
|
20258
|
+
`<p class="meta" id="run-peek-stamp"></p>`;
|
|
20259
|
+
const transcript = liveRun.provider !== "claude"
|
|
20260
|
+
? `<p class="meta">The live transcript needs the claude harness for now \u2014 this build runs on ${escape(liveRun.provider)}</p>`
|
|
20261
|
+
: `<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>` +
|
|
20262
|
+
`<pre id="live-transcript" class="mono" style="max-height:18rem;overflow:auto;white-space:pre-wrap"></pre>` +
|
|
20263
|
+
`<p class="meta" id="live-transcript-state"></p>`;
|
|
19836
20264
|
return `<div class="card attempt-live" data-live-run="${liveRun.id}">${head}${peek}${transcript}${door}</div>`;
|
|
19837
20265
|
})();
|
|
19838
20266
|
// One truthful answer to the first question on a queued task: "will this
|
|
@@ -19935,7 +20363,7 @@ function taskBodyParts(data) {
|
|
|
19935
20363
|
`<p class="meta">Saved assessment history. Current task status and checks are shown above.</p>` +
|
|
19936
20364
|
(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
20365
|
(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 }) +
|
|
20366
|
+
criterionMatrixHtml(proof.proofMatrix, { compact: true, runId: proof.runId, links: proof.proofMatrixLinks, verdict: proof.proofVerdict }) +
|
|
19939
20367
|
semanticCoverageHtml(proof.proofMatrix, proof.qualityMode ?? "default") + `</div></details>`;
|
|
19940
20368
|
}
|
|
19941
20369
|
// v50: the independent review's own card — attempt counts, what is
|
|
@@ -19987,7 +20415,7 @@ function taskBodyParts(data) {
|
|
|
19987
20415
|
const proofDetails = proof.proofMatrix.length === 0 && machineNote === "" && chainHtml === ""
|
|
19988
20416
|
? ""
|
|
19989
20417
|
: `<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>`;
|
|
20418
|
+
`<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
20419
|
// The publication fact is separate from the evidence fact: a PR or
|
|
19992
20420
|
// an observed merge is named from its record; nothing is "deployed".
|
|
19993
20421
|
const publicationWords = (data.publication === null || data.publication === undefined ? "" : ` ${escape(receiptPublicationWords(publicationFactsOf(data.publication)))}`) +
|
|
@@ -20047,26 +20475,30 @@ function taskBodyParts(data) {
|
|
|
20047
20475
|
: `<form method="post" action="${taskHref(task.id)}/steer" class="row">` +
|
|
20048
20476
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
|
|
20049
20477
|
`<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>
|
|
20478
|
+
`<button type="submit">Steer</button></form>` +
|
|
20479
|
+
`<p class="meta">Lands when the next attempt starts — a running agent is not interrupted, and a note cannot widen the approved scope</p>`;
|
|
20480
|
+
const steeringCard = steerRows === "" && steerForm === "" ? "" : `<h2>Steering</h2>${steerRows}${steerForm}`;
|
|
20053
20481
|
const holds = data.holds.length === 0
|
|
20054
20482
|
? ""
|
|
20055
|
-
: `<h2>
|
|
20483
|
+
: `<h2>Holds</h2>` +
|
|
20056
20484
|
data.holds
|
|
20057
20485
|
.map(hold => `<p class="row">${escape(holdOwnerWords(hold.ownerKind))} — ${escape(hold.reason)}` +
|
|
20058
20486
|
`${hold.until === null ? "" : ` <span class="meta">until ${whenTime(hold.until)}</span>`}</p>`)
|
|
20059
20487
|
.join("\n") +
|
|
20060
|
-
`<p class="meta">
|
|
20488
|
+
`<p class="meta">Only your hold can be lifted here — waits caused by questions, incidents, or retry delays clear on their own</p>`;
|
|
20061
20489
|
const approval = approvalOf(scope);
|
|
20490
|
+
// A refused save from the sheet's in-place editor reopens that editor with
|
|
20491
|
+
// its draft; the full scope form below stays as it was.
|
|
20492
|
+
const inlineScopeDraft = data.scopeDraft?.has("requirement-new") === true;
|
|
20493
|
+
const scopeDraft = inlineScopeDraft ? undefined : data.scopeDraft;
|
|
20062
20494
|
const scopeCard = scope === null
|
|
20063
|
-
? `<p class="meta">
|
|
20495
|
+
? `<p class="meta">No scope proposed — nothing builds this until one is approved</p>`
|
|
20064
20496
|
: [
|
|
20065
20497
|
`<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>
|
|
20498
|
+
`<p><strong>Goal</strong></p><p class="recap">${escape(scope.goal)}</p>`,
|
|
20499
|
+
scope.outOfScope === null ? "" : `<p><strong>Not this</strong></p><p class="recap">${escape(scope.outOfScope)}</p>`,
|
|
20500
|
+
scope.touches.length === 0 ? "" : `<p class="scope-paths"><strong>Touches</strong> ${scope.touches.map(one => escape(one)).join(", ")}</p>`,
|
|
20501
|
+
`<p><strong>Quality</strong> ${escape(qualityModeTitle(scope.qualityMode ?? "default"))}</p>`,
|
|
20070
20502
|
// The fallback chain, on the card the yes reads (Layer F): the
|
|
20071
20503
|
// digest binds the WHOLE chain, so every entry — credential
|
|
20072
20504
|
// included — is said before anyone signs.
|
|
@@ -20074,7 +20506,7 @@ function taskBodyParts(data) {
|
|
|
20074
20506
|
const chain = chainFromJson(scope.proposedChainJson ?? null);
|
|
20075
20507
|
if (chain === null || chain.length < 2)
|
|
20076
20508
|
return "";
|
|
20077
|
-
return `<p><strong>
|
|
20509
|
+
return `<p><strong>If its subscription runs out</strong></p><p class="recap">${chain
|
|
20078
20510
|
.slice(1)
|
|
20079
20511
|
.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
20512
|
.join("; ")}</p>`;
|
|
@@ -20092,10 +20524,10 @@ function taskBodyParts(data) {
|
|
|
20092
20524
|
// blank — the same rule as the revision brief.
|
|
20093
20525
|
const reportCard = data.report === null || data.report === undefined
|
|
20094
20526
|
? data.deliverable === "report"
|
|
20095
|
-
? `<div class="card"><p><strong>
|
|
20527
|
+
? `<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
20528
|
: ""
|
|
20097
20529
|
: !data.report.ok
|
|
20098
|
-
? `<div class="card"><p><strong>
|
|
20530
|
+
? `<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
20531
|
: [
|
|
20100
20532
|
`<div class="card report">`,
|
|
20101
20533
|
`<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 +20536,11 @@ function taskBodyParts(data) {
|
|
|
20104
20536
|
...(data.report.report.followUps.length === 0
|
|
20105
20537
|
? []
|
|
20106
20538
|
: [
|
|
20107
|
-
`<p><strong>
|
|
20539
|
+
`<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
20540
|
...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
20541
|
(data.csrf === "" || data.repo === null
|
|
20110
20542
|
? `<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">
|
|
20543
|
+
: `<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
20544
|
`</div>`),
|
|
20113
20545
|
]),
|
|
20114
20546
|
`</div>`,
|
|
@@ -20140,7 +20572,7 @@ function taskBodyParts(data) {
|
|
|
20140
20572
|
? data.revision != null && !("problem" in data.revision)
|
|
20141
20573
|
? `<div class="card planner-status"><span class="planner-orb" aria-hidden="true"></span><p><strong>Updating the plan</strong>` +
|
|
20142
20574
|
`<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>
|
|
20575
|
+
: `<div class="card planner-status"><span class="planner-orb" aria-hidden="true"></span><p><strong>Planning requested</strong>` +
|
|
20144
20576
|
`<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
20577
|
: ""
|
|
20146
20578
|
: `${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 +20585,7 @@ function taskBodyParts(data) {
|
|
|
20153
20585
|
? ""
|
|
20154
20586
|
: `<details class="plan-editor" id="plan-edit"><summary>Edit plan</summary><form method="post" action="${taskHref(task.id)}/plan-edit">` +
|
|
20155
20587
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}"><input type="hidden" name="saw-plan" value="${escape(data.planSha)}">` +
|
|
20156
|
-
`<label>
|
|
20588
|
+
`<label>Plan details <span class="meta">Keep the five headings. Approval locks this version for the build.</span>` +
|
|
20157
20589
|
`<textarea name="plan-document" rows="14">${escape(data.planDocument)}</textarea></label>` +
|
|
20158
20590
|
`<button type="submit">Save plan</button></form></details>`) +
|
|
20159
20591
|
`${approval.approved ? `</div></details>` : `</section>`}`;
|
|
@@ -20165,7 +20597,7 @@ function taskBodyParts(data) {
|
|
|
20165
20597
|
const revisionCard = data.revision === null || data.revision === undefined
|
|
20166
20598
|
? ""
|
|
20167
20599
|
: "problem" in data.revision
|
|
20168
|
-
? `<div class="card"><p><strong>
|
|
20600
|
+
? `<div class="card"><p><strong>Revision brief</strong></p><p class="meta">${escape(data.revision.problem)}</p></div>`
|
|
20169
20601
|
: [
|
|
20170
20602
|
`<div class="revision-card" data-revision-feedback>`,
|
|
20171
20603
|
`<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 +20625,7 @@ function taskBodyParts(data) {
|
|
|
20193
20625
|
: scope.profileState === "unresolved"
|
|
20194
20626
|
? `<div class="card approve-form" id="approve"><p><strong>This task is waiting on you: its scope cannot be approved yet.</strong></p>` +
|
|
20195
20627
|
profileWords(scope) +
|
|
20196
|
-
`<p class="ceremony-road"><a class="button-link" href="#scope">
|
|
20628
|
+
`<p class="ceremony-road"><a class="button-link" href="#scope">Edit the scope to fix it →</a></p></div>`
|
|
20197
20629
|
: !consentDoor.open
|
|
20198
20630
|
? consentClosedHtml(task.id, consentDoor, "task")
|
|
20199
20631
|
: approvalSheetHtml({
|
|
@@ -20216,6 +20648,11 @@ function taskBodyParts(data) {
|
|
|
20216
20648
|
editHref: data.csrf !== "" && data.planSha != null && data.planDocument !== null ? "#plan-edit" : "#scope",
|
|
20217
20649
|
notNowHref: "/work",
|
|
20218
20650
|
sticky: data.dispatch?.action === "approve-scope",
|
|
20651
|
+
earlier: { active: data.assignment?.earlierActive ?? 0, running: data.assignment?.earlierRunning ?? 0 },
|
|
20652
|
+
// A tournament's terms live in the full scope editor; every other plan edits in place.
|
|
20653
|
+
edit: data.csrf === "" || (data.raceTerms ?? null) !== null ? null
|
|
20654
|
+
: { action: `${taskHref(task.id)}/scope`, draft: inlineScopeDraft ? data.scopeDraft ?? null : null, problem: inlineScopeDraft ? data.problem : null,
|
|
20655
|
+
open: data.editPlan === true, stepsHref: data.planSha != null && data.planDocument !== null ? "#plan-edit" : null },
|
|
20219
20656
|
});
|
|
20220
20657
|
// The attended road (Phase 2E): beside the approval, never replacing it.
|
|
20221
20658
|
// The mint button leads to the CONFIRM screen where every term renders
|
|
@@ -20228,12 +20665,12 @@ function taskBodyParts(data) {
|
|
|
20228
20665
|
: attended.open !== null
|
|
20229
20666
|
? [
|
|
20230
20667
|
`<div class="card" data-attended="${escape(attended.open.id)}">`,
|
|
20231
|
-
`<p><strong>
|
|
20668
|
+
`<p><strong>Attended session</strong> <span class="meta">${escape(attended.open.state)}</span></p>`,
|
|
20232
20669
|
`<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">
|
|
20670
|
+
`<p class="meta">This page being open keeps it alive — close it and the session winds down within a minute</p>`,
|
|
20234
20671
|
`<form method="post" action="${taskHref(task.id)}/attend-revoke" class="inline">`,
|
|
20235
20672
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
|
|
20236
|
-
`<button type="submit">
|
|
20673
|
+
`<button type="submit">Revoke — stop the session</button>`,
|
|
20237
20674
|
`</form>`,
|
|
20238
20675
|
`</div>`,
|
|
20239
20676
|
].join("\n")
|
|
@@ -20241,36 +20678,36 @@ function taskBodyParts(data) {
|
|
|
20241
20678
|
? [
|
|
20242
20679
|
`<form method="post" action="${taskHref(task.id)}/attend-preview" class="card">`,
|
|
20243
20680
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
|
|
20244
|
-
`<p><strong>
|
|
20245
|
-
`<p class="meta">
|
|
20681
|
+
`<p><strong>Or run it once while you watch</strong></p>`,
|
|
20682
|
+
`<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
20683
|
attended.mint === undefined || attended.mint.models.length <= 1
|
|
20247
20684
|
? ""
|
|
20248
|
-
: `<label>
|
|
20685
|
+
: `<label>Model <span class="meta">(watched sessions run claude only)</span><select name="model">` +
|
|
20249
20686
|
attended.mint.models
|
|
20250
20687
|
.map(model => `<option value="${escape(model)}"${model === attended.mint?.pinnedModel ? " selected" : ""}>${escape(model)}</option>`)
|
|
20251
20688
|
.join("") +
|
|
20252
20689
|
`</select></label>`,
|
|
20253
20690
|
attended.mint === undefined
|
|
20254
20691
|
? ""
|
|
20255
|
-
: `<label>
|
|
20692
|
+
: `<label>Permissions<select name="posture">` +
|
|
20256
20693
|
`<option value="auto"${attended.mint.posture === "auto" ? " selected" : ""}>safe unattended — routine project commands and edits proceed</option>` +
|
|
20257
20694
|
`<option value="acceptEdits"${attended.mint.posture === "acceptEdits" ? " selected" : ""}>legacy acceptEdits — commands that ask are denied unattended</option>` +
|
|
20258
20695
|
`<option value="bypassPermissions"${attended.mint.posture === "bypassPermissions" ? " selected" : ""}>full permissions — nothing asks</option>` +
|
|
20259
20696
|
`</select></label>`,
|
|
20260
|
-
`<button type="submit">
|
|
20697
|
+
`<button type="submit">Read the terms</button>`,
|
|
20261
20698
|
`</form>`,
|
|
20262
20699
|
].join("\n")
|
|
20263
20700
|
: "";
|
|
20264
20701
|
const scopeForm = [
|
|
20265
|
-
`<details${scope === null ||
|
|
20702
|
+
`<details${scope === null || scopeDraft !== undefined ? " open" : ""}><summary>${scope === null ? "Write the scope" : "Edit the scope"}${approval.approved ? " (editing voids the approval)" : ""}</summary>`,
|
|
20266
20703
|
`<form method="post" action="${taskHref(task.id)}/scope" class="scope-editor">`,
|
|
20267
|
-
|
|
20704
|
+
scopeDraft === undefined || data.problem === null ? "" : `<div class="problem" role="alert" id="scope-error">${escape(data.problem)}</div>`,
|
|
20268
20705
|
`<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>
|
|
20706
|
+
`<input type="hidden" name="sawDigest" value="${escape(scopeDraft?.get("sawDigest") ?? scope?.digest ?? "")}">`,
|
|
20707
|
+
`<label>Goal<textarea name="goal" rows="3"${scopeDraft === undefined ? "" : ' autofocus aria-describedby="scope-error"'}>${escape(scopeDraft?.get("goal") ?? scope?.goal ?? "")}</textarea></label>`,
|
|
20708
|
+
`<label>Not this<textarea name="not" rows="2">${escape(scopeDraft?.get("not") ?? scope?.outOfScope ?? "")}</textarea></label>`,
|
|
20709
|
+
`<label>Touches <span class="meta">(one per line)</span><textarea name="touches" rows="2">${escape(scopeDraft?.get("touches") ?? (scope?.touches ?? []).join("\n"))}</textarea></label>`,
|
|
20710
|
+
`<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
20711
|
(() => {
|
|
20275
20712
|
const defaults = data.spendDefaults ?? null;
|
|
20276
20713
|
const budgetPrefill = scope?.budgetMicrousd != null
|
|
@@ -20278,8 +20715,8 @@ function taskBodyParts(data) {
|
|
|
20278
20715
|
: defaults?.buildPerRunMicrousd != null
|
|
20279
20716
|
? (defaults.buildPerRunMicrousd / 1_000_000).toFixed(2)
|
|
20280
20717
|
: "";
|
|
20281
|
-
return `<label>
|
|
20282
|
-
`<input type="number" name="budget-usd" step="0.01" min="0.01" value="${escape(
|
|
20718
|
+
return `<label>Agent-reported usage cap <span class="meta">(optional — leave blank for uncapped subscription work)</span>` +
|
|
20719
|
+
`<input type="number" name="budget-usd" step="0.01" min="0.01" value="${escape(scopeDraft?.get("budget-usd") ?? budgetPrefill)}" placeholder="no cap"></label>` +
|
|
20283
20720
|
`<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
20721
|
})(),
|
|
20285
20722
|
(() => {
|
|
@@ -20290,14 +20727,14 @@ function taskBodyParts(data) {
|
|
|
20290
20727
|
: scope?.profile?.provider === "codex" || scope?.profile?.provider === "openrouter"
|
|
20291
20728
|
? scope.profile.sandboxMode === "danger-full-access" ? "bypassPermissions" : "auto"
|
|
20292
20729
|
: null;
|
|
20293
|
-
const selected =
|
|
20294
|
-
return `<fieldset class="permission-field"><legend>
|
|
20730
|
+
const selected = scopeDraft?.get("permission-mode") === "bypassPermissions" ? "bypassPermissions" : scopeDraft?.get("permission-mode") === "auto" ? "auto" : data.permissionMode ?? profileMode ?? data.permissionDefault ?? "auto";
|
|
20731
|
+
return `<fieldset class="permission-field"><legend>Agent permissions</legend>` +
|
|
20295
20732
|
permissionModeChoices("permission-mode", selected) +
|
|
20296
20733
|
`<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
20734
|
})(),
|
|
20298
20735
|
(() => {
|
|
20299
|
-
const selected =
|
|
20300
|
-
return `<fieldset class="permission-field"><legend>
|
|
20736
|
+
const selected = scopeDraft?.get("quality-mode") === "strict" ? "strict" : scopeDraft?.get("quality-mode") === "default" ? "default" : scope?.qualityMode ?? data.qualityMode ?? data.qualityDefault ?? "default";
|
|
20737
|
+
return `<fieldset class="permission-field"><legend>Quality</legend>` +
|
|
20301
20738
|
qualityModeChoices("quality-mode", selected) +
|
|
20302
20739
|
`<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
20740
|
})(),
|
|
@@ -20308,8 +20745,8 @@ function taskBodyParts(data) {
|
|
|
20308
20745
|
// approval above restates and covers both.
|
|
20309
20746
|
const defaults = data.spendDefaults ?? null;
|
|
20310
20747
|
const terms = data.raceTerms ?? null;
|
|
20311
|
-
const selectedCount =
|
|
20312
|
-
const selectedModel =
|
|
20748
|
+
const selectedCount = scopeDraft === undefined ? terms !== null ? terms.n : defaults?.raceAgents ?? 0 : Number(scopeDraft.get("race-count") ?? "0");
|
|
20749
|
+
const selectedModel = scopeDraft?.get("race-model") ?? terms?.agents[0]?.model ?? "claude-sonnet-5";
|
|
20313
20750
|
const perPrefill = terms !== null
|
|
20314
20751
|
? (terms.perAgentBudgetMicrousd / 1_000_000).toFixed(2)
|
|
20315
20752
|
: defaults?.racePerAgentMicrousd != null
|
|
@@ -20321,31 +20758,31 @@ function taskBodyParts(data) {
|
|
|
20321
20758
|
? (defaults.raceTotalMicrousd / 1_000_000).toFixed(2)
|
|
20322
20759
|
: "";
|
|
20323
20760
|
return [
|
|
20324
|
-
`<details class="more-agents"${selectedCount > 0 ? " open" : ""}><summary>
|
|
20325
|
-
`<label>
|
|
20761
|
+
`<details class="more-agents"${selectedCount > 0 ? " open" : ""}><summary>More than one agent (optional)</summary>`,
|
|
20762
|
+
`<label>How many agents compete <span class="meta">(a tournament builds the task independently N times — you compare and pick one)</span>` +
|
|
20326
20763
|
`<select name="race-count">` +
|
|
20327
20764
|
`<option value=""${selectedCount === 0 ? " selected" : ""}>one agent — no tournament</option>` +
|
|
20328
20765
|
[2, 3, 4].map(count => `<option value="${count}"${selectedCount === count ? " selected" : ""}>${count} agents</option>`).join("") +
|
|
20329
20766
|
`</select></label>`,
|
|
20330
|
-
`<label>
|
|
20767
|
+
`<label>Competing model <select name="race-model">` +
|
|
20331
20768
|
PRICED_BUILD_MODELS.map(model => `<option value="${escape(model)}"${model === selectedModel ? " selected" : ""}>${escape(model)}</option>`).join("") +
|
|
20332
20769
|
`</select></label>`,
|
|
20333
|
-
`<label>
|
|
20334
|
-
`<label>
|
|
20770
|
+
`<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>`,
|
|
20771
|
+
`<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
20772
|
// The comparison lanes (Phase 3 slice B): 2-4 rows, any registered
|
|
20336
20773
|
// provider, exact model required — no dollar fields exist. Blank
|
|
20337
20774
|
// rows are unused; filling any row files a comparison INSTEAD of a
|
|
20338
20775
|
// tournament, and mixing the two refuses in words.
|
|
20339
20776
|
`<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>
|
|
20777
|
+
...[1, 2, 3, 4].map(lane => `<div class="row"><label>Agent ${lane} <select name="compare-provider-${lane}">` +
|
|
20341
20778
|
`<option value="">—</option>` +
|
|
20342
|
-
PROVIDER_IDS.map(provider => `<option value="${escape(provider)}"${
|
|
20779
|
+
PROVIDER_IDS.map(provider => `<option value="${escape(provider)}"${scopeDraft?.get(`compare-provider-${lane}`) === provider ? " selected" : ""}>${escape(provider)}</option>`).join("") +
|
|
20343
20780
|
`</select></label>` +
|
|
20344
|
-
`<label>
|
|
20781
|
+
`<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
20782
|
`</details>`,
|
|
20346
20783
|
].join("\n");
|
|
20347
20784
|
})(),
|
|
20348
|
-
`<button type="submit">
|
|
20785
|
+
`<button type="submit">Save scope</button>`,
|
|
20349
20786
|
`</form></details>`,
|
|
20350
20787
|
].join("\n");
|
|
20351
20788
|
// 41_237 → "41k": token counts read at a glance; exactness lives on the run page.
|
|
@@ -20367,7 +20804,7 @@ function taskBodyParts(data) {
|
|
|
20367
20804
|
// duration · tokens · measured-or-unmeasured cost.
|
|
20368
20805
|
const runs = data.runs.length === 0
|
|
20369
20806
|
? ""
|
|
20370
|
-
: `<h2>
|
|
20807
|
+
: `<h2>Attempts</h2>` +
|
|
20371
20808
|
data.runs
|
|
20372
20809
|
.map(run => {
|
|
20373
20810
|
const bits = [
|
|
@@ -20560,20 +20997,20 @@ function taskBodyParts(data) {
|
|
|
20560
20997
|
})();
|
|
20561
20998
|
const decisions = data.decisions.length === 0
|
|
20562
20999
|
? ""
|
|
20563
|
-
: `<h2>
|
|
21000
|
+
: `<h2>Decisions</h2>` +
|
|
20564
21001
|
data.decisions
|
|
20565
21002
|
.map(decision => `<p class="row"><a href="/d/${decision.id}">${escape(decision.question)}</a> ` +
|
|
20566
21003
|
`<span class="meta">${escape(decision.state)}${isOverdue(decision, data.now) ? " · overdue" : ""}</span></p>`)
|
|
20567
21004
|
.join("\n");
|
|
20568
21005
|
const incidents = data.incidents.length === 0
|
|
20569
21006
|
? ""
|
|
20570
|
-
: `<h2>
|
|
21007
|
+
: `<h2>Incidents</h2>` +
|
|
20571
21008
|
data.incidents
|
|
20572
21009
|
.map(one => one.resolvedAt === null
|
|
20573
21010
|
? `<p class="row">${escape(incidentWords(one.kind))} ` +
|
|
20574
21011
|
`<form method="post" action="/i/${one.id}/resolve" class="inline">` +
|
|
20575
21012
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
|
|
20576
|
-
`<button type="submit">
|
|
21013
|
+
`<button type="submit">Resolve</button></form></p>`
|
|
20577
21014
|
: `<p class="row meta">${escape(incidentWords(one.kind))} — resolved by ${escape(one.resolvedBy ?? "?")}</p>`)
|
|
20578
21015
|
.join("\n");
|
|
20579
21016
|
const stalled = task.state === "failed" || data.incidents.some(one => one.resolvedAt === null);
|
|
@@ -20585,11 +21022,11 @@ function taskBodyParts(data) {
|
|
|
20585
21022
|
const waitsFor = data.waitsFor ?? [];
|
|
20586
21023
|
const candidates = (data.waitCandidates ?? []).filter(one => !waitsFor.some(existing => existing.id === one.id));
|
|
20587
21024
|
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>`}` +
|
|
21025
|
+
.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
21026
|
`<form method="post" action="${taskHref(task.id)}/unblock" class="inline">` +
|
|
20590
21027
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
|
|
20591
21028
|
`<input type="hidden" name="on" value="${escape(one.id)}">` +
|
|
20592
|
-
`<button type="submit">
|
|
21029
|
+
`<button type="submit">Don't wait for this</button></form></p>`)
|
|
20593
21030
|
.join("\n");
|
|
20594
21031
|
const waitAdd = data.csrf === "" || candidates.length === 0
|
|
20595
21032
|
? ""
|
|
@@ -20598,11 +21035,11 @@ function taskBodyParts(data) {
|
|
|
20598
21035
|
`<select name="on" aria-label="task to wait for">` +
|
|
20599
21036
|
candidates.map(one => `<option value="${escape(one.id)}">${escape(one.id)} — ${escape(one.title)}</option>`).join("") +
|
|
20600
21037
|
`</select>` +
|
|
20601
|
-
`<button type="submit">
|
|
21038
|
+
`<button type="submit">Wait for this task</button>` +
|
|
20602
21039
|
`<span class="meta"> — this task starts only after it finishes</span></form>`;
|
|
20603
21040
|
const waitsForCard = waitRows === "" && waitAdd === ""
|
|
20604
21041
|
? ""
|
|
20605
|
-
: `<h2>
|
|
21042
|
+
: `<h2>Waits for</h2>${waitRows === "" ? `<p class="meta">Nothing — it starts when a worker is free</p>` : waitRows}${waitAdd}`;
|
|
20606
21043
|
// The acts bar (task page pass): every verb in one row under the title,
|
|
20607
21044
|
// the one that resolves this task's state first and primary — retry on a
|
|
20608
21045
|
// stalled task, build-next in the queue, plan-first with no scope. The
|
|
@@ -20664,9 +21101,10 @@ function taskBodyParts(data) {
|
|
|
20664
21101
|
const section = (title, html, open, count, id = title.replace(/\s+/g, "-")) => {
|
|
20665
21102
|
if (html === "")
|
|
20666
21103
|
return "";
|
|
20667
|
-
const
|
|
20668
|
-
|
|
20669
|
-
|
|
21104
|
+
const heading = sentenceCase(title);
|
|
21105
|
+
const inner = html.replace(`<h2>${title}</h2>`, "").replace(`<h2>${heading}</h2>`, "");
|
|
21106
|
+
sectionParts.push({ id, title: heading, html: inner, open, count: count ?? null });
|
|
21107
|
+
return `<details class="section" id="${id}"${open ? " open" : ""}><summary><h2>${heading}${count === undefined ? "" : ` <span class="lane-count">${count}</span>`}</h2></summary>` + inner + `</details>`;
|
|
20670
21108
|
};
|
|
20671
21109
|
const receiptLeads = data.assignment == null && task.state === "done" && data.completion?.receipt != null;
|
|
20672
21110
|
// Exact identity stays available in Task options; failures stay in the
|
|
@@ -20678,7 +21116,7 @@ function taskBodyParts(data) {
|
|
|
20678
21116
|
? ` · filed by <span class="mono">${escape(data.coordinator.label)}</span>${data.coordinator.filedAgo === null ? "" : ` ${escape(data.coordinator.filedAgo)}`}`
|
|
20679
21117
|
: data.filedVia === null || data.filedVia === undefined
|
|
20680
21118
|
? ""
|
|
20681
|
-
: ` · filed via ${escape(data.filedVia)}`}${data.deliverable === "report" ? ` · <span class="badge">
|
|
21119
|
+
: ` · filed via ${escape(data.filedVia)}`}${data.deliverable === "report" ? ` · <span class="badge">Scout</span>` : ""}</p>`;
|
|
20682
21120
|
// A pull request that can be merged (or opened) owns the one primary action; the result stays in its section.
|
|
20683
21121
|
const pullRequestActs = data.csrf !== "" && (data.pullRequest?.view?.canMerge === true || (data.pullRequest?.view == null && data.pullRequest?.offer != null));
|
|
20684
21122
|
const pullRequestFact = data.pullRequest?.fact != null && data.pullRequest.view?.runId === data.assignment?.receipt?.runId ? data.pullRequest.fact : undefined;
|
|
@@ -20719,12 +21157,12 @@ function taskBodyParts(data) {
|
|
|
20719
21157
|
: "gone from the tracker";
|
|
20720
21158
|
const reopenable = mirror.remoteState === "open" && !mirror.dispatchOk && mirror.closeGeneration !== null &&
|
|
20721
21159
|
mirror.syncGeneration > mirror.closeGeneration && ["cancelled", "failed", "queued"].includes(task.state) && data.csrf !== "";
|
|
20722
|
-
return (`<div class="card"><p><strong>
|
|
21160
|
+
return (`<div class="card"><p><strong>External work</strong> <span class="meta">${link} · ${escape(state)}</span></p>` +
|
|
20723
21161
|
(reopenable
|
|
20724
21162
|
? `<form method="post" action="${taskHref(task.id)}/reopen" class="row">` +
|
|
20725
21163
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
|
|
20726
21164
|
`<input type="password" name="token" placeholder="your password" aria-label="your password" autocomplete="current-password">` +
|
|
20727
|
-
`<button type="submit">
|
|
21165
|
+
`<button type="submit">Reopen — the approved scope stands</button></form>`
|
|
20728
21166
|
: "") +
|
|
20729
21167
|
`</div>`);
|
|
20730
21168
|
})();
|
|
@@ -20738,7 +21176,7 @@ function taskBodyParts(data) {
|
|
|
20738
21176
|
// is as fenced as the builder on a coordinator filing, so "plan
|
|
20739
21177
|
// first" would recommend a road that refuses.
|
|
20740
21178
|
? `<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">
|
|
21179
|
+
`<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
21180
|
: `<div class="card task-scope-needed"><p><strong>No approved scope yet</strong></p>` +
|
|
20743
21181
|
`<p class="meta"><strong>Plan first</strong> drafts it from the repository, or <a href="#scope">write it yourself</a>.</p></div>`
|
|
20744
21182
|
: "";
|
|
@@ -20773,7 +21211,7 @@ function taskBodyParts(data) {
|
|
|
20773
21211
|
attemptPanel,
|
|
20774
21212
|
data.coordinatorProposals == null || data.coordinatorProposals.rows.length === 0
|
|
20775
21213
|
? ""
|
|
20776
|
-
: section("proposals", `<h2>
|
|
21214
|
+
: section("proposals", `<h2>Proposed by coordinators</h2>` +
|
|
20777
21215
|
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
21216
|
section("decisions", decisions, true, data.decisions.length),
|
|
20779
21217
|
section("incidents", incidents, true, data.incidents.length),
|
|
@@ -20785,13 +21223,13 @@ function taskBodyParts(data) {
|
|
|
20785
21223
|
? ` · CI ${data.publication.lastCheckState} at last observation`
|
|
20786
21224
|
: " · no checks observed"}</span></p>`,
|
|
20787
21225
|
section("report", reportCard, true),
|
|
20788
|
-
data.assignment == null ? section("attempts", runs, true, data.runs.length) : section("Build activity", runs.replace("<h2>
|
|
21226
|
+
data.assignment == null ? section("attempts", runs, true, data.runs.length) : section("Build activity", runs.replace("<h2>Attempts</h2>", ""), false, data.runs.length, "attempts"),
|
|
20789
21227
|
section("usage", spendCard, false),
|
|
20790
21228
|
section("steering", steeringCard, (data.steering ?? []).length > 0, (data.steering ?? []).length),
|
|
20791
21229
|
section("scope",
|
|
20792
21230
|
// The recipe road rides with the scope it reuses (UI polish
|
|
20793
21231
|
// 2026-09-13), off the title-to-action path.
|
|
20794
|
-
["<h2>
|
|
21232
|
+
["<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
21233
|
dependencyChoiceNeeded ? "" : section("waits for", waitsForCard, (data.waitsFor ?? []).length > 0, (data.waitsFor ?? []).length),
|
|
20796
21234
|
section("holds", holds, true, data.holds.length),
|
|
20797
21235
|
cancelAct,
|
|
@@ -20811,7 +21249,19 @@ function taskBodyParts(data) {
|
|
|
20811
21249
|
thread.push({ key: "filed", at: rootFiled, kind: "filed", who: "person", author: filedBy ?? "", title: "Filed the task",
|
|
20812
21250
|
text: (() => { const goal = data.family?.root.goal ?? scope?.goal ?? null; return goal === null ? null : oneLineOf(goal, 280); })(), link: null, html: "", more: null });
|
|
20813
21251
|
const newestBuilt = familyRuns.filter(run => run.outcome === "built" && run.role !== "planner" && run.role !== "reviewer").sort((a, b) => b.id - a.id)[0];
|
|
21252
|
+
// Failed: the card says what went wrong in one line, and its one act is Retry itself (the same requeue), never a link to
|
|
21253
|
+
// this page; someone who can't retry here (a viewer, a live claim) gets no act at all.
|
|
21254
|
+
const statusCard = data.assignment != null && assignmentOptions !== null ? assignmentCardOf(data.assignment, assignmentOptions) : null;
|
|
21255
|
+
// A live build says which step it is on, or which step it is stuck on and why.
|
|
21256
|
+
const building = statusCard?.status.headline === "Building" && liveRun !== undefined && liveRun.role !== "planner" && liveRun.role !== "reviewer";
|
|
21257
|
+
// While a build runs, the attempts that stopped before it fold into one quiet line under its Building card. Only
|
|
21258
|
+
// there: with any other card (or none) they stay in the thread, never vanish.
|
|
21259
|
+
const earlierStopped = !building ? []
|
|
21260
|
+
: familyRuns.filter(run => run.id < liveRun.id && (run.role === "builder" || run.role === "scout") && run.outcome !== "built" && run.outcome !== "no-change" && run.id !== liveHistoryRunId);
|
|
21261
|
+
const foldedAttempts = new Set(earlierStopped.map(run => run.id));
|
|
20814
21262
|
for (const run of familyRuns) {
|
|
21263
|
+
if (foldedAttempts.has(run.id))
|
|
21264
|
+
continue;
|
|
20815
21265
|
const live = run.id === liveHistoryRunId || (run.outcome === null && run.id === liveRunId);
|
|
20816
21266
|
const noun = runNoun(run);
|
|
20817
21267
|
const href = { label: `${noun[0].toUpperCase()}${noun.slice(1)} #${run.id}`, href: `/r/${run.id}` };
|
|
@@ -20837,7 +21287,7 @@ function taskBodyParts(data) {
|
|
|
20837
21287
|
thread.push({ key: `run-${run.id}`, at: run.finishedAt ?? run.startedAt, kind: "result", who: "agent", author: run.runner,
|
|
20838
21288
|
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
21289
|
link: href, html: "",
|
|
20840
|
-
more: previousResult !== null && run.id === newestBuilt?.id && run.taskId === task.id ? { summary:
|
|
21290
|
+
more: previousResult !== null && run.id === newestBuilt?.id && run.taskId === task.id ? { summary: STATUS_MORE, html: previousResult.html } : null });
|
|
20841
21291
|
}
|
|
20842
21292
|
// What the agent is working through now, and how its plan changed.
|
|
20843
21293
|
const liveAt = liveRun?.startedAt ?? task.updatedAt;
|
|
@@ -20853,7 +21303,7 @@ function taskBodyParts(data) {
|
|
|
20853
21303
|
const feedback = data.revision;
|
|
20854
21304
|
thread.push({ key: "revision", at: task.createdAt, kind: "reply", who: "person", author: feedback.comments[0]?.author ?? "You",
|
|
20855
21305
|
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"),
|
|
21306
|
+
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
21307
|
link: { label: `From build #${feedback.sourceRun}`, href: `/r/${feedback.sourceRun}` }, html: "", more: null });
|
|
20858
21308
|
}
|
|
20859
21309
|
for (const decision of data.decisions) {
|
|
@@ -20930,9 +21380,6 @@ function taskBodyParts(data) {
|
|
|
20930
21380
|
// needs a person, then facts, then folds; the mechanics under Manage.
|
|
20931
21381
|
const MANAGE = new Set(["steering", "waits-for", "holds"]);
|
|
20932
21382
|
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
21383
|
const failedCard = statusCard?.status.headline === "Failed";
|
|
20937
21384
|
// A link to this very page, with no section to open, goes nowhere: a card never offers one.
|
|
20938
21385
|
const bareSelfLink = (href) => {
|
|
@@ -20945,7 +21392,33 @@ function taskBodyParts(data) {
|
|
|
20945
21392
|
// Build again, in place, names its form after the card's own act.
|
|
20946
21393
|
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
21394
|
? { action: `${taskHref(data.assignment.rootId)}/requeue` } : null;
|
|
20948
|
-
const retry = failedCard && task.state === "failed" && !data.claimed && !stopControlsActive && data.csrf !== ""
|
|
21395
|
+
const retry = failedCard && task.state === "failed" && !data.claimed && !stopControlsActive && data.csrf !== ""
|
|
21396
|
+
? { action: `${taskHref(task.id)}/requeue`, ...(data.failure == null ? {} : { note: retryNoteOf(data.failure.suggestion) }) } : null;
|
|
21397
|
+
// Run checks happens in place: a status row's Run checks posts here and comes back; with nothing to run, the row offers nothing.
|
|
21398
|
+
const runChecks = data.runChecks ?? null;
|
|
21399
|
+
// A row's link to a result in Chat opens that result's own page instead: Chat would only lead back here.
|
|
21400
|
+
const resultPageOf = (href) => {
|
|
21401
|
+
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);
|
|
21402
|
+
return chat === null ? href : `/review?result=${chat[1]}&run=${chat[2]}${chat[3] === undefined ? "" : `&tab=${chat[3]}`}${chat[4] ?? ""}`;
|
|
21403
|
+
};
|
|
21404
|
+
const inPlaceChecks = (card) => {
|
|
21405
|
+
const status = data.demo === true ? demoChecksOf(card.status) : card.status;
|
|
21406
|
+
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 } }
|
|
21407
|
+
: { ...one, href: resultPageOf(one.href), action: one.action === null ? null : { ...one.action, href: resultPageOf(one.action.href) } }) } };
|
|
21408
|
+
};
|
|
21409
|
+
const steps = building ? buildProgressOf(data.milestoneProgress ?? null) : null;
|
|
21410
|
+
const progress = steps === null ? null : { line: steps.line,
|
|
21411
|
+
stuck: steps.stuck === null ? null : { ...steps.stuck, action: data.csrf === "" ? null : { label: "Send the agent a note", href: "#steering" } } };
|
|
21412
|
+
// The Building card carries Stop for the build it describes (the same form), in place of a second card below it.
|
|
21413
|
+
const stop = building && data.control?.kind === "stop" && data.control.run === liveRun.id && data.csrf !== ""
|
|
21414
|
+
? { action: `${taskHref(task.id)}/stop`, run: data.control.run } : null;
|
|
21415
|
+
// The Building card keeps its way to the build's own record (/r/<id>), whatever act it shows.
|
|
21416
|
+
const record = building ? { label: `Build #${liveRun.id} record`, href: `/r/${liveRun.id}` } : null;
|
|
21417
|
+
const earlier = earlierStopped.length === 0 ? null : {
|
|
21418
|
+
summary: earlierAttemptsWords(earlierStopped.length),
|
|
21419
|
+
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}`,
|
|
21420
|
+
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)) })),
|
|
21421
|
+
};
|
|
20949
21422
|
const view = {
|
|
20950
21423
|
kind: "task",
|
|
20951
21424
|
id: task.id,
|
|
@@ -20957,18 +21430,24 @@ function taskBodyParts(data) {
|
|
|
20957
21430
|
{ label: "Ask", href: taskChatHref(data.rootId ?? task.id), active: false },
|
|
20958
21431
|
],
|
|
20959
21432
|
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
|
-
:
|
|
21433
|
+
status: statusCard === null ? null : inPlaceChecks(failedCard ? { ...statusCard, action: null, status: { ...statusCard.status, sentence: data.failure?.line ?? statusCard.status.sentence } }
|
|
21434
|
+
: progress !== null || record !== null ? { ...statusCard, action: statusCard.action?.href === record?.href ? null : statusCard.action, status: { ...statusCard.status, sentence: progress?.line ?? statusCard.status.sentence } }
|
|
21435
|
+
: statusCard.action !== null && rebuild === null && bareSelfLink(statusCard.action.href) ? { ...statusCard, action: null } : statusCard),
|
|
20962
21436
|
statusHtml,
|
|
20963
|
-
failure: failedCard
|
|
21437
|
+
failure: failedCard && data.failure != null ? { line: data.failure.line, evidence: data.failure.evidence, suggestion: data.failure.suggestion, link: data.failure.link } : null,
|
|
20964
21438
|
retry,
|
|
21439
|
+
runChecks,
|
|
21440
|
+
progress,
|
|
21441
|
+
stop,
|
|
21442
|
+
record,
|
|
21443
|
+
earlier,
|
|
20965
21444
|
approval: approvalHtml,
|
|
20966
21445
|
confirmStopped: data.csrf !== "" && data.assignment?.primaryAction?.code === "confirm-stopped" && data.assignment.primaryAction.target.runId !== null
|
|
20967
21446
|
? { action: `${taskHref(data.assignment.rootId)}/confirm-stopped`, run: data.assignment.primaryAction.target.runId, checked: needsCheck(data.assignment) } : null,
|
|
20968
21447
|
rebuild,
|
|
20969
21448
|
// The plan, progress and plan changes are thread entries now; the rest still needs a person here.
|
|
20970
21449
|
lead: [
|
|
20971
|
-
{ key: "history", html: data.history ?? "" }, { key: "control", html: controlHtml }, { key: "problem", html: problemHtml },
|
|
21450
|
+
{ 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
21451
|
{ key: "pull-request", html: pullRequestCard },
|
|
20973
21452
|
{ key: "needs-scope", html: needsScopeCard },
|
|
20974
21453
|
{ key: "mirror", html: mirrorCard }, { key: "contest", html: contestCard }, { key: "attempt", html: attemptPanel },
|
|
@@ -20979,7 +21458,7 @@ function taskBodyParts(data) {
|
|
|
20979
21458
|
sections: [
|
|
20980
21459
|
...(previousResult === null || thread.some(one => one.more !== null) ? [] : [{ id: "result", title: previousResult.title, html: previousResult.html, open: false, count: null }]),
|
|
20981
21460
|
// 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" &&
|
|
21461
|
+
...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
21462
|
],
|
|
20984
21463
|
manage: [
|
|
20985
21464
|
...sectionParts.filter(one => MANAGE.has(one.id)),
|
|
@@ -21384,12 +21863,13 @@ function reviewCockpitPage(chrome, data) {
|
|
|
21384
21863
|
capped: queue.length >= data.queueCap ? data.queueCap : null,
|
|
21385
21864
|
missing: data.missing === null ? null : `No completed task ${data.missing} is in view here — it may not be finished, or it is outside this console's projects.`,
|
|
21386
21865
|
beyond: data.beyondQueue && selected !== null,
|
|
21387
|
-
selected: detailParts === null ? null : detailParts.selected,
|
|
21866
|
+
selected: detailParts === null ? null : data.refusal == null ? detailParts.selected : { ...detailParts.selected, problem: data.refusal },
|
|
21388
21867
|
};
|
|
21389
21868
|
return screen("review", [
|
|
21390
21869
|
selected === null ? `<h1>Results</h1>` : "",
|
|
21391
21870
|
missingNote,
|
|
21392
21871
|
beyondNote,
|
|
21872
|
+
data.refusal == null || selected === null ? "" : `<p class="problem" data-result-refusal>${escape(data.refusal)}</p>`,
|
|
21393
21873
|
`<div class="cockpit">${queuePane}${detail}</div>`,
|
|
21394
21874
|
].join("\n"), {
|
|
21395
21875
|
chrome,
|
|
@@ -21425,26 +21905,27 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
|
|
|
21425
21905
|
const intent = view.intent;
|
|
21426
21906
|
let intentView = null;
|
|
21427
21907
|
if (intent === null) {
|
|
21428
|
-
parts.push(`<section class="card cockpit-section" data-cockpit-section="intent"><h3>
|
|
21908
|
+
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
21909
|
}
|
|
21430
21910
|
else {
|
|
21911
|
+
// The time itself goes to the page as a stamp: the one formatter words it in the viewer's zone.
|
|
21431
21912
|
const approvalWords = intent.approval.approved
|
|
21432
|
-
? `
|
|
21913
|
+
? `Approved by ${intent.approvedBy ?? "an operator"}`
|
|
21433
21914
|
: intent.approval.reason === "changed"
|
|
21434
|
-
? "
|
|
21435
|
-
: "
|
|
21915
|
+
? "The scope changed after approval. The words below are the current text, not the signed one."
|
|
21916
|
+
: "Never approved. The result was built without a signed scope.";
|
|
21436
21917
|
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">
|
|
21918
|
+
? `Approved by ${escape(intent.approvedBy ?? "an operator")} · ${whenTime(intent.approval.at)}`
|
|
21919
|
+
: escape(approvalWords);
|
|
21920
|
+
const body = `<p class="recap" style="margin-top:.25rem"><strong>Goal</strong> ${escape(intent.goal)}</p>` +
|
|
21921
|
+
(intent.outOfScope === null ? `<p class="meta">No boundary was stated</p>` : `<p class="recap"><strong>Not this</strong> ${escape(intent.outOfScope)}</p>`) +
|
|
21441
21922
|
(intent.touches.length === 0
|
|
21442
|
-
? `<p class="meta">
|
|
21443
|
-
: `<p class="row"><span class="meta">
|
|
21923
|
+
? `<p class="meta">No expected paths were signed — every changed file reads as in bounds</p>`
|
|
21924
|
+
: `<p class="row"><span class="meta">Expected to touch</span> ${intent.touches.map(one => `<span class="mono">${escape(one)}</span>`).join(" ")}</p>`) +
|
|
21444
21925
|
(view.plan === null
|
|
21445
21926
|
? ""
|
|
21446
|
-
: `<p class="meta">
|
|
21447
|
-
intentView = { approval: approvalWords, html: body };
|
|
21927
|
+
: `<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>`);
|
|
21928
|
+
intentView = { approval: approvalWords, approvedAt: intent.approval.approved ? intent.approval.at ?? null : null, html: body };
|
|
21448
21929
|
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
21930
|
`<div class="cockpit-disclosure-body">` + body + `</div></details>`);
|
|
21450
21931
|
}
|
|
@@ -21466,7 +21947,7 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
|
|
|
21466
21947
|
] },
|
|
21467
21948
|
};
|
|
21468
21949
|
if (run === null || view.detail === null) {
|
|
21469
|
-
parts.push(`<section class="card cockpit-section" id="verification" data-cockpit-section="proof"><h3>
|
|
21950
|
+
parts.push(`<section class="card cockpit-section" id="verification" data-cockpit-section="proof"><h3>Verification</h3>` +
|
|
21470
21951
|
`<p class="meta">This task has no finished build record, so there are no captured changes or checks to review.</p>` +
|
|
21471
21952
|
`<p class="row"><a href="${taskHref(view.taskId)}">Open the task →</a></p></section>`);
|
|
21472
21953
|
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 +21980,31 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
|
|
|
21499
21980
|
});
|
|
21500
21981
|
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
21982
|
parts.push(`<div id="verification" data-cockpit-section="result">` + panel.html + contest + `</div>`);
|
|
21983
|
+
const blocked = cantAcceptYetOf(proof?.verdict ?? null, proof?.reasons ?? [], accepted);
|
|
21984
|
+
const youCheck = panel.panel.youCheck;
|
|
21985
|
+
// Accept and finish posts the completion; when an acceptance is owed (the person's own checks, or a reason a
|
|
21986
|
+
// report that doesn't match its changes asks for), the same request records it first.
|
|
21987
|
+
const owed = accepted ? null : youCheck?.accept != null ? { note: null } : panel.panel.need?.accept != null ? { note: panel.panel.need.accept.note }
|
|
21988
|
+
: blocked === ACCEPT_NEEDS_REASON ? { note: "Why is this safe to accept?" } : null;
|
|
21502
21989
|
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;
|
|
21990
|
+
? { action: `${taskHref(view.taskId)}/complete`, receipt: assignment.receipt.digest, run: run.id, accept: owed } : null;
|
|
21504
21991
|
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;
|
|
21992
|
+
parts.push(completionForm(view.taskId, run.id, complete.receipt, csrf, view.detail?.pullRequestTo ?? null, owed));
|
|
21993
|
+
// The one decision, after the evidence: Accept and finish only when every requirement is met and the checks passed.
|
|
21994
|
+
const acceptsHere = complete !== null || panel.panel.need?.accept != null || youCheck?.accept != null;
|
|
21508
21995
|
const matrix = proof === null || proof.proofProblem !== null ? [] : proof.matrix;
|
|
21509
|
-
const
|
|
21510
|
-
const
|
|
21511
|
-
|
|
21512
|
-
|
|
21513
|
-
|
|
21514
|
-
|
|
21515
|
-
|
|
21516
|
-
|
|
21517
|
-
|
|
21518
|
-
|
|
21519
|
-
const decision =
|
|
21996
|
+
const unanswered = accepted || youCheck == null ? [] : youCheck.items.length > 0 ? youCheck.items.map(one => one.statement === "" ? one.words : one.statement) : youCheck.lines;
|
|
21997
|
+
const base = !acceptsHere ? null : acceptWordsOf({
|
|
21998
|
+
checks: checks === undefined ? null : checks.running != null ? "running" : checks.level === "off" && checks.status !== "passed" ? "off" : checks.status,
|
|
21999
|
+
unmet: matrix.filter(row => row.state !== "pass" && row.state !== "manual-review").length,
|
|
22000
|
+
action: complete !== null ? "complete" : "accept",
|
|
22001
|
+
publishing: view.detail?.publishing ?? "other",
|
|
22002
|
+
proof: proof !== null && proof.proof !== null && proof.proofProblem === null,
|
|
22003
|
+
});
|
|
22004
|
+
// Refuted: plain Accept and finish stays as allowed, in outline, and the one line before the acts (acts.line) says why.
|
|
22005
|
+
const settled = base === null || blocked === null ? base : { ...base, label: "Accept and finish", ready: false };
|
|
22006
|
+
const decision = settled === null ? null : { sentence: RESULT_DECISION_SENTENCE, ...settled, ...acceptWithChecksOf(settled, { unanswered, notRight: [] }),
|
|
22007
|
+
base: { label: settled.label, ready: settled.ready, why: settled.why } };
|
|
21520
22008
|
// 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
22009
|
const followUps = view.detail.followUps ?? null;
|
|
21522
22010
|
const checksRunning = checks?.running != null || (followUps?.checks.some(one => one.state === "waiting" || one.state === "running") ?? false);
|
|
@@ -21526,18 +22014,27 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
|
|
|
21526
22014
|
? { action: `/r/${run.id}/checks`, level: followUps.full ? "full" : "quick", returnTo: `${here}&run=${run.id}&tab=checks` } : null;
|
|
21527
22015
|
const need = panel.panel.need;
|
|
21528
22016
|
const nextKind = selected.next?.kind;
|
|
21529
|
-
|
|
21530
|
-
|
|
21531
|
-
|
|
21532
|
-
|
|
21533
|
-
|
|
21534
|
-
|
|
21535
|
-
|
|
21536
|
-
|
|
21537
|
-
|
|
22017
|
+
// 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.
|
|
22018
|
+
const failure = view.failure == null ? null : { line: view.failure.line, evidence: view.failure.evidence, suggestion: view.failure.suggestion, retry: view.failure.retry,
|
|
22019
|
+
link: view.failure.link === null || (view.failure.link.href.startsWith(`${here}&run=${run.id}`) && !view.failure.link.href.includes("#")) ? null : view.failure.link,
|
|
22020
|
+
...(view.failure.acceptAnyway == null ? {} : { acceptAnyway: { ...view.failure.acceptAnyway, returnTo: `${here}&run=${run.id}` } }) };
|
|
22021
|
+
const actFacts = failure !== null
|
|
22022
|
+
? { accept: null, runChecks: runChecks !== null, checksRunning, blocked: null, canRequest: false, need: null, next: null, failed: { retry: failure.retry !== null, acceptAnyway: failure.acceptAnyway !== undefined } }
|
|
22023
|
+
: {
|
|
22024
|
+
accept: settled === null ? null : { ready: settled.ready },
|
|
22025
|
+
runChecks: runChecks !== null,
|
|
22026
|
+
checksRunning,
|
|
22027
|
+
blocked,
|
|
22028
|
+
canRequest: panel.panel.canRequest && panel.panel.request !== null,
|
|
22029
|
+
need: need === null || need.accept != null ? null : need.rebuild != null ? "rebuild" : need.confirm !== null ? "confirm-stopped" : null,
|
|
22030
|
+
next: nextKind === "revise" || nextKind === "draft-repair" ? nextKind : null,
|
|
22031
|
+
unanswered: settled === null ? 0 : unanswered.length,
|
|
22032
|
+
notRight: 0,
|
|
22033
|
+
};
|
|
22034
|
+
const acts = resultActsOf(actFacts);
|
|
21538
22035
|
// A refuted result's card lists every recorded disagreement, each tied to its lines; when the report itself doesn't
|
|
21539
22036
|
// match the changes (not a failed check), that is the headline.
|
|
21540
|
-
const mismatch = proof === null || accepted || proof.verdict !== "refuted" ? null : (() => {
|
|
22037
|
+
const mismatch = failure !== null || proof === null || accepted || proof.verdict !== "refuted" ? null : (() => {
|
|
21541
22038
|
const patch = view.detail?.terminal?.patch ?? null;
|
|
21542
22039
|
const files = patch === null || "problem" in patch ? [] : parseReviewDiff(patch.text).files;
|
|
21543
22040
|
// Each file's first change: the lines it added (else every line it shows), and where that change starts (its link target).
|
|
@@ -21570,25 +22067,51 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
|
|
|
21570
22067
|
};
|
|
21571
22068
|
})();
|
|
21572
22069
|
if (view.notes.length > 0) {
|
|
21573
|
-
parts.push(`<section class="card cockpit-section" data-cockpit-section="notes"><h3>
|
|
22070
|
+
parts.push(`<section class="card cockpit-section" data-cockpit-section="notes"><h3>Operator notes</h3>` +
|
|
21574
22071
|
view.notes.map(one => `<p class="row"><span class="meta">${escape(one.author)} · ${whenTime(one.createdAt)}</span> ${escape(one.note)}</p>`).join("\n") +
|
|
21575
22072
|
`</section>`);
|
|
21576
22073
|
}
|
|
21577
22074
|
return {
|
|
21578
22075
|
html: `<section class="cockpit-detail">${parts.join("\n")}</section>`,
|
|
21579
22076
|
selected: {
|
|
21580
|
-
...selected, complete, decision
|
|
22077
|
+
...selected, complete: failure === null ? complete : null, decision: failure === null ? decision : null, contest, acts, actFacts, runChecks, mismatch, failure,
|
|
22078
|
+
// A failed build reads Failed, whatever its task has done since; its outcome is what went wrong.
|
|
22079
|
+
// Run checks is the decision's own act on this page; a status row never sends the person to Chat for it.
|
|
22080
|
+
panel: (() => {
|
|
22081
|
+
// A Chat link to this same result would lead back here: it isn't one.
|
|
22082
|
+
const elsewhere = (href) => href !== null && !href.endsWith("#follow-ups") && !new RegExp(`^/chat\\?task=[^&]+&result=${run.id}(?:&|$)`).test(href);
|
|
22083
|
+
const shown = panel.panel.status === null ? null : view.demo === true ? demoChecksOf(panel.panel.status) : panel.panel.status;
|
|
22084
|
+
const missed = proof === null || proof.proofProblem !== null ? 0 : requirementsOf(proof.matrix)?.missed ?? 0;
|
|
22085
|
+
const status = shown === null ? null : { ...shown, details: shown.details.map(one => ({ ...one,
|
|
22086
|
+
href: elsewhere(one.href) ? one.href : null, action: one.action !== null && elsewhere(one.action.href) ? one.action : null })) };
|
|
22087
|
+
return failure === null ? { ...panel.panel, status }
|
|
22088
|
+
: { ...panel.panel, outcome: failure.line, need: null, youCheck: null, status: status === null ? null : { ...status, headline: "Failed", tone: "danger", sentence: failure.line, need: null,
|
|
22089
|
+
// Read as Failed, the Requirements row counts what it missed, as the task's card does.
|
|
22090
|
+
details: status.details.map(one => one.key !== "requirements" || missed === 0 ? one : { ...one, text: `${missed} missed`, mark: "failed", action: null }) },
|
|
22091
|
+
// What it missed is said once, in plain words above; the recorded wording stays under Details.
|
|
22092
|
+
attention: panel.panel.attention.filter(one => !(proof?.reasons ?? []).includes(one) && !(proof?.matrix ?? []).some(row => row.detail.includes(one))) };
|
|
22093
|
+
})(),
|
|
22094
|
+
...(failure === null ? {} : { status: { label: "Failed", tone: "problem", token: "failed" } }),
|
|
21581
22095
|
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
22096
|
},
|
|
21583
22097
|
};
|
|
21584
22098
|
}
|
|
21585
|
-
|
|
21586
|
-
|
|
22099
|
+
/** What Accept and finish owes before it completes, as the form asks for it: the server's own rule
|
|
22100
|
+
* (owedAcceptance), so the form asks for exactly what the completion requires. */
|
|
22101
|
+
function owedAcceptanceOf(receipt) {
|
|
22102
|
+
const owed = owedAcceptance(receipt);
|
|
22103
|
+
return owed === null ? null : owed === "person-check" ? { note: null } : { note: "Why is this safe to accept?" };
|
|
22104
|
+
}
|
|
22105
|
+
function completionForm(taskId, runId, digest, csrf, pullRequestTo = null, accept = null) {
|
|
22106
|
+
// Accept and finish, in one request: with an acceptance owed (the person's own checks, or an exception and
|
|
22107
|
+
// its reason, asked for right here), the same post records it first.
|
|
22108
|
+
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">`}` +
|
|
22109
|
+
(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
22110
|
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">
|
|
22111
|
+
// With pull requests set up: the PR is the primary road, a bare finish the quiet one beside it.
|
|
22112
|
+
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
22113
|
}
|
|
21591
|
-
return `<form method="post" action="${taskHref(taskId)}/complete" class="card result-complete">${hidden}<p class="meta">
|
|
22114
|
+
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
22115
|
}
|
|
21593
22116
|
/** The pull request, in one card: its state and link, one line of detail, and the one action it needs — Merge
|
|
21594
22117
|
* behind the password when checks passed, or Open a pull request for a result completed without one. */
|
|
@@ -21726,13 +22249,13 @@ function shortDigest(digest) {
|
|
|
21726
22249
|
* orphaned run keeps saying what actually became of it. */
|
|
21727
22250
|
function runOutcomeBadge(run, live) {
|
|
21728
22251
|
if (!live && run.role === "planner" && run.reason === "plan-drafted") {
|
|
21729
|
-
return `<span class="badge">
|
|
22252
|
+
return `<span class="badge">Planned</span>`;
|
|
21730
22253
|
}
|
|
21731
22254
|
if (!live && run.reason === "interrupted")
|
|
21732
|
-
return `<span class="badge">
|
|
22255
|
+
return `<span class="badge">Interrupted</span>`;
|
|
21733
22256
|
return live
|
|
21734
|
-
? `<span class="badge badge-running">
|
|
21735
|
-
: `<span class="badge badge-${escape(run.outcome ?? "cut")}">${escape(run.outcome ?? "never finished")}</span>`;
|
|
22257
|
+
? `<span class="badge badge-running">Running</span>`
|
|
22258
|
+
: `<span class="badge badge-${escape(run.outcome ?? "cut")}">${escape(sentenceCase(run.outcome ?? "never finished"))}</span>`;
|
|
21736
22259
|
}
|
|
21737
22260
|
const runNoun = (run) => run.role === "planner" ? "plan" : run.role === "reviewer" ? "review" : run.role === "scout" ? "report" : "build";
|
|
21738
22261
|
function runsPage(chrome, rows, liveIds, nextCursor, verdicts = new Map(), accepted = new Set()) {
|
|
@@ -21747,7 +22270,7 @@ function runsPage(chrome, rows, liveIds, nextCursor, verdicts = new Map(), accep
|
|
|
21747
22270
|
return (`<p class="row"><a href="/r/${run.id}" class="mono">#${run.id}</a> ` +
|
|
21748
22271
|
`<a href="${taskHref(run.taskId)}" class="mono">${escape(run.taskId)}</a> ` +
|
|
21749
22272
|
runOutcomeBadge(run, liveIds.has(run.id)) +
|
|
21750
|
-
`${run.qualityMode === "strict" ? ` <span class="badge">
|
|
22273
|
+
`${run.qualityMode === "strict" ? ` <span class="badge">Strict review</span>` : ""}` +
|
|
21751
22274
|
needsVerification +
|
|
21752
22275
|
criterionMatrixSummary(verdicts.get(run.id)?.matrix ?? []) +
|
|
21753
22276
|
`${run.provider === "claude" ? "" : ` <span class="meta mono">${escape(run.provider)}</span>`}` +
|
|
@@ -21756,7 +22279,7 @@ function runsPage(chrome, rows, liveIds, nextCursor, verdicts = new Map(), accep
|
|
|
21756
22279
|
})
|
|
21757
22280
|
.join("\n");
|
|
21758
22281
|
const older = nextCursor === null ? "" : `<p><a href="/runs?before=${nextCursor}">older →</a></p>`;
|
|
21759
|
-
return screen("builds", [`<h1>
|
|
22282
|
+
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
22283
|
}
|
|
21761
22284
|
function structuredHandoffView(artifacts, root) {
|
|
21762
22285
|
const artifact = [...artifacts].reverse().find(one => one.kind === "handoff");
|
|
@@ -22111,7 +22634,7 @@ function reviewDiffHtml(patch, stat, runId, commentable, anchors = new Map()) {
|
|
|
22111
22634
|
const parsed = parseReviewDiff(patch.text);
|
|
22112
22635
|
const structured = parsed.files.some(file => file.hunks.length > 0);
|
|
22113
22636
|
if (!structured) {
|
|
22114
|
-
return (`<details><summary>
|
|
22637
|
+
return (`<details><summary>The patch${patch.truncated ? " (TRUNCATED — the raw record says how much was cut)" : ""}</summary>` +
|
|
22115
22638
|
`<pre class="mono" style="overflow-x:auto">${escape(patch.text)}</pre></details>` +
|
|
22116
22639
|
storedDownloadLink(runId, patch.artifactId, "diff", patch.truncated));
|
|
22117
22640
|
}
|
|
@@ -22185,12 +22708,12 @@ function storedDownloadLink(runId, artifactId, what, truncated) {
|
|
|
22185
22708
|
* page's prefill script reads — a real button, keyboard-reachable, separate
|
|
22186
22709
|
* from the link so the two never fight over one click (finding 4). */
|
|
22187
22710
|
function terminalDiffCard(view, runId, editor = null, commentable = false) {
|
|
22188
|
-
const parts = ["<h2>
|
|
22711
|
+
const parts = ["<h2>What changed</h2>"];
|
|
22189
22712
|
if (view.stat === null) {
|
|
22190
|
-
parts.push(`<p class="meta">
|
|
22713
|
+
parts.push(`<p class="meta">No change summary was captured for this build</p>`);
|
|
22191
22714
|
}
|
|
22192
22715
|
else if ("problem" in view.stat) {
|
|
22193
|
-
parts.push(`<p class="meta">
|
|
22716
|
+
parts.push(`<p class="meta">Stat: ${escape(view.stat.problem)}</p>`);
|
|
22194
22717
|
}
|
|
22195
22718
|
else {
|
|
22196
22719
|
const s = view.stat;
|
|
@@ -22212,23 +22735,23 @@ function terminalDiffCard(view, runId, editor = null, commentable = false) {
|
|
|
22212
22735
|
.slice(0, 40)
|
|
22213
22736
|
.map(file => `<p class="row mono">${fileName(file.path)}${file.renamedFrom === undefined ? "" : ` (was ${escape(file.renamedFrom)})`} ` +
|
|
22214
22737
|
`<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)}">
|
|
22738
|
+
`${commentable ? ` <button type="button" class="pick-file" data-path="${escape(file.path)}">Comment</button>` : ""}</p>`)
|
|
22216
22739
|
.join("\n") +
|
|
22217
22740
|
(s.files.length > 40 ? `<p class="meta">…and ${s.files.length - 40} more file(s)</p>` : "") +
|
|
22218
22741
|
`</div>`);
|
|
22219
22742
|
if (editor !== null) {
|
|
22220
|
-
parts.push(`<p class="meta">
|
|
22743
|
+
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
22744
|
}
|
|
22222
22745
|
}
|
|
22223
22746
|
}
|
|
22224
22747
|
if (view.patch === null) {
|
|
22225
|
-
parts.push(`<p class="meta">
|
|
22748
|
+
parts.push(`<p class="meta">The final diff was not captured for this build</p>`);
|
|
22226
22749
|
}
|
|
22227
22750
|
else if ("problem" in view.patch) {
|
|
22228
|
-
parts.push(`<p class="meta">
|
|
22751
|
+
parts.push(`<p class="meta">Patch: ${escape(view.patch.problem)}</p>`);
|
|
22229
22752
|
}
|
|
22230
22753
|
else if (view.patch.text.trim() === "") {
|
|
22231
|
-
parts.push(`<p class="meta">
|
|
22754
|
+
parts.push(`<p class="meta">Empty diff — captured successfully, nothing changed</p>`);
|
|
22232
22755
|
}
|
|
22233
22756
|
else {
|
|
22234
22757
|
parts.push(reviewDiffHtml(view.patch, view.stat, runId, commentable));
|
|
@@ -22247,7 +22770,7 @@ function evidenceBundleCard(view, run, publication = null, review = null) {
|
|
|
22247
22770
|
if (view === null)
|
|
22248
22771
|
return "";
|
|
22249
22772
|
const runId = run.id;
|
|
22250
|
-
const parts = ["<h2>
|
|
22773
|
+
const parts = ["<h2>Verification and evidence</h2>"];
|
|
22251
22774
|
if (view.verdict !== null) {
|
|
22252
22775
|
// The same status line every other surface renders for this run
|
|
22253
22776
|
// (workspace package 1), beside the verdict's own explanation.
|
|
@@ -22257,7 +22780,7 @@ function evidenceBundleCard(view, run, publication = null, review = null) {
|
|
|
22257
22780
|
if (view.accepted !== null) {
|
|
22258
22781
|
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
22782
|
}
|
|
22260
|
-
parts.push(criterionMatrixHtml(view.matrix, { runId, links: view.matrixLinks }));
|
|
22783
|
+
parts.push(criterionMatrixHtml(view.matrix, { runId, links: view.matrixLinks, verdict: view.verdict }));
|
|
22261
22784
|
parts.push(semanticCoverageHtml(view.matrix, view.qualityMode));
|
|
22262
22785
|
if (reviewConflict(view.matrix, view.machineVerdict, view.verdict)) {
|
|
22263
22786
|
parts.push(`<p class="meta">An independent review found conflicting evidence.</p>`);
|
|
@@ -22268,25 +22791,25 @@ function evidenceBundleCard(view, run, publication = null, review = null) {
|
|
|
22268
22791
|
}
|
|
22269
22792
|
else if (view.proof !== null) {
|
|
22270
22793
|
if (view.proof.criteria.length > 0) {
|
|
22271
|
-
parts.push(`<div class="result-section"><strong>
|
|
22794
|
+
parts.push(`<div class="result-section"><strong>Acceptance criteria</strong><ul>` +
|
|
22272
22795
|
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>`)
|
|
22796
|
+
.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
22797
|
.join("") +
|
|
22275
22798
|
`</ul></div>`);
|
|
22276
22799
|
}
|
|
22277
22800
|
if (view.proof.checks.length > 0) {
|
|
22278
|
-
parts.push(`<div class="result-section"><strong>
|
|
22801
|
+
parts.push(`<div class="result-section"><strong>Checks reported by the agent</strong><ul>` +
|
|
22279
22802
|
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
22803
|
`</ul></div>`);
|
|
22281
22804
|
}
|
|
22282
22805
|
if (view.proof.caveats.length > 0) {
|
|
22283
|
-
parts.push(`<div class="result-section"><strong>
|
|
22806
|
+
parts.push(`<div class="result-section"><strong>Caveats</strong><ul>${view.proof.caveats.map(one => `<li>${escape(one)}</li>`).join("")}</ul></div>`);
|
|
22284
22807
|
}
|
|
22285
22808
|
}
|
|
22286
22809
|
if (view.checkLog !== null)
|
|
22287
22810
|
parts.push(checkLogHtml(view.checkLog, runId, ""));
|
|
22288
22811
|
if (view.screenshots.length > 0) {
|
|
22289
|
-
parts.push(`<div class="result-section"><strong>
|
|
22812
|
+
parts.push(`<div class="result-section"><strong>Screenshots</strong>` +
|
|
22290
22813
|
view.screenshots
|
|
22291
22814
|
.map(shot => `<p class="row"><a href="/r/${runId}/evidence/${shot.artifactId}">` +
|
|
22292
22815
|
`<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 +22926,7 @@ function completionReceiptCard(view, taskId, place, standing = receiptStatusOf(v
|
|
|
22403
22926
|
(view.publication?.state === "failed" ? `<p class="receipt-note">${statusIconSvg("note")} Pull request couldn't open. The commit is safe locally.</p>` : "") +
|
|
22404
22927
|
caveats +
|
|
22405
22928
|
coverage +
|
|
22406
|
-
`<details class="receipt-details"><summary
|
|
22929
|
+
`<details class="receipt-details"><summary>${STATUS_MORE}</summary>` +
|
|
22407
22930
|
(view.summary !== null && view.summary !== conciseOutcomeOf(view.summary) ? `<p class="recap">${escape(view.summary)}</p>` : "") +
|
|
22408
22931
|
`<p class="receipt-publication" data-receipt-publication="${escape(facts.publicationState)}">${escape(facts.publicationWords)}</p>` +
|
|
22409
22932
|
// A review in flight is the receipt's primary status too (its chip
|
|
@@ -22534,9 +23057,10 @@ function resultPanelParts(detail, o) {
|
|
|
22534
23057
|
items: personCheckItems(proof, patchOk ? patch.text : null, shown, runId),
|
|
22535
23058
|
accept: acceptable && need?.accept == null ? { action: `${taskHref(detail.taskId)}/accept-proof`, run: run.id, returnTo: o.returnTo } : null,
|
|
22536
23059
|
};
|
|
23060
|
+
const youCheckForm = youCheck?.accept != null && o.finishes !== true ? youCheck.accept : null;
|
|
22537
23061
|
const youCheckHtml = youCheck === null ? "" :
|
|
22538
23062
|
`<div class="result-you-check" data-result-you-check="${youCheck.lines.length}"><ul>${youCheck.lines.map(one => `<li>${escape(one)}</li>`).join("")}</ul>` +
|
|
22539
|
-
(
|
|
23063
|
+
(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
23064
|
`</div>`;
|
|
22541
23065
|
const attentionHtml = attention.length === 0
|
|
22542
23066
|
? ""
|
|
@@ -22663,9 +23187,9 @@ function resultPanelParts(detail, o) {
|
|
|
22663
23187
|
detail.files
|
|
22664
23188
|
.map(file => {
|
|
22665
23189
|
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">
|
|
23190
|
+
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
23191
|
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)}">
|
|
23192
|
+
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
23193
|
})
|
|
22670
23194
|
.join("") +
|
|
22671
23195
|
`</ol>` +
|
|
@@ -22706,14 +23230,14 @@ function resultPanelParts(detail, o) {
|
|
|
22706
23230
|
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
23231
|
}
|
|
22708
23232
|
else {
|
|
22709
|
-
checkParts.push(criterionMatrixHtml(proof.matrix, { runId, links: proof.matrixLinks, fileAnchors: detail.fileAnchors }));
|
|
23233
|
+
checkParts.push(criterionMatrixHtml(proof.matrix, { runId, links: proof.matrixLinks, fileAnchors: detail.fileAnchors, verdict: proof.verdict }));
|
|
22710
23234
|
}
|
|
22711
23235
|
checkParts.push(semanticCoverageHtml(proof.matrix, proof.qualityMode));
|
|
22712
23236
|
if (reviewConflict(proof.matrix, proof.machineVerdict, proof.verdict))
|
|
22713
23237
|
checkParts.push(`<p class="meta">An independent review found conflicting evidence.</p>`);
|
|
22714
23238
|
checkParts.push(repairChainHtml(proof.repairChain));
|
|
22715
23239
|
if (detail.reviewerFindings.length > 0) {
|
|
22716
|
-
checkParts.push(`<div class="result-section" data-cockpit-source="reviewer"><strong>
|
|
23240
|
+
checkParts.push(`<div class="result-section" data-cockpit-source="reviewer"><strong>Independent review</strong><ul>` +
|
|
22717
23241
|
reviewerNotes +
|
|
22718
23242
|
`</ul></div>`);
|
|
22719
23243
|
}
|
|
@@ -22742,7 +23266,7 @@ function resultPanelParts(detail, o) {
|
|
|
22742
23266
|
// The handoff's own account of its checks — the agent's words, labeled
|
|
22743
23267
|
// as such, whether or not a proof exists.
|
|
22744
23268
|
if (!directAssessment && handoff !== null && handoff.verification.length > 0)
|
|
22745
|
-
checkParts.push(`<div class="result-section" data-cockpit-source="agent-words"><strong>
|
|
23269
|
+
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
23270
|
if (o.extraChecks !== undefined)
|
|
22747
23271
|
checkParts.push(o.extraChecks);
|
|
22748
23272
|
// ---- request changes: both feedback styles, one sealed road ------------
|
|
@@ -22787,8 +23311,8 @@ function resultPanelParts(detail, o) {
|
|
|
22787
23311
|
`<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
23312
|
`<span class="meta diff-comment-limit" id="comment-note-limit">up to ${LIMITS.note} characters</span>` +
|
|
22789
23313
|
`<details class="result-pin"><summary>Attach to a file or line</summary>` +
|
|
22790
|
-
`<div class="diff-comment-target"><label>
|
|
22791
|
-
`<label>
|
|
23314
|
+
`<div class="diff-comment-target"><label>File<input type="text" name="path" placeholder="src/…" aria-label="file" class="mono"></label>` +
|
|
23315
|
+
`<label>Line<input type="text" name="line" placeholder="—" aria-label="line" inputmode="numeric"></label></div></details>` +
|
|
22792
23316
|
`<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
23317
|
}
|
|
22794
23318
|
// ---- the panel ---------------------------------------------------------
|
|
@@ -22830,6 +23354,8 @@ function resultPanelParts(detail, o) {
|
|
|
22830
23354
|
reviewHistory: REVIEW_TOKENS.has(status.token) ? status.detail : null,
|
|
22831
23355
|
attention: attention.filter(one => !shortenedMaterialReason(one)).map(plainReasonWords),
|
|
22832
23356
|
youCheck,
|
|
23357
|
+
// The same counts as the Requirements row (a refuted report verifies none of them, and says Unverified).
|
|
23358
|
+
requirements: proof === null || proof.verdict === "refuted" ? null : requirementsOf(proof.matrix),
|
|
22833
23359
|
limits: attention.filter(one => shortenedMaterialReason(one)),
|
|
22834
23360
|
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
23361
|
views: [{ key: "summary", html: summaryParts.join("\n") }, { key: "changes", html: changeParts.join("\n") }, { key: "checks", html: checkParts.join("\n") }],
|
|
@@ -23024,9 +23550,9 @@ function runPage(chrome, run, taskId, artifacts, terminal = null, notes = [], cs
|
|
|
23024
23550
|
: " · the agent is working on this";
|
|
23025
23551
|
const conversation = heldTurns === null
|
|
23026
23552
|
? ""
|
|
23027
|
-
: `<h2>
|
|
23553
|
+
: `<h2>Conversation</h2>` +
|
|
23028
23554
|
(heldTurns.turns.length === 0
|
|
23029
|
-
? `<p class="meta">
|
|
23555
|
+
? `<p class="meta">Nothing said yet</p>`
|
|
23030
23556
|
: heldTurns.turns
|
|
23031
23557
|
.map(turn => `<p class="row"><span class="meta">${turn.sourceKind === "operator"
|
|
23032
23558
|
? escape(turn.author ?? "operator")
|
|
@@ -23040,11 +23566,11 @@ function runPage(chrome, run, taskId, artifacts, terminal = null, notes = [], cs
|
|
|
23040
23566
|
? `<form method="post" action="/r/${run.id}/turn" class="row">` +
|
|
23041
23567
|
`<input type="hidden" name="csrf" value="${escape(csrf)}">` +
|
|
23042
23568
|
`<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">
|
|
23569
|
+
`<button type="submit">Send</button></form>` +
|
|
23044
23570
|
`<p class="meta">${escape(heldTurns.state)} · ${heldTurns.turns.length}/${heldTurns.cap} messages · a waiting question must be answered before free-form messages</p>`
|
|
23045
23571
|
: heldTurns.open
|
|
23046
23572
|
? ""
|
|
23047
|
-
: `<p class="meta">
|
|
23573
|
+
: `<p class="meta">The session has ended — the record above is complete</p>`);
|
|
23048
23574
|
// The live peek region (A2): the poller fills it only on a serve that
|
|
23049
23575
|
// asserted its runner. Without the assertion the section still appears
|
|
23050
23576
|
// for a running build and says honestly why it is empty \u2014 a page that
|
|
@@ -23053,24 +23579,24 @@ function runPage(chrome, run, taskId, artifacts, terminal = null, notes = [], cs
|
|
|
23053
23579
|
const peek = !running
|
|
23054
23580
|
? ""
|
|
23055
23581
|
: peekable
|
|
23056
|
-
? `<h2>
|
|
23057
|
-
`<div id="run-peek"><p class="meta">
|
|
23582
|
+
? `<h2>What is changing right now</h2>` +
|
|
23583
|
+
`<div id="run-peek"><p class="meta">Watching\u2026 the first look lands within 15 seconds</p></div>` +
|
|
23058
23584
|
`<p class="meta" id="run-peek-stamp"></p>`
|
|
23059
|
-
: `<h2>
|
|
23060
|
-
`<p class="meta">
|
|
23585
|
+
: `<h2>What is changing right now</h2>` +
|
|
23586
|
+
`<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
23587
|
// The live transcript (arc 1): the agent's own words, streamed to a file
|
|
23062
23588
|
// beside the run and polled as raw text. Honesty stated on the surface:
|
|
23063
23589
|
// this is display only, and the machine running the agent could alter it.
|
|
23064
23590
|
const transcript = !running
|
|
23065
23591
|
? ""
|
|
23066
23592
|
: !peekable
|
|
23067
|
-
? `<h2>
|
|
23068
|
-
`<p class="meta">
|
|
23593
|
+
? `<h2>What the agent is saying</h2>` +
|
|
23594
|
+
`<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
23595
|
: run.provider !== "claude"
|
|
23070
|
-
? `<h2>
|
|
23071
|
-
`<p class="meta">
|
|
23072
|
-
: `<h2>
|
|
23073
|
-
`<p class="meta">
|
|
23596
|
+
? `<h2>What the agent is saying</h2>` +
|
|
23597
|
+
`<p class="meta">The live transcript needs the claude harness for now \u2014 this build runs on ${escape(run.provider)}</p>`
|
|
23598
|
+
: `<h2>What the agent is saying</h2>` +
|
|
23599
|
+
`<p class="meta">Display only \u2014 this is not evidence, and the machine running the agent could alter it</p>` +
|
|
23074
23600
|
`<pre id="live-transcript" class="mono" style="max-height:24rem;overflow:auto;white-space:pre-wrap"></pre>` +
|
|
23075
23601
|
`<p class="meta" id="live-transcript-state"></p>`;
|
|
23076
23602
|
const resultSummary = structuredHandoff?.conclusion ?? run.handoff;
|
|
@@ -23079,7 +23605,7 @@ function runPage(chrome, run, taskId, artifacts, terminal = null, notes = [], cs
|
|
|
23079
23605
|
: `<div class="result-section"><strong>${escape(label)}</strong><ul>${items.map(one => `<li>${escape(one)}</li>`).join("")}</ul></div>`;
|
|
23080
23606
|
const handoff = resultSummary === null
|
|
23081
23607
|
? ""
|
|
23082
|
-
: `<section class="card result-card"><h2>
|
|
23608
|
+
: `<section class="card result-card"><h2>Result</h2><p class="recap">${escape(resultSummary)}</p>` +
|
|
23083
23609
|
(structuredHandoff === null
|
|
23084
23610
|
? ""
|
|
23085
23611
|
: resultList("completed", structuredHandoff.changes) +
|
|
@@ -23115,9 +23641,9 @@ function runPage(chrome, run, taskId, artifacts, terminal = null, notes = [], cs
|
|
|
23115
23641
|
? ""
|
|
23116
23642
|
: `<form method="post" action="/r/${run.id}/comment" class="card diff-comment-form" id="comment-form">` +
|
|
23117
23643
|
`<input type="hidden" name="csrf" value="${escape(csrf)}">` +
|
|
23118
|
-
`<div class="diff-comment-target"><label>
|
|
23119
|
-
`<label>
|
|
23120
|
-
`<label>
|
|
23644
|
+
`<div class="diff-comment-target"><label>File<input type="text" name="path" placeholder="select a line above" aria-label="file" class="mono"></label>` +
|
|
23645
|
+
`<label>Line<input type="text" name="line" placeholder="—" aria-label="line" inputmode="numeric"></label></div>` +
|
|
23646
|
+
`<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
23647
|
`<span class="meta diff-comment-limit" id="comment-note-limit">up to ${LIMITS.note} characters</span>` +
|
|
23122
23648
|
`<button type="submit">Add annotation</button></form>`;
|
|
23123
23649
|
// The device-side half of the editor-link activation (arc 6, finding 1):
|
|
@@ -23145,7 +23671,7 @@ function runPage(chrome, run, taskId, artifacts, terminal = null, notes = [], cs
|
|
|
23145
23671
|
: `<div class="card"><p><strong>CI is failing on PR #${ciRepair.pr}</strong> <span class="meta">observed by the episode watcher</span></p>` +
|
|
23146
23672
|
`<form method="post" action="/r/${run.id}/draft-repair">` +
|
|
23147
23673
|
`<input type="hidden" name="csrf" value="${escape(csrf)}">` +
|
|
23148
|
-
`<button type="submit">
|
|
23674
|
+
`<button type="submit">Draft a repair task</button>` +
|
|
23149
23675
|
`<span class="meta"> — one unapproved task; you approve its scope before anything builds</span></form></div>`;
|
|
23150
23676
|
const reviewCard = (commentRows === "" && commentForm === "" ? "" : `<h2 id="review">Review and revise</h2>` +
|
|
23151
23677
|
`<p class="meta">Annotate the diff above. When the batch is ready, create one scoped revision task from it.</p>` +
|
|
@@ -23161,8 +23687,8 @@ function runPage(chrome, run, taskId, artifacts, terminal = null, notes = [], cs
|
|
|
23161
23687
|
: `<form method="post" action="/r/${run.id}/note" class="row">` +
|
|
23162
23688
|
`<input type="hidden" name="csrf" value="${escape(csrf)}">` +
|
|
23163
23689
|
`<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>
|
|
23690
|
+
`<button type="submit">Add note</button></form>`;
|
|
23691
|
+
const notesCard = noteRows === "" && noteForm === "" ? "" : `<h2>Operator notes</h2>${noteRows}${noteForm}`;
|
|
23166
23692
|
// Continuation (Phase 2E, A4): a finished attempt offers a watched
|
|
23167
23693
|
// follow-up — the text you type here enters the SIGNED terms on the
|
|
23168
23694
|
// confirm screen; nothing runs until your password agrees to exactly it.
|
|
@@ -23302,7 +23828,7 @@ function capsPage(chrome, caps, gaps, repo, now) {
|
|
|
23302
23828
|
if (caps === null) {
|
|
23303
23829
|
return screen("requirements", [
|
|
23304
23830
|
`<h1>Requirements</h1>`,
|
|
23305
|
-
`<p class="meta">
|
|
23831
|
+
`<p class="meta">Open a project to see its requirements — <a href="/projects">projects</a></p>`,
|
|
23306
23832
|
].join("\n"), { chrome });
|
|
23307
23833
|
}
|
|
23308
23834
|
const list = caps.length === 0
|
|
@@ -23312,7 +23838,7 @@ function capsPage(chrome, caps, gaps, repo, now) {
|
|
|
23312
23838
|
`${escape(describeCapability(capability, now ?? new Date()))}</p>`)
|
|
23313
23839
|
.join("\n");
|
|
23314
23840
|
const blocked = gaps.length === 0
|
|
23315
|
-
? `<p class="meta">
|
|
23841
|
+
? `<p class="meta">No gaps — everything recorded is verified</p>`
|
|
23316
23842
|
: gaps
|
|
23317
23843
|
.map(gap => `<div class="card"><p>${escape(gap.key)} — ${escape(gap.state)}</p>` +
|
|
23318
23844
|
`<p class="meta">${gap.unblocks.length > 0
|
|
@@ -23320,16 +23846,16 @@ function capsPage(chrome, caps, gaps, repo, now) {
|
|
|
23320
23846
|
: gap.alsoBlocks.length > 0
|
|
23321
23847
|
? `part of what holds: ${gap.alsoBlocks.map(one => escape(one)).join(", ")}`
|
|
23322
23848
|
: "nothing queued needs it yet"}</p>` +
|
|
23323
|
-
`<p class="meta">
|
|
23849
|
+
`<p class="meta">Verify: ${escape(gap.verify)}</p>` +
|
|
23324
23850
|
`<p class="meta">${escape(gap.instructions)}</p></div>`)
|
|
23325
23851
|
.join("\n");
|
|
23326
23852
|
return screen("requirements", [
|
|
23327
23853
|
`<h1>Requirements</h1>`,
|
|
23328
23854
|
`<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
23855
|
list,
|
|
23330
|
-
`<h2>
|
|
23856
|
+
`<h2>Missing, ranked by what filling them frees</h2>`,
|
|
23331
23857
|
blocked,
|
|
23332
|
-
`<p class="meta">
|
|
23858
|
+
`<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
23859
|
].join("\n"), { chrome });
|
|
23334
23860
|
}
|
|
23335
23861
|
/** v98: how the bot's messages reach Toolroll, in words: pushed to the public address, or asked for. */
|
|
@@ -23358,7 +23884,7 @@ function settingsPage(chrome, existing, hasEnv, csrf, problem, messaging = null,
|
|
|
23358
23884
|
`<input type="hidden" name="csrf" value="${escape(csrf)}">` +
|
|
23359
23885
|
permissionModeChoices("permission-mode", permissionDefault.mode) +
|
|
23360
23886
|
`<button type="submit">Save default</button></form>`
|
|
23361
|
-
: `<div class="card"><p><strong>${permissionDefault.mode === "bypassPermissions" ? "Full access" : "Auto"}</strong></p><p class="meta">
|
|
23887
|
+
: `<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
23888
|
permissionDefault.updatedAt === null
|
|
23363
23889
|
? ""
|
|
23364
23890
|
: `<p class="meta settings-changed">Changed ${whenTime(permissionDefault.updatedAt)}${permissionDefault.updatedBy === null ? "" : ` by ${escape(permissionDefault.updatedBy)}`}</p>`,
|
|
@@ -23372,7 +23898,7 @@ function settingsPage(chrome, existing, hasEnv, csrf, problem, messaging = null,
|
|
|
23372
23898
|
`<input type="hidden" name="csrf" value="${escape(csrf)}">` +
|
|
23373
23899
|
qualityModeChoices("quality-mode", qualityDefault.mode) +
|
|
23374
23900
|
`<button type="submit">Save default</button></form>`
|
|
23375
|
-
: `<div class="card"><p><strong>${escape(qualityModeTitle(qualityDefault.mode))}</strong></p><p class="meta">
|
|
23901
|
+
: `<div class="card"><p><strong>${escape(qualityModeTitle(qualityDefault.mode))}</strong></p><p class="meta">An approver can change this default</p></div>`,
|
|
23376
23902
|
qualityDefault.updatedAt === null
|
|
23377
23903
|
? ""
|
|
23378
23904
|
: `<p class="meta settings-changed">Changed ${whenTime(qualityDefault.updatedAt)}${qualityDefault.updatedBy === null ? "" : ` by ${escape(qualityDefault.updatedBy)}`}</p>`,
|
|
@@ -23762,18 +24288,18 @@ function nextPage(chrome, data) {
|
|
|
23762
24288
|
`<p>${escape(item.approval.title)}</p>` +
|
|
23763
24289
|
(data.planDocument === null
|
|
23764
24290
|
? ""
|
|
23765
|
-
: `<div class="card"><p><strong>
|
|
24291
|
+
: `<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
24292
|
`<form method="post" action="${taskHref(item.approval.taskId)}/approve" class="card approve-form">` +
|
|
23767
24293
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
|
|
23768
24294
|
`<input type="hidden" name="nonce" value="${escape(data.nonce)}">` +
|
|
23769
24295
|
`<input type="hidden" name="digest" value="${escape(data.approvalDigest ?? item.approval.digest)}">` +
|
|
23770
24296
|
`<input type="hidden" name="return" value="next">` +
|
|
23771
|
-
`<p><strong>
|
|
23772
|
-
(data.deliverable === "report" ? `<p class="meta"><span class="badge">
|
|
24297
|
+
`<p><strong>Approve exactly this:</strong></p>` +
|
|
24298
|
+
(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
24299
|
(scope === null || scope.profileState !== "unresolved" ? "" : profileWords(scope)) +
|
|
23774
|
-
`<p class="meta">
|
|
23775
|
-
`<p class="meta">
|
|
23776
|
-
`<p class="meta">
|
|
24300
|
+
`<p class="meta">Goal</p><p class="recap" style="margin-top:0">${escape(scope?.goal ?? item.approval.goal)}</p>` +
|
|
24301
|
+
`<p class="meta">Not this</p><p class="recap" style="margin-top:0">${scope?.outOfScope == null ? "<em>no exclusions</em>" : escape(scope.outOfScope)}</p>` +
|
|
24302
|
+
`<p class="meta">Touches · ${scope === null || scope.touches.length === 0 ? "anything" : scope.touches.map(one => escape(one)).join(", ")}</p>` +
|
|
23777
24303
|
(scope === null ? "" : acceptanceCeremonyHtml(scope.acceptance)) +
|
|
23778
24304
|
(data.raceTerms === null
|
|
23779
24305
|
? ""
|
|
@@ -23783,8 +24309,8 @@ function nextPage(chrome, data) {
|
|
|
23783
24309
|
// tap away, never in the way.
|
|
23784
24310
|
agentsCeremonyHtml(data.route) +
|
|
23785
24311
|
(scope === null ? "" : runtimeDetailsHtml(scope)) +
|
|
23786
|
-
`<label>
|
|
23787
|
-
`<div class="sticky-actions"><button type="submit">
|
|
24312
|
+
`<label>Your password, typed again — a signed-in session alone cannot agree to work<input type="password" name="token" autocomplete="current-password"></label>` +
|
|
24313
|
+
`<div class="sticky-actions"><button type="submit">Approve this scope</button></div>` +
|
|
23788
24314
|
`</form>` +
|
|
23789
24315
|
`<p class="meta"><a href="${taskHref(item.approval.taskId)}">open the full task</a> to edit the scope first</p>`;
|
|
23790
24316
|
}
|
|
@@ -23792,22 +24318,22 @@ function nextPage(chrome, data) {
|
|
|
23792
24318
|
card =
|
|
23793
24319
|
`<h1>${escape(item.stalled.taskId)}</h1>` +
|
|
23794
24320
|
`<p>${escape(item.stalled.title)}</p>` +
|
|
23795
|
-
`<p class="meta">
|
|
24321
|
+
`<p class="meta">Stopped — ${item.stalled.incidentCount} incident(s)${item.stalled.strikes > 0 ? ` after ${item.stalled.strikes} attempt(s)` : ""}</p>` +
|
|
23796
24322
|
`<form method="post" action="${taskHref(item.stalled.taskId)}/requeue" class="card">` +
|
|
23797
24323
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
|
|
23798
24324
|
`<input type="hidden" name="return" value="next">` +
|
|
23799
|
-
`<p class="meta">
|
|
23800
|
-
`<button type="submit">
|
|
24325
|
+
`<p class="meta">Requeue resolves the incidents, clears the failed attempts, and puts it back in line</p>` +
|
|
24326
|
+
`<button type="submit">Retry this work</button>` +
|
|
23801
24327
|
`</form>` +
|
|
23802
24328
|
`<p class="meta"><a href="${taskHref(item.stalled.taskId)}">open the full task</a> to read the runs first</p>`;
|
|
23803
24329
|
}
|
|
23804
24330
|
else {
|
|
23805
24331
|
const { gap } = item;
|
|
23806
24332
|
card =
|
|
23807
|
-
`<h1>
|
|
24333
|
+
`<h1>Supply ${escape(gap.key)}</h1>` +
|
|
23808
24334
|
`<p class="meta">${escape(gap.state)}</p>` +
|
|
23809
24335
|
`<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">
|
|
24336
|
+
`<div class="card"><p class="meta">Prove it filled from the terminal:</p><pre class="recap">${escape(gap.verify)}</pre></div>`;
|
|
23811
24337
|
}
|
|
23812
24338
|
return screen("next", [header, card].join("\n"), { chrome });
|
|
23813
24339
|
}
|
|
@@ -23828,9 +24354,9 @@ function decisionOptionForms(decision, csrf, returnTo) {
|
|
|
23828
24354
|
`<input type="hidden" name="choice" value="${escape(option.id)}">`,
|
|
23829
24355
|
...(returnTo === null ? [] : [`<input type="hidden" name="return" value="${escape(returnTo)}">`]),
|
|
23830
24356
|
...(option.reversible ? [] : [`<input type="hidden" name="confirm" value="yes">`]),
|
|
23831
|
-
recommended ? `<p class="meta" style="margin:0 0 .375rem"><span class="badge">
|
|
24357
|
+
recommended ? `<p class="meta" style="margin:0 0 .375rem"><span class="badge">Recommended</span></p>` : "",
|
|
23832
24358
|
`<p class="consequence">${escape(option.consequence)}</p>`,
|
|
23833
|
-
`<button type="submit">${escape(option.label)}${option.reversible ? "" : ` <span class="badge badge-overdue">
|
|
24359
|
+
`<button type="submit">${escape(option.label)}${option.reversible ? "" : ` <span class="badge badge-overdue">Irreversible</span>`}</button>`,
|
|
23834
24360
|
`<input type="text" name="note" placeholder="optional note — travels with this answer" aria-label="optional note">`,
|
|
23835
24361
|
`</form>`,
|
|
23836
24362
|
].join("\n");
|
|
@@ -23848,14 +24374,14 @@ function decisionPage(chrome, decision, taskId, artifacts, who, now, returnTo =
|
|
|
23848
24374
|
: "";
|
|
23849
24375
|
const evidence = artifacts.length === 0
|
|
23850
24376
|
? ""
|
|
23851
|
-
: `<div class="evidence"><strong>
|
|
24377
|
+
: `<div class="evidence"><strong>Evidence</strong>` +
|
|
23852
24378
|
artifacts
|
|
23853
24379
|
.map(artifact => `<a href="/d/${decision.id}/evidence/${artifact.id}">${escape(evidenceWords(artifact.kind))}` +
|
|
23854
24380
|
`${artifact.truncated ? " (truncated)" : ""} · ${artifact.bytesStored} bytes</a>`)
|
|
23855
24381
|
.join("\n") +
|
|
23856
24382
|
"</div>";
|
|
23857
24383
|
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">
|
|
24384
|
+
`<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
24385
|
`<div class="recap">${escape(decision.recap)}</div>`,
|
|
23860
24386
|
`<div class="question">${escape(decision.question)}</div>`,
|
|
23861
24387
|
decision.state === "answered" ? answered : options,
|