toolroll 0.9.49 → 0.9.50
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 +8 -0
- package/dist/cli-http.js +3 -0
- package/dist/flow-triggers.js +7 -0
- package/dist/mcp-http.js +6 -11
- package/dist/mcp-oauth.js +5 -21
- package/dist/mcp-person.d.ts +0 -9
- package/dist/mcp-person.js +2 -1
- package/dist/operate.js +4 -1
- package/dist/serve.d.ts +2 -605
- package/dist/serve.js +423 -23783
- package/dist/server/chat.d.ts +41 -0
- package/dist/server/chat.js +1458 -0
- package/dist/server/edge-refusal.d.ts +5 -0
- package/dist/server/edge-refusal.js +56 -0
- package/dist/server/flows.d.ts +12 -0
- package/dist/server/flows.js +832 -0
- package/dist/server/guards.d.ts +72 -0
- package/dist/server/guards.js +357 -0
- package/dist/server/handler-context.d.ts +22 -0
- package/dist/server/handler-context.js +1 -0
- package/dist/server/handler-registry.d.ts +21 -0
- package/dist/server/handler-registry.js +34 -0
- package/dist/server/pages-assets.d.ts +10 -0
- package/dist/server/pages-assets.js +103 -0
- package/dist/server/pages.d.ts +11 -0
- package/dist/server/pages.js +910 -0
- package/dist/server/people-tokens.d.ts +20 -0
- package/dist/server/people-tokens.js +694 -0
- package/dist/server/remote-hooks.d.ts +29 -0
- package/dist/server/remote-hooks.js +130 -0
- package/dist/server/remote.d.ts +59 -0
- package/dist/server/remote.js +185 -0
- package/dist/server/route-policy.d.ts +18 -0
- package/dist/server/route-policy.js +30 -0
- package/dist/server/route-table.d.ts +74 -0
- package/dist/server/route-table.js +350 -0
- package/dist/server/runtime.d.ts +280 -0
- package/dist/server/runtime.js +25 -0
- package/dist/server/settings.d.ts +22 -0
- package/dist/server/settings.js +2388 -0
- package/dist/server/shared.d.ts +3334 -0
- package/dist/server/shared.js +12708 -0
- package/dist/server/task-views.d.ts +82 -0
- package/dist/server/task-views.js +1099 -0
- package/dist/server/tasks.d.ts +87 -0
- package/dist/server/tasks.js +3579 -0
- package/dist/session-http.js +3 -0
- package/dist/team-http.d.ts +5 -1
- package/dist/team-http.js +6 -2
- package/dist/teams.js +3 -0
- package/package.json +1 -1
|
@@ -0,0 +1,3579 @@
|
|
|
1
|
+
/** tasks handlers, moved without changing their route bodies. */
|
|
2
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
3
|
+
import { lstatSync, realpathSync } from "node:fs";
|
|
4
|
+
import {} from "node:http";
|
|
5
|
+
import { withActor } from "../actor.js";
|
|
6
|
+
import { gateWords } from "../approval-policy.js";
|
|
7
|
+
import { acceptAndCompleteAsOperator, assignmentOf, checkAssignmentAsOperator } from '../assignment.js';
|
|
8
|
+
import { attentionCardForUnverifiedDone, classify } from "../board.js";
|
|
9
|
+
import { codingHandoffPreview, createCodingHandoff } from '../coding-handoff.js';
|
|
10
|
+
import { codingShippingHtml } from '../coding-shipping-ui.js';
|
|
11
|
+
import { codingWorkspaceHtml, codingWorkspaceScript } from '../coding-ui.js';
|
|
12
|
+
import { CodingActionError } from '../coding-workspace.js';
|
|
13
|
+
import { abandonContest, buildPickView, computePickPlan, finalizeContestPick, nonceHashOf, pickTupleDigest, planComparison, planTournament } from "../contest.js";
|
|
14
|
+
import { checkResponse, CONSOLE_FORMS, readForm } from "../contracts/console-api.js";
|
|
15
|
+
import { hasForbiddenControls, validateNote } from "../decision.js";
|
|
16
|
+
import { diagnoseTaskDispatch } from "../dispatch.js";
|
|
17
|
+
import { evidencePack, evidencePackHtml } from "../evidence-pack.js";
|
|
18
|
+
import { readVerifiedArtifact, readVerifiedReport, writeEvidenceFile } from "../evidence.js";
|
|
19
|
+
import { computeGaps } from "../gaps.js";
|
|
20
|
+
import { limitsView } from "../limits-ui.js";
|
|
21
|
+
import { readLiveWindow } from "../live.js";
|
|
22
|
+
import { modeTermsFromJson } from "../modes.js";
|
|
23
|
+
import { latestFinishedAttempt, retryNoteOf } from '../needs-you.js';
|
|
24
|
+
import { REMOTE_MESSAGES } from "../operate-remote.js";
|
|
25
|
+
import { aggregateNewNames, observeWorktree, parseBaseTreeSnapshot, PEEK_LIMITS } from "../peek.js";
|
|
26
|
+
import { isRiskLevel, isTaskSize, PHASES as ROUTE_PHASES } from "../phase-routing.js";
|
|
27
|
+
import { applyModeToNewFiling, authorizePlanUnderMode } from "../plan-auto.js";
|
|
28
|
+
import { milestonesOf, parseExecutionPlanDocument } from "../plan.js";
|
|
29
|
+
import { levelOfProfile } from "../policy.js";
|
|
30
|
+
import { authorizedProject, canonicalProject, projectName, rowVisible } from "../project.js";
|
|
31
|
+
import { fileRoutineProposal, fileTaskProposal } from "../proposal.js";
|
|
32
|
+
import { isProviderId, validateSpec } from "../provider.js";
|
|
33
|
+
import { completeAndOpenPullRequest, mergeAsPerson } from '../pull-request-flow.js';
|
|
34
|
+
import { isQualityMode } from "../quality.js";
|
|
35
|
+
import { createResultRevision, requestResultChanges } from "../result-actions.js";
|
|
36
|
+
import { structuredHandoffView, terminalDiffView } from "../result-evidence-readers.js";
|
|
37
|
+
import { fileAddTestsTask, requestFollowUpChecks } from '../result-follow-ups.js';
|
|
38
|
+
import { commentSourceKey, isRevisionFeedback, parseResultTab, resultReturnTarget } from "../result-review.js";
|
|
39
|
+
import { approveRoutine, fireRoutine, refreshRoutineAgents, routineAgentsState } from "../routine.js";
|
|
40
|
+
import { isAlive as runnerAlive } from "../runner.js";
|
|
41
|
+
import { acceptanceLinesToInput, acceptanceToLines, approvalOf, approve as approveScope, attendedDigestOf, attendedTermsJson, canonicalProfileJson, hasFreshIdentitySignIn, modeFilingCoverage, PLACEHOLDER_RUBRIC, profileDigestOf, profileFromJson, proposeGuarded } from "../scope.js";
|
|
42
|
+
import { teammateNames as teammateNamesOf } from "../spend.js";
|
|
43
|
+
import { verifiedAuthor } from "../store.js";
|
|
44
|
+
import { runCostWords } from "../summary.js";
|
|
45
|
+
import { composerSchedule } from "../task-composer.js";
|
|
46
|
+
import { requestTaskStop, resumeTaskStop } from "../task-control.js";
|
|
47
|
+
import { templateByName } from "../templates.js";
|
|
48
|
+
import { TEXT_LIMITS } from "../text-limits.js";
|
|
49
|
+
import { WorkIndexCursorError, workIndexPage } from "../work-index.js";
|
|
50
|
+
import { buildProgressOf, parseWorkView } from "../workspace-ui.js";
|
|
51
|
+
import { ACCEPT_ANYWAY_NEEDS_REASON, approvalFormDigest, approveRefusalWords, attendedWatchWords, boardBody, chatReturnWithSaid, checkProgressHtml, consentDoorOf, contestCeremonyPage, contestPage, decisionPage, donePage, editorFileHref, escape, inboxFingerprints, inboxPage, matchTaskPath, newTaskPage, nextPage, oneLineOf, parseInboxTab, PermissionsWouldChange, proofBundleView, QUEUE_FRONT, QUEUE_VIEW, queueBody, queueScript, rankReviewQueue, redirect, refuse, regionScript, requestContext, requirementsFromEditor, respond, RESULT_REFUSALS, resumeDigestOf, REVIEW_QUEUE_CAP, reviewCockpitPage, reviewHref, reviewPriorityOf, routineScreenPage, routinesPage, runFactsFragment, runOutcomeBadge, runPage, RUNS_PAGE, runsPage, safeChatReturn, safeReturn, SAFETY, screen, TASK_STATES, taskChatHref, taskHref, taskOf, tasksPage, transcriptScript, whenTime, withRefusal, workPage } from "./shared.js";
|
|
52
|
+
export function createTasksHandlers(runtime) {
|
|
53
|
+
const { store, peekSay, peekCache, PEEK_CACHE_TTL_MS, peekInFlight, PEEK_GLOBAL_INFLIGHT, peekBySession, PEEK_SESSION_INFLIGHT, clock, peekName, PEEK_FRAGMENT_BYTES, peekEvict, evidenceRoot, sendScreen, chromeFor, visible, consumeApprovalNonce, authenticateApprover, allowedHost, options, mintApprovalNonce, taskScreen, unscopedMode, admissionList, planViewOf, revisionViewOf, runIsTaskResult, matePrincipal, familyOf, armTaskResume, runIsLive, revisionLedgerOf, resultDetailOf, reviewFactsFor, taskRepoOf, runVisible, restricted, codingActorAllowed, codingProjectAllowed, managedRepos, routeViewOf, workAccess, firstRunStepsNow, revisionDocOf, failureOf, dockedConversation, planContractViewOf, familiesInView, explainAttempt, taskChatFocus, familyTasksInView, taskRooms, identify, liveCeiling, projectOf, ceiling, bustBadge, revisionDestination } = runtime;
|
|
54
|
+
async function get(ctx) {
|
|
55
|
+
const { url, who, request, response, now, project, chosenProject, posted, route } = ctx;
|
|
56
|
+
if (url.pathname === '/code' || url.pathname.startsWith('/code/')) {
|
|
57
|
+
if (who.via !== 'cookie' || !codingActorAllowed({ name: who.name, generation: who.session.generation }))
|
|
58
|
+
return refuse(response, who, 403, 'Coding sessions require installation-wide operator access. You can manage project tasks in Work.', '/work');
|
|
59
|
+
const actor = { name: who.name, generation: who.session.generation };
|
|
60
|
+
const match = /^\/code\/([a-f0-9]{32})(?:\/(state|changes|ship))?$/.exec(url.pathname);
|
|
61
|
+
try {
|
|
62
|
+
const selected = match && runtime.coding ? runtime.coding.get(match[1], actor) : null;
|
|
63
|
+
if (selected && !codingProjectAllowed(selected.repo))
|
|
64
|
+
return refuse(response, who, 403, 'That project is outside your access.', '/projects');
|
|
65
|
+
if (match?.[2] === 'ship' && selected) {
|
|
66
|
+
const preview = codingHandoffPreview(store, { sessionId: selected.id, actor: who.name });
|
|
67
|
+
return sendScreen(response, 200, screen('Review for shipping', codingShippingHtml(preview, who.session.csrf), { chrome: chromeFor(selected.repo, 'code') }));
|
|
68
|
+
}
|
|
69
|
+
if (match?.[2]) {
|
|
70
|
+
if (!selected || !runtime.coding)
|
|
71
|
+
throw Error('The coding session is unavailable.');
|
|
72
|
+
if (match[2] === 'state' && url.searchParams.get('revision') === String(runtime.coding.revision(selected.id, actor)))
|
|
73
|
+
return respond(response, 200, 'application/json', JSON.stringify({ unchanged: true }));
|
|
74
|
+
const result = match[2] === 'changes' ? await runtime.coding.changes(selected.id, actor) : runtime.coding.snapshot(selected.id, actor);
|
|
75
|
+
if (!codingActorAllowed(actor) || !codingProjectAllowed(selected.repo))
|
|
76
|
+
return respond(response, 403, 'application/json', JSON.stringify({ ok: false, error: 'Your access changed. Sign in again.' }));
|
|
77
|
+
return respond(response, 200, 'application/json', JSON.stringify(result));
|
|
78
|
+
}
|
|
79
|
+
if (url.pathname !== '/code' && !match)
|
|
80
|
+
return refuse(response, who, 404, 'Coding session not found.', '/code');
|
|
81
|
+
const requestedProject = url.searchParams.get('project');
|
|
82
|
+
if (requestedProject !== null && (!visible(requestedProject) || ![...managedRepos(), ...store.listProjects().map(p => p.path)].includes(requestedProject)))
|
|
83
|
+
return refuse(response, who, 403, 'That project is outside your access.', '/projects');
|
|
84
|
+
const codeProject = selected?.repo ?? requestedProject ?? project;
|
|
85
|
+
const chrome = chromeFor(codeProject, 'code');
|
|
86
|
+
const content = codingWorkspaceHtml({ owner: who.name, projects: chrome.projects ?? [], sessions: runtime.coding?.list(actor).filter(s => codingProjectAllowed(s.repo)) ?? [], selected: selected && runtime.coding ? runtime.coding.snapshot(selected.id, actor) : null, csrf: who.session.csrf, project: codeProject, available: runtime.coding !== null, ...(runtime.codingProblem ? { error: runtime.codingProblem } : {}) });
|
|
87
|
+
return sendScreen(response, 200, screen('Code', content, { chrome, functional: { script: codingWorkspaceScript(), fetches: true } }));
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
if (!codingActorAllowed(actor))
|
|
91
|
+
return match?.[2] && match[2] !== 'ship'
|
|
92
|
+
? respond(response, 403, 'application/json', JSON.stringify({ ok: false, error: 'Your access changed. Sign in again.' }))
|
|
93
|
+
: refuse(response, who, 403, 'Your access changed. Sign in again.', '/work');
|
|
94
|
+
const message = error instanceof Error ? error.message : 'The coding workspace is unavailable.';
|
|
95
|
+
if (match?.[2] === 'ship')
|
|
96
|
+
return sendScreen(response, 409, screen('Review for shipping', `<h1>Prepare this result</h1><p>${escape(message)}</p><a href="/code/${match[1]}">Back to coding</a>`, { chrome: chromeFor(project, 'code') }));
|
|
97
|
+
if (match?.[2])
|
|
98
|
+
return respond(response, 409, 'application/json', JSON.stringify({ ok: false, error: message }));
|
|
99
|
+
return sendScreen(response, 404, screen('Coding session unavailable', `<h1>Session unavailable</h1><p>${escape(message)}</p><a href="/code">Open coding workspace</a>`, { chrome: chromeFor(project, 'code') }));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (url.pathname === "/inbox") {
|
|
103
|
+
// With no project open in scoped mode this is the ROLL-UP inbox:
|
|
104
|
+
// admission binds inside the bounded queries, every row's repo is
|
|
105
|
+
// re-proved here, and rows render as links only (Codex roll-up
|
|
106
|
+
// review, findings 6–8). Gaps stay per-project — they are derived
|
|
107
|
+
// against one repo's capabilities and roll up dishonestly.
|
|
108
|
+
const rollup = project === null && !unscopedMode;
|
|
109
|
+
const admission = rollup ? admissionList() : null;
|
|
110
|
+
const cancelled = store
|
|
111
|
+
.listCancelledBlockersScoped(project, 10, admission)
|
|
112
|
+
.filter(one => visible(one.repo) && visible(one.blockerRepo));
|
|
113
|
+
const inboxData = {
|
|
114
|
+
csrf: who.via === "cookie" ? who.session.csrf : "",
|
|
115
|
+
revision: who.via === "cookie" ? who.session.projectRevision : 0,
|
|
116
|
+
rollup,
|
|
117
|
+
interactive: project !== null,
|
|
118
|
+
decisions: store.listDecisionsScoped(project).filter(one => visible(one.repo)).slice(0, 10),
|
|
119
|
+
// Each waiting scope wears its consent door (v48 authority repair): a scope whose
|
|
120
|
+
// approval is closed — unreadable route, a route that cannot run,
|
|
121
|
+
// a pre-routing row whose approval lapsed — reads as needing
|
|
122
|
+
// attention, never as something to approve.
|
|
123
|
+
approvals: store.scopesAwaitingApproval(project, 10, admission).filter(one => visible(one.repo)).map(one => {
|
|
124
|
+
const ref = store.lookupRef(one.taskId);
|
|
125
|
+
const scope = store.getScope(one.taskId);
|
|
126
|
+
const door = consentDoorOf(scope, routeViewOf(one.taskId, ref, scope, now, who));
|
|
127
|
+
return { ...one, closed: door.open ? null : door.title };
|
|
128
|
+
}),
|
|
129
|
+
requeueables: store.listRequeueablesScoped(project, now, 10, admission).filter(one => visible(one.repo)),
|
|
130
|
+
needsVerification: store
|
|
131
|
+
.listCompletedWorkScoped(project, 10, admission)
|
|
132
|
+
.filter(one => visible(one.repo) && (one.proofVerdict === "short" || one.proofVerdict === "refuted") && !one.proofAccepted &&
|
|
133
|
+
assignmentOf(store, one.taskId, now, workAccess(), evidenceRoot) === null)
|
|
134
|
+
.map(one => ({
|
|
135
|
+
taskId: one.taskId,
|
|
136
|
+
title: one.title,
|
|
137
|
+
verdict: one.proofVerdict,
|
|
138
|
+
repo: one.repo,
|
|
139
|
+
matrix: one.proofMatrix,
|
|
140
|
+
repairChain: one.runId === null ? null : store.repairChainFor(one.runId),
|
|
141
|
+
})),
|
|
142
|
+
cancelledBlockers: cancelled,
|
|
143
|
+
gaps: project === null ? [] : computeGaps(store, project, now).filter(gap => gap.unblocks.length > 0).slice(0, 10),
|
|
144
|
+
wizard: firstRunStepsNow(now),
|
|
145
|
+
worker: (() => {
|
|
146
|
+
// The one fact the inbox must never hide (install review): with
|
|
147
|
+
// no worker answering, nothing here will ever build, and every
|
|
148
|
+
// approval below is a promise nobody is there to keep.
|
|
149
|
+
const runners = store.listRunners().filter(one => one.retiredAt === null && (!restricted() || one.repos.some(visible)));
|
|
150
|
+
const answering = runners.filter(one => runnerAlive(one, now));
|
|
151
|
+
const lastHeard = runners.map(one => one.heartbeatAt).sort().at(-1) ?? null;
|
|
152
|
+
return { answering: answering.length, registered: runners.length, lastHeard };
|
|
153
|
+
})(),
|
|
154
|
+
now,
|
|
155
|
+
// Console v2: results ready to review and work running now, from the same admitted work index.
|
|
156
|
+
...(() => {
|
|
157
|
+
const row = (one) => ({ taskId: one.rootId, title: one.title, detail: one.status.detail, repo: one.repo });
|
|
158
|
+
const needsYou = workIndexPage(store, now, workAccess(), { view: "needs-you", limit: 40, project });
|
|
159
|
+
const runningNow = workIndexPage(store, now, workAccess(), { view: "running", limit: 40, project });
|
|
160
|
+
return { ready: needsYou.items.filter(one => one.assignmentState === "ready-to-check").map(row), running: runningNow.items.map(row) };
|
|
161
|
+
})(),
|
|
162
|
+
};
|
|
163
|
+
// Which tabs hold something new since this browser last looked (the dots on a phone).
|
|
164
|
+
const tab = parseInboxTab(url.searchParams.get("tab")) ?? "all";
|
|
165
|
+
const prints = inboxFingerprints(inboxData);
|
|
166
|
+
const seenWords = /(?:^|;\s*)so-inbox-seen=([0-9a-f]{8}(?:\.[0-9a-f]{8}){3})(?:;|$)/.exec(request.headers.cookie ?? "")?.[1]?.split(".") ?? null;
|
|
167
|
+
const order = ["needs-you", "ready", "running", "all"];
|
|
168
|
+
const seen = Object.fromEntries(order.map((one, index) => [one, seenWords?.[index] ?? ""]));
|
|
169
|
+
const unread = order.filter(one => seenWords !== null && seen[one] !== prints[one]);
|
|
170
|
+
for (const one of order)
|
|
171
|
+
if (tab === "all" || one === tab)
|
|
172
|
+
seen[one] = prints[one];
|
|
173
|
+
response.setHeader("Set-Cookie", `so-inbox-seen=${order.map(one => seen[one] || prints[one]).join(".")}; SameSite=Lax; Path=/; Max-Age=31536000`);
|
|
174
|
+
return sendScreen(response, 200, inboxPage(chromeFor(project, "inbox"), { ...inboxData, tab, unread }));
|
|
175
|
+
}
|
|
176
|
+
if (url.pathname === "/work") {
|
|
177
|
+
// The Work destination (workspace package 1): every task in view as
|
|
178
|
+
// one row wearing the shared status projection, with All, Needs you,
|
|
179
|
+
// Running, and Completed as shortcuts over the same rows — never a
|
|
180
|
+
// persisted state. With no project open in scoped mode this rolls
|
|
181
|
+
// up like the inbox: admission binds the query and every row's repo
|
|
182
|
+
// is re-proved here.
|
|
183
|
+
const view = parseWorkView(url.searchParams.get("view"));
|
|
184
|
+
const rollup = project === null && !unscopedMode;
|
|
185
|
+
let work;
|
|
186
|
+
try {
|
|
187
|
+
work = workIndexPage(store, now, workAccess(), { view, limit: 40, cursor: url.searchParams.get('cursor'), project });
|
|
188
|
+
const facts = requestContext.getStore();
|
|
189
|
+
if (facts !== undefined) {
|
|
190
|
+
// These counts cover the exact admitted project lens, including
|
|
191
|
+
// unplaced tasks where permitted. Crew excludes unplaced rows.
|
|
192
|
+
if (project === null)
|
|
193
|
+
facts.workCounts = work.projects;
|
|
194
|
+
const admitted = workAccess().repos;
|
|
195
|
+
const crewLens = project !== null || admitted !== null && JSON.stringify([...admitted].sort()) === JSON.stringify(managedRepos().sort()) && !work.projects.some(one => one.repo === null);
|
|
196
|
+
if (view === 'all' && !url.searchParams.has('cursor') && crewLens)
|
|
197
|
+
facts.workCrew = { project, page: work };
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
if (error instanceof WorkIndexCursorError)
|
|
202
|
+
return refuse(response, who, 400, 'This task page has expired. Open the first page.', '/work');
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
// A failed row says what its latest attempt missed, as its task page does (a stop reason already reads so), from
|
|
206
|
+
// 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
|
|
207
|
+
// stuck on, from that live attempt's own progress only, never a stopped attempt's; the step's words come from its
|
|
208
|
+
// current plan (one saved file, for live rows only), and without it the row names the step by number.
|
|
209
|
+
work = { ...work, items: work.items.map((item) => {
|
|
210
|
+
if (item.liveRunId !== null) {
|
|
211
|
+
const ref = store.lookupRef(item.activeTaskId);
|
|
212
|
+
const recorded = ref === null ? null : store.latestCheckpointForRun(item.liveRunId);
|
|
213
|
+
if (ref === null || recorded === null || recorded.taskRef !== ref.id)
|
|
214
|
+
return item;
|
|
215
|
+
const plan = store.currentPlanRevision(ref.id);
|
|
216
|
+
const parsed = plan === null ? null : (() => { const doc = revisionDocOf(plan); return doc === null ? null : parseExecutionPlanDocument(doc.document); })();
|
|
217
|
+
const words = new Map(parsed?.ok === true ? milestonesOf(parsed.document).map(one => [one.id, one.description]) : []);
|
|
218
|
+
const steps = buildProgressOf(recorded.snapshot.milestones.map(one => ({ description: words.get(one.id) ?? null, state: one.state, note: one.note ?? null })));
|
|
219
|
+
return steps === null ? item : { ...item, progress: steps.line };
|
|
220
|
+
}
|
|
221
|
+
if (item.state !== "failed")
|
|
222
|
+
return item;
|
|
223
|
+
const family = familyOf(item.activeTaskId);
|
|
224
|
+
if (family === null || family.problem !== null)
|
|
225
|
+
return item;
|
|
226
|
+
const failure = failureOf(family.versions.flatMap(version => store.runsFor(version.refId)), null, false);
|
|
227
|
+
return failure.kind === "reason" ? item : { ...item, status: { ...item.status, detail: failure.line } };
|
|
228
|
+
}) };
|
|
229
|
+
const page = workPage(chromeFor(project, "work", undefined, rollup ? "all" : undefined), {
|
|
230
|
+
view,
|
|
231
|
+
...(url.searchParams.has('project') && project !== null ? { projectFilter: project } : {}),
|
|
232
|
+
work,
|
|
233
|
+
previous: url.searchParams.has('cursor'),
|
|
234
|
+
multiProject: new Set(work.items.map(task => task.repo ?? "")).size > 1,
|
|
235
|
+
now,
|
|
236
|
+
// v105: subscription windows and monthly budgets, for whoever runs the installation.
|
|
237
|
+
limits: store.isInstanceOperator(who.name)
|
|
238
|
+
? limitsView(store.providerLimits(), store.budgets().length === 0 ? [] : store.monthSpendCached(now).budgets, { project: projectName, teammate: id => teammateNamesOf(store.handle).get(id) ?? `Teammate ${id}` }, now)
|
|
239
|
+
: null,
|
|
240
|
+
});
|
|
241
|
+
// One project's Tasks dock that project's own conversation (v77).
|
|
242
|
+
const projectThread = url.searchParams.has('project') && project !== null && managedRepos().includes(project) ? project : null;
|
|
243
|
+
const docked = projectThread === null ? null : dockedConversation(who, null, projectThread, now, `/work?project=${encodeURIComponent(projectThread)}`);
|
|
244
|
+
if (docked !== null)
|
|
245
|
+
page.workspace = { ...page.workspace, conversation: docked, pageHtml: page.body };
|
|
246
|
+
// Without a docked chat the list reads itself: 10 s while any of its tasks is building, else 30 s.
|
|
247
|
+
page.refreshSeconds = work.totals.running > 0 ? 10 : 30;
|
|
248
|
+
return sendScreen(response, 200, page);
|
|
249
|
+
}
|
|
250
|
+
if (url.pathname === "/next") {
|
|
251
|
+
// Triage: everything waiting on a person, one at a time, hardest-
|
|
252
|
+
// blocked first — oldest question, then plans and scopes to approve,
|
|
253
|
+
// then stalled work to retry, then requirement gaps. `skip` is a
|
|
254
|
+
// bounded, session-free cursor of keys the operator set aside; every
|
|
255
|
+
// act 303s back here, which is what makes it a flow and not a list.
|
|
256
|
+
const skipped = new Set((url.searchParams.get("skip") ?? "").split(",").filter(one => /^[darg]:[A-Za-z0-9._:-]{1,80}$/.test(one)).slice(0, 20));
|
|
257
|
+
const decisions = store.listDecisionsScoped(project).filter(one => one.state !== "answered");
|
|
258
|
+
const approvals = store.scopesAwaitingApproval(project, 20);
|
|
259
|
+
const requeueables = store.listRequeueablesScoped(project, now, 20);
|
|
260
|
+
const gaps = project === null ? [] : computeGaps(store, project, now).filter(gap => gap.unblocks.length > 0);
|
|
261
|
+
const queue = [
|
|
262
|
+
...decisions.map(decision => ({ key: `d:${decision.id}`, kind: "decision", decision })),
|
|
263
|
+
...approvals.map(approval => ({ key: `a:${approval.taskId}`, kind: "approval", approval })),
|
|
264
|
+
...requeueables.map(stalled => ({ key: `r:${stalled.taskId}`, kind: "requeue", stalled })),
|
|
265
|
+
...gaps.map(gap => ({ key: `g:${gap.key.replace(/[^A-Za-z0-9._:-]/g, "_")}`, kind: "gap", gap })),
|
|
266
|
+
];
|
|
267
|
+
const remaining = queue.filter(one => !skipped.has(one.key));
|
|
268
|
+
const item = remaining[0] ?? null;
|
|
269
|
+
const csrf = who.via === "cookie" ? who.session.csrf : "";
|
|
270
|
+
if (item !== null && item.kind === "approval") {
|
|
271
|
+
// The card restates the digest-bound terms, so the nonce may be
|
|
272
|
+
// minted here — same rule as the task screen, same binding.
|
|
273
|
+
const ref = store.lookupRef(item.approval.taskId);
|
|
274
|
+
const scope = store.getScope(item.approval.taskId);
|
|
275
|
+
const planView = ref !== null && ref.plan === "drafted" ? planViewOf(ref.id) : null;
|
|
276
|
+
const raceTerms = ref === null ? null : store.activeTournamentTerms(ref.id);
|
|
277
|
+
const approvalDigest = approvalFormDigest(item.approval.digest, raceTerms?.raceDigest ?? null, planView?.sha256 ?? null);
|
|
278
|
+
// The same agents block the task page and chat sign under (v48),
|
|
279
|
+
// and the same consent door: closed, it mints no nonce.
|
|
280
|
+
const route = routeViewOf(item.approval.taskId, ref, scope, now, who);
|
|
281
|
+
const nonce = who.via === "cookie" && consentDoorOf(scope, route).open ? mintApprovalNonce(who.name, item.approval.taskId, approvalDigest) : "";
|
|
282
|
+
return sendScreen(response, 200, nextPage(chromeFor(project, "inbox"), {
|
|
283
|
+
item, scope,
|
|
284
|
+
planDocument: planView?.document ?? null,
|
|
285
|
+
planContract: ref === null || planView === null ? null : planContractViewOf(ref.id, scope),
|
|
286
|
+
approvalDigest,
|
|
287
|
+
raceTerms,
|
|
288
|
+
deliverable: ref?.deliverable ?? "branch",
|
|
289
|
+
route,
|
|
290
|
+
csrf, nonce, remaining: remaining.length, skipped: [...skipped], now,
|
|
291
|
+
}));
|
|
292
|
+
}
|
|
293
|
+
return sendScreen(response, 200, nextPage(chromeFor(project, "inbox"), {
|
|
294
|
+
item, scope: null, planDocument: null, planContract: null, csrf, nonce: "",
|
|
295
|
+
approvalDigest: null, raceTerms: null, route: null,
|
|
296
|
+
remaining: remaining.length, skipped: [...skipped], now,
|
|
297
|
+
}));
|
|
298
|
+
}
|
|
299
|
+
if (url.pathname === "/board") {
|
|
300
|
+
// scope=all is the rolled-up view: every project this server was
|
|
301
|
+
// allowed to serve, on one board. The ceiling still rules row by row
|
|
302
|
+
// (rowVisible, the same predicate as every list) — a repo outside
|
|
303
|
+
// the server's configuration never renders a card, whatever the
|
|
304
|
+
// database holds. Unplaced work (repo NULL) appears: it dispatches
|
|
305
|
+
// anywhere, so every board honestly owns it.
|
|
306
|
+
const all = url.searchParams.get("scope") === "all";
|
|
307
|
+
// Roll-up admission happens BEFORE the query limit (Codex round 2,
|
|
308
|
+
// finding 11) — and root ceilings enumerate too, through the STORED
|
|
309
|
+
// repos that pass the ceiling (attended review, finding 3); the
|
|
310
|
+
// per-row visible() re-check below stays either way.
|
|
311
|
+
const admission = all ? admissionList() : null;
|
|
312
|
+
const snapshot = store.boardScoped(all ? null : project, now, 200, admission);
|
|
313
|
+
const admitted = all
|
|
314
|
+
? snapshot.tasks.filter(facts => facts.repo === null || visible(facts.repo))
|
|
315
|
+
: snapshot.tasks;
|
|
316
|
+
const done = all
|
|
317
|
+
? snapshot.done.filter(row => row.repo === null || visible(row.repo))
|
|
318
|
+
: snapshot.done;
|
|
319
|
+
// A blocker may live in a repo this server must not speak about —
|
|
320
|
+
// redact its state before the pure classifier composes a sentence
|
|
321
|
+
// from it (Codex round 2, finding 12). The dependency's NAME stays:
|
|
322
|
+
// the edge belongs to the visible task; the other project's live
|
|
323
|
+
// status does not.
|
|
324
|
+
const cards = admitted.map(facts => classify(facts.blockerRepo !== null && !visible(facts.blockerRepo)
|
|
325
|
+
? { ...facts, blockerState: null }
|
|
326
|
+
: facts, now));
|
|
327
|
+
// "Since you last looked": what concluded between this session's
|
|
328
|
+
// previous full board read and now. Fragment polls never move the
|
|
329
|
+
// anchor — an open tab is not a person looking.
|
|
330
|
+
let delta = null;
|
|
331
|
+
if (who.via === "cookie" && url.searchParams.get("fragment") !== "1") {
|
|
332
|
+
const prev = who.session.sawBoardAt;
|
|
333
|
+
if (prev !== null && now.getTime() - prev > 5 * 60_000) {
|
|
334
|
+
const sinceIso = new Date(prev).toISOString();
|
|
335
|
+
const runs = store.runsSinceScoped(sinceIso, all ? null : project).filter(one => all ? one.taskId !== "" : true);
|
|
336
|
+
delta = {
|
|
337
|
+
agoMinutes: Math.round((now.getTime() - prev) / 60_000),
|
|
338
|
+
built: runs.filter(one => one.outcome === "built" || one.outcome === "no-change").length,
|
|
339
|
+
failed: runs.filter(one => one.outcome === "failed").length,
|
|
340
|
+
questions: store.listDecisionsScoped(all ? null : project).filter(one => one.createdAt >= sinceIso && one.state !== "answered").length,
|
|
341
|
+
};
|
|
342
|
+
if (delta.built === 0 && delta.failed === 0 && delta.questions === 0)
|
|
343
|
+
delta = null;
|
|
344
|
+
}
|
|
345
|
+
who.session.sawBoardAt = now.getTime();
|
|
346
|
+
}
|
|
347
|
+
const buildingCount = cards.filter(card => card.lane === "building").length;
|
|
348
|
+
// A completed task whose proof is short or refuted and not yet
|
|
349
|
+
// accepted (Priority 2) reads "needs verification", not done — the
|
|
350
|
+
// board's once-and-only-once rule holds because this split is the
|
|
351
|
+
// ONE place a done row becomes either lane; the done-lane render
|
|
352
|
+
// below never sees the rows filtered out here.
|
|
353
|
+
const unverifiedDone = done.filter(row => (row.proofVerdict === "short" || row.proofVerdict === "refuted") && !row.proofAccepted);
|
|
354
|
+
const verifiedDone = done.filter(row => !unverifiedDone.includes(row));
|
|
355
|
+
const unverifiedCards = unverifiedDone.map(row => {
|
|
356
|
+
// v40: the SAME chip gains one more word when a repair chain
|
|
357
|
+
// exists for this row's own run — never a second card.
|
|
358
|
+
const chain = row.runId === null ? null : store.repairChainFor(row.runId);
|
|
359
|
+
const repairChain = chain === null
|
|
360
|
+
? null
|
|
361
|
+
: {
|
|
362
|
+
attempt: chain.attempt,
|
|
363
|
+
outcome: chain.outcome,
|
|
364
|
+
approved: chain.draftTask !== null && (store.getScope(chain.draftTask)?.approvedAt ?? null) !== null,
|
|
365
|
+
};
|
|
366
|
+
return attentionCardForUnverifiedDone({
|
|
367
|
+
taskId: row.taskId,
|
|
368
|
+
title: row.title,
|
|
369
|
+
repo: row.repo,
|
|
370
|
+
completedAt: row.completedAt,
|
|
371
|
+
proofVerdict: row.proofVerdict,
|
|
372
|
+
proofMatrix: row.proofMatrix,
|
|
373
|
+
repairChain,
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
// Instances belong to their track row, not the main lanes — the board
|
|
377
|
+
// is for one-off work; tracks are the heartbeat. The one exception is
|
|
378
|
+
// attention: anything needing a person surfaces, wearing its routine.
|
|
379
|
+
const laneCards = [...cards.filter(card => card.routineName === null || card.lane === "attention"), ...unverifiedCards];
|
|
380
|
+
const tracks = store
|
|
381
|
+
.routineTracks(all ? null : project, now, admission)
|
|
382
|
+
.filter(track => visible(track.routine.repo));
|
|
383
|
+
const body = boardBody({ cards: laneCards, tracks, done: verifiedDone, saturated: snapshot.saturated, now, all, project, delta }, pr => store.ciFailureObserved(pr));
|
|
384
|
+
if (url.searchParams.get("fragment") === "1") {
|
|
385
|
+
// The live region alone — the in-page swapper's diet. Same auth,
|
|
386
|
+
// same ceiling, no shell, no scripts (finding 2).
|
|
387
|
+
return respond(response, 200, "text/html; charset=utf-8", body);
|
|
388
|
+
}
|
|
389
|
+
if (url.searchParams.get("view") === "order") {
|
|
390
|
+
// The board's ORDER view (operator request): the same screen, flipped
|
|
391
|
+
// to dispatch order with the queue's drag handles. Reordering and
|
|
392
|
+
// reserving are scheduling, not authority, so this is the one view
|
|
393
|
+
// where a drag does anything; the state view stays a view.
|
|
394
|
+
const csrf = who.via === "cookie" ? who.session.csrf : "";
|
|
395
|
+
const revision = who.via === "cookie" ? who.session.projectRevision : 0;
|
|
396
|
+
const region = queueRegionFor(project, csrf, revision, now);
|
|
397
|
+
return sendScreen(response, 200, screen("board", [
|
|
398
|
+
`<h1>Board</h1>`,
|
|
399
|
+
`<p class="meta board-view"><a href="/board">state</a> \u00b7 <strong>order</strong> <span class="meta">\u2014 drag to reorder, or onto a worker to reserve; the state view is where cards move on their own</span></p>`,
|
|
400
|
+
`<div id="queue-region">${region}</div>`,
|
|
401
|
+
`<p class="meta" id="queue-region-stamp"></p>`,
|
|
402
|
+
].join("\n"), { chrome: chromeFor(project, "board"), functional: { script: queueScript(), fetches: true } }));
|
|
403
|
+
}
|
|
404
|
+
const regionBody = `<div id="board-region">${body}</div><p class="meta" id="board-region-stamp"></p>`;
|
|
405
|
+
return sendScreen(response, 200, screen("board", regionBody, {
|
|
406
|
+
chrome: chromeFor(project, "board", undefined, all ? "board-all" : undefined),
|
|
407
|
+
functional: { script: regionScript("board-region", "1", buildingCount > 0 ? 10 : 30), fetches: true },
|
|
408
|
+
}));
|
|
409
|
+
}
|
|
410
|
+
if (url.pathname === "/review") {
|
|
411
|
+
// The review cockpit (Priority 5): every visible COMPLETED task,
|
|
412
|
+
// ranked by review priority, with one selected result projected
|
|
413
|
+
// from the records the task, run, and done pages already read —
|
|
414
|
+
// scope, plan, run, artifacts, verdict, contest, publication. The
|
|
415
|
+
// ranking is a labeled presentation aid: it never rewrites the
|
|
416
|
+
// stored verdict, and the sealed patch downloads exactly as stored.
|
|
417
|
+
// Admission binds BEFORE the SQL limit (the done page's own rule),
|
|
418
|
+
// and every row is re-proved against the ceiling before ranking.
|
|
419
|
+
const resultRow = (row) => {
|
|
420
|
+
const assignment = assignmentOf(store, row.taskId, now, workAccess(), evidenceRoot);
|
|
421
|
+
return { ...row, proofReasons: row.runId === null ? [] : store.proofVerdictFor(row.runId)?.reasons ?? [], ciFailing: ciFailingFor(row.runId, row.prNumber),
|
|
422
|
+
assignment: assignment?.activeTaskId === row.taskId && (assignment.receipt?.runId ?? null) === row.runId ? assignment : null };
|
|
423
|
+
};
|
|
424
|
+
const rows = familiesInView(project, { states: ["done"], limit: REVIEW_QUEUE_CAP }).flatMap(family => {
|
|
425
|
+
const row = completedRowFor(family.current.id, project);
|
|
426
|
+
return row === null ? [] : [{ ...resultRow(row), title: family.root.title }];
|
|
427
|
+
});
|
|
428
|
+
const ranked = rankReviewQueue(rows);
|
|
429
|
+
const wanted = url.searchParams.get("result");
|
|
430
|
+
const wantedId = wanted === null || wanted.length === 0 || wanted.length > 64 || hasForbiddenControls(wanted) ? null : wanted;
|
|
431
|
+
const inQueue = wantedId === null ? null : ranked.find(one => one.taskId === wantedId) ?? null;
|
|
432
|
+
// A stable deep link outlives the queue window (v2 review, comment
|
|
433
|
+
// 1): a completion older than the newest REVIEW_QUEUE_CAP resolves
|
|
434
|
+
// directly — re-proved as done, in this project, and inside the
|
|
435
|
+
// ceiling — so an old receipt never reads as unfinished or foreign.
|
|
436
|
+
// The ranked queue itself stays bounded and says so.
|
|
437
|
+
const beyond = wantedId === null || inQueue !== null ? null : completedRowFor(wantedId, project);
|
|
438
|
+
const beyondRow = beyond === null ? null : resultRow(beyond);
|
|
439
|
+
const completedChoice = inQueue ?? (beyondRow === null ? null : { ...beyondRow, priority: reviewPriorityOf(beyondRow) });
|
|
440
|
+
const expectedRun = url.searchParams.get("run");
|
|
441
|
+
const namedRun = expectedRun !== null && /^[1-9]\d*$/.test(expectedRun) ? Number(expectedRun) : null;
|
|
442
|
+
// A build that failed (or stopped) has its own result page too: the run named, or a failed task's latest attempt.
|
|
443
|
+
const attempt = wantedId === null || (completedChoice !== null && (namedRun === null || completedChoice.runId === namedRun)) ? null : attemptRowFor(wantedId, namedRun, project);
|
|
444
|
+
const attemptRow = attempt === null ? null : resultRow(attempt);
|
|
445
|
+
const chosen = attemptRow !== null ? { ...attemptRow, priority: reviewPriorityOf(attemptRow) } : completedChoice;
|
|
446
|
+
const selectedRow = wantedId === null ? ranked[0] ?? null : chosen;
|
|
447
|
+
const csrf = who.via === "cookie" ? who.session.csrf : "";
|
|
448
|
+
// An exact link to a result this person can't see (or that never existed) reads the same either way.
|
|
449
|
+
if (expectedRun !== null && wantedId !== null && chosen === null)
|
|
450
|
+
return refuse(response, who, 404, "No such result in your projects.", "/work");
|
|
451
|
+
if (expectedRun !== null && (!/^[1-9]\d*$/.test(expectedRun) || selectedRow?.runId !== Number(expectedRun))) {
|
|
452
|
+
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") }));
|
|
453
|
+
}
|
|
454
|
+
const selected = selectedRow === null ? null : reviewCockpitViewOf(selectedRow, who, now);
|
|
455
|
+
// A build that delivered nothing reads Failed here, and so does a failed task's delivered result: what went wrong
|
|
456
|
+
// is the card and Retry the ink act. That result may still be accepted, but only in outline, as Accept anyway, with
|
|
457
|
+
// a reason.
|
|
458
|
+
const delivered = selected?.run?.outcome === "built" || selected?.run?.outcome === "no-change";
|
|
459
|
+
const failedTask = selected === null ? null : store.getTask(selected.taskId);
|
|
460
|
+
if (selected !== null && attemptRow !== null && selected.run !== null && (!delivered || failedTask?.state === "failed")) {
|
|
461
|
+
const attemptRun = store.getRun(selected.run.id);
|
|
462
|
+
const runs = attemptRun === null ? [] : store.runsFor(attemptRun.taskRef);
|
|
463
|
+
const latest = latestFinishedAttempt(runs);
|
|
464
|
+
// Retry from here only when the task failed, nothing holds it, and this is its latest attempt or a result it delivered.
|
|
465
|
+
const retry = failedTask?.state === "failed" && (latest?.id === selected.run.id || delivered) && csrf !== "" && who.role === "approver" && store.currentLiveLease(attemptRun.taskRef, now) === null
|
|
466
|
+
? { action: `${taskHref(selected.taskId)}/requeue` } : null;
|
|
467
|
+
// Accept anyway: the same acceptance this task's latest result takes, never for an older one or one already accepted.
|
|
468
|
+
const family = delivered ? familyOf(selected.taskId) : null;
|
|
469
|
+
const acceptAnyway = delivered && csrf !== "" && who.role === "approver" && family?.current.id === selected.taskId && family.problem === null &&
|
|
470
|
+
runs.find(runIsTaskResult)?.id === selected.run.id && store.proofAcceptance(selected.run.id) === null
|
|
471
|
+
? { action: `${taskHref(selected.taskId)}/accept-proof`, run: selected.run.id } : null;
|
|
472
|
+
if (attemptRun !== null) {
|
|
473
|
+
// A delivered result didn't fail itself: the task's failure is its latest attempt's.
|
|
474
|
+
const failure = delivered ? failureOf(runs, null) : explainAttempt(attemptRun, null);
|
|
475
|
+
selected.failure = { ...failure, retry: retry === null ? null : { ...retry, note: retryNoteOf(failure.suggestion) }, ...(acceptAnyway === null ? {} : { acceptAnyway }) };
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
const reviewPage = reviewCockpitPage(chromeFor(wantedId === null ? project : chosenProject, "runs"), {
|
|
479
|
+
queue: ranked,
|
|
480
|
+
queueCap: REVIEW_QUEUE_CAP,
|
|
481
|
+
selected,
|
|
482
|
+
beyondQueue: beyond !== null && attemptRow === null,
|
|
483
|
+
// A deep link to a result this console cannot show — not done,
|
|
484
|
+
// not admitted, or never existed — says so in one sentence and
|
|
485
|
+
// shows the top of the queue; the three cases read identically.
|
|
486
|
+
missing: wantedId !== null && chosen === null ? wantedId : null,
|
|
487
|
+
csrf,
|
|
488
|
+
canRetryReview: who.via === "cookie" && who.role === "approver",
|
|
489
|
+
noted: url.searchParams.get("noted") !== null,
|
|
490
|
+
refusal: Object.hasOwn(RESULT_REFUSALS, url.searchParams.get("refused") ?? "") ? RESULT_REFUSALS[url.searchParams.get("refused")] : null,
|
|
491
|
+
tab: parseResultTab(url.searchParams.get("tab")),
|
|
492
|
+
user: who.name,
|
|
493
|
+
now,
|
|
494
|
+
});
|
|
495
|
+
// The result's task conversation, docked beside it (v77); messages
|
|
496
|
+
// sent here carry the result being viewed.
|
|
497
|
+
const reviewFocus = selected === null ? null : taskChatFocus(selected.taskId, now, who, { mintNonce: false });
|
|
498
|
+
const reviewDocked = reviewFocus === null || selected === null ? null
|
|
499
|
+
: dockedConversation(who, reviewFocus, null, now, reviewHref(selected.taskId), selected.run !== null && reviewFocus.family.versions.some(one => one.refId === store.getRun(selected.run.id)?.taskRef) ? selected.run.id : null);
|
|
500
|
+
if (reviewDocked !== null)
|
|
501
|
+
reviewPage.workspace = { ...reviewPage.workspace, conversation: reviewDocked, pageHtml: reviewPage.body };
|
|
502
|
+
return sendScreen(response, 200, reviewPage);
|
|
503
|
+
}
|
|
504
|
+
if (url.pathname === "/done") {
|
|
505
|
+
return sendScreen(response, 200, donePage(chromeFor(project, "runs"), store.listCompletedWorkScoped(project, 50), pr => store.ciFailureObserved(pr)));
|
|
506
|
+
}
|
|
507
|
+
if (url.pathname === "/tasks") {
|
|
508
|
+
const wanted = url.searchParams.get("state");
|
|
509
|
+
if (wanted !== null && !TASK_STATES.includes(wanted)) {
|
|
510
|
+
return refuse(response, who, 400, "no such state", "/tasks");
|
|
511
|
+
}
|
|
512
|
+
// ?template=<name> pre-fills the add form from the shipped library —
|
|
513
|
+
// a pre-filled form and nothing more; the submission path is the
|
|
514
|
+
// same guarded handler either way.
|
|
515
|
+
const fromTemplate = url.searchParams.get("template");
|
|
516
|
+
const picked = fromTemplate === null ? null : templateByName(fromTemplate);
|
|
517
|
+
const prefill = picked !== null && picked.kind === "task"
|
|
518
|
+
? {
|
|
519
|
+
title: picked.title, goal: picked.goal, not: picked.outOfScope ?? "", touches: picked.touches.join(", "),
|
|
520
|
+
acceptance: acceptanceToLines(picked.acceptance).join("\n"),
|
|
521
|
+
}
|
|
522
|
+
: null;
|
|
523
|
+
const csrf = who.via === "cookie" ? who.session.csrf : "";
|
|
524
|
+
return sendScreen(response, 200, tasksPage(chromeFor(project, "tasks"), familyTasksInView(project, wanted === null ? undefined : wanted).slice(0, 200), wanted, csrf, null, project, prefill, store.permissionDefault().mode, store.qualityDefault().mode, store.replacements()));
|
|
525
|
+
}
|
|
526
|
+
if (url.pathname === "/queue") {
|
|
527
|
+
const csrf = who.via === "cookie" ? who.session.csrf : "";
|
|
528
|
+
const revision = who.via === "cookie" ? who.session.projectRevision : 0;
|
|
529
|
+
// Column headers read one thing beyond the queue snapshot: live claims
|
|
530
|
+
// in THIS project, per worker (the capacity is global; attended claims
|
|
531
|
+
// may exceed it), so the header names both and never a ratio.
|
|
532
|
+
if (url.searchParams.get("fragment") === "1") {
|
|
533
|
+
return respond(response, 200, "text/html; charset=utf-8", queueRegionFor(project, csrf, revision, clock()));
|
|
534
|
+
}
|
|
535
|
+
// The queue is the board's order view (reduction pass §1): the URL
|
|
536
|
+
// keeps answering, as a redirect, so nothing anyone bookmarked 404s.
|
|
537
|
+
return redirect(response, QUEUE_VIEW);
|
|
538
|
+
}
|
|
539
|
+
if (url.pathname === "/tasks/new") {
|
|
540
|
+
const csrf = who.via === "cookie" ? who.session.csrf : "";
|
|
541
|
+
const revision = who.via === "cookie" ? who.session.projectRevision : 0;
|
|
542
|
+
const chainable = store
|
|
543
|
+
.listTasksScoped(project, undefined, 100, null)
|
|
544
|
+
.filter(one => one.state !== "done" && one.state !== "cancelled" && visible(one.repo))
|
|
545
|
+
.map(one => ({ id: one.id, title: one.title }));
|
|
546
|
+
const chrome = chromeFor(project, "tasks");
|
|
547
|
+
return sendScreen(response, 200, newTaskPage(chrome, project, csrf, revision, null, chainable, store.permissionDefault().mode, store.qualityDefault().mode, chrome.projects ?? []));
|
|
548
|
+
}
|
|
549
|
+
const taskLive = matchTaskPath(url.pathname, "/live$");
|
|
550
|
+
if (taskLive !== null) {
|
|
551
|
+
// The live task page: a nudge the moment the task changes, and who else
|
|
552
|
+
// has it open. Hints only — the page reads the task the usual way.
|
|
553
|
+
if (who.via !== "cookie")
|
|
554
|
+
return respond(response, 403, "application/json", JSON.stringify({ error: "session" }));
|
|
555
|
+
const family = familyOf(taskLive.taskId);
|
|
556
|
+
if (family === null || family.problem !== null)
|
|
557
|
+
return respond(response, 404, "application/json", JSON.stringify({ error: "task" }));
|
|
558
|
+
response.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-store", "x-content-type-options": "nosniff", "x-accel-buffering": "no" });
|
|
559
|
+
const name = who.name, repo = family.root.repo;
|
|
560
|
+
taskRooms.join(family.root.id, {
|
|
561
|
+
name, response,
|
|
562
|
+
// Rechecked while open, from the cookie and the account (never a request's context): a sign-out or a narrowed account stops it.
|
|
563
|
+
valid: () => { const again = identify(request, false); return again !== null && again.name === name && rowVisible(liveCeiling(), repo) && store.accountCanAccess(name, repo); },
|
|
564
|
+
});
|
|
565
|
+
request.once("close", () => { if (!response.writableEnded)
|
|
566
|
+
response.end(); });
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
const task = matchTaskPath(url.pathname, "");
|
|
570
|
+
if (task !== null) {
|
|
571
|
+
const family = familyOf(task.taskId);
|
|
572
|
+
if (family === null)
|
|
573
|
+
return refuse(response, who, 404, "no such task", "/tasks");
|
|
574
|
+
const version = url.searchParams.get("version");
|
|
575
|
+
if (version !== null && !family.versions.some(one => one.id === version))
|
|
576
|
+
return refuse(response, who, 404, "That version is not available for this task.", taskHref(family.root.id));
|
|
577
|
+
// A version's own address lands on its family's page, still asking for the plan editor when it did (Chat's Edit plan).
|
|
578
|
+
if (task.taskId !== family.root.id)
|
|
579
|
+
return redirect(response, `${taskHref(family.root.id)}?version=${encodeURIComponent(task.taskId)}${url.searchParams.get("edit") === "plan" ? "&edit=plan" : ""}`);
|
|
580
|
+
return taskScreen(response, who, version ?? family.current.id, null, 200, undefined, undefined, url.searchParams.get("edit") === "plan");
|
|
581
|
+
}
|
|
582
|
+
// v103: a task's evidence pack, as a printable page or JSON (the whole family, sealed ledger entries included).
|
|
583
|
+
const evidence = matchTaskPath(url.pathname, "/evidence$");
|
|
584
|
+
if (evidence !== null) {
|
|
585
|
+
const access = workAccess();
|
|
586
|
+
const family = store.taskFamilyOf(evidence.taskId, access.repos, access.includeUnplaced);
|
|
587
|
+
if (family === null || !visible(family.root.repo))
|
|
588
|
+
return refuse(response, who, 404, "no such task", "/tasks");
|
|
589
|
+
if (evidence.taskId !== family.root.id)
|
|
590
|
+
return redirect(response, `${taskHref(family.root.id)}/evidence${url.search}`);
|
|
591
|
+
const pack = evidencePack(store, family.root.id, access, who.name, now, evidenceRoot);
|
|
592
|
+
if (pack === null)
|
|
593
|
+
return refuse(response, who, 404, "no such task", "/tasks");
|
|
594
|
+
if (url.searchParams.get("format") === "json") {
|
|
595
|
+
response.writeHead(200, { "content-type": "application/json; charset=utf-8", "content-disposition": `attachment; filename="evidence-${family.root.id.replace(/[^A-Za-z0-9._-]/g, "_")}.json"`,
|
|
596
|
+
"cache-control": "no-store", "x-content-type-options": "nosniff" });
|
|
597
|
+
return void response.end(JSON.stringify(checkResponse("evidencePack", pack), null, 2));
|
|
598
|
+
}
|
|
599
|
+
return sendScreen(response, 200, screen(`Evidence pack · ${family.root.id}`, evidencePackHtml(pack), { chrome: chromeFor(family.root.repo, "tasks") }));
|
|
600
|
+
}
|
|
601
|
+
if (url.pathname === "/runs") {
|
|
602
|
+
const raw = url.searchParams.get("before");
|
|
603
|
+
let before = null;
|
|
604
|
+
if (raw !== null) {
|
|
605
|
+
if (!/^[1-9][0-9]{0,14}$/.test(raw) || !Number.isSafeInteger(Number(raw))) {
|
|
606
|
+
return refuse(response, who, 400, "that cursor is not a page", "/runs");
|
|
607
|
+
}
|
|
608
|
+
before = Number(raw);
|
|
609
|
+
}
|
|
610
|
+
const rows = store.listRunsBefore(before, RUNS_PAGE, project);
|
|
611
|
+
const verdicts = store.proofVerdictsFor(rows.map(one => one.id));
|
|
612
|
+
const accepted = new Set(rows.filter(one => store.proofAcceptance(one.id) !== null).map(one => one.id));
|
|
613
|
+
return sendScreen(response, 200, runsPage(chromeFor(project, "runs"), rows, liveRunIds(rows), rows.length === RUNS_PAGE ? rows[rows.length - 1]?.id ?? null : null, verdicts, accepted));
|
|
614
|
+
}
|
|
615
|
+
const run = /^\/r\/([0-9]{1,15})$/.exec(url.pathname);
|
|
616
|
+
if (run !== null) {
|
|
617
|
+
const found = store.getRun(Number(run[1]));
|
|
618
|
+
if (found === null || !runVisible(found)) {
|
|
619
|
+
return refuse(response, who, 404, "no such run", "/runs");
|
|
620
|
+
}
|
|
621
|
+
const taskId = store.externalIdFor(found.taskRef) ?? "?";
|
|
622
|
+
const running = runIsLive(found);
|
|
623
|
+
if (url.searchParams.get("fragment") === "check") {
|
|
624
|
+
response.setHeader("cache-control", "no-store");
|
|
625
|
+
return respond(response, 200, "text/html; charset=utf-8", checkProgressHtml(store.checkProgress(found.id)));
|
|
626
|
+
}
|
|
627
|
+
if (url.searchParams.get("fragment") === "facts") {
|
|
628
|
+
// The facts region alone — same auth and ceiling as the page; a
|
|
629
|
+
// finished or no-longer-live run says so rather than growing forms
|
|
630
|
+
// (finding 5; round-4 finding 15).
|
|
631
|
+
return respond(response, 200, "text/html; charset=utf-8", runFactsFragment(found, taskId, running, store.runRoute(found.id)));
|
|
632
|
+
}
|
|
633
|
+
if (url.searchParams.get("fragment") === "peek") {
|
|
634
|
+
// The live peek: cookie sessions only (v2 §3), never stored, never
|
|
635
|
+
// cached by anything downstream.
|
|
636
|
+
if (who.via !== "cookie")
|
|
637
|
+
return respond(response, 403, "text/plain; charset=utf-8", "the live peek is a browser session's view");
|
|
638
|
+
const peeked = await peekFragment(found.id, who.session.csrf, options.editorLinks !== undefined && found.runner === options.localRunner && who.session.editorLinks === true);
|
|
639
|
+
response.setHeader("cache-control", "no-store");
|
|
640
|
+
if (peeked.retryAfter !== undefined)
|
|
641
|
+
response.setHeader("retry-after", String(peeked.retryAfter));
|
|
642
|
+
return respond(response, peeked.status, "text/html; charset=utf-8", peeked.body);
|
|
643
|
+
}
|
|
644
|
+
if (url.searchParams.get("fragment") === "transcript") {
|
|
645
|
+
// The live transcript window (arc 1): raw sanitized TEXT as JSON —
|
|
646
|
+
// the sink is textContent, never innerHTML. Guards, enumerated:
|
|
647
|
+
// cookie session; run visible (proved above); this machine's
|
|
648
|
+
// runner asserted; byte offsets validated; the file read through
|
|
649
|
+
// its own descriptor with the exact numeric-id name. Display
|
|
650
|
+
// state, not evidence — and never cached.
|
|
651
|
+
response.setHeader("cache-control", "no-store");
|
|
652
|
+
if (who.via !== "cookie")
|
|
653
|
+
return respond(response, 403, "application/json", JSON.stringify({ error: "session" }));
|
|
654
|
+
if (options.localRunner === undefined) {
|
|
655
|
+
return respond(response, 200, "application/json", JSON.stringify({ error: "off" }));
|
|
656
|
+
}
|
|
657
|
+
const fromRaw = url.searchParams.get("from") ?? "0";
|
|
658
|
+
const from = /^[0-9]{1,15}$/.test(fromRaw) ? Number(fromRaw) : Number.NaN;
|
|
659
|
+
if (!Number.isSafeInteger(from) || from < 0) {
|
|
660
|
+
return respond(response, 400, "application/json", JSON.stringify({ error: "offset" }));
|
|
661
|
+
}
|
|
662
|
+
const live = runIsLive(found);
|
|
663
|
+
// The final drain: a finished run's tail — including a torn last
|
|
664
|
+
// line — may be read until the sweep removes the file.
|
|
665
|
+
const window = readLiveWindow(evidenceRoot, found.id, from, !live);
|
|
666
|
+
if (!window.ok) {
|
|
667
|
+
if (window.reason === "replaced") {
|
|
668
|
+
return respond(response, 409, "application/json", JSON.stringify({ error: "replaced" }));
|
|
669
|
+
}
|
|
670
|
+
// Missing or unreadable: nothing to show. Final only when the
|
|
671
|
+
// run can never write again.
|
|
672
|
+
return respond(response, 200, "application/json", JSON.stringify({ text: "", nextOffset: from, final: !live }));
|
|
673
|
+
}
|
|
674
|
+
// Re-proof after the read (peek discipline): the run row still says
|
|
675
|
+
// what admission said, or nothing is shown.
|
|
676
|
+
const again = store.getRun(found.id);
|
|
677
|
+
if (again === null || !runVisible(again)) {
|
|
678
|
+
return respond(response, 200, "application/json", JSON.stringify({ text: "", nextOffset: from, final: true }));
|
|
679
|
+
}
|
|
680
|
+
return respond(response, 200, "application/json", JSON.stringify({ text: window.text, nextOffset: window.nextOffset, final: !live && window.eof }));
|
|
681
|
+
}
|
|
682
|
+
// One result page (2026-10-02): a builder's finished result opens on
|
|
683
|
+
// its task's result page, titled with the task. The raw run record
|
|
684
|
+
// waits there under Details, and here with ?record=1.
|
|
685
|
+
if (url.searchParams.get("record") === null && !running && found.role === "builder" && (found.outcome === "built" || found.outcome === "no-change") &&
|
|
686
|
+
completedRowFor(taskId, null)?.runId === found.id) {
|
|
687
|
+
const tab = parseResultTab(url.searchParams.get("tab"));
|
|
688
|
+
return redirect(response, `${reviewHref(taskId, found.id)}${tab === "summary" ? "" : `&tab=${tab}`}`);
|
|
689
|
+
}
|
|
690
|
+
// A build that failed or stopped opens on its own result page too: its changes, its checks and what went wrong.
|
|
691
|
+
if (url.searchParams.get("record") === null && !running && (found.role === "builder" || found.role === "scout") && attemptRowFor(taskId, found.id, null) !== null) {
|
|
692
|
+
const tab = parseResultTab(url.searchParams.get("tab"));
|
|
693
|
+
return redirect(response, `${reviewHref(taskId, found.id)}${tab === "summary" ? "" : `&tab=${tab}`}`);
|
|
694
|
+
}
|
|
695
|
+
// Pollers exist only for a LIVE run — an orphaned null-outcome run
|
|
696
|
+
// would otherwise be refetched forever (finding 15). The nonce is
|
|
697
|
+
// sendScreen's business now.
|
|
698
|
+
const artifacts = store.artifactsFor(found.id);
|
|
699
|
+
return sendScreen(response, 200, runPage(chromeFor(project, "runs", runListPane(project, found.id)), found, taskId, artifacts, terminalDiffView(artifacts, evidenceRoot), store.notesForRun(found.id), who.via === "cookie" ? who.session.csrf : "", store.liveDiffComments(found.id).filter(isRevisionFeedback), (() => {
|
|
700
|
+
const publication = store.publicationForRun(found.id);
|
|
701
|
+
return publication !== null && publication.prNumber !== null && store.hasOpenCiEpisode(publication.githubRepo, publication.prNumber)
|
|
702
|
+
? { pr: publication.prNumber }
|
|
703
|
+
: null;
|
|
704
|
+
})(), !running
|
|
705
|
+
? undefined
|
|
706
|
+
: regionScript("run-facts", "facts", 10) +
|
|
707
|
+
(options.localRunner === undefined ? "" : regionScript("run-peek", "peek", 15)) +
|
|
708
|
+
(options.localRunner === undefined ? "" : transcriptScript()), options.localRunner !== undefined, running,
|
|
709
|
+
// Editor links (arc 6): three statements align or nothing renders —
|
|
710
|
+
// the deployment capability, THIS machine's runner owning the run,
|
|
711
|
+
// and the session's own device-side yes.
|
|
712
|
+
options.editorLinks !== undefined &&
|
|
713
|
+
options.localRunner !== undefined &&
|
|
714
|
+
found.runner === options.localRunner &&
|
|
715
|
+
who.via === "cookie" &&
|
|
716
|
+
who.session.editorLinks === true &&
|
|
717
|
+
// A reviewer run (v29) never had a checkout — no files to open.
|
|
718
|
+
found.worktree !== null
|
|
719
|
+
? { worktree: found.worktree }
|
|
720
|
+
: null, options.editorLinks !== undefined &&
|
|
721
|
+
options.localRunner !== undefined &&
|
|
722
|
+
found.runner === options.localRunner &&
|
|
723
|
+
who.via === "cookie"
|
|
724
|
+
? { on: who.session.editorLinks === true }
|
|
725
|
+
: null, url.searchParams.get("noted") !== null, (() => {
|
|
726
|
+
const held = store.heldSessionOf(found.id);
|
|
727
|
+
if (held === null)
|
|
728
|
+
return null;
|
|
729
|
+
const authorization = store.readAuthorization(held.authorizationId);
|
|
730
|
+
return {
|
|
731
|
+
turns: store.sessionTurnsOf(found.id),
|
|
732
|
+
open: held.endedAt === null && held.state === "open",
|
|
733
|
+
state: authorization === null
|
|
734
|
+
? "session record"
|
|
735
|
+
: attendedWatchWords(authorization.lastBeatAt, now, authorization.absoluteExpiry),
|
|
736
|
+
cap: authorization?.maxSessionTurns ?? 0,
|
|
737
|
+
};
|
|
738
|
+
})(), options.attended !== undefined &&
|
|
739
|
+
who.via === "cookie" &&
|
|
740
|
+
!store.isDemo() &&
|
|
741
|
+
(found.outcome === "built" || found.outcome === "no-change") &&
|
|
742
|
+
store.openAuthorizationFor(found.taskRef) === null
|
|
743
|
+
? { taskId }
|
|
744
|
+
: null, structuredHandoffView(artifacts, evidenceRoot), proofBundleView(store, found, artifacts, evidenceRoot), store.runRoute(found.id), store.publicationForRun(found.id), reviewFactsFor(found.id),
|
|
745
|
+
// The shared result presentation (package 3) for a finished result.
|
|
746
|
+
!running && (found.outcome === "built" || found.outcome === "no-change")
|
|
747
|
+
? { detail: resultDetailOf(found, who, now), tab: parseResultTab(url.searchParams.get("tab")), user: who.name, requestToken: randomBytes(16).toString("hex") }
|
|
748
|
+
: null, store.getScope(taskId)?.digest ?? null));
|
|
749
|
+
}
|
|
750
|
+
const runArtifact = /^\/r\/([0-9]{1,15})\/evidence\/([0-9]{1,15})$/.exec(url.pathname);
|
|
751
|
+
if (runArtifact !== null) {
|
|
752
|
+
return runEvidence(response, Number(runArtifact[1]), Number(runArtifact[2]));
|
|
753
|
+
}
|
|
754
|
+
const contestPath = /^\/contest\/([0-9]{1,15})$/.exec(url.pathname);
|
|
755
|
+
if (contestPath !== null) {
|
|
756
|
+
return contestScreen(response, who, Number(contestPath[1]), null, 200);
|
|
757
|
+
}
|
|
758
|
+
if (url.pathname === "/routines") {
|
|
759
|
+
// The ceiling row by row, exactly as everywhere: a routine placed in
|
|
760
|
+
// a repo this server may not serve does not exist here.
|
|
761
|
+
const tracks = store
|
|
762
|
+
.routineTracks(project, now)
|
|
763
|
+
.filter(track => visible(track.routine.repo));
|
|
764
|
+
// ?template=<name> pre-fills the filing form from the shipped
|
|
765
|
+
// library — a pre-filled form, same guarded submission path.
|
|
766
|
+
const fromTemplate = url.searchParams.get("template");
|
|
767
|
+
const picked = fromTemplate === null ? null : templateByName(fromTemplate);
|
|
768
|
+
return sendScreen(response, 200, routinesPage(chromeFor(project, "routines"), tracks, {
|
|
769
|
+
csrf: who.via === "cookie" ? who.session.csrf : "",
|
|
770
|
+
revision: who.via === "cookie" ? who.session.projectRevision : 0,
|
|
771
|
+
problem: null,
|
|
772
|
+
prefill: picked !== null && picked.kind === "routine"
|
|
773
|
+
? {
|
|
774
|
+
name: picked.routineName,
|
|
775
|
+
goal: picked.goal,
|
|
776
|
+
not: picked.outOfScope ?? "",
|
|
777
|
+
touches: picked.touches.join(", "),
|
|
778
|
+
schedule: picked.schedule,
|
|
779
|
+
acceptance: acceptanceToLines(picked.acceptance).join("\n"),
|
|
780
|
+
}
|
|
781
|
+
: null,
|
|
782
|
+
}));
|
|
783
|
+
}
|
|
784
|
+
const routineScreen = /^\/routines\/([0-9]{1,15})$/.exec(url.pathname);
|
|
785
|
+
if (routineScreen !== null) {
|
|
786
|
+
const routine = store.getRoutine(Number(routineScreen[1]));
|
|
787
|
+
if (routine === null || !visible(routine.repo)) {
|
|
788
|
+
return refuse(response, who, 404, "no such routine", "/routines");
|
|
789
|
+
}
|
|
790
|
+
return routinePage(response, who, routine.id, null, 200);
|
|
791
|
+
}
|
|
792
|
+
const one = /^\/d\/([0-9]{1,15})$/.exec(url.pathname);
|
|
793
|
+
if (one !== null) {
|
|
794
|
+
const decision = store.getDecision(Number(one[1]));
|
|
795
|
+
if (decision === null)
|
|
796
|
+
return refuse(response, who, 404, "no such decision");
|
|
797
|
+
const run = store.getRun(decision.run);
|
|
798
|
+
if (run !== null && !visible(taskRepoOf(run.taskRef))) {
|
|
799
|
+
return refuse(response, who, 404, "no such decision");
|
|
800
|
+
}
|
|
801
|
+
const taskId = taskOf(store, decision);
|
|
802
|
+
const decisionReturn = url.searchParams.get("return");
|
|
803
|
+
const back = decisionReturn === null ? null : safeChatReturn(decisionReturn);
|
|
804
|
+
return sendScreen(response, 200, decisionPage(chromeFor(project, "none"), decision, taskId, store.evidenceFor(decision.id), who, now, back));
|
|
805
|
+
}
|
|
806
|
+
const artifact = /^\/d\/([0-9]{1,15})\/evidence\/([0-9]{1,15})$/.exec(url.pathname);
|
|
807
|
+
if (artifact !== null) {
|
|
808
|
+
return decisionEvidence(response, Number(artifact[1]), Number(artifact[2]));
|
|
809
|
+
}
|
|
810
|
+
return refuse(response, who, 404, "There's no page at this address.", "/chat");
|
|
811
|
+
}
|
|
812
|
+
async function post(ctx) {
|
|
813
|
+
const { url, who, request, response, now, project, chosenProject, posted, route } = ctx;
|
|
814
|
+
// The attended beat answers BEFORE the shared mutation guard (v28): it
|
|
815
|
+
// carries no parameters, so there is no csrf token to check — its OWN
|
|
816
|
+
// guard is complete and STRICTER for the browsers this console
|
|
817
|
+
// supports: cookie session, form content-type (form() enforced it),
|
|
818
|
+
// and `Sec-Fetch-Site: same-origin`, which cross-site POSTs cannot
|
|
819
|
+
// send and same-origin fetch always does. Renewal-only: an attacker
|
|
820
|
+
// who somehow posted could only keep the operator's OWN sessions from
|
|
821
|
+
// lapsing — no mint, no spend, no read.
|
|
822
|
+
if (url.pathname === "/session/attended-beats") {
|
|
823
|
+
return attendedBeats(who, request, response, clock());
|
|
824
|
+
}
|
|
825
|
+
if (url.pathname === '/code' || url.pathname.startsWith('/code/')) {
|
|
826
|
+
const body = readForm(posted, CONSOLE_FORMS.code);
|
|
827
|
+
const wantsJson = request.headers.accept?.includes('application/json') === true;
|
|
828
|
+
const fail = (status, message, delivery = 'rejected', sessionId) => wantsJson
|
|
829
|
+
? respond(response, status, 'application/json', JSON.stringify({ ok: false, error: message, delivery, ...(sessionId ? { sessionId } : {}) }))
|
|
830
|
+
: refuse(response, who, status, message, '/code');
|
|
831
|
+
if (who.via !== 'cookie' || !codingActorAllowed({ name: who.name, generation: who.session.generation }) || store.isDemo())
|
|
832
|
+
return fail(403, 'Coding sessions require installation-wide operator access. You can manage project tasks in Work.');
|
|
833
|
+
if (!runtime.coding)
|
|
834
|
+
return fail(409, runtime.codingProblem || 'Open Toolroll on the machine with your installed coding agent.');
|
|
835
|
+
if ([...new Set(body.keys())].some(key => body.sent.getAll(key).length !== 1))
|
|
836
|
+
return fail(400, 'Submit one value for each field.');
|
|
837
|
+
const actor = { name: who.name, generation: who.session.generation };
|
|
838
|
+
const permitted = (repo) => visible(repo) && codingProjectAllowed(repo);
|
|
839
|
+
// Sprint 8: a coding session is Codex taking turns: the organisation policy decides whether it may (provider,
|
|
840
|
+
// model, and a ceiling Codex can't run under), when it starts and on every action that makes it take another turn.
|
|
841
|
+
const turnAction = /^\/code\/[a-f0-9]{32}\/(send|resume|recover|continue|answer)$/.test(url.pathname);
|
|
842
|
+
if (url.pathname === '/code/start' || turnAction) {
|
|
843
|
+
const stopped = store.sessionPolicyRefusal('codex', url.pathname === '/code/start' ? body.get('model')?.trim() || null : null, 'coding sessions');
|
|
844
|
+
if (stopped !== null)
|
|
845
|
+
return fail(403, stopped);
|
|
846
|
+
}
|
|
847
|
+
try {
|
|
848
|
+
let id;
|
|
849
|
+
if (url.pathname === '/code/start') {
|
|
850
|
+
const repo = body.get('repo') ?? '';
|
|
851
|
+
if (!permitted(repo))
|
|
852
|
+
return fail(403, 'Choose a project available to your account.');
|
|
853
|
+
if (!authenticateApprover(who, body.get('password') ?? '').ok)
|
|
854
|
+
return fail(403, 'Enter your Toolroll password to authorize this coding session.');
|
|
855
|
+
const session = await runtime.coding.start(actor, { repo, title: body.get('title') ?? '', model: body.get('model')?.trim() || null, prompt: body.get('prompt') ?? '', requestId: body.get('requestId') ?? '' });
|
|
856
|
+
id = session.id;
|
|
857
|
+
}
|
|
858
|
+
else {
|
|
859
|
+
const match = /^\/code\/([a-f0-9]{32})\/(send|stop|resume|recover|continue|ship|answer)$/.exec(url.pathname);
|
|
860
|
+
if (!match)
|
|
861
|
+
return fail(404, 'Coding action not found.');
|
|
862
|
+
id = match[1];
|
|
863
|
+
const session = runtime.coding.get(id, actor);
|
|
864
|
+
if (!permitted(session.repo))
|
|
865
|
+
return fail(403, 'That project is outside your access.');
|
|
866
|
+
if (match[2] === 'ship') {
|
|
867
|
+
const made = createCodingHandoff(store, { sessionId: id, actor: who.name, repo: session.repo, base: body.get('base') ?? '', candidate: body.get('candidate') ?? '', title: body.get('title') ?? '', goal: body.get('goal') ?? '', acceptance: (body.get('acceptance') ?? '').split('\n').map(line => line.trim()).filter(Boolean).map((statement, index) => ({ id: `c${index + 1}`, statement, evidence: ['check', 'changed-path', ...(body.get('visual') === 'yes' ? ['screenshot'] : [])] })) });
|
|
868
|
+
return wantsJson ? respond(response, 200, 'application/json', JSON.stringify({ ok: true, taskId: made.taskId })) : redirect(response, `/t/${encodeURIComponent(made.taskId)}`);
|
|
869
|
+
}
|
|
870
|
+
if (match[2] === 'send')
|
|
871
|
+
await runtime.coding.send(id, actor, body.get('prompt') ?? '', body.get('requestId') ?? '');
|
|
872
|
+
else if (match[2] === 'stop')
|
|
873
|
+
await runtime.coding.stop(id, actor);
|
|
874
|
+
else if (match[2] === 'resume')
|
|
875
|
+
await runtime.coding.resume(id, actor);
|
|
876
|
+
else if (match[2] === 'recover')
|
|
877
|
+
await runtime.coding.recover(id, actor);
|
|
878
|
+
else if (match[2] === 'continue')
|
|
879
|
+
await runtime.coding.continueSaved(id, actor);
|
|
880
|
+
else {
|
|
881
|
+
const token = body.get('requestId') ?? '';
|
|
882
|
+
const pending = runtime.coding.snapshot(id, actor).requests.find(r => r.id === token);
|
|
883
|
+
if (!pending)
|
|
884
|
+
return fail(409, 'This request is no longer waiting for an answer.');
|
|
885
|
+
const decision = body.get('decision');
|
|
886
|
+
if (decision !== 'accept' && decision !== 'decline' && decision !== 'cancel')
|
|
887
|
+
return fail(400, 'Choose an explicit decision for this request.');
|
|
888
|
+
if (pending.kind !== 'questions' && decision === 'accept' && !authenticateApprover(who, body.get('password') ?? '').ok)
|
|
889
|
+
return fail(403, 'Enter your password to approve this additional access.');
|
|
890
|
+
const answers = body.has('answers') ? JSON.parse(body.get('answers')) : Object.fromEntries(pending.questions.map(q => [q.id, { answers: [body.get(`question:${q.id}`) ?? ''] }]));
|
|
891
|
+
runtime.coding.answer(id, actor, token, decision, answers);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
// Authorization is checked again after asynchronous startup/transport work.
|
|
895
|
+
if (!codingActorAllowed(actor))
|
|
896
|
+
return fail(403, 'Your access changed. Sign in again.', 'unknown', id);
|
|
897
|
+
return wantsJson ? respond(response, 200, 'application/json', JSON.stringify({ ok: true, id })) : redirect(response, `/code/${id}`);
|
|
898
|
+
}
|
|
899
|
+
catch (error) {
|
|
900
|
+
if (!codingActorAllowed(actor))
|
|
901
|
+
return fail(403, 'Your access changed. Sign in again.', error instanceof CodingActionError ? error.delivery : 'rejected', error instanceof CodingActionError ? error.sessionId : undefined);
|
|
902
|
+
const message = error instanceof Error ? error.message : 'The coding action could not finish. Your draft is preserved.';
|
|
903
|
+
const ship = /^\/code\/([a-f0-9]{32})\/ship$/.exec(url.pathname);
|
|
904
|
+
if (ship && !wantsJson) {
|
|
905
|
+
try {
|
|
906
|
+
const preview = codingHandoffPreview(store, { sessionId: ship[1], actor: who.name });
|
|
907
|
+
return sendScreen(response, 409, screen('Review for shipping', codingShippingHtml(preview, who.session.csrf, body.sent, message), { chrome: chromeFor(preview.repo, 'code') }));
|
|
908
|
+
}
|
|
909
|
+
catch { }
|
|
910
|
+
}
|
|
911
|
+
return fail(409, message, error instanceof CodingActionError ? error.delivery : 'rejected', error instanceof CodingActionError ? error.sessionId : undefined);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
if (url.pathname === "/tasks/add") {
|
|
915
|
+
const body = readForm(posted, CONSOLE_FORMS.tasksAdd);
|
|
916
|
+
const project = projectOf(who, request);
|
|
917
|
+
if (project === undefined) {
|
|
918
|
+
return refuse(response, who, 403, "that project is outside what this server was configured to show");
|
|
919
|
+
}
|
|
920
|
+
// Stale-tab guard: a form rendered under one open project must not
|
|
921
|
+
// create into a different one switched-to since (finding 6).
|
|
922
|
+
if (who.via === "cookie") {
|
|
923
|
+
const seen = body.get("projectRevision");
|
|
924
|
+
if (seen !== null && seen !== String(who.session.projectRevision)) {
|
|
925
|
+
return refuse(response, who, 409, "the open project changed since this form was rendered — reload and try again", "/tasks");
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
const id = (body.get("id") ?? "").trim();
|
|
929
|
+
const title = body.get("title") ?? "";
|
|
930
|
+
// The EFFECTIVE placement (repo onboarding, findings 15/35): the
|
|
931
|
+
// trimmed, nonempty posted repo, else the open project — an empty
|
|
932
|
+
// input falls through correctly. In root mode the effective path is
|
|
933
|
+
// proved by authorizedProject EXACTLY as typed-then-canonicalized,
|
|
934
|
+
// and [canonical] is the admitted list — a fresh clone under a root
|
|
935
|
+
// must be able to receive its first task without waiting to appear
|
|
936
|
+
// in any table.
|
|
937
|
+
const repoGiven = (body.get("repo") ?? "").trim();
|
|
938
|
+
const effective = repoGiven !== "" ? repoGiven : (project ?? "");
|
|
939
|
+
if (restricted() && !visible(effective === "" ? null : effective))
|
|
940
|
+
return refuse(response, who, 403, "That project is outside your access.", "/projects");
|
|
941
|
+
// A scoped console refuses an EMPTY placement server-side (verification
|
|
942
|
+
// finding 1): the form's `required` is a courtesy, not the guard — a
|
|
943
|
+
// direct POST with no project open must not mint an unplaced task
|
|
944
|
+
// under a ceiling. Unscoped mode keeps its historic unplaced filings.
|
|
945
|
+
if (!unscopedMode && effective === "") {
|
|
946
|
+
const csrf = who.via === "cookie" ? who.session.csrf : "";
|
|
947
|
+
return sendScreen(response, 400, tasksPage(chromeFor(project, "tasks"), familyTasksInView(project).slice(0, 200), null, csrf, "name a repository — no project is open, so the task must say where it belongs", project, null, store.permissionDefault().mode, store.qualityDefault().mode, store.replacements()));
|
|
948
|
+
}
|
|
949
|
+
let repo = effective;
|
|
950
|
+
let admitted = unscopedMode ? null : admissionList() ?? [];
|
|
951
|
+
const rootMode = !unscopedMode && ceiling.roots.length > 0;
|
|
952
|
+
if (rootMode && effective !== "") {
|
|
953
|
+
const canonical = (await authorizedProject(liveCeiling(), effective)) ? canonicalProject(effective) : null;
|
|
954
|
+
if (canonical === null || canonical === undefined) {
|
|
955
|
+
const csrf = who.via === "cookie" ? who.session.csrf : "";
|
|
956
|
+
return sendScreen(response, 403, tasksPage(chromeFor(project, "tasks"), familyTasksInView(project).slice(0, 200), null, csrf, `${effective} is outside what this server was configured to show`, project, null, store.permissionDefault().mode, store.qualityDefault().mode, store.replacements()));
|
|
957
|
+
}
|
|
958
|
+
repo = canonical;
|
|
959
|
+
admitted = [canonical];
|
|
960
|
+
}
|
|
961
|
+
const goal = body.get("goal") ?? "";
|
|
962
|
+
const notThis = body.get("not") ?? "";
|
|
963
|
+
const touchesGiven = (body.get("touches") ?? "")
|
|
964
|
+
.split(/[\n,]/)
|
|
965
|
+
.map(one => one.trim())
|
|
966
|
+
.filter(one => one !== "");
|
|
967
|
+
const scout = body.get("scout") === "1";
|
|
968
|
+
const permissionMode = body.get("permission-mode");
|
|
969
|
+
if (permissionMode !== null && permissionMode !== "auto" && permissionMode !== "bypassPermissions") {
|
|
970
|
+
return refuse(response, who, 400, "permissions must be Auto or Full access", "/tasks/new");
|
|
971
|
+
}
|
|
972
|
+
const qualityMode = body.get("quality-mode");
|
|
973
|
+
if (qualityMode !== null && !isQualityMode(qualityMode)) {
|
|
974
|
+
return refuse(response, who, 400, "quality must be Default or Strict / release", "/tasks/new");
|
|
975
|
+
}
|
|
976
|
+
// One filing door for every surface (Codex adoption review, finding 7).
|
|
977
|
+
const after = (body.get("after") ?? "").trim();
|
|
978
|
+
let chainProblem = null;
|
|
979
|
+
const made = store.transact(() => {
|
|
980
|
+
const filed = fileTaskProposal(store, {
|
|
981
|
+
...(id === "" ? {} : { id }),
|
|
982
|
+
title,
|
|
983
|
+
...(repo === "" ? {} : { repo }),
|
|
984
|
+
...(goal === "" ? {} : { goal, acceptance: acceptanceLinesToInput((body.get("acceptance") ?? "").split("\n")) }),
|
|
985
|
+
outOfScope: notThis === "" ? null : notThis,
|
|
986
|
+
touches: touchesGiven,
|
|
987
|
+
...(permissionMode === null ? {} : { permissionMode }),
|
|
988
|
+
...(qualityMode === null ? {} : { qualityMode }),
|
|
989
|
+
...(scout ? { deliverable: "report" } : {}),
|
|
990
|
+
planning: scout
|
|
991
|
+
? "skip"
|
|
992
|
+
: body.get("planning-policy") === "choice"
|
|
993
|
+
? body.get("plan-first") === "1" ? "required" : "skip"
|
|
994
|
+
: "auto",
|
|
995
|
+
filedVia: "console", filedBy: { name: who.name, kind: "person" },
|
|
996
|
+
...(admitted === null ? {} : { admittedRepos: admitted }),
|
|
997
|
+
}, now);
|
|
998
|
+
if (filed.ok && after !== "") {
|
|
999
|
+
const afterRef = store.lookupRef(after);
|
|
1000
|
+
const chained = store.getTask(after) !== null && afterRef !== null && visible(afterRef.repo)
|
|
1001
|
+
? store.addEdge(filed.id, after) : { ok: false, reason: "that task does not exist here" };
|
|
1002
|
+
if (!chained.ok)
|
|
1003
|
+
chainProblem = chained.reason;
|
|
1004
|
+
}
|
|
1005
|
+
// Missing dependencies leave a reviewable, inert task. They must
|
|
1006
|
+
// never be silently dropped before automatic approval starts work.
|
|
1007
|
+
if (filed.ok && chainProblem === null && who.via === "cookie")
|
|
1008
|
+
applyModeToNewFiling(store, filed.id, who.name, now);
|
|
1009
|
+
return filed;
|
|
1010
|
+
});
|
|
1011
|
+
if (!made.ok) {
|
|
1012
|
+
const csrf = who.via === "cookie" ? who.session.csrf : "";
|
|
1013
|
+
return sendScreen(response, made.reason === "backlog-full" ? 429 : 400, tasksPage(chromeFor(project, "tasks"), familyTasksInView(project).slice(0, 200), null, csrf, made.message, project, { title, goal, not: notThis, touches: body.get("touches") ?? "", acceptance: body.get("acceptance") ?? "", values: body.sent }, store.permissionDefault().mode, store.qualityDefault().mode, store.replacements()));
|
|
1014
|
+
}
|
|
1015
|
+
const actionContext = requestContext.getStore();
|
|
1016
|
+
if (actionContext !== undefined)
|
|
1017
|
+
actionContext.createdTask = made.id;
|
|
1018
|
+
// A proved root-mode placement joins the project table (finding 15):
|
|
1019
|
+
// the new task's home is openable and admissible from now on.
|
|
1020
|
+
if (rootMode && repo !== "")
|
|
1021
|
+
store.upsertProject(repo, projectName(repo), now);
|
|
1022
|
+
if (chainProblem !== null)
|
|
1023
|
+
return taskScreen(response, who, made.id, `the task was created, but could not be made to wait for ${after} — ${chainProblem}`, 200);
|
|
1024
|
+
return redirect(response, taskHref(made.id));
|
|
1025
|
+
}
|
|
1026
|
+
if (url.pathname === "/queue/move" || url.pathname === "/queue/note") {
|
|
1027
|
+
const body = readForm(posted, CONSOLE_FORMS.queue);
|
|
1028
|
+
const project = projectOf(who, request);
|
|
1029
|
+
if (project === undefined) {
|
|
1030
|
+
return refuse(response, who, 403, "that project is outside what this server was configured to show");
|
|
1031
|
+
}
|
|
1032
|
+
// The note is a global runner label, not project-scoped work: the
|
|
1033
|
+
// fleet screen (project-less) posts it too, so the null-project gate
|
|
1034
|
+
// applies only to moves, which the task-level check below re-proves.
|
|
1035
|
+
if (url.pathname === "/queue/note") {
|
|
1036
|
+
const worker = (body.get("runner") ?? "").trim();
|
|
1037
|
+
const note = (body.get("note") ?? "").trim();
|
|
1038
|
+
if (note.length > 200 || hasForbiddenControls(note)) {
|
|
1039
|
+
return refuse(response, who, 400, "that note will not render, so it will not store", QUEUE_VIEW);
|
|
1040
|
+
}
|
|
1041
|
+
const set = store.setRunnerQueueNote(worker, note === "" ? null : note);
|
|
1042
|
+
if (!set.ok)
|
|
1043
|
+
return refuse(response, who, 404, "no such worker", QUEUE_VIEW);
|
|
1044
|
+
return redirect(response, body.get("from") === "fleet" ? "/fleet" : QUEUE_VIEW);
|
|
1045
|
+
}
|
|
1046
|
+
if (who.via === "cookie") {
|
|
1047
|
+
const seen = body.get("projectRevision");
|
|
1048
|
+
if (seen !== null && seen !== String(who.session.projectRevision)) {
|
|
1049
|
+
return refuse(response, who, 409, "the open project changed since this form was rendered — reload and try again", QUEUE_VIEW);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
// A drag posts in place (fetch) when it can; the page re-renders its
|
|
1053
|
+
// own fragment on success. A plain form still works with no script.
|
|
1054
|
+
const inPlace = body.get("respond") === "fragment";
|
|
1055
|
+
const fromFleet = who.via === "cookie" && body.get("projectRevision") === null;
|
|
1056
|
+
const respondMove = (status, message) => {
|
|
1057
|
+
if (!inPlace)
|
|
1058
|
+
return status === 409 ? refuse(response, who, 409, message, QUEUE_VIEW) : redirect(response, QUEUE_VIEW);
|
|
1059
|
+
respond(response, status, "text/plain; charset=utf-8", message);
|
|
1060
|
+
};
|
|
1061
|
+
const moveReason = (reason) => reason === "stale"
|
|
1062
|
+
? "the queue moved underneath you — it just reloaded"
|
|
1063
|
+
: reason === "claimed" || reason === "contest-open"
|
|
1064
|
+
? "that task is being taken right now — it keeps its claim"
|
|
1065
|
+
: reason === "worker-retired"
|
|
1066
|
+
? "that worker is retired — drag its work elsewhere, or register the name again"
|
|
1067
|
+
: reason === "no-such-worker"
|
|
1068
|
+
? "no such worker"
|
|
1069
|
+
: "that task is not in this queue any more";
|
|
1070
|
+
const taskId = (body.get("task") ?? "").trim();
|
|
1071
|
+
const columnGiven = (body.get("column") ?? "").trim();
|
|
1072
|
+
const toRunner = columnGiven === "" || columnGiven === "anyone" ? null : columnGiven;
|
|
1073
|
+
const beforeGiven = (body.get("before") ?? "").trim();
|
|
1074
|
+
const revisionGiven = Number(body.get("queueRevision") ?? "");
|
|
1075
|
+
// The queue's own screen enforces the open project; the fleet screen
|
|
1076
|
+
// is cross-project, so the ceiling is the only wall it needs.
|
|
1077
|
+
const belongs = (id) => {
|
|
1078
|
+
const ref = store.lookupRef(id);
|
|
1079
|
+
return ref !== null && visible(ref.repo) && (fromFleet || project === null || ref.repo === null || ref.repo === project);
|
|
1080
|
+
};
|
|
1081
|
+
if (!belongs(taskId) || (beforeGiven !== "" && beforeGiven !== QUEUE_FRONT && !belongs(beforeGiven))) {
|
|
1082
|
+
return respondMove(404, "that task is not in this queue");
|
|
1083
|
+
}
|
|
1084
|
+
// The no-script "move to the front" button cannot name the front: the
|
|
1085
|
+
// front of a task's partition is decided by the store's exact repo AND
|
|
1086
|
+
// assignment, and the page's snapshot is bounded — so the sentinel is
|
|
1087
|
+
// resolved HERE, against a fresh snapshot, into a real task id (slice
|
|
1088
|
+
// 1b, fix 1). It is honored only within the task's own column; a
|
|
1089
|
+
// cross-column front is not provable from a bounded snapshot.
|
|
1090
|
+
let beforeTaskId = beforeGiven === "" ? null : beforeGiven;
|
|
1091
|
+
if (beforeGiven === QUEUE_FRONT) {
|
|
1092
|
+
const ref = store.lookupRef(taskId);
|
|
1093
|
+
if (ref === null)
|
|
1094
|
+
return respondMove(404, "that task is not in this queue");
|
|
1095
|
+
if (ref.assignedRunner !== toRunner) {
|
|
1096
|
+
return respondMove(409, "move to the front works inside a task's own column — drag it across to reserve it elsewhere");
|
|
1097
|
+
}
|
|
1098
|
+
const now = clock();
|
|
1099
|
+
const snapshot = store.queueScoped(ref.repo, now);
|
|
1100
|
+
const self = snapshot.find(one => one.id === taskId);
|
|
1101
|
+
// A claim or a contest can land after the form rendered WITHOUT
|
|
1102
|
+
// bumping queueRevision, so the snapshot — not the form — decides
|
|
1103
|
+
// whether the card is still free. A taken or vanished card is the
|
|
1104
|
+
// typed refusal, never a silent no-op that would skip moveTask()'s
|
|
1105
|
+
// own claimed/contest recheck.
|
|
1106
|
+
if (self === undefined)
|
|
1107
|
+
return respondMove(409, moveReason("unknown-task"));
|
|
1108
|
+
if (self.taken)
|
|
1109
|
+
return respondMove(409, moveReason("claimed"));
|
|
1110
|
+
const partition = snapshot.filter(one => one.repo === ref.repo && one.assignedRunner === ref.assignedRunner && !one.taken);
|
|
1111
|
+
const front = partition[0];
|
|
1112
|
+
if (front === undefined)
|
|
1113
|
+
return respondMove(409, moveReason("unknown-task"));
|
|
1114
|
+
if (front.id === taskId) {
|
|
1115
|
+
// Already the front: a no-op, but only against the revision the
|
|
1116
|
+
// form was rendered with — this branch never reaches moveTask()'s
|
|
1117
|
+
// CAS, so the check is made here.
|
|
1118
|
+
if (!Number.isSafeInteger(revisionGiven) || revisionGiven !== store.queueRevision()) {
|
|
1119
|
+
return respondMove(409, moveReason("stale"));
|
|
1120
|
+
}
|
|
1121
|
+
return respondMove(200, "already at the front");
|
|
1122
|
+
}
|
|
1123
|
+
beforeTaskId = front.id;
|
|
1124
|
+
}
|
|
1125
|
+
const moved = store.moveTask({
|
|
1126
|
+
taskId,
|
|
1127
|
+
toRunner,
|
|
1128
|
+
beforeTaskId,
|
|
1129
|
+
...(Number.isSafeInteger(revisionGiven) ? { queueRevision: revisionGiven } : {}),
|
|
1130
|
+
}, clock());
|
|
1131
|
+
if (!moved.ok) {
|
|
1132
|
+
return respondMove(409, moveReason(moved.reason));
|
|
1133
|
+
}
|
|
1134
|
+
return respondMove(200, "moved");
|
|
1135
|
+
}
|
|
1136
|
+
const answer = /^\/d\/([0-9]{1,15})\/answer$/.exec(url.pathname);
|
|
1137
|
+
if (answer !== null) {
|
|
1138
|
+
const body = readForm(posted, CONSOLE_FORMS.decisionAnswer);
|
|
1139
|
+
const id = Number(answer[1]);
|
|
1140
|
+
const requestedReturn = body.get("return");
|
|
1141
|
+
const decisionBack = requestedReturn === "next"
|
|
1142
|
+
? "/next"
|
|
1143
|
+
: requestedReturn === null
|
|
1144
|
+
? `/d/${id}`
|
|
1145
|
+
: safeChatReturn(requestedReturn);
|
|
1146
|
+
const decision = store.getDecision(id);
|
|
1147
|
+
if (decision === null)
|
|
1148
|
+
return refuse(response, who, 404, "no such decision");
|
|
1149
|
+
const answeringRun = store.getRun(decision.run);
|
|
1150
|
+
if (answeringRun !== null && !visible(taskRepoOf(answeringRun.taskRef))) {
|
|
1151
|
+
return refuse(response, who, 404, "no such decision");
|
|
1152
|
+
}
|
|
1153
|
+
const choice = body.get("choice") ?? "";
|
|
1154
|
+
const chosen = decision.options.find(option => option.id === choice);
|
|
1155
|
+
// Irreversible options never ride one accidental tap: the form arms
|
|
1156
|
+
// them behind an explicit confirmation field, and the server checks —
|
|
1157
|
+
// the client rendering is convenience, this is the rule.
|
|
1158
|
+
if (chosen !== undefined && !chosen.reversible && body.get("confirm") !== "yes") {
|
|
1159
|
+
return refuse(response, who, 400, "an irreversible choice must be confirmed", requestedReturn === null ? `/d/${id}` : `/d/${id}?return=${encodeURIComponent(decisionBack)}`);
|
|
1160
|
+
}
|
|
1161
|
+
const note = body.get("note");
|
|
1162
|
+
const answered = store.answerDecision({
|
|
1163
|
+
id,
|
|
1164
|
+
choice,
|
|
1165
|
+
by: who.name,
|
|
1166
|
+
via: "web",
|
|
1167
|
+
...(note === null || note === "" ? {} : { note }),
|
|
1168
|
+
}, now);
|
|
1169
|
+
if (!answered.ok) {
|
|
1170
|
+
const status = answered.reason === "bad-option" || answered.reason === "bad-note" ? 400 : 409;
|
|
1171
|
+
const why = answered.reason === "already-answered" ? "already answered — somebody got there first" : answered.reason;
|
|
1172
|
+
return refuse(response, who, status, why, requestedReturn === null ? `/d/${id}` : decisionBack);
|
|
1173
|
+
}
|
|
1174
|
+
return redirect(response, decisionBack);
|
|
1175
|
+
}
|
|
1176
|
+
const contestAct = /^\/contest\/([0-9]{1,15})\/(arm|pick|abandon)$/.exec(url.pathname);
|
|
1177
|
+
if (contestAct !== null) {
|
|
1178
|
+
if (who.via !== "cookie")
|
|
1179
|
+
return refuse(response, who, 403, REMOTE_MESSAGES["step-up"]);
|
|
1180
|
+
const body = readForm(posted, CONSOLE_FORMS.contest);
|
|
1181
|
+
const contestId = Number(contestAct[1]);
|
|
1182
|
+
const verb = contestAct[2];
|
|
1183
|
+
const data = contestData(contestId);
|
|
1184
|
+
if (data === null)
|
|
1185
|
+
return refuse(response, who, 404, "no such tournament", "/board");
|
|
1186
|
+
const { view } = data;
|
|
1187
|
+
if (verb === "arm") {
|
|
1188
|
+
// The ceremony's nonce is minted by THIS POST, never by a GET
|
|
1189
|
+
// (round-3 finding 30): a prefetched or crawled page must not mint
|
|
1190
|
+
// anything. The response is the ceremony form itself, carrying the
|
|
1191
|
+
// nonce value; its hash lives in a durable row bound to the exact
|
|
1192
|
+
// tuple digest being restated.
|
|
1193
|
+
const abandoning = body.get("act") === "abandon";
|
|
1194
|
+
if (abandoning) {
|
|
1195
|
+
if (!["pick-wait", "exhausted", "interrupted", "decision-wait"].includes(view.contest.state)) {
|
|
1196
|
+
return contestScreen(response, who, contestId, "this tournament is not in a state an operator can abandon", 409);
|
|
1197
|
+
}
|
|
1198
|
+
const digest = pickTupleDigest(view, { abandon: true }, { grant: null, head: null });
|
|
1199
|
+
const nonceValue = randomBytes(18).toString("base64url");
|
|
1200
|
+
const minted = store.mintCeremonyNonce({ hash: nonceHashOf(nonceValue), approver: who.name, subject: "contest-abandon", subjectId: contestId, digest, ttlMs: 15 * 60_000 }, now);
|
|
1201
|
+
if (!minted.ok)
|
|
1202
|
+
return contestScreen(response, who, contestId, "too many unfinished confirmations are open — finish or let them expire", 429);
|
|
1203
|
+
return sendScreen(response, 200, contestCeremonyPage(chromeFor(who.via === "cookie" ? who.session.project : null, "tasks"), {
|
|
1204
|
+
kind: "abandon", contestKind: view.contest.kind, contestId, taskId: data.taskId, taskTitle: data.taskTitle,
|
|
1205
|
+
agents: view.agents.length, totalMicrousd: data.totalMicrousd, anyUnknown: data.anyUnknown,
|
|
1206
|
+
nonceValue, csrf: who.via === "cookie" ? who.session.csrf : "",
|
|
1207
|
+
}));
|
|
1208
|
+
}
|
|
1209
|
+
if (view.contest.state !== "pick-wait") {
|
|
1210
|
+
return contestScreen(response, who, contestId, "this tournament is not waiting for a pick", 409);
|
|
1211
|
+
}
|
|
1212
|
+
const choice = Number(body.get("choice") ?? "");
|
|
1213
|
+
const plan = computePickPlan(store, view, choice, data.repo, data.refOrigin);
|
|
1214
|
+
if (!plan.ok)
|
|
1215
|
+
return contestScreen(response, who, contestId, plan.message, 409);
|
|
1216
|
+
const nonceValue = randomBytes(18).toString("base64url");
|
|
1217
|
+
const minted = store.mintCeremonyNonce({ hash: nonceHashOf(nonceValue), approver: who.name, subject: "contest-pick", subjectId: contestId, digest: plan.digest, ttlMs: 15 * 60_000 }, now);
|
|
1218
|
+
if (!minted.ok)
|
|
1219
|
+
return contestScreen(response, who, contestId, "too many unfinished confirmations are open — finish or let them expire", 429);
|
|
1220
|
+
return sendScreen(response, 200, contestCeremonyPage(chromeFor(who.via === "cookie" ? who.session.project : null, "tasks"), {
|
|
1221
|
+
kind: "pick", contestKind: view.contest.kind, contestId, taskId: data.taskId, taskTitle: data.taskTitle,
|
|
1222
|
+
agents: view.agents.length, totalMicrousd: data.totalMicrousd, anyUnknown: data.anyUnknown,
|
|
1223
|
+
chosen: plan.chosen,
|
|
1224
|
+
publication: plan.publishable && plan.grant !== null && plan.chosen.run.branch !== null ? { githubRepo: plan.grant.githubRepo, branch: plan.chosen.run.branch, draft: plan.grant.draft } : null,
|
|
1225
|
+
nonceValue, csrf: who.via === "cookie" ? who.session.csrf : "",
|
|
1226
|
+
}));
|
|
1227
|
+
}
|
|
1228
|
+
// pick and abandon: the password, typed again, plus the nonce the arm
|
|
1229
|
+
// POST minted. The store consumes the nonce conditionally inside the
|
|
1230
|
+
// same transaction that moves the tournament — replay finds it gone.
|
|
1231
|
+
const token = body.get("token") ?? "";
|
|
1232
|
+
if (!authenticateApprover(who, token).ok) {
|
|
1233
|
+
return contestScreen(response, who, contestId, "that decision takes your password, typed again", 403);
|
|
1234
|
+
}
|
|
1235
|
+
const nonceValue = body.get("nonce") ?? "";
|
|
1236
|
+
if (nonceValue === "")
|
|
1237
|
+
return contestScreen(response, who, contestId, "that confirmation form was incomplete — start again", 400);
|
|
1238
|
+
if (verb === "pick") {
|
|
1239
|
+
const choice = Number(body.get("choice") ?? "");
|
|
1240
|
+
const picked = finalizeContestPick(store, {
|
|
1241
|
+
contestId, contestantId: choice, approver: who.name, nonceValue,
|
|
1242
|
+
evidenceRoot, repo: data.repo, taskId: data.taskId, refOrigin: data.refOrigin,
|
|
1243
|
+
}, now);
|
|
1244
|
+
if (!picked.ok)
|
|
1245
|
+
return contestScreen(response, who, contestId, picked.message, 409);
|
|
1246
|
+
return redirect(response, `/contest/${contestId}`);
|
|
1247
|
+
}
|
|
1248
|
+
const gone = abandonContest(store, { contestId, approver: who.name, nonceValue, evidenceRoot, taskId: data.taskId }, now);
|
|
1249
|
+
if (!gone.ok)
|
|
1250
|
+
return contestScreen(response, who, contestId, gone.message, 409);
|
|
1251
|
+
return redirect(response, `/contest/${contestId}`);
|
|
1252
|
+
}
|
|
1253
|
+
const attendAct = matchTaskPath(url.pathname, "/(attend-preview|attend|attend-revoke)$");
|
|
1254
|
+
if (attendAct !== null) {
|
|
1255
|
+
return attendMutation(response, who, attendAct.taskId, attendAct.verb, readForm(posted, CONSOLE_FORMS.attend), now);
|
|
1256
|
+
}
|
|
1257
|
+
const act = matchTaskPath(url.pathname, "/(hold|unhold|requeue|cancel|scope|approve|plan|plan-edit|block|unblock|repair-dependency|next|reopen|steer|follow-up|accept-proof|accept-revision|reject-revision|route|retry-review|complete|merge|confirm-stopped|stop|resume-arm|resume)$");
|
|
1258
|
+
if (act !== null && act.verb === "confirm-stopped") {
|
|
1259
|
+
const body = readForm(posted, CONSOLE_FORMS.confirmStopped);
|
|
1260
|
+
// Confirming a finished build stopped is an approver's act behind the password, typed again — the same
|
|
1261
|
+
// record as `toolroll run settle`, refused while anything of the run may still be running.
|
|
1262
|
+
const back = taskHref(act.taskId);
|
|
1263
|
+
if (who.via !== "cookie" || who.role !== "approver")
|
|
1264
|
+
return refuse(response, who, 403, "Only an approver can confirm a build stopped.", back);
|
|
1265
|
+
const ref = store.lookupRef(act.taskId);
|
|
1266
|
+
if (ref === null || !visible(ref.repo))
|
|
1267
|
+
return refuse(response, who, 404, "no such task", "/tasks");
|
|
1268
|
+
const named = (body.get("run") ?? "").trim();
|
|
1269
|
+
const run = /^[0-9]{1,15}$/.test(named) ? store.getRun(Number(named)) : null;
|
|
1270
|
+
const runTask = run === null ? null : store.externalIdFor(run.taskRef);
|
|
1271
|
+
if (run === null || runTask === null || familyOf(runTask)?.root.id !== (familyOf(act.taskId)?.root.id ?? act.taskId)) {
|
|
1272
|
+
return taskScreen(response, who, act.taskId, "That build isn't part of this task.", 409);
|
|
1273
|
+
}
|
|
1274
|
+
if (!authenticateApprover(who, body.get("token") ?? "", ref.repo).ok || !store.accountCanAccess(who.name, ref.repo)) {
|
|
1275
|
+
return taskScreen(response, who, act.taskId, "That password didn't match. Nothing changed.", 403);
|
|
1276
|
+
}
|
|
1277
|
+
// When Toolroll can't check at all, the approver also says they checked: never a one-click confirmation.
|
|
1278
|
+
if (store.stopQuiescenceFact(run.id)?.kind === "unknown" && body.get("checked") !== "yes") {
|
|
1279
|
+
return taskScreen(response, who, act.taskId, `Make sure nothing from build #${run.id} is running, then tick the box to confirm.`, 409);
|
|
1280
|
+
}
|
|
1281
|
+
const settled = store.settleRunWitnessesByApprover({ runId: run.id, by: who.name, why: "Confirmed in the console that nothing from this build is running." }, now);
|
|
1282
|
+
if (!settled.ok) {
|
|
1283
|
+
return taskScreen(response, who, act.taskId, settled.reason === "alive" || settled.reason === "still-running"
|
|
1284
|
+
? `Something from build #${run.id} may still be running, so it can't be confirmed stopped yet.` : `Build #${run.id} can't be confirmed stopped.`, 409);
|
|
1285
|
+
}
|
|
1286
|
+
bustBadge();
|
|
1287
|
+
const to = resultReturnTarget(body.get("return"), run.id);
|
|
1288
|
+
return redirect(response, body.get("return") ? to : back);
|
|
1289
|
+
}
|
|
1290
|
+
if (act !== null && act.verb === "merge") {
|
|
1291
|
+
const body = readForm(posted, CONSOLE_FORMS.merge);
|
|
1292
|
+
// Merging is a person's act behind their password, typed again: a browser session, an approver, the exact
|
|
1293
|
+
// result's pull request. The flow re-reads GitHub before merging and records who merged in the ledger.
|
|
1294
|
+
if (who.via !== "cookie" || who.role !== "approver")
|
|
1295
|
+
return refuse(response, who, 403, "Only an approver can merge.");
|
|
1296
|
+
if (store.isDemo())
|
|
1297
|
+
return taskScreen(response, who, act.taskId, "The demo merges nothing.", 403);
|
|
1298
|
+
const ref = store.lookupRef(act.taskId);
|
|
1299
|
+
if (ref === null || !visible(ref.repo))
|
|
1300
|
+
return refuse(response, who, 404, "no such task", "/tasks");
|
|
1301
|
+
const named = (body.get("run") ?? "").trim();
|
|
1302
|
+
const run = /^[0-9]{1,15}$/.test(named) ? store.getRun(Number(named)) : null;
|
|
1303
|
+
const runTask = run === null ? null : store.externalIdFor(run.taskRef);
|
|
1304
|
+
if (run === null || runTask === null || familyOf(runTask)?.root.id !== (familyOf(act.taskId)?.root.id ?? act.taskId)) {
|
|
1305
|
+
return taskScreen(response, who, act.taskId, "That pull request isn't part of this task.", 409);
|
|
1306
|
+
}
|
|
1307
|
+
const merged = await mergeAsPerson(store, { runId: run.id, name: who.name, password: body.get("token") ?? "", ...(body.get("anyway") === "1" ? { anyway: true } : {}), ...(options.publishExec === undefined ? {} : { exec: options.publishExec }), clock });
|
|
1308
|
+
if (!merged.ok)
|
|
1309
|
+
return taskScreen(response, who, act.taskId, merged.message, merged.reason === "password" ? 403 : 409);
|
|
1310
|
+
return redirect(response, `${taskHref(act.taskId)}#merge`);
|
|
1311
|
+
}
|
|
1312
|
+
if (act !== null) {
|
|
1313
|
+
return taskMutation(response, who, act.taskId, act.verb, posted, now);
|
|
1314
|
+
}
|
|
1315
|
+
if (url.pathname === "/routines/add") {
|
|
1316
|
+
const body = readForm(posted, CONSOLE_FORMS.routinesAdd);
|
|
1317
|
+
// A standing order files into the OPEN project — never a typed path,
|
|
1318
|
+
// so the ceiling question never even arises — and lands on its own
|
|
1319
|
+
// screen where the approval step-up already lives: filing is cheap,
|
|
1320
|
+
// the yes is the ceremony.
|
|
1321
|
+
const project = projectOf(who, request);
|
|
1322
|
+
if (project === undefined || project === null) {
|
|
1323
|
+
return refuse(response, who, 403, "open a project first — a standing order lives somewhere specific");
|
|
1324
|
+
}
|
|
1325
|
+
if (who.via === "cookie") {
|
|
1326
|
+
const seen = body.get("projectRevision");
|
|
1327
|
+
if (seen !== null && seen !== String(who.session.projectRevision)) {
|
|
1328
|
+
return refuse(response, who, 409, "the open project changed since this form was rendered — reload and try again", "/routines");
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
const name = (body.get("name") ?? "").trim();
|
|
1332
|
+
const ceilingGiven = (body.get("ceiling") ?? "").trim();
|
|
1333
|
+
// Root mode proves the CURRENT project exactly (repo onboarding,
|
|
1334
|
+
// finding 24): canonicalize, authorize, and pass [canonical] as the
|
|
1335
|
+
// admitted list — the same discipline as task filing, so a routine
|
|
1336
|
+
// can land in a fresh clone too.
|
|
1337
|
+
const routineRootMode = !unscopedMode && ceiling.roots.length > 0;
|
|
1338
|
+
let routineRepo = project;
|
|
1339
|
+
let routineAdmitted = unscopedMode ? null : admissionList() ?? [];
|
|
1340
|
+
if (routineRootMode) {
|
|
1341
|
+
const canonical = (await authorizedProject(liveCeiling(), project)) ? canonicalProject(project) : null;
|
|
1342
|
+
if (canonical === null || canonical === undefined) {
|
|
1343
|
+
return refuse(response, who, 403, "the open project is outside what this server was configured to show", "/routines");
|
|
1344
|
+
}
|
|
1345
|
+
routineRepo = canonical;
|
|
1346
|
+
routineAdmitted = [canonical];
|
|
1347
|
+
}
|
|
1348
|
+
// One filing door for every surface (Codex adoption review, finding
|
|
1349
|
+
// 7): the service validates, canonicalizes, digests, and stamps
|
|
1350
|
+
// provenance; the admission list makes the ceiling explicit even
|
|
1351
|
+
// though `project` was already proved inside it.
|
|
1352
|
+
const scheduled = body.has("repeat") ? composerSchedule(body.sent) : { ok: true, schedule: (body.get("schedule") ?? "").trim() };
|
|
1353
|
+
if (!scheduled.ok || scheduled.schedule === null) {
|
|
1354
|
+
return sendScreen(response, 400, routinesPage(chromeFor(project, "routines"), store.routineTracks(project, now).filter(track => visible(track.routine.repo)), {
|
|
1355
|
+
csrf: who.via === "cookie" ? who.session.csrf : "", revision: who.via === "cookie" ? who.session.projectRevision : 0,
|
|
1356
|
+
problem: scheduled.ok ? "Choose a recurring schedule." : scheduled.message, values: body.sent,
|
|
1357
|
+
}));
|
|
1358
|
+
}
|
|
1359
|
+
const created = fileRoutineProposal(store, {
|
|
1360
|
+
name,
|
|
1361
|
+
repo: routineRepo,
|
|
1362
|
+
goal: (body.get("goal") ?? "").trim(),
|
|
1363
|
+
outOfScope: (body.get("not") ?? "").trim() || null,
|
|
1364
|
+
touches: (body.get("touches") ?? "").split(/[\n,]/).map(one => one.trim()).filter(one => one !== ""),
|
|
1365
|
+
acceptance: acceptanceLinesToInput((body.get("acceptance") ?? "").split("\n")),
|
|
1366
|
+
requirements: [],
|
|
1367
|
+
schedule: scheduled.schedule,
|
|
1368
|
+
costCeilingUsd: ceilingGiven === "" ? null : Number(ceilingGiven),
|
|
1369
|
+
filedVia: "console", createdBy: who.name,
|
|
1370
|
+
...(routineAdmitted === null ? {} : { admittedRepos: routineAdmitted }),
|
|
1371
|
+
}, now);
|
|
1372
|
+
if (created.ok && routineRootMode)
|
|
1373
|
+
store.upsertProject(routineRepo, projectName(routineRepo), now);
|
|
1374
|
+
if (!created.ok) {
|
|
1375
|
+
const tracks = store.routineTracks(project, now).filter(track => visible(track.routine.repo));
|
|
1376
|
+
return sendScreen(response, created.reason === "duplicate" ? 409 : 400, routinesPage(chromeFor(project, "routines"), tracks, {
|
|
1377
|
+
csrf: who.via === "cookie" ? who.session.csrf : "",
|
|
1378
|
+
revision: who.via === "cookie" ? who.session.projectRevision : 0,
|
|
1379
|
+
problem: created.message, values: body.sent,
|
|
1380
|
+
}));
|
|
1381
|
+
}
|
|
1382
|
+
return redirect(response, `/routines/${created.id}`);
|
|
1383
|
+
}
|
|
1384
|
+
const routineAct = /^\/routines\/([0-9]{1,15})\/(approve|refresh|pause|resume|run-now)$/.exec(url.pathname);
|
|
1385
|
+
if (routineAct !== null) {
|
|
1386
|
+
return routineMutation(response, who, Number(routineAct[1]), routineAct[2], readForm(posted, CONSOLE_FORMS.routine), now);
|
|
1387
|
+
}
|
|
1388
|
+
const runNote = /^\/r\/([0-9]{1,15})\/note$/.exec(url.pathname);
|
|
1389
|
+
if (runNote !== null) {
|
|
1390
|
+
const body = readForm(posted, CONSOLE_FORMS.runNote);
|
|
1391
|
+
// An operator's verdict beside the machine's record (M6): immutable,
|
|
1392
|
+
// bounded by the same validator as decision notes, ceiling-checked
|
|
1393
|
+
// like every run resource. Ordinary authenticated mutation — no
|
|
1394
|
+
// nonce, because nothing here approves anything.
|
|
1395
|
+
const id = Number(runNote[1]);
|
|
1396
|
+
const found = store.getRun(id);
|
|
1397
|
+
if (found === null || !visible(taskRepoOf(found.taskRef))) {
|
|
1398
|
+
return refuse(response, who, 404, "no such run");
|
|
1399
|
+
}
|
|
1400
|
+
const note = validateNote(body.get("note") ?? "");
|
|
1401
|
+
if (!note.ok)
|
|
1402
|
+
return refuse(response, who, 400, note.problem);
|
|
1403
|
+
store.addRunNote(id, who.name, note.note, now);
|
|
1404
|
+
return redirect(response, `/r/${id}`);
|
|
1405
|
+
}
|
|
1406
|
+
const diffComment = /^\/r\/([0-9]{1,15})\/comment$/.exec(url.pathname);
|
|
1407
|
+
if (diffComment !== null) {
|
|
1408
|
+
const body = readForm(posted, CONSOLE_FORMS.diffComment);
|
|
1409
|
+
// A review comment on the IMMUTABLE terminal diff (M6.8): bound to
|
|
1410
|
+
// the exact artifact and its hash. Ordinary authenticated mutation —
|
|
1411
|
+
// the nonce belongs to the approval screen that later restates the
|
|
1412
|
+
// batch, never to the comment box.
|
|
1413
|
+
const id = Number(diffComment[1]);
|
|
1414
|
+
const found = store.getRun(id);
|
|
1415
|
+
if (found === null || !visible(taskRepoOf(found.taskRef))) {
|
|
1416
|
+
return refuse(response, who, 404, "no such run");
|
|
1417
|
+
}
|
|
1418
|
+
// Where the reader came from (package 3): the cockpit's deep link,
|
|
1419
|
+
// the chat's result view, or the run page — validated to those exact
|
|
1420
|
+
// shapes. A refusal sends them back THERE, where the browser has kept
|
|
1421
|
+
// their draft, so a failed submission is recoverable in place.
|
|
1422
|
+
const returnTo = resultReturnTarget(body.get("return"), id);
|
|
1423
|
+
const selectedTab = parseResultTab(body.get("tab"));
|
|
1424
|
+
const back = selectedTab === "summary" ? returnTo : `${returnTo}${returnTo.includes("?") ? "&" : "?"}tab=${selectedTab}`;
|
|
1425
|
+
if (body.get("intent") === "revise") {
|
|
1426
|
+
const result = requestResultChanges(store, evidenceRoot, {
|
|
1427
|
+
run: id, batch: body.get("batch"), source: body.get("source"), actor: who.name,
|
|
1428
|
+
repos: admissionList(), includeUnplaced: visible(null), allowMode: who.via === "cookie",
|
|
1429
|
+
note: body.get("note") ?? "", path: body.get("path") ?? "", line: body.get("line") ?? "", request: body.get("request"),
|
|
1430
|
+
}, now);
|
|
1431
|
+
if (!result.ok) {
|
|
1432
|
+
const request = body.get("request") ?? "";
|
|
1433
|
+
const retryBack = /^[a-f0-9]{32}$/.test(request) ? `${back}${back.includes("?") ? "&" : "?"}conflict=${request}#request-changes` : back;
|
|
1434
|
+
return refuse(response, who, result.status, result.message, retryBack);
|
|
1435
|
+
}
|
|
1436
|
+
return redirect(response, revisionDestination(result.id, returnTo));
|
|
1437
|
+
}
|
|
1438
|
+
const terminal = store.artifactsFor(id).find(one => one.kind === "terminal-diff");
|
|
1439
|
+
if (terminal === undefined) {
|
|
1440
|
+
return refuse(response, who, 400, "this run has no terminal diff to comment on", back);
|
|
1441
|
+
}
|
|
1442
|
+
// The bytes must VERIFY before words attach to them (audit IV-10):
|
|
1443
|
+
// "a comment on the exact reviewed bytes" is a lie if the bytes are
|
|
1444
|
+
// gone or no longer hash to their record.
|
|
1445
|
+
const proven = readVerifiedArtifact(evidenceRoot, terminal);
|
|
1446
|
+
if (!proven.ok) {
|
|
1447
|
+
return refuse(response, who, 409, `the terminal diff no longer verifies (${proven.problem}) — nothing to comment on`, back);
|
|
1448
|
+
}
|
|
1449
|
+
const note = validateNote(body.get("note") ?? "");
|
|
1450
|
+
if (!note.ok)
|
|
1451
|
+
return refuse(response, who, 400, note.problem, back);
|
|
1452
|
+
const rawPath = (body.get("path") ?? "").trim();
|
|
1453
|
+
if (rawPath.length > 300 || hasForbiddenControls(rawPath)) {
|
|
1454
|
+
return refuse(response, who, 400, "Enter a file path of up to 300 characters, without control characters.", back);
|
|
1455
|
+
}
|
|
1456
|
+
const rawLine = (body.get("line") ?? "").trim();
|
|
1457
|
+
const line = rawLine === "" ? null : Number(rawLine);
|
|
1458
|
+
if (line !== null && (!Number.isInteger(line) || line < 1 || line > 1_000_000)) {
|
|
1459
|
+
return refuse(response, who, 400, "Enter a whole line number from 1 to 1,000,000, or leave it blank.", back);
|
|
1460
|
+
}
|
|
1461
|
+
// The form's own request token (package 3) is the dedupe key: a
|
|
1462
|
+
// replayed or double submission finds its note already recorded and
|
|
1463
|
+
// lands on the same receipt instead of a second note. The token is
|
|
1464
|
+
// bound to the account, so nobody else's replay can consume it.
|
|
1465
|
+
const request = body.get("request");
|
|
1466
|
+
const sourceKey = commentSourceKey(who.name, request);
|
|
1467
|
+
const path = rawPath === "" ? null : rawPath;
|
|
1468
|
+
const inserted = store.addDiffComment({ artifactId: terminal.id, runId: id, path, line, note: note.note, author: who.name, ...(sourceKey === undefined ? {} : { sourceKey }) }, now);
|
|
1469
|
+
if (inserted === null && sourceKey !== undefined) {
|
|
1470
|
+
// The token was used before (repair 2026-09-14, finding 1). A
|
|
1471
|
+
// request identity is IMMUTABLE: it names exactly one note on
|
|
1472
|
+
// exactly one result. The unchanged retry — same run, same file,
|
|
1473
|
+
// line and words — lands on its original receipt; any other reuse
|
|
1474
|
+
// is refused, and the refusal's way back carries `conflict=<token>`
|
|
1475
|
+
// so the browser mints a fresh identity for the words it still
|
|
1476
|
+
// holds. Nothing typed is lost either way.
|
|
1477
|
+
const earlier = store.diffCommentBySourceKey(sourceKey);
|
|
1478
|
+
const conflictBack = `${back}${back.includes("?") ? "&" : "?"}conflict=${request}#request-changes`;
|
|
1479
|
+
if (earlier === null || earlier.run !== id) {
|
|
1480
|
+
return refuse(response, who, 409, `this note's request identity was already used on another result (build #${earlier === null ? "?" : earlier.run}) — go back; your words are kept, and the form will carry a new identity`, conflictBack);
|
|
1481
|
+
}
|
|
1482
|
+
if (earlier.path !== path || earlier.line !== line || earlier.note !== note.note) {
|
|
1483
|
+
return refuse(response, who, 409, `this request identity already recorded a different note on this result (${earlier.path === null ? "no file" : `${earlier.path}${earlier.line === null ? "" : `:${earlier.line}`}`}: "${oneLineOf(earlier.note, 80)}") — go back; your edited words are kept as a new note, and the form will carry a new identity`, conflictBack);
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
// Land back AT the request-changes form with the note field ready —
|
|
1487
|
+
// writing five notes in a row must cost five keystrokes of
|
|
1488
|
+
// navigation, not five scrolls (arc 6, finding 5/6). The receipt
|
|
1489
|
+
// names the request token, so the browser clears exactly the draft
|
|
1490
|
+
// that landed and no other.
|
|
1491
|
+
const receipt = sourceKey === undefined ? "1" : request;
|
|
1492
|
+
return redirect(response, `${back}${back.includes("?") ? "&" : "?"}noted=${receipt}#request-changes`);
|
|
1493
|
+
}
|
|
1494
|
+
if (url.pathname === "/session/editor-links") {
|
|
1495
|
+
const body = readForm(posted, CONSOLE_FORMS.editorLinks);
|
|
1496
|
+
// The session half of the editor-link activation (arc 6, finding 1):
|
|
1497
|
+
// only the person at the browser can say "this device holds the
|
|
1498
|
+
// worktrees". Per-session, dies with the session, grants nothing —
|
|
1499
|
+
// it only lets already-authorized pages RENDER vscode links.
|
|
1500
|
+
if (who.via !== "cookie")
|
|
1501
|
+
return refuse(response, who, 403, "editor links are a browser session's choice");
|
|
1502
|
+
if (options.editorLinks === undefined)
|
|
1503
|
+
return refuse(response, who, 404, "editor links are not enabled on this server");
|
|
1504
|
+
who.session.editorLinks = body.get("on") === "1";
|
|
1505
|
+
const back = body.get("return") ?? "/";
|
|
1506
|
+
return redirect(response, /^\/[a-z0-9/_-]*$/i.test(back) ? back : "/");
|
|
1507
|
+
}
|
|
1508
|
+
const turnAct = /^\/r\/([0-9]{1,15})\/turn$/.exec(url.pathname);
|
|
1509
|
+
if (turnAct !== null) {
|
|
1510
|
+
const body = readForm(posted, CONSOLE_FORMS.turn);
|
|
1511
|
+
// The operator's turn (Phase 2E, v2 S1g): cookie-only — a watching
|
|
1512
|
+
// person, never a bearer machine — and every hard gate (custody,
|
|
1513
|
+
// lease, cap, budget, open decision, single flight) re-proves
|
|
1514
|
+
// ATOMICALLY inside the recording transaction. Words here only map
|
|
1515
|
+
// the refusal tokens to sentences.
|
|
1516
|
+
if (who.via !== "cookie")
|
|
1517
|
+
return refuse(response, who, 403, "turns are a browser session's act");
|
|
1518
|
+
const id = Number(turnAct[1]);
|
|
1519
|
+
const found = store.getRun(id);
|
|
1520
|
+
if (found === null || !visible(taskRepoOf(found.taskRef))) {
|
|
1521
|
+
return refuse(response, who, 404, "no such run");
|
|
1522
|
+
}
|
|
1523
|
+
const text = (body.get("text") ?? "").trim();
|
|
1524
|
+
if (text === "" || text.length > 500) {
|
|
1525
|
+
return refuse(response, who, 400, "a turn is 1 to 500 characters", `/r/${id}`);
|
|
1526
|
+
}
|
|
1527
|
+
const coordinator = options.attended?.coordinator;
|
|
1528
|
+
if (coordinator === undefined)
|
|
1529
|
+
return refuse(response, who, 409, "this console is not holding the session", `/r/${id}`);
|
|
1530
|
+
const injected = coordinator.injectOperatorTurn(id, who.name, text);
|
|
1531
|
+
if (!injected.ok) {
|
|
1532
|
+
const words = {
|
|
1533
|
+
"no-held-session": "the session is not held here anymore",
|
|
1534
|
+
fenced: "the session is winding down — nothing more reaches it",
|
|
1535
|
+
"turn-open": "the agent is still working on the last message — wait for it to settle",
|
|
1536
|
+
"turn-cap": "the session's message cap is reached — authorize a new session for more",
|
|
1537
|
+
"budget-exhausted": "the session's budget is spent",
|
|
1538
|
+
"decision-open": "answer the waiting question first — it is on this page",
|
|
1539
|
+
"write-failed": "the message could not reach the agent — it was not charged",
|
|
1540
|
+
};
|
|
1541
|
+
return refuse(response, who, 409, words[injected.reason] ?? injected.reason, `/r/${id}`);
|
|
1542
|
+
}
|
|
1543
|
+
return redirect(response, `/r/${id}`);
|
|
1544
|
+
}
|
|
1545
|
+
const revise = /^\/r\/([0-9]{1,15})\/revise$/.exec(url.pathname);
|
|
1546
|
+
if (revise !== null) {
|
|
1547
|
+
const body = readForm(posted, CONSOLE_FORMS.revise);
|
|
1548
|
+
// Seal the live comment batch into ONE unapproved revision task with
|
|
1549
|
+
// an immutable brief (M6.8). Deterministic — no model reads anything
|
|
1550
|
+
// here — and every revision takes its own approval: comments can
|
|
1551
|
+
// semantically widen work, and no path check can prove they did not.
|
|
1552
|
+
const id = Number(revise[1]);
|
|
1553
|
+
const found = store.getRun(id);
|
|
1554
|
+
if (found === null || !visible(taskRepoOf(found.taskRef))) {
|
|
1555
|
+
return refuse(response, who, 404, "no such run");
|
|
1556
|
+
}
|
|
1557
|
+
const back = resultReturnTarget(body.get("return"), id);
|
|
1558
|
+
const result = createResultRevision(store, evidenceRoot, {
|
|
1559
|
+
run: id, batch: body.get("batch"), source: body.get("source"), actor: who.name,
|
|
1560
|
+
repos: admissionList(), includeUnplaced: visible(null), allowMode: who.via === "cookie",
|
|
1561
|
+
}, now);
|
|
1562
|
+
if (!result.ok)
|
|
1563
|
+
return refuse(response, who, result.status, result.message, back);
|
|
1564
|
+
return redirect(response, revisionDestination(result.id, back));
|
|
1565
|
+
}
|
|
1566
|
+
// Follow-ups on a result: Run checks on its exact commit (a worker runs the
|
|
1567
|
+
// approved command once and seals the log), or file a task to add tests.
|
|
1568
|
+
const followUp = /^\/r\/([0-9]{1,15})\/(checks|add-tests)$/.exec(url.pathname);
|
|
1569
|
+
if (followUp !== null) {
|
|
1570
|
+
const body = readForm(posted, CONSOLE_FORMS.followUp);
|
|
1571
|
+
const id = Number(followUp[1]);
|
|
1572
|
+
const found = store.getRun(id);
|
|
1573
|
+
if (found === null || !visible(taskRepoOf(found.taskRef)))
|
|
1574
|
+
return refuse(response, who, 404, "no such run");
|
|
1575
|
+
// The task page that offered Run checks in place is a return target too: this run's own task, or its family's root.
|
|
1576
|
+
const runTask = store.externalIdFor(found.taskRef);
|
|
1577
|
+
const back = resultReturnTarget(body.get("return"), id, runTask === null ? [] : [runTask, familyOf(runTask)?.root.id ?? runTask]);
|
|
1578
|
+
if (who.via !== "cookie")
|
|
1579
|
+
return refuse(response, who, 403, "Sign in with a browser session to do this.", back);
|
|
1580
|
+
if (followUp[2] === "checks") {
|
|
1581
|
+
if (who.role !== "approver")
|
|
1582
|
+
return refuse(response, who, 403, "An approver runs checks.", back);
|
|
1583
|
+
if (store.isDemo())
|
|
1584
|
+
return refuse(response, who, 403, "The demo runs no checks.", back);
|
|
1585
|
+
const level = body.get("level") === "full" ? "full" : "quick";
|
|
1586
|
+
const asked = requestFollowUpChecks(store, { runId: id, level, actor: who.name }, now);
|
|
1587
|
+
if (!asked.ok)
|
|
1588
|
+
return refuse(response, who, 409, asked.message, back);
|
|
1589
|
+
// Back where it was asked: the task page's status row says the checks are running; a result page shows them under #follow-ups.
|
|
1590
|
+
return redirect(response, back.startsWith("/t/") ? back : `${back.split("#")[0]}#follow-ups`);
|
|
1591
|
+
}
|
|
1592
|
+
const filed = fileAddTestsTask(store, evidenceRoot, { runId: id, actor: who.name, filedVia: "console", ...(admissionList() === null ? {} : { admittedRepos: admissionList() }) }, now);
|
|
1593
|
+
if (!filed.ok)
|
|
1594
|
+
return refuse(response, who, 409, filed.message, back);
|
|
1595
|
+
return redirect(response, taskHref(filed.id));
|
|
1596
|
+
}
|
|
1597
|
+
const draftRepair = /^\/r\/([0-9]{1,15})\/draft-repair$/.exec(url.pathname);
|
|
1598
|
+
if (draftRepair !== null) {
|
|
1599
|
+
// CI repair, suggestion-first (M8.18): a red episode never spawns an
|
|
1600
|
+
// agent by itself — it EARNS a button, and the button creates one
|
|
1601
|
+
// unapproved task through the same revision machinery as review
|
|
1602
|
+
// comments. Deterministic id = one draft per task/PR, ever.
|
|
1603
|
+
const id = Number(draftRepair[1]);
|
|
1604
|
+
const found = store.getRun(id);
|
|
1605
|
+
if (found === null || !visible(taskRepoOf(found.taskRef))) {
|
|
1606
|
+
return refuse(response, who, 404, "no such run");
|
|
1607
|
+
}
|
|
1608
|
+
const publication = store.publicationForRun(id);
|
|
1609
|
+
if (publication === null || publication.prNumber === null) {
|
|
1610
|
+
return refuse(response, who, 400, "this run published no pull request", `/r/${id}`);
|
|
1611
|
+
}
|
|
1612
|
+
if (!store.hasOpenCiEpisode(publication.githubRepo, publication.prNumber)) {
|
|
1613
|
+
return refuse(response, who, 400, "no failing CI is observed on this PR right now", `/r/${id}`);
|
|
1614
|
+
}
|
|
1615
|
+
const sourceTaskId = store.externalIdFor(found.taskRef) ?? "?";
|
|
1616
|
+
const sourceScope = store.getScope(sourceTaskId);
|
|
1617
|
+
// The observed episode, not the click: the brief binds the head the
|
|
1618
|
+
// failure was SEEN on and when (audit C-2) — a PR that advanced since
|
|
1619
|
+
// is a different failure, and the click time is not an observation.
|
|
1620
|
+
const episode = store.latestOpenCiEpisode(publication.githubRepo, publication.prNumber);
|
|
1621
|
+
// Suffixes survive truncation (audit C-7): the prefix gives way, the
|
|
1622
|
+
// identity-bearing tail never does.
|
|
1623
|
+
const suffix = `-ci-${publication.prNumber}`;
|
|
1624
|
+
const draftId = `${sourceTaskId.slice(0, 64 - suffix.length)}${suffix}`;
|
|
1625
|
+
const brief = {
|
|
1626
|
+
schema: 1,
|
|
1627
|
+
kind: "ci-repair",
|
|
1628
|
+
sourceTask: sourceTaskId,
|
|
1629
|
+
sourceRun: id,
|
|
1630
|
+
sourceScopeDigest: sourceScope?.digest ?? null,
|
|
1631
|
+
head: found.headRevision,
|
|
1632
|
+
pr: publication.prNumber,
|
|
1633
|
+
prUrl: publication.prUrl,
|
|
1634
|
+
publishedHeadSha: publication.headSha,
|
|
1635
|
+
observedFailingHead: episode?.headSha ?? null,
|
|
1636
|
+
observedAt: episode?.createdAt ?? null,
|
|
1637
|
+
};
|
|
1638
|
+
const briefBytes = Buffer.from(JSON.stringify(brief, null, 2), "utf8");
|
|
1639
|
+
const briefName = `ci-repair-brief-${randomBytes(6).toString("hex")}.json`;
|
|
1640
|
+
const key = writeEvidenceFile(evidenceRoot, id, briefName, briefBytes);
|
|
1641
|
+
// The SAME revision boundary the annotation road and the criterion
|
|
1642
|
+
// repair use (contract handoff task 2): the inherited terms come from
|
|
1643
|
+
// the source rows inside the seal; the scope read above only names
|
|
1644
|
+
// the digest this draft was composed against.
|
|
1645
|
+
const sealed = store.sealRevision({
|
|
1646
|
+
source: { task: sourceTaskId, run: id, scopeDigest: sourceScope?.digest ?? null },
|
|
1647
|
+
brief: { evidenceRoot, key, sha256: createHash("sha256").update(briefBytes).digest("hex"), bytes: briefBytes.length, capture: "machine-authored ci-repair brief (exit 0)" },
|
|
1648
|
+
child: {
|
|
1649
|
+
id: draftId,
|
|
1650
|
+
title: `repair ${sourceTaskId}: CI failing on PR #${publication.prNumber}`,
|
|
1651
|
+
repair: `repair the failing CI on PR #${publication.prNumber} (failing head ${(episode?.headSha ?? publication.headSha).slice(0, 12)}). ` +
|
|
1652
|
+
`Read the failing checks on GitHub before approving; this draft carries no log content.`,
|
|
1653
|
+
},
|
|
1654
|
+
commentIds: null,
|
|
1655
|
+
requestedBy: who.name,
|
|
1656
|
+
}, now);
|
|
1657
|
+
if (!sealed.ok) {
|
|
1658
|
+
return refuse(response, who, sealed.reason === "duplicate" ? 409 : sealed.reason === "stale-source" || sealed.reason === "comments-taken" ? 409 : 400, sealed.reason === "duplicate" ? `already drafted as ${draftId}` : `could not draft: ${sealed.detail}`, `/r/${id}`);
|
|
1659
|
+
}
|
|
1660
|
+
// The merge blocker rides the SAME breath as the draft (merge grant,
|
|
1661
|
+
// findings 3/12/13): while this repair exists, the source PR merges
|
|
1662
|
+
// NOTHING — sticky until the operator's unblock act or the PR closes.
|
|
1663
|
+
store.createMergeBlocker(publication.id, sealed.id, now);
|
|
1664
|
+
return redirect(response, taskHref(sealed.id));
|
|
1665
|
+
}
|
|
1666
|
+
const resolve = /^\/i\/([0-9]{1,15})\/resolve$/.exec(url.pathname);
|
|
1667
|
+
if (resolve !== null) {
|
|
1668
|
+
const body = readForm(posted, CONSOLE_FORMS.resolveIncident);
|
|
1669
|
+
const id = Number(resolve[1]);
|
|
1670
|
+
// The ceiling applies to incident mutation exactly as to every other
|
|
1671
|
+
// resource (v3 review, finding 3): resolve incident → run → task, and
|
|
1672
|
+
// an incident outside this server's scope does not exist here. The
|
|
1673
|
+
// task id is captured BEFORE resolving — afterwards the incident is
|
|
1674
|
+
// no longer open and could not be found again.
|
|
1675
|
+
const openRow = store.openIncidents().find(one => one.id === id);
|
|
1676
|
+
const incidentRun = openRow === undefined ? null : store.getRun(openRow.run);
|
|
1677
|
+
if (openRow !== undefined && incidentRun !== null && !visible(taskRepoOf(incidentRun.taskRef))) {
|
|
1678
|
+
return refuse(response, who, 404, "no such incident");
|
|
1679
|
+
}
|
|
1680
|
+
const resolved = store.resolveIncident(id, who.name, now);
|
|
1681
|
+
if (!resolved)
|
|
1682
|
+
return refuse(response, who, 409, "already resolved, or never open");
|
|
1683
|
+
const back = body.get("return") === "inbox" ? "/inbox" : openRow === undefined ? "/" : taskHref(openRow.taskId);
|
|
1684
|
+
return redirect(response, back);
|
|
1685
|
+
}
|
|
1686
|
+
return refuse(response, who, 404, "There's no page at this address.", "/chat");
|
|
1687
|
+
}
|
|
1688
|
+
/** The compact recent-runs list for the master pane. */
|
|
1689
|
+
function runListPane(project, currentId) {
|
|
1690
|
+
const rows = store.listRunsBefore(null, 50, project);
|
|
1691
|
+
const live = liveRunIds(rows);
|
|
1692
|
+
const items = rows
|
|
1693
|
+
.map(run => `<a class="item${run.id === currentId ? " current" : ""}" href="/r/${run.id}">` +
|
|
1694
|
+
`<span class="t">#${run.id} \u00b7 ${escape(run.taskId)}</span>` +
|
|
1695
|
+
`<span class="m">${runOutcomeBadge(run, live.has(run.id))}` +
|
|
1696
|
+
`<span class="mono">${whenTime(run.startedAt)}</span></span></a>`)
|
|
1697
|
+
.join("\n");
|
|
1698
|
+
return `<h2>Builds</h2>\n${items === "" ? `<p class="meta">None yet</p>` : items}`;
|
|
1699
|
+
}
|
|
1700
|
+
async function peekFragment(runId, sessionKey, editorMode = false) {
|
|
1701
|
+
const guarded = peekGuards(runId);
|
|
1702
|
+
if (!guarded.ok)
|
|
1703
|
+
return { status: 200, body: peekSay(guarded.message, guarded.final === true) };
|
|
1704
|
+
const { run, worktree, epoch, entries } = guarded.admit;
|
|
1705
|
+
// The cache and the in-flight coalescer both vary by LINK MODE (arc 6,
|
|
1706
|
+
// finding 3): a linked fragment rendered for one session must never be
|
|
1707
|
+
// served to a session that has not activated links on its device.
|
|
1708
|
+
const key = `${runId}:${run.baseRevision}:${epoch}:${editorMode ? "links" : "plain"}`;
|
|
1709
|
+
const cached = peekCache.get(key);
|
|
1710
|
+
if (cached !== undefined && Date.now() - cached.at <= PEEK_CACHE_TTL_MS) {
|
|
1711
|
+
return { status: 200, body: cached.fragment };
|
|
1712
|
+
}
|
|
1713
|
+
if (cached !== undefined) {
|
|
1714
|
+
peekCache.delete(key);
|
|
1715
|
+
runtime.peekCacheBytes -= Buffer.byteLength(cached.fragment);
|
|
1716
|
+
}
|
|
1717
|
+
// Coalesce per run; bound per session and globally (finding 10).
|
|
1718
|
+
const flightKey = `${runId}:${editorMode ? "links" : "plain"}`;
|
|
1719
|
+
const inFlight = peekInFlight.get(flightKey);
|
|
1720
|
+
if (inFlight !== undefined)
|
|
1721
|
+
return { status: 200, body: await inFlight };
|
|
1722
|
+
if (peekInFlight.size >= PEEK_GLOBAL_INFLIGHT)
|
|
1723
|
+
return { status: 429, body: peekSay("the live view is busy — it retries by itself"), retryAfter: 10 };
|
|
1724
|
+
if ((peekBySession.get(sessionKey) ?? 0) >= PEEK_SESSION_INFLIGHT) {
|
|
1725
|
+
return { status: 429, body: peekSay("too many live views from this session"), retryAfter: 10 };
|
|
1726
|
+
}
|
|
1727
|
+
peekBySession.set(sessionKey, (peekBySession.get(sessionKey) ?? 0) + 1);
|
|
1728
|
+
const work = (async () => {
|
|
1729
|
+
const seen = await observeWorktree(worktree, entries.entries, PEEK_LIMITS);
|
|
1730
|
+
// The fence, proved AGAIN after the walk (findings 16/28): the same
|
|
1731
|
+
// run still open, the same claim, the SAME epoch — or the whole
|
|
1732
|
+
// observation is discarded, never rendered, never cached.
|
|
1733
|
+
const after = peekGuards(runId);
|
|
1734
|
+
if (!after.ok || after.admit.epoch !== epoch) {
|
|
1735
|
+
return peekSay("the checkout changed hands mid-look — nothing is shown");
|
|
1736
|
+
}
|
|
1737
|
+
if (!seen.ok)
|
|
1738
|
+
return peekSay(seen.reason);
|
|
1739
|
+
const stamp = clock().toISOString().slice(11, 19);
|
|
1740
|
+
const parts = [
|
|
1741
|
+
`<p class="meta">Best-effort look at ${escape(stamp)} UTC — files can change mid-read</p>`,
|
|
1742
|
+
];
|
|
1743
|
+
const changed = seen.rows.filter(one => one.kind === "changed");
|
|
1744
|
+
const deleted = seen.rows.filter(one => one.kind === "deleted");
|
|
1745
|
+
const unchecked = seen.rows.filter(one => one.kind === "unchecked");
|
|
1746
|
+
const fresh = aggregateNewNames(seen.newPaths);
|
|
1747
|
+
if (changed.length === 0 && deleted.length === 0 && fresh.total === 0) {
|
|
1748
|
+
parts.push(`<p class="row">nothing has changed against the starting point yet</p>`);
|
|
1749
|
+
}
|
|
1750
|
+
// A name is linked ONLY when sanitize provably changed nothing (arc 6,
|
|
1751
|
+
// finding 3): a masked or normalized label must never carry an href
|
|
1752
|
+
// that discloses what the mask hid. Collapsed labels never link.
|
|
1753
|
+
const linkedName = (path) => {
|
|
1754
|
+
const shown = peekName(path);
|
|
1755
|
+
if (!editorMode || shown !== escape(path))
|
|
1756
|
+
return shown;
|
|
1757
|
+
const href = editorFileHref(worktree, path);
|
|
1758
|
+
return href === null ? shown : `<a href="${escape(href)}">${shown}</a>`;
|
|
1759
|
+
};
|
|
1760
|
+
const line = (row, mark) => `<p class="row mono">${mark} ${linkedName(row.path)} <span class="meta">${escape(row.detail)}</span></p>`;
|
|
1761
|
+
for (const row of changed)
|
|
1762
|
+
parts.push(line(row, "~"));
|
|
1763
|
+
for (const row of deleted)
|
|
1764
|
+
parts.push(line(row, "−"));
|
|
1765
|
+
if (fresh.total > 0) {
|
|
1766
|
+
parts.push(`<p class="meta">New files · ${fresh.total}</p>`);
|
|
1767
|
+
for (const row of fresh.rows) {
|
|
1768
|
+
parts.push(row.collapsed
|
|
1769
|
+
? `<p class="row mono">+ ${peekName(row.label)} <span class="meta">collapsed names — ${row.count} files</span></p>`
|
|
1770
|
+
: `<p class="row mono">+ ${peekName(row.label)}</p>`);
|
|
1771
|
+
}
|
|
1772
|
+
if (fresh.renderedFiles < fresh.total) {
|
|
1773
|
+
parts.push(`<p class="meta">…and ${fresh.total - fresh.renderedFiles} more (${fresh.total} new files total)</p>`);
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
if (unchecked.length > 0) {
|
|
1777
|
+
parts.push(`<p class="meta">Not verified this look — absence above does not mean unchanged:</p>`);
|
|
1778
|
+
for (const row of unchecked)
|
|
1779
|
+
parts.push(line(row, "?"));
|
|
1780
|
+
}
|
|
1781
|
+
if (seen.partial !== null)
|
|
1782
|
+
parts.push(`<p class="meta">${escape(seen.partial)}</p>`);
|
|
1783
|
+
let fragment = parts.join("\n");
|
|
1784
|
+
if (Buffer.byteLength(fragment) > PEEK_FRAGMENT_BYTES) {
|
|
1785
|
+
// The byte cap is enforced AFTER escaping (finding 32): an oversize
|
|
1786
|
+
// rendering is replaced whole by its exact counts.
|
|
1787
|
+
fragment =
|
|
1788
|
+
`<p class="meta">Best-effort look at ${escape(stamp)} UTC</p>` +
|
|
1789
|
+
`<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>`;
|
|
1790
|
+
}
|
|
1791
|
+
peekCache.set(key, { fragment, at: Date.now() });
|
|
1792
|
+
runtime.peekCacheBytes += Buffer.byteLength(fragment);
|
|
1793
|
+
peekEvict();
|
|
1794
|
+
return fragment;
|
|
1795
|
+
})();
|
|
1796
|
+
peekInFlight.set(flightKey, work);
|
|
1797
|
+
try {
|
|
1798
|
+
return { status: 200, body: await work };
|
|
1799
|
+
}
|
|
1800
|
+
finally {
|
|
1801
|
+
peekInFlight.delete(flightKey);
|
|
1802
|
+
const left = (peekBySession.get(sessionKey) ?? 1) - 1;
|
|
1803
|
+
if (left <= 0)
|
|
1804
|
+
peekBySession.delete(sessionKey);
|
|
1805
|
+
else
|
|
1806
|
+
peekBySession.set(sessionKey, left);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
function contestScreen(response, who, contestId, problem, status) {
|
|
1810
|
+
const data = contestData(contestId);
|
|
1811
|
+
if (data === null)
|
|
1812
|
+
return refuse(response, who, 404, "no such tournament", "/board");
|
|
1813
|
+
const paneProject = who.via === "cookie" ? who.session.project : null;
|
|
1814
|
+
const diffs = new Map();
|
|
1815
|
+
for (const agent of data.view.agents) {
|
|
1816
|
+
if (agent.run !== null)
|
|
1817
|
+
diffs.set(agent.contestant.id, terminalDiffView(store.artifactsFor(agent.run.id), evidenceRoot));
|
|
1818
|
+
}
|
|
1819
|
+
return sendScreen(response, status, contestPage(chromeFor(paneProject, "tasks"), {
|
|
1820
|
+
...data,
|
|
1821
|
+
diffs,
|
|
1822
|
+
csrf: who.via === "cookie" ? who.session.csrf : "",
|
|
1823
|
+
problem,
|
|
1824
|
+
}));
|
|
1825
|
+
}
|
|
1826
|
+
/**
|
|
1827
|
+
* Routine verbs. The ceiling check is independent of authorizeMutation
|
|
1828
|
+
* (Codex round 2, finding 7): the routine's repository is resolved
|
|
1829
|
+
* server-side and proved against this server's configuration before any
|
|
1830
|
+
* verb runs, whatever the request named.
|
|
1831
|
+
*/
|
|
1832
|
+
function routineMutation(response, who, routineId, verb, body, now) {
|
|
1833
|
+
const routine = store.getRoutine(routineId);
|
|
1834
|
+
if (routine === null || !visible(routine.repo)) {
|
|
1835
|
+
return refuse(response, who, 404, "no such routine", "/routines");
|
|
1836
|
+
}
|
|
1837
|
+
switch (verb) {
|
|
1838
|
+
case "approve": {
|
|
1839
|
+
if (who.via !== "cookie")
|
|
1840
|
+
return refuse(response, who, 403, REMOTE_MESSAGES["step-up"]);
|
|
1841
|
+
// Step-up, identical to a scope's: the session got you here; only
|
|
1842
|
+
// the password agrees. The digest names what was seen.
|
|
1843
|
+
const digest = body.get("digest") ?? "";
|
|
1844
|
+
const token = body.get("token") ?? "";
|
|
1845
|
+
const nonce = body.get("nonce") ?? "";
|
|
1846
|
+
if (!consumeApprovalNonce(nonce, who.name, `routine:${routine.id}`, digest)) {
|
|
1847
|
+
return routinePage(response, who, routineId, "that approval form is stale — read it again", 409);
|
|
1848
|
+
}
|
|
1849
|
+
if (token === "" && !hasFreshIdentitySignIn(who.name)) {
|
|
1850
|
+
return routinePage(response, who, routineId, "approval requires your password, typed again", 400);
|
|
1851
|
+
}
|
|
1852
|
+
const approved = approveRoutine(store, routineId, who.name, now, digest, token);
|
|
1853
|
+
if (!approved.ok) {
|
|
1854
|
+
const status = approved.reason === "changed" ? 409 : 403;
|
|
1855
|
+
const words = approved.reason === "profile-unresolved"
|
|
1856
|
+
? "not approved: the routine cannot name an exact agent for every role — configure the project's agents, then file the standing order again"
|
|
1857
|
+
: approved.reason === "requester"
|
|
1858
|
+
? "You made this standing order, and this project needs someone else to approve it."
|
|
1859
|
+
: `not approved: ${approved.reason}`;
|
|
1860
|
+
return routinePage(response, who, routineId, words, status);
|
|
1861
|
+
}
|
|
1862
|
+
return redirect(response, `/routines/${routineId}`);
|
|
1863
|
+
}
|
|
1864
|
+
case "refresh": {
|
|
1865
|
+
// THE RECOVERY ROAD (v48): re-resolve the agents from today's
|
|
1866
|
+
// configuration and file them as the order's working agents. This
|
|
1867
|
+
// approves nothing — the page then shows the exact agents and asks
|
|
1868
|
+
// for the password again.
|
|
1869
|
+
const refreshed = refreshRoutineAgents(store, routineId, now);
|
|
1870
|
+
if (!refreshed.ok) {
|
|
1871
|
+
return routinePage(response, who, routineId, `agents not refreshed: ${refreshed.problem}`, 409);
|
|
1872
|
+
}
|
|
1873
|
+
return redirect(response, `/routines/${routineId}`);
|
|
1874
|
+
}
|
|
1875
|
+
case "pause":
|
|
1876
|
+
case "resume": {
|
|
1877
|
+
store.setRoutinePaused(routineId, verb === "pause", now);
|
|
1878
|
+
return redirect(response, `/routines/${routineId}`);
|
|
1879
|
+
}
|
|
1880
|
+
case "run-now": {
|
|
1881
|
+
if (who.via !== "cookie")
|
|
1882
|
+
return refuse(response, who, 403, REMOTE_MESSAGES["step-up"]);
|
|
1883
|
+
// Step-up (Codex Phase C review, M3): run-now is spend outside the
|
|
1884
|
+
// approved schedule, so a session alone cannot ask for it — the
|
|
1885
|
+
// password is typed again, like an approval.
|
|
1886
|
+
const token = body.get("token") ?? "";
|
|
1887
|
+
if (token === "" && !hasFreshIdentitySignIn(who.name)) {
|
|
1888
|
+
return routinePage(response, who, routineId, "run now requires your password, typed again", 400);
|
|
1889
|
+
}
|
|
1890
|
+
const authenticated = authenticateApprover(who, token);
|
|
1891
|
+
if (!authenticated.ok) {
|
|
1892
|
+
return routinePage(response, who, routineId, "that is not your password", 403);
|
|
1893
|
+
}
|
|
1894
|
+
const outcome = fireRoutine(store, routineId, now, { manual: true });
|
|
1895
|
+
if (!outcome.ok) {
|
|
1896
|
+
return routinePage(response, who, routineId, `not fired: ${outcome.detail ?? outcome.reason}`, 409);
|
|
1897
|
+
}
|
|
1898
|
+
return redirect(response, `/routines/${routineId}`);
|
|
1899
|
+
}
|
|
1900
|
+
default:
|
|
1901
|
+
return refuse(response, who, 404, "That action isn't available here.", `/routines/${routineId}`);
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
// ---- mutations -----------------------------------------------------------
|
|
1905
|
+
/** v28: the console-scoped liveness beat — see the route note in
|
|
1906
|
+
* handlePost. Supersedes v2 S2f's per-page id binding, reversed
|
|
1907
|
+
* KNOWINGLY: with parallel sessions one foregrounded tab per session is
|
|
1908
|
+
* impossible, and page-binding was attention theater over what was
|
|
1909
|
+
* always renewal-of-use. Approver-bound; never extends cookies, never
|
|
1910
|
+
* touches absolute expiry, never mints. */
|
|
1911
|
+
function attendedBeats(who, request, response, now) {
|
|
1912
|
+
if (who.via !== "cookie")
|
|
1913
|
+
return refuse(response, who, 403, "watching is a browser session's act");
|
|
1914
|
+
// Round-2 finding 2's named miss: renewal is an approver's act — a
|
|
1915
|
+
// viewer's open tab keeps nothing alive.
|
|
1916
|
+
if (who.role !== "approver")
|
|
1917
|
+
return refuse(response, who, 403, "your login can watch, not keep sessions alive");
|
|
1918
|
+
if (request.headers["sec-fetch-site"] !== "same-origin") {
|
|
1919
|
+
return refuse(response, who, 403, "the beat only answers this console's own pages");
|
|
1920
|
+
}
|
|
1921
|
+
// Belt with the braces: a PRESENT Origin/Referer must also name this
|
|
1922
|
+
// server — the same allowlist the shared guard applies.
|
|
1923
|
+
const namedOrigin = typeof request.headers.origin === "string" && request.headers.origin !== "null"
|
|
1924
|
+
? request.headers.origin
|
|
1925
|
+
: typeof request.headers.referer === "string"
|
|
1926
|
+
? request.headers.referer
|
|
1927
|
+
: null;
|
|
1928
|
+
if (namedOrigin !== null && !allowedHost(namedOrigin.replace(/^https?:[/][/]/, "").split("/")[0])) {
|
|
1929
|
+
return refuse(response, who, 403, "origin not allowed");
|
|
1930
|
+
}
|
|
1931
|
+
const attendedRunner = options.attended?.runner;
|
|
1932
|
+
let beaten = 0;
|
|
1933
|
+
if (attendedRunner !== undefined) {
|
|
1934
|
+
for (const open of store.openAuthorizationsOf(attendedRunner)) {
|
|
1935
|
+
if (open.approver !== who.name)
|
|
1936
|
+
continue;
|
|
1937
|
+
if (Date.parse(open.absoluteExpiry) <= now.getTime())
|
|
1938
|
+
continue;
|
|
1939
|
+
// Only terms SIGNED for console-wide renewal are renewed here
|
|
1940
|
+
// (round-1 finding 2): a legacy page-bound signature never
|
|
1941
|
+
// silently acquires the wider mode — it simply lapses.
|
|
1942
|
+
try {
|
|
1943
|
+
const signedMode = JSON.parse(open.termsJson).attentionMode;
|
|
1944
|
+
if (signedMode !== "console-visible")
|
|
1945
|
+
continue;
|
|
1946
|
+
}
|
|
1947
|
+
catch {
|
|
1948
|
+
continue;
|
|
1949
|
+
}
|
|
1950
|
+
const beatRef = store.refForId(open.taskRef);
|
|
1951
|
+
if (beatRef === null || !visible(beatRef.repo))
|
|
1952
|
+
continue;
|
|
1953
|
+
store.beatAuthorization(open.id, now);
|
|
1954
|
+
if (open.attemptRun !== null)
|
|
1955
|
+
options.attended?.coordinator?.poke(open.attemptRun);
|
|
1956
|
+
beaten++;
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
return respond(response, 200, "application/json; charset=utf-8", JSON.stringify({ ok: true, beaten }));
|
|
1960
|
+
}
|
|
1961
|
+
async function attendMutation(response, who, taskId, verb, body, now) {
|
|
1962
|
+
const ref = store.lookupRef(taskId);
|
|
1963
|
+
if (ref === null || store.getTask(taskId) === null || !visible(ref.repo)) {
|
|
1964
|
+
return refuse(response, who, 404, "no such task", "/tasks");
|
|
1965
|
+
}
|
|
1966
|
+
if (who.via !== "cookie")
|
|
1967
|
+
return refuse(response, who, 403, "watching is a browser session's act");
|
|
1968
|
+
if (store.isDemo())
|
|
1969
|
+
return refuse(response, who, 403, "the demo authorizes nothing");
|
|
1970
|
+
if (verb === "attend-revoke") {
|
|
1971
|
+
const open = store.openAuthorizationFor(ref.id);
|
|
1972
|
+
if (open === null)
|
|
1973
|
+
return refuse(response, who, 409, "nothing to revoke", taskHref(taskId));
|
|
1974
|
+
store.closeAuthorization(open.id, "revoked", now);
|
|
1975
|
+
if (open.attemptRun !== null)
|
|
1976
|
+
options.attended?.coordinator?.poke(open.attemptRun);
|
|
1977
|
+
return redirect(response, taskHref(taskId));
|
|
1978
|
+
}
|
|
1979
|
+
const parentGiven = body.get("parent");
|
|
1980
|
+
const followupGiven = body.get("followup");
|
|
1981
|
+
const inputs = {
|
|
1982
|
+
minutes: Number(body.get("minutes") ?? "60"),
|
|
1983
|
+
turns: Number(body.get("turns") ?? "20"),
|
|
1984
|
+
budgetMicrousd: Number(body.get("budget") ?? String(store.getScope(taskId)?.budgetMicrousd ?? 2_000_000)),
|
|
1985
|
+
...(body.get("expiry") === null ? {} : { expiry: body.get("expiry") }),
|
|
1986
|
+
...(parentGiven === null || parentGiven === "" ? {} : { parent: Number(parentGiven) }),
|
|
1987
|
+
...(followupGiven === null ? {} : { followup: followupGiven }),
|
|
1988
|
+
...(body.get("model") === null ? {} : { model: body.get("model") }),
|
|
1989
|
+
...(body.get("posture") === null ? {} : { posture: body.get("posture") }),
|
|
1990
|
+
};
|
|
1991
|
+
if (store.openAuthorizationFor(ref.id) !== null) {
|
|
1992
|
+
return refuse(response, who, 409, "an authorization is already open — revoke it first", taskHref(taskId));
|
|
1993
|
+
}
|
|
1994
|
+
const live = await liveAttendedTerms(taskId, inputs, now);
|
|
1995
|
+
if (!live.ok)
|
|
1996
|
+
return refuse(response, who, 409, live.problem, taskHref(taskId));
|
|
1997
|
+
// Sprint 8: an attended session runs at exactly what the person signs, so the organisation policy refuses it
|
|
1998
|
+
// outright (a provider, a model, or a posture above the permission ceiling) before anyone signs.
|
|
1999
|
+
{
|
|
2000
|
+
const pinned = profileFromJson(live.terms.profileJson);
|
|
2001
|
+
const refused = pinned === null ? null : store.attendedPolicyRefusal(pinned);
|
|
2002
|
+
if (refused !== null)
|
|
2003
|
+
return refuse(response, who, 403, refused, taskHref(taskId));
|
|
2004
|
+
}
|
|
2005
|
+
const digest = attendedDigestOf(live.terms);
|
|
2006
|
+
if (verb === "attend-preview") {
|
|
2007
|
+
const nonce = mintApprovalNonce(who.name, `attend-${taskId}`, digest);
|
|
2008
|
+
return attendConfirmScreen(response, who, live.terms, inputs, digest, nonce, who.session.csrf);
|
|
2009
|
+
}
|
|
2010
|
+
// attend: the yes. The nonce proves THIS form; the digest re-derived
|
|
2011
|
+
// from live state proves the world held between reading and signing.
|
|
2012
|
+
const nonce = body.get("nonce") ?? "";
|
|
2013
|
+
if (!consumeApprovalNonce(nonce, who.name, `attend-${taskId}`, body.get("digest") ?? "")) {
|
|
2014
|
+
return refuse(response, who, 409, "that form is stale — read it again", taskHref(taskId));
|
|
2015
|
+
}
|
|
2016
|
+
if (digest !== (body.get("digest") ?? "")) {
|
|
2017
|
+
return refuse(response, who, 409, "the world moved while you were reading (scope, model, or head) — read it again", taskHref(taskId));
|
|
2018
|
+
}
|
|
2019
|
+
const token = body.get("token") ?? "";
|
|
2020
|
+
let basis;
|
|
2021
|
+
if (token === "" && !hasFreshIdentitySignIn(who.name)) {
|
|
2022
|
+
// Quick mint (C2/M4): no password typed — valid ONLY when a live
|
|
2023
|
+
// mode with quickMint was signed by THIS session's person. The mint
|
|
2024
|
+
// transaction re-proves it; this pre-check only shapes the refusal.
|
|
2025
|
+
const mode = ref.repo === null ? null : store.activeMode(ref.repo, now);
|
|
2026
|
+
const modeTerms = mode === null ? null : modeTermsFromJson(mode.termsJson);
|
|
2027
|
+
if (mode === null || modeTerms === null || !modeTerms.quickMint || mode.signedBy !== who.name) {
|
|
2028
|
+
return refuse(response, who, 403, "authorizing takes your password, typed again", taskHref(taskId));
|
|
2029
|
+
}
|
|
2030
|
+
basis = { kind: "mode", digest: mode.digest };
|
|
2031
|
+
}
|
|
2032
|
+
else if (!authenticateApprover(who, token).ok) {
|
|
2033
|
+
return refuse(response, who, 403, "authorizing takes your password, typed again", taskHref(taskId));
|
|
2034
|
+
}
|
|
2035
|
+
const minted = store.mintAttendedAuthorization({
|
|
2036
|
+
id: randomUUID(),
|
|
2037
|
+
taskRef: ref.id,
|
|
2038
|
+
approver: who.name,
|
|
2039
|
+
runner: live.terms.runner,
|
|
2040
|
+
runnerGeneration: live.terms.runnerGeneration,
|
|
2041
|
+
compositeDigest: digest,
|
|
2042
|
+
termsJson: attendedTermsJson(live.terms),
|
|
2043
|
+
maxSessionTurns: live.terms.maxSessionTurns,
|
|
2044
|
+
budgetMicrousd: live.terms.budgetMicrousd,
|
|
2045
|
+
...(live.terms.parentRun == null ? {} : { parentRun: live.terms.parentRun }),
|
|
2046
|
+
...(live.terms.followup == null ? {} : { followup: live.terms.followup }),
|
|
2047
|
+
absoluteExpiry: live.terms.absoluteExpiry,
|
|
2048
|
+
...(basis === undefined ? {} : { basis }),
|
|
2049
|
+
now,
|
|
2050
|
+
});
|
|
2051
|
+
if (!minted.ok) {
|
|
2052
|
+
return refuse(response, who, minted.reason === "authorization-open" ? 409 : 403, minted.reason === "mode-ended"
|
|
2053
|
+
? "the mode that covered quick minting has ended — your password, typed again, still works"
|
|
2054
|
+
: minted.reason === "approval-rules"
|
|
2055
|
+
? "this project's approval rules need someone else's approval (or two people's) — a watched run can't stand in for it"
|
|
2056
|
+
: "an authorization is already open — revoke it first", taskHref(taskId));
|
|
2057
|
+
}
|
|
2058
|
+
// The first beat is the mint itself: the person is visibly here.
|
|
2059
|
+
store.beatAuthorization(minted.authorization.id, now);
|
|
2060
|
+
return redirect(response, taskHref(taskId));
|
|
2061
|
+
}
|
|
2062
|
+
function taskMutation(response, who, taskId, verb, posted, now) {
|
|
2063
|
+
const ref = store.lookupRef(taskId);
|
|
2064
|
+
if (ref === null || store.getTask(taskId) === null) {
|
|
2065
|
+
return refuse(response, who, 404, "no such task", "/tasks");
|
|
2066
|
+
}
|
|
2067
|
+
if (!visible(ref.repo)) {
|
|
2068
|
+
return refuse(response, who, 404, "no such task", "/tasks");
|
|
2069
|
+
}
|
|
2070
|
+
switch (verb) {
|
|
2071
|
+
case "steer": {
|
|
2072
|
+
const body = readForm(posted, CONSOLE_FORMS.taskSteer);
|
|
2073
|
+
// Steering is a browser session's act, explicitly (arc 1 v2 §3):
|
|
2074
|
+
// identify() accepts bearer credentials generically, and those are
|
|
2075
|
+
// for machines — a person watching steers, cookie + CSRF only.
|
|
2076
|
+
if (who.via !== "cookie") {
|
|
2077
|
+
return refuse(response, who, 403, "steering is a browser session's act");
|
|
2078
|
+
}
|
|
2079
|
+
// The session IS the verified principal here — cookie + CSRF proved it.
|
|
2080
|
+
const filed = store.fileSteerNote(taskId, verifiedAuthor(who.name), body.get("note") ?? "", now);
|
|
2081
|
+
if (!filed.ok) {
|
|
2082
|
+
const said = filed.reason === "contest-open"
|
|
2083
|
+
? "agents are racing on this task — steering waits until the tournament settles"
|
|
2084
|
+
: filed.reason === "task-finished"
|
|
2085
|
+
? "this task is finished — a note has no next attempt to reach"
|
|
2086
|
+
: filed.reason === "invalid-note"
|
|
2087
|
+
? (filed.problem ?? "that note will not store")
|
|
2088
|
+
: "no such task";
|
|
2089
|
+
return taskScreen(response, who, taskId, said, 400);
|
|
2090
|
+
}
|
|
2091
|
+
return redirect(response, taskHref(taskId));
|
|
2092
|
+
}
|
|
2093
|
+
case "hold": {
|
|
2094
|
+
const body = readForm(posted, CONSOLE_FORMS.taskHold);
|
|
2095
|
+
const reason = (body.get("reason") ?? "").trim() || "held from the console";
|
|
2096
|
+
if (reason.length > 200 || hasForbiddenControls(reason)) {
|
|
2097
|
+
return taskScreen(response, who, taskId, "that reason will not render, so it will not store", 400);
|
|
2098
|
+
}
|
|
2099
|
+
// Operator-owned only, always: the form supplies a reason, never an
|
|
2100
|
+
// owner. Decision, incident, and backoff holds are not reachable
|
|
2101
|
+
// from here, whatever a request claims.
|
|
2102
|
+
store.hold(ref.id, reason, null, now);
|
|
2103
|
+
return redirect(response, taskHref(taskId));
|
|
2104
|
+
}
|
|
2105
|
+
case "block": {
|
|
2106
|
+
const body = readForm(posted, CONSOLE_FORMS.taskBlock);
|
|
2107
|
+
// Chains are scheduling, not authority (chains-and-next review,
|
|
2108
|
+
// finding 2): the edge decides WHEN the ready set admits the task;
|
|
2109
|
+
// approval still decides WHAT may build. Both ends are re-proved
|
|
2110
|
+
// here — existence, ceiling, and the tournament guard — and the
|
|
2111
|
+
// cycle refusal comes from the store's own closure check.
|
|
2112
|
+
const on = (body.get("on") ?? "").trim();
|
|
2113
|
+
const blocker = on === "" ? null : store.getTask(on);
|
|
2114
|
+
const blockerRef = on === "" ? null : store.lookupRef(on);
|
|
2115
|
+
if (blocker === null || blockerRef === null || !visible(blockerRef.repo)) {
|
|
2116
|
+
return taskScreen(response, who, taskId, "that task to wait for does not exist here", 404);
|
|
2117
|
+
}
|
|
2118
|
+
if (store.openContestFor(ref.id) !== null) {
|
|
2119
|
+
return taskScreen(response, who, taskId, "a tournament is running on this task — let it finish, then pick or abandon it", 409);
|
|
2120
|
+
}
|
|
2121
|
+
const added = store.addEdge(taskId, on);
|
|
2122
|
+
if (!added.ok) {
|
|
2123
|
+
return taskScreen(response, who, taskId, `could not make ${taskId} wait for ${on} — ${added.reason}`, 409);
|
|
2124
|
+
}
|
|
2125
|
+
return redirect(response, taskHref(taskId));
|
|
2126
|
+
}
|
|
2127
|
+
case "unblock": {
|
|
2128
|
+
const body = readForm(posted, CONSOLE_FORMS.taskBlock);
|
|
2129
|
+
const on = (body.get("on") ?? "").trim();
|
|
2130
|
+
if (store.openContestFor(ref.id) !== null) {
|
|
2131
|
+
return taskScreen(response, who, taskId, "a tournament is running on this task — let it finish, then pick or abandon it", 409);
|
|
2132
|
+
}
|
|
2133
|
+
const removed = store.removeEdge(taskId, on);
|
|
2134
|
+
if (!removed.ok) {
|
|
2135
|
+
return taskScreen(response, who, taskId, `${taskId} was not waiting on ${on}`, 409);
|
|
2136
|
+
}
|
|
2137
|
+
return redirect(response, taskHref(taskId));
|
|
2138
|
+
}
|
|
2139
|
+
case "repair-dependency": {
|
|
2140
|
+
const body = readForm(posted, CONSOLE_FORMS.taskRepairDependency);
|
|
2141
|
+
const blocker = (body.get("blocker") ?? "").trim();
|
|
2142
|
+
const operation = (body.get("operation") ?? "").trim();
|
|
2143
|
+
const blockerTask = blocker === "" || !store.blockers(taskId).includes(blocker) ? null : store.getTask(blocker);
|
|
2144
|
+
if (blockerTask === null || (blockerTask.state !== "failed" && blockerTask.state !== "cancelled")) {
|
|
2145
|
+
return taskScreen(response, who, taskId, "what this task waits for changed — refresh the page and choose again", 409);
|
|
2146
|
+
}
|
|
2147
|
+
if (store.openContestFor(ref.id) !== null) {
|
|
2148
|
+
return taskScreen(response, who, taskId, "a tournament is running on this task — let it finish before changing its dependencies", 409);
|
|
2149
|
+
}
|
|
2150
|
+
if (operation === "retry") {
|
|
2151
|
+
const blockerRef = store.lookupRef(blocker);
|
|
2152
|
+
if (blockerTask.state !== "failed" || blockerRef === null || !visible(blockerRef.repo)) {
|
|
2153
|
+
return taskScreen(response, who, taskId, "that failed task cannot be tried again here — wait for a different task or continue without it", 409);
|
|
2154
|
+
}
|
|
2155
|
+
const retried = store.requeueTask(blocker, who.name, now);
|
|
2156
|
+
if (!retried.ok)
|
|
2157
|
+
return taskScreen(response, who, taskId, `that task could not be queued again — ${retried.reason}`, 409);
|
|
2158
|
+
return redirect(response, taskHref(taskId));
|
|
2159
|
+
}
|
|
2160
|
+
if (operation === "unlink") {
|
|
2161
|
+
const removed = store.removeEdge(taskId, blocker);
|
|
2162
|
+
if (!removed.ok)
|
|
2163
|
+
return taskScreen(response, who, taskId, "what this task waits for changed — refresh the page and choose again", 409);
|
|
2164
|
+
return redirect(response, taskHref(taskId));
|
|
2165
|
+
}
|
|
2166
|
+
if (operation === "replace") {
|
|
2167
|
+
const replacement = (body.get("replacement") ?? "").trim();
|
|
2168
|
+
const replacementTask = replacement === "" ? null : store.getTask(replacement);
|
|
2169
|
+
const replacementRef = replacement === "" ? null : store.lookupRef(replacement);
|
|
2170
|
+
if (replacementTask === null ||
|
|
2171
|
+
replacementRef === null ||
|
|
2172
|
+
!visible(replacementRef.repo) ||
|
|
2173
|
+
(replacementTask.state !== "queued" && replacementTask.state !== "running")) {
|
|
2174
|
+
return taskScreen(response, who, taskId, "choose another unfinished task from a project you can manage", 409);
|
|
2175
|
+
}
|
|
2176
|
+
const replaced = store.replaceEdge(taskId, blocker, replacement);
|
|
2177
|
+
if (!replaced.ok)
|
|
2178
|
+
return taskScreen(response, who, taskId, `this task could not wait for the selected work — ${replaced.reason}`, 409);
|
|
2179
|
+
return redirect(response, taskHref(taskId));
|
|
2180
|
+
}
|
|
2181
|
+
return taskScreen(response, who, taskId, "choose whether to try that task again, wait for a different task, or continue without it", 400);
|
|
2182
|
+
}
|
|
2183
|
+
case "next": {
|
|
2184
|
+
const body = readForm(posted, CONSOLE_FORMS.taskNext);
|
|
2185
|
+
if (body.get("undo") !== null) {
|
|
2186
|
+
const cleared = store.clearTaskPriority(taskId);
|
|
2187
|
+
if (!cleared.ok)
|
|
2188
|
+
return taskScreen(response, who, taskId, "this task could not be put back in filing order", 409);
|
|
2189
|
+
return redirect(response, taskHref(taskId));
|
|
2190
|
+
}
|
|
2191
|
+
const moved = store.moveTaskNext(taskId, now);
|
|
2192
|
+
if (!moved.ok) {
|
|
2193
|
+
const said = moved.reason === "not-queued"
|
|
2194
|
+
? "only queued work can move up — this task is not waiting in the queue"
|
|
2195
|
+
: moved.reason === "claimed"
|
|
2196
|
+
? "this task is being built right now — it needs no place in line"
|
|
2197
|
+
: moved.reason === "contest-open"
|
|
2198
|
+
? "a tournament is running on this task — let it finish, then pick or abandon it"
|
|
2199
|
+
: "the queue rank could not be raised";
|
|
2200
|
+
return taskScreen(response, who, taskId, said, 409);
|
|
2201
|
+
}
|
|
2202
|
+
return redirect(response, taskHref(taskId));
|
|
2203
|
+
}
|
|
2204
|
+
case "reopen": {
|
|
2205
|
+
const body = readForm(posted, CONSOLE_FORMS.taskReopen);
|
|
2206
|
+
// Authenticated like every approving act: the session alone may
|
|
2207
|
+
// read; resuming external work takes the password, typed again.
|
|
2208
|
+
const token = (body.get("token") ?? "").trim();
|
|
2209
|
+
const authenticated = token !== "" ? authenticateApprover(who, token) : null;
|
|
2210
|
+
if (authenticated === null || !authenticated.ok) {
|
|
2211
|
+
return taskScreen(response, who, taskId, "reopening takes your password, typed again", 403);
|
|
2212
|
+
}
|
|
2213
|
+
const reopened = store.reopenMirror(taskId, who.name, now);
|
|
2214
|
+
if (!reopened.ok) {
|
|
2215
|
+
const said = {
|
|
2216
|
+
"unknown-task": "this task is not external work",
|
|
2217
|
+
"not-latched": "the tracker never closed this — there is nothing to reopen",
|
|
2218
|
+
"not-seen-open": "the tracker has not been seen open again since the close — reopen it there first; the next sync notices",
|
|
2219
|
+
claimed: "this task is being built right now",
|
|
2220
|
+
"contest-open": "a tournament is open on this task — decide it first",
|
|
2221
|
+
held: "a hold stands — lift it first",
|
|
2222
|
+
"question-open": "an unanswered question stands — answer or close it first",
|
|
2223
|
+
"incident-open": "an unresolved incident stands — resolve it first",
|
|
2224
|
+
"bad-state": "this task is not in a state reopen can take",
|
|
2225
|
+
};
|
|
2226
|
+
return taskScreen(response, who, taskId, said[reopened.reason] ?? "the task could not be reopened", 409);
|
|
2227
|
+
}
|
|
2228
|
+
return redirect(response, taskHref(taskId));
|
|
2229
|
+
}
|
|
2230
|
+
case "unhold": {
|
|
2231
|
+
store.unhold(ref.id);
|
|
2232
|
+
return redirect(response, taskHref(taskId));
|
|
2233
|
+
}
|
|
2234
|
+
case "requeue": {
|
|
2235
|
+
const body = readForm(posted, CONSOLE_FORMS.taskRequeue);
|
|
2236
|
+
// Retry may carry a note for the next attempt: the same steering note, checked before anything changes.
|
|
2237
|
+
const note = (body.get("note") ?? "").trim();
|
|
2238
|
+
if (note !== "" && who.via !== "cookie")
|
|
2239
|
+
return refuse(response, who, 403, "steering is a browser session's act");
|
|
2240
|
+
if (note !== "" && !validateNote(note).ok)
|
|
2241
|
+
return taskScreen(response, who, taskId, "that note will not store, so nothing was retried", 400);
|
|
2242
|
+
const requeued = store.requeueTask(taskId, who.name, now);
|
|
2243
|
+
if (!requeued.ok) {
|
|
2244
|
+
return taskScreen(response, who, taskId, `not requeued: ${requeued.reason}`, 409);
|
|
2245
|
+
}
|
|
2246
|
+
if (note !== "") {
|
|
2247
|
+
const filed = store.fileSteerNote(taskId, verifiedAuthor(who.name), note, now);
|
|
2248
|
+
if (!filed.ok)
|
|
2249
|
+
return taskScreen(response, who, taskId, "Retried. The note wasn't saved; add it under Steering.", 409);
|
|
2250
|
+
}
|
|
2251
|
+
// Allow-listed return only — never an arbitrary URL from the form.
|
|
2252
|
+
return redirect(response, body.get("return") === "inbox" ? "/inbox" : body.get("return") === "next" ? "/next" : taskHref(taskId));
|
|
2253
|
+
}
|
|
2254
|
+
case "follow-up": {
|
|
2255
|
+
const body = readForm(posted, CONSOLE_FORMS.taskFollowUp);
|
|
2256
|
+
// A scout's proposed follow-up, filed by the operator's tap (mate
|
|
2257
|
+
// arc §10): the ONE filing door, this task's repository, the scope
|
|
2258
|
+
// text stamped as the scout's — mode coverage never seals it.
|
|
2259
|
+
const indexRaw = body.get("index") ?? "";
|
|
2260
|
+
if (!/^[0-9]{1,2}$/.test(indexRaw))
|
|
2261
|
+
return taskScreen(response, who, taskId, "which follow-up?", 400);
|
|
2262
|
+
const view = readVerifiedReport(store, evidenceRoot, ref.id);
|
|
2263
|
+
if (view === null || !view.ok)
|
|
2264
|
+
return taskScreen(response, who, taskId, "this task has no report to file from", 409);
|
|
2265
|
+
const followUp = view.report.followUps[Number(indexRaw)];
|
|
2266
|
+
if (followUp === undefined)
|
|
2267
|
+
return taskScreen(response, who, taskId, "the report proposes no such follow-up", 409);
|
|
2268
|
+
if (ref.repo === null)
|
|
2269
|
+
return taskScreen(response, who, taskId, "this task has no repository — file the follow-up by hand", 409);
|
|
2270
|
+
if (!visible(ref.repo))
|
|
2271
|
+
return taskScreen(response, who, taskId, "that repository is outside what this server shows", 403);
|
|
2272
|
+
// Idempotent by construction (v4 review, finding 10): the filing's
|
|
2273
|
+
// id is derived from the source task, the report's run, and the
|
|
2274
|
+
// follow-up's place — a retried POST lands on the task the first
|
|
2275
|
+
// one filed instead of minting a twin.
|
|
2276
|
+
const followUpId = `${taskId.slice(0, 40)}-r${view.run}-f${Number(indexRaw) + 1}`;
|
|
2277
|
+
if (store.getTask(followUpId) !== null)
|
|
2278
|
+
return redirect(response, taskHref(followUpId));
|
|
2279
|
+
const filed = fileTaskProposal(store, {
|
|
2280
|
+
id: followUpId,
|
|
2281
|
+
title: followUp.title,
|
|
2282
|
+
repo: ref.repo,
|
|
2283
|
+
goal: followUp.goal,
|
|
2284
|
+
// v39: the scout's report format does not yet draft a rubric
|
|
2285
|
+
// per follow-up — a placeholder names the operator's own
|
|
2286
|
+
// review as the outstanding work, the same posture a
|
|
2287
|
+
// coordinator's bare intent takes.
|
|
2288
|
+
acceptance: PLACEHOLDER_RUBRIC,
|
|
2289
|
+
filedVia: "console", filedBy: { name: who.name, kind: "person" },
|
|
2290
|
+
proposedVia: "scout",
|
|
2291
|
+
...(unscopedMode ? {} : { admittedRepos: admissionList() ?? [] }),
|
|
2292
|
+
}, now);
|
|
2293
|
+
if (!filed.ok) {
|
|
2294
|
+
if (filed.reason === "duplicate")
|
|
2295
|
+
return redirect(response, taskHref(followUpId));
|
|
2296
|
+
return taskScreen(response, who, taskId, `not filed: ${filed.message}`, filed.reason === "backlog-full" ? 429 : 400);
|
|
2297
|
+
}
|
|
2298
|
+
return redirect(response, taskHref(filed.id));
|
|
2299
|
+
}
|
|
2300
|
+
case "plan": {
|
|
2301
|
+
// The operator's explicit ask, refused transactionally when the
|
|
2302
|
+
// moment has passed — approved scope, live claim, or a plan
|
|
2303
|
+
// already under way (Codex planning review's requestPlan guard).
|
|
2304
|
+
const asked = store.requestPlan(ref.id, now);
|
|
2305
|
+
if (!asked.ok) {
|
|
2306
|
+
return taskScreen(response, who, taskId, `not planned: ${asked.reason}`, 409);
|
|
2307
|
+
}
|
|
2308
|
+
if (who.via === "cookie")
|
|
2309
|
+
authorizePlanUnderMode(store, taskId, who.name, now);
|
|
2310
|
+
return redirect(response, taskHref(taskId));
|
|
2311
|
+
}
|
|
2312
|
+
case "plan-edit": {
|
|
2313
|
+
const body = readForm(posted, CONSOLE_FORMS.taskPlanEdit);
|
|
2314
|
+
if (who.via !== "cookie") {
|
|
2315
|
+
return refuse(response, who, 403, "editing a plan is a browser session's act");
|
|
2316
|
+
}
|
|
2317
|
+
const scope = store.getScope(taskId);
|
|
2318
|
+
const current = planViewOf(ref.id);
|
|
2319
|
+
if (ref.plan !== "drafted" || scope === null || current === null) {
|
|
2320
|
+
return taskScreen(response, who, taskId, "there is no editable plan on this task", 409);
|
|
2321
|
+
}
|
|
2322
|
+
if (approvalOf(scope).approved) {
|
|
2323
|
+
return taskScreen(response, who, taskId, "this plan is already approved and locked — file a revision instead", 409);
|
|
2324
|
+
}
|
|
2325
|
+
if (store.hasLiveClaim(ref.id, now)) {
|
|
2326
|
+
return taskScreen(response, who, taskId, "this task is running — its plan cannot change underneath the agent", 409);
|
|
2327
|
+
}
|
|
2328
|
+
if ((body.get("saw-plan") ?? "") !== current.sha256) {
|
|
2329
|
+
return taskScreen(response, who, taskId, "the plan changed while this editor was open — read the latest version and try again", 409);
|
|
2330
|
+
}
|
|
2331
|
+
const document = (body.get("plan-document") ?? "").replace(/\r\n?/g, "\n").trim();
|
|
2332
|
+
const parsed = parseExecutionPlanDocument(document);
|
|
2333
|
+
if (!parsed.ok) {
|
|
2334
|
+
return taskScreen(response, who, taskId, `plan not saved: ${parsed.problems.map(one => one.message).join("; ")}`, 400);
|
|
2335
|
+
}
|
|
2336
|
+
const proof = parsed.document.proof.join("\n");
|
|
2337
|
+
const missing = scope.acceptance.filter(criterion => {
|
|
2338
|
+
const id = criterion.id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2339
|
+
return !new RegExp(`(^|[^A-Za-z0-9_-])${id}([^A-Za-z0-9_-]|$)`).test(proof);
|
|
2340
|
+
});
|
|
2341
|
+
if (missing.length > 0) {
|
|
2342
|
+
return taskScreen(response, who, taskId, `plan not saved: Proof must name ${missing.map(one => one.id).join(", ")}`, 400);
|
|
2343
|
+
}
|
|
2344
|
+
if (document === current.document.trim())
|
|
2345
|
+
return redirect(response, taskHref(taskId));
|
|
2346
|
+
const content = Buffer.from(`${document}\n`, "utf8");
|
|
2347
|
+
if (content.length > TEXT_LIMITS.planDocumentBytes) {
|
|
2348
|
+
return taskScreen(response, who, taskId, `plan not saved: it is over ${TEXT_LIMITS.planDocumentBytes} bytes`, 400);
|
|
2349
|
+
}
|
|
2350
|
+
try {
|
|
2351
|
+
const name = `plan-edit-${randomUUID().replace(/-/g, "")}.md`;
|
|
2352
|
+
const key = writeEvidenceFile(evidenceRoot, current.run, name, content);
|
|
2353
|
+
store.saveArtifact({
|
|
2354
|
+
run: current.run,
|
|
2355
|
+
kind: "plan",
|
|
2356
|
+
key,
|
|
2357
|
+
bytesOriginal: content.length,
|
|
2358
|
+
bytesStored: content.length,
|
|
2359
|
+
truncated: false,
|
|
2360
|
+
sha256: createHash("sha256").update(content).digest("hex"),
|
|
2361
|
+
capture: `operator edit by ${who.name} (verified session)`,
|
|
2362
|
+
}, now);
|
|
2363
|
+
}
|
|
2364
|
+
catch {
|
|
2365
|
+
return taskScreen(response, who, taskId, "the plan could not be saved safely — nothing changed", 500);
|
|
2366
|
+
}
|
|
2367
|
+
return redirect(response, taskHref(taskId));
|
|
2368
|
+
}
|
|
2369
|
+
case "accept-revision":
|
|
2370
|
+
case "reject-revision": {
|
|
2371
|
+
const body = readForm(posted, CONSOLE_FORMS.taskRevision);
|
|
2372
|
+
// Adaptive execution plans (v44): resolving a revision the builder
|
|
2373
|
+
// filed while its authority snapshot no longer matched the run it
|
|
2374
|
+
// started under — the ONLY road a 'blocked' plan_revision reaches,
|
|
2375
|
+
// since a plan-only proposal auto-applies without ever pausing here.
|
|
2376
|
+
if (who.via !== "cookie") {
|
|
2377
|
+
return refuse(response, who, 403, "resolving a plan revision is a browser session's act");
|
|
2378
|
+
}
|
|
2379
|
+
const revisionId = Number(body.get("revision-id") ?? "");
|
|
2380
|
+
const revision = Number.isFinite(revisionId) ? store.getPlanRevision(revisionId) : null;
|
|
2381
|
+
if (revision === null || revision.taskRef !== ref.id || revision.status !== "blocked") {
|
|
2382
|
+
return taskScreen(response, who, taskId, "that plan revision is no longer waiting on a decision — refresh and try again", 409);
|
|
2383
|
+
}
|
|
2384
|
+
const outcome = verb === "accept-revision" ? "applied" : "rejected";
|
|
2385
|
+
if (outcome === "applied") {
|
|
2386
|
+
// Accepting a revision that changed signed scope or publication
|
|
2387
|
+
// authority takes the password ceremony again — the same act
|
|
2388
|
+
// that would be required to approve that authority from scratch.
|
|
2389
|
+
const token = (body.get("token") ?? "").trim();
|
|
2390
|
+
if (!authenticateApprover(who, token).ok) {
|
|
2391
|
+
return taskScreen(response, who, taskId, "accepting this revision takes your password, typed again", 403);
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
const resolved = store.resolvePlanRevision(revision.id, outcome, who.name, now);
|
|
2395
|
+
if (!resolved) {
|
|
2396
|
+
return taskScreen(response, who, taskId, "that plan revision is no longer waiting on a decision — refresh and try again", 409);
|
|
2397
|
+
}
|
|
2398
|
+
store.releaseOwnedHold("revision", String(revision.id));
|
|
2399
|
+
return redirect(response, taskHref(taskId));
|
|
2400
|
+
}
|
|
2401
|
+
case "cancel": {
|
|
2402
|
+
const body = readForm(posted, CONSOLE_FORMS.taskCancel);
|
|
2403
|
+
const reason = body.get("reason") ?? undefined;
|
|
2404
|
+
const cancelled = withActor({ account: who.name, lead: false }, () => store.cancelTask(taskId, now, reason));
|
|
2405
|
+
if (!cancelled.ok) {
|
|
2406
|
+
if (cancelled.reason === "reason-required" || cancelled.reason === "bad-reason") {
|
|
2407
|
+
return taskScreen(response, who, taskId, cancelled.reason === "reason-required"
|
|
2408
|
+
? "Enter a reason for cancelling this coordinator filing."
|
|
2409
|
+
: "Use at most 500 plain characters, without hidden or control characters.", 400, undefined, reason ?? "");
|
|
2410
|
+
}
|
|
2411
|
+
return taskScreen(response, who, taskId, `not cancelled: ${cancelled.reason}`, 409);
|
|
2412
|
+
}
|
|
2413
|
+
return redirect(response, taskHref(taskId));
|
|
2414
|
+
}
|
|
2415
|
+
case "route": {
|
|
2416
|
+
const body = readForm(posted, CONSOLE_FORMS.taskRoute);
|
|
2417
|
+
// THE AGENTS EDIT (v47): an approver's session declares the risk,
|
|
2418
|
+
// overrides one role to an exact agent, or clears an override — ONE
|
|
2419
|
+
// authenticated transaction in the store that CAS-checks the scope
|
|
2420
|
+
// digest the form rendered (empty = "I saw no scope"), records the
|
|
2421
|
+
// change under this name, reconciles the planner pin, re-files the
|
|
2422
|
+
// scope so a sealed route goes visibly stale, and re-requests a
|
|
2423
|
+
// drafted plan when the planner changed. The viewer gate above
|
|
2424
|
+
// already refused a watcher; a live claim or a tournament refuses
|
|
2425
|
+
// inside the transaction.
|
|
2426
|
+
const riskGiven = body.get("risk");
|
|
2427
|
+
const sizeGiven = body.get("size");
|
|
2428
|
+
const riskyGiven = body.getAll("risky");
|
|
2429
|
+
const phaseGiven = body.get("phase");
|
|
2430
|
+
const clearGiven = body.get("clear-phase");
|
|
2431
|
+
// The agent arrives as the form's `provider|model` choice (v48) or,
|
|
2432
|
+
// for older clients, as separate fields — either way it must be one
|
|
2433
|
+
// of the CONFIGURED, role-valid choices right now: the form offers
|
|
2434
|
+
// nothing else, and the server believes nothing else.
|
|
2435
|
+
const agentGiven = (body.get("agent") ?? "").trim();
|
|
2436
|
+
const providerGiven = agentGiven === "" ? body.get("provider") : agentGiven.slice(0, agentGiven.indexOf("|") === -1 ? agentGiven.length : agentGiven.indexOf("|"));
|
|
2437
|
+
const modelGiven = agentGiven === "" ? (body.get("model") ?? "").trim() : agentGiven.indexOf("|") === -1 ? "" : agentGiven.slice(agentGiven.indexOf("|") + 1).trim();
|
|
2438
|
+
const sawDigest = body.get("sawDigest");
|
|
2439
|
+
if (riskGiven !== null && !isRiskLevel(riskGiven))
|
|
2440
|
+
return taskScreen(response, who, taskId, "risk is routine, elevated, or high", 400);
|
|
2441
|
+
const phase = phaseGiven ?? clearGiven;
|
|
2442
|
+
if (phase !== null && !ROUTE_PHASES.includes(phase))
|
|
2443
|
+
return taskScreen(response, who, taskId, "the role is planner, builder, or revision", 400);
|
|
2444
|
+
if (sizeGiven !== null && !isTaskSize(sizeGiven))
|
|
2445
|
+
return taskScreen(response, who, taskId, "size is small, medium, or large", 400);
|
|
2446
|
+
if (riskyGiven.some(one => one !== "yes" && one !== "no"))
|
|
2447
|
+
return taskScreen(response, who, taskId, "risky is yes or no", 400);
|
|
2448
|
+
// A size keeps the task's risky flag unless the form says otherwise; marking risky keeps the size.
|
|
2449
|
+
const sizeEdit = sizeGiven === null && riskyGiven.length === 0 ? undefined : {
|
|
2450
|
+
size: sizeGiven !== null && isTaskSize(sizeGiven) ? sizeGiven : ref.sizing?.size ?? "medium",
|
|
2451
|
+
risky: riskyGiven.includes("yes") ? true : riskyGiven.includes("no") ? false : ref.sizing?.risky ?? false,
|
|
2452
|
+
};
|
|
2453
|
+
if (riskGiven === null && phase === null && sizeEdit === undefined)
|
|
2454
|
+
return taskScreen(response, who, taskId, "nothing to change about the agents", 400);
|
|
2455
|
+
if (phaseGiven !== null) {
|
|
2456
|
+
if (providerGiven === null || !isProviderId(providerGiven))
|
|
2457
|
+
return taskScreen(response, who, taskId, "provider is claude, codex, openrouter, or gemini", 400);
|
|
2458
|
+
if (modelGiven === "")
|
|
2459
|
+
return taskScreen(response, who, taskId, "name the exact model id — approvals bind exact agents", 400);
|
|
2460
|
+
const valid = validateSpec({ provider: providerGiven, model: modelGiven });
|
|
2461
|
+
if (!valid.ok)
|
|
2462
|
+
return taskScreen(response, who, taskId, `agents not changed: ${valid.problem}`, 400);
|
|
2463
|
+
if (phaseGiven === "review" && providerGiven === "gemini")
|
|
2464
|
+
return taskScreen(response, who, taskId, "gemini cannot review yet — choose claude or codex", 400);
|
|
2465
|
+
}
|
|
2466
|
+
// The configured-choice proof lives INSIDE the edit transaction
|
|
2467
|
+
// (`configured`): the pair is held to the role's choices under the
|
|
2468
|
+
// configuration standing at that instant, and a stale choice
|
|
2469
|
+
// mutates nothing.
|
|
2470
|
+
const edited = store.editTaskRoute(ref.id, {
|
|
2471
|
+
by: who.name,
|
|
2472
|
+
authenticate: () => {
|
|
2473
|
+
const account = store.accountOf(who.name);
|
|
2474
|
+
return account !== null && account.revokedAt === null && account.role === "approver" ? { ok: true } : { ok: false, reason: "your login is no longer an approver" };
|
|
2475
|
+
},
|
|
2476
|
+
...(riskGiven === null ? {} : { risk: riskGiven }),
|
|
2477
|
+
// A person's size replaces the classifier's; the risky flag rides with it.
|
|
2478
|
+
...(sizeEdit === undefined ? {} : { size: sizeEdit }),
|
|
2479
|
+
...(phase === null
|
|
2480
|
+
? {}
|
|
2481
|
+
: { override: phaseGiven !== null ? { phase: phase, provider: providerGiven, model: modelGiven } : { phase: phase, clear: true } }),
|
|
2482
|
+
...(sawDigest === null ? {} : { expectDigest: sawDigest === "" ? null : sawDigest }),
|
|
2483
|
+
configured: true,
|
|
2484
|
+
}, now);
|
|
2485
|
+
if (!edited.ok) {
|
|
2486
|
+
return taskScreen(response, who, taskId, edited.reason === "not-configured" ? `agents not changed: ${edited.detail}` : edited.detail, edited.reason === "unauthenticated" ? 403 : edited.reason === "nothing" || edited.reason === "not-configured" ? 400 : 409);
|
|
2487
|
+
}
|
|
2488
|
+
// v102: changing how a task runs is authoring it — the requester rule then refuses this person too.
|
|
2489
|
+
const routed = store.getScope(taskId);
|
|
2490
|
+
if (routed !== null)
|
|
2491
|
+
store.recordScopeAuthor(taskId, routed.digest, who.name, now);
|
|
2492
|
+
const toChat = body.get("return") === "chat";
|
|
2493
|
+
const back = toChat ? taskChatHref(taskId) : taskHref(taskId);
|
|
2494
|
+
const said = [
|
|
2495
|
+
...(edited.staled ? ["agents changed — the earlier approval no longer covers this task; approve it again"] : []),
|
|
2496
|
+
...(edited.replanned ? ["the planner changed after a draft landed — a new plan was requested"] : []),
|
|
2497
|
+
];
|
|
2498
|
+
const target = said.length === 0 ? back : chatReturnWithSaid(back, said.join("; "));
|
|
2499
|
+
return redirect(response, toChat ? target : `${target}#agents`);
|
|
2500
|
+
}
|
|
2501
|
+
case "scope": {
|
|
2502
|
+
const body = readForm(posted, CONSOLE_FORMS.taskScope);
|
|
2503
|
+
const sawDigest = body.get("sawDigest");
|
|
2504
|
+
const permissionGiven = body.get("permission-mode");
|
|
2505
|
+
if (permissionGiven !== null && permissionGiven !== "" && permissionGiven !== "auto" && permissionGiven !== "bypassPermissions") {
|
|
2506
|
+
return taskScreen(response, who, taskId, "permissions must be Auto or Full access", 400);
|
|
2507
|
+
}
|
|
2508
|
+
const permissionMode = permissionGiven === "bypassPermissions"
|
|
2509
|
+
? "bypassPermissions"
|
|
2510
|
+
: permissionGiven === "auto"
|
|
2511
|
+
? "auto"
|
|
2512
|
+
: ref.permissionMode ?? store.permissionDefault().mode;
|
|
2513
|
+
const qualityGiven = body.get("quality-mode");
|
|
2514
|
+
if (qualityGiven !== null && qualityGiven !== "" && !isQualityMode(qualityGiven)) {
|
|
2515
|
+
return taskScreen(response, who, taskId, "quality must be Default or Strict / release", 400);
|
|
2516
|
+
}
|
|
2517
|
+
const qualityMode = qualityGiven === "strict"
|
|
2518
|
+
? "strict"
|
|
2519
|
+
: qualityGiven === "default"
|
|
2520
|
+
? "default"
|
|
2521
|
+
: ref.qualityMode ?? store.qualityDefault().mode;
|
|
2522
|
+
// The optional per-attempt dollar cap (v15) rides the same form.
|
|
2523
|
+
const budgetGiven = (body.get("budget-usd") ?? "").trim();
|
|
2524
|
+
const budgetUsd = budgetGiven === "" ? null : Number(budgetGiven);
|
|
2525
|
+
if (budgetUsd !== null && (!Number.isFinite(budgetUsd) || budgetUsd <= 0)) {
|
|
2526
|
+
return taskScreen(response, who, taskId, "the dollar cap is a positive amount", 400);
|
|
2527
|
+
}
|
|
2528
|
+
// The approval sheet's in-place editor carries the cap it was shown
|
|
2529
|
+
// exactly, in millionths, and "none" for no limit (never a default).
|
|
2530
|
+
const exactBudgetGiven = body.get("budget-microusd");
|
|
2531
|
+
const exactBudget = exactBudgetGiven === null ? undefined : exactBudgetGiven === "none" ? null : /^[1-9][0-9]{0,14}$/.test(exactBudgetGiven) ? Number(exactBudgetGiven) : NaN;
|
|
2532
|
+
if (Number.isNaN(exactBudget))
|
|
2533
|
+
return taskScreen(response, who, taskId, "the dollar cap is a positive amount", 400);
|
|
2534
|
+
const inPlace = body.has("requirement-new");
|
|
2535
|
+
// The tournament controls (operator request): a count of 2–4 files
|
|
2536
|
+
// race terms BESIDE the scope — validated and priced BEFORE anything
|
|
2537
|
+
// saves, so a bad tournament never half-lands on a good scope.
|
|
2538
|
+
const raceCountGiven = (body.get("race-count") ?? "").trim();
|
|
2539
|
+
// The comparison lanes (slice B): any filled row files a comparison
|
|
2540
|
+
// INSTEAD of a tournament — planned and refused BEFORE the save,
|
|
2541
|
+
// filed INSIDE the same transaction (round-6 finding 5 discipline).
|
|
2542
|
+
const comparisonLanes = [1, 2, 3, 4]
|
|
2543
|
+
.map(lane => ({
|
|
2544
|
+
provider: (body.get(`compare-provider-${lane}`) ?? "").trim(),
|
|
2545
|
+
model: (body.get(`compare-model-${lane}`) ?? "").trim(),
|
|
2546
|
+
permissionMode,
|
|
2547
|
+
}))
|
|
2548
|
+
.filter(lane => lane.provider !== "" || lane.model !== "");
|
|
2549
|
+
let plannedComparison = null;
|
|
2550
|
+
if (comparisonLanes.length > 0) {
|
|
2551
|
+
if (raceCountGiven !== "") {
|
|
2552
|
+
return taskScreen(response, who, taskId, "a tournament and a comparison are different ceremonies — file one or the other", 400);
|
|
2553
|
+
}
|
|
2554
|
+
if (store.mirrorByTask(taskId) !== null) {
|
|
2555
|
+
return taskScreen(response, who, taskId, "external work compares in a follow-up release — file the comparison on a local task", 409);
|
|
2556
|
+
}
|
|
2557
|
+
plannedComparison = planComparison({ agents: comparisonLanes });
|
|
2558
|
+
if (!plannedComparison.ok) {
|
|
2559
|
+
return taskScreen(response, who, taskId, `comparison not filed: ${plannedComparison.message}`, 400);
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
let plannedRace = null;
|
|
2563
|
+
if (raceCountGiven !== "") {
|
|
2564
|
+
const count = Number(raceCountGiven);
|
|
2565
|
+
const model = body.get("race-model") ?? "";
|
|
2566
|
+
if (!Number.isInteger(count) || count < 2 || count > 4) {
|
|
2567
|
+
return taskScreen(response, who, taskId, "a tournament races 2 to 4 agents", 400);
|
|
2568
|
+
}
|
|
2569
|
+
const perUsd = Number((body.get("race-per-usd") ?? "").trim());
|
|
2570
|
+
const totalUsd = Number((body.get("race-total-usd") ?? "").trim());
|
|
2571
|
+
const planned = planTournament({
|
|
2572
|
+
agents: Array.from({ length: count }, () => ({ provider: "claude", model, permissionMode })),
|
|
2573
|
+
perAgentBudgetUsd: perUsd,
|
|
2574
|
+
totalBudgetUsd: totalUsd,
|
|
2575
|
+
});
|
|
2576
|
+
if (!planned.ok) {
|
|
2577
|
+
return taskScreen(response, who, taskId, `tournament not filed: ${planned.message}`, 400);
|
|
2578
|
+
}
|
|
2579
|
+
plannedRace = planned.plan;
|
|
2580
|
+
}
|
|
2581
|
+
// Refusals that must hold ATOMICALLY with the save (round-6 finding
|
|
2582
|
+
// 5): a race request refused AFTER proposeGuarded would still have
|
|
2583
|
+
// rewritten the scope — so proposal, the attended exclusion, and
|
|
2584
|
+
// race-term filing share one transaction, and every refusal inside
|
|
2585
|
+
// it rolls the whole act back.
|
|
2586
|
+
if (plannedRace !== null && store.mirrorByTask(taskId) !== null) {
|
|
2587
|
+
return taskScreen(response, who, taskId, "external work races in a follow-up release — file the tournament on a local task", 409);
|
|
2588
|
+
}
|
|
2589
|
+
let saved;
|
|
2590
|
+
try {
|
|
2591
|
+
saved = store.transact(() => {
|
|
2592
|
+
if ((plannedRace !== null || plannedComparison !== null) && store.openAuthorizationFor(ref.id) !== null) {
|
|
2593
|
+
return { ok: false, status: 409, message: "an attended authorization is open on this task — revoke it before filing a tournament or comparison" };
|
|
2594
|
+
}
|
|
2595
|
+
// C1/M3: the signer's own credentialed filing auto-approves —
|
|
2596
|
+
// coverage is asked INSIDE this transaction, the escalated
|
|
2597
|
+
// default and budget ride the filing, and the seal commits
|
|
2598
|
+
// atomically with it. Plain scopes only: a tournament or
|
|
2599
|
+
// comparison keeps its own human ceremony.
|
|
2600
|
+
// COOKIE ONLY (C1's channel table; surfaces round 1, finding 1):
|
|
2601
|
+
// bearer credentials are for machines, and a machine road must
|
|
2602
|
+
// never spend the signer's mode.
|
|
2603
|
+
const coverage = who.via === "cookie" && plannedRace === null && plannedComparison === null
|
|
2604
|
+
? modeFilingCoverage(store, ref.repo, who.name, now)
|
|
2605
|
+
: null;
|
|
2606
|
+
const before = store.getScope(taskId);
|
|
2607
|
+
const proposed = proposeGuarded(store, {
|
|
2608
|
+
taskId, author: who.name,
|
|
2609
|
+
goal: body.has("goal-brief") ? `${(body.get("goal") ?? "").trim()}${body.get("goal-brief")}` : body.get("goal") ?? "",
|
|
2610
|
+
outOfScope: body.get("not") ?? null,
|
|
2611
|
+
touches: (body.get("touches") ?? "").split(/[\n,]/),
|
|
2612
|
+
// The approval sheet's in-place editor sends Done when line by line.
|
|
2613
|
+
acceptance: inPlace
|
|
2614
|
+
? requirementsFromEditor(body, before?.acceptance ?? [])
|
|
2615
|
+
: acceptanceLinesToInput((body.get("acceptance") ?? "").split("\n")),
|
|
2616
|
+
permissionMode,
|
|
2617
|
+
qualityMode,
|
|
2618
|
+
...(exactBudget !== undefined
|
|
2619
|
+
? exactBudget === null ? {} : { budgetMicrousd: exactBudget }
|
|
2620
|
+
: budgetUsd !== null
|
|
2621
|
+
? { budgetMicrousd: Math.round(budgetUsd * 1_000_000) }
|
|
2622
|
+
: coverage?.defaultBudgetMicrousd != null
|
|
2623
|
+
? { budgetMicrousd: coverage.defaultBudgetMicrousd }
|
|
2624
|
+
: {}),
|
|
2625
|
+
...(coverage?.escalated === true ? { posture: "escalated" } : {}),
|
|
2626
|
+
sawDigest: sawDigest === null || sawDigest === "" ? null : sawDigest,
|
|
2627
|
+
taskRef: ref.id,
|
|
2628
|
+
now,
|
|
2629
|
+
});
|
|
2630
|
+
if (!proposed.ok) {
|
|
2631
|
+
return {
|
|
2632
|
+
ok: false,
|
|
2633
|
+
status: proposed.reason === "changed" || proposed.reason === "claimed" ? 409 : 400,
|
|
2634
|
+
message: proposed.message ?? (proposed.reason === "acceptance-required" ? "Not saved: add at least one requirement under Done when." : `scope not saved: ${proposed.reason}`),
|
|
2635
|
+
};
|
|
2636
|
+
}
|
|
2637
|
+
// Editing the plan in place never changes what the agent may do:
|
|
2638
|
+
// a save that would (a legacy setting, a newer policy or mode)
|
|
2639
|
+
// rolls back whole.
|
|
2640
|
+
if (inPlace && before?.profile != null && proposed.scope.profile != null && levelOfProfile(before.profile) !== levelOfProfile(proposed.scope.profile)) {
|
|
2641
|
+
throw new PermissionsWouldChange();
|
|
2642
|
+
}
|
|
2643
|
+
if (coverage !== null && ref.plan === "requested") {
|
|
2644
|
+
authorizePlanUnderMode(store, taskId, who.name, now);
|
|
2645
|
+
}
|
|
2646
|
+
else if (coverage !== null) {
|
|
2647
|
+
const filedScope = store.getScope(taskId);
|
|
2648
|
+
if (filedScope?.profileState === "resolved") {
|
|
2649
|
+
const sealedUnderMode = store.sealScopeApproval(taskId, who.name, now, {}, { kind: "mode", modeDigest: coverage.digest });
|
|
2650
|
+
// The quarantine speaks at the caller (review finding 7).
|
|
2651
|
+
if (!sealedUnderMode) {
|
|
2652
|
+
return { ok: false, status: 200, message: "scope saved — coordinator-filed: mode coverage cannot admit it; sign the scope" };
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
if (plannedComparison !== null && plannedComparison.ok) {
|
|
2657
|
+
store.fileTournamentTerms({
|
|
2658
|
+
taskRef: ref.id,
|
|
2659
|
+
kind: "comparison",
|
|
2660
|
+
raceDigest: plannedComparison.plan.comparisonDigest,
|
|
2661
|
+
agents: plannedComparison.plan.agents,
|
|
2662
|
+
perAgentBudgetMicrousd: 0,
|
|
2663
|
+
overrunReserveMicrousd: 0,
|
|
2664
|
+
totalBudgetMicrousd: 0,
|
|
2665
|
+
priceVersion: 0,
|
|
2666
|
+
publicationPolicy: plannedComparison.plan.publicationPolicy,
|
|
2667
|
+
}, now);
|
|
2668
|
+
return { ok: true };
|
|
2669
|
+
}
|
|
2670
|
+
if (plannedRace === null) {
|
|
2671
|
+
// Switching back to "one agent" withdraws a standing race OR
|
|
2672
|
+
// comparison — the deactivated row survives as history, and
|
|
2673
|
+
// the approval card returns to the scope alone.
|
|
2674
|
+
store.retractTournamentTerms(ref.id);
|
|
2675
|
+
return { ok: true };
|
|
2676
|
+
}
|
|
2677
|
+
store.fileTournamentTerms({
|
|
2678
|
+
taskRef: ref.id,
|
|
2679
|
+
raceDigest: plannedRace.raceDigest,
|
|
2680
|
+
agents: plannedRace.agents,
|
|
2681
|
+
perAgentBudgetMicrousd: plannedRace.perAgentBudgetMicrousd,
|
|
2682
|
+
overrunReserveMicrousd: plannedRace.overrunReserveMicrousd,
|
|
2683
|
+
totalBudgetMicrousd: plannedRace.totalBudgetMicrousd,
|
|
2684
|
+
priceVersion: plannedRace.priceVersion,
|
|
2685
|
+
publicationPolicy: plannedRace.publicationPolicy,
|
|
2686
|
+
}, now);
|
|
2687
|
+
return { ok: true };
|
|
2688
|
+
});
|
|
2689
|
+
}
|
|
2690
|
+
catch (error) {
|
|
2691
|
+
if (!(error instanceof PermissionsWouldChange))
|
|
2692
|
+
throw error;
|
|
2693
|
+
saved = { ok: false, status: 409, message: "Not saved: this would change what the agent may do. Change permissions in Details, under edit the scope." };
|
|
2694
|
+
}
|
|
2695
|
+
if (!saved.ok) {
|
|
2696
|
+
// Refused because the plan moved on: the draft comes back with the version now on file, so saving it
|
|
2697
|
+
// again (having read the reason) can succeed instead of being refused the same way.
|
|
2698
|
+
const draft = new URLSearchParams(body.sent);
|
|
2699
|
+
if (saved.status === 409)
|
|
2700
|
+
draft.set("sawDigest", store.getScope(taskId)?.digest ?? "");
|
|
2701
|
+
return taskScreen(response, who, taskId, saved.message, saved.status, draft);
|
|
2702
|
+
}
|
|
2703
|
+
return redirect(response, taskHref(taskId));
|
|
2704
|
+
}
|
|
2705
|
+
case "approve": {
|
|
2706
|
+
if (who.via !== "cookie")
|
|
2707
|
+
return refuse(response, who, 403, REMOTE_MESSAGES["step-up"]);
|
|
2708
|
+
const body = readForm(posted, CONSOLE_FORMS.taskApprove);
|
|
2709
|
+
const requestedReturn = body.get("return");
|
|
2710
|
+
const approvalBack = requestedReturn === "next"
|
|
2711
|
+
? "/next"
|
|
2712
|
+
: requestedReturn === null
|
|
2713
|
+
? taskHref(taskId)
|
|
2714
|
+
: safeChatReturn(requestedReturn);
|
|
2715
|
+
const approvalProblem = (message, status) => {
|
|
2716
|
+
if (requestedReturn !== null && requestedReturn !== "next") {
|
|
2717
|
+
return redirect(response, chatReturnWithSaid(approvalBack, message));
|
|
2718
|
+
}
|
|
2719
|
+
return taskScreen(response, who, taskId, message, status);
|
|
2720
|
+
};
|
|
2721
|
+
const approvingRef = store.lookupRef(taskId);
|
|
2722
|
+
if (approvingRef?.plan === "requested") {
|
|
2723
|
+
return approvalProblem("approval is blocked while planning is in progress — review the drafted plan first", 409);
|
|
2724
|
+
}
|
|
2725
|
+
// Step-up: the session got you here; only the token agrees. The
|
|
2726
|
+
// digest names what was seen; the nonce proves this exact form was
|
|
2727
|
+
// rendered to this approver and is spent either way.
|
|
2728
|
+
const digest = body.get("digest") ?? "";
|
|
2729
|
+
const token = body.get("token") ?? "";
|
|
2730
|
+
const nonce = body.get("nonce") ?? "";
|
|
2731
|
+
if (!consumeApprovalNonce(nonce, who.name, taskId, digest)) {
|
|
2732
|
+
return approvalProblem("that approval form is stale — read it again", 409);
|
|
2733
|
+
}
|
|
2734
|
+
if (token === "" && !hasFreshIdentitySignIn(who.name)) {
|
|
2735
|
+
return approvalProblem("approval requires your password, typed again", 400);
|
|
2736
|
+
}
|
|
2737
|
+
const scopeRow = store.getScope(taskId);
|
|
2738
|
+
if (scopeRow === null)
|
|
2739
|
+
return approvalProblem("this task has no scope to approve", 409);
|
|
2740
|
+
const raceTerms = store.activeTournamentTerms(ref.id);
|
|
2741
|
+
const planView = planViewOf(ref.id);
|
|
2742
|
+
const expectedDigest = approvalFormDigest(scopeRow.digest, raceTerms?.raceDigest ?? null, planView?.sha256 ?? null);
|
|
2743
|
+
if (digest !== expectedDigest) {
|
|
2744
|
+
return approvalProblem("the scope or plan changed while this form was open — read the latest version and approve again", 409);
|
|
2745
|
+
}
|
|
2746
|
+
// A revision approves ONLY against a brief that still verifies
|
|
2747
|
+
// (Codex M5-M8 audit, IV-3): the batch the screen restated must be
|
|
2748
|
+
// provably the batch on disk at the moment of the yes — a brief
|
|
2749
|
+
// deleted or corrupted between render and click blocks the
|
|
2750
|
+
// approval instead of silently approving comment-free work.
|
|
2751
|
+
if (approvingRef !== null && approvingRef.revisionBriefArtifact !== null) {
|
|
2752
|
+
const view = revisionViewOf(approvingRef);
|
|
2753
|
+
if (view !== null && "problem" in view) {
|
|
2754
|
+
return approvalProblem(`approval is blocked: ${view.problem}`, 409);
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
// A tournament task's yes covers BOTH documents (finding 31): the
|
|
2758
|
+
// form bound the joint fingerprint, and the scope and race terms
|
|
2759
|
+
// approve together, in one transaction, or not at all.
|
|
2760
|
+
if (raceTerms !== null) {
|
|
2761
|
+
const both = store.transact(() => {
|
|
2762
|
+
const scopeApproved = approveScope(store, taskId, who.name, now, scopeRow.digest, token);
|
|
2763
|
+
if (!scopeApproved.ok)
|
|
2764
|
+
return scopeApproved;
|
|
2765
|
+
if (!store.approveTournamentTerms(raceTerms.id, who.name, raceTerms.raceDigest, now)) {
|
|
2766
|
+
throw new Error("the race terms changed while you were reading — nothing was approved");
|
|
2767
|
+
}
|
|
2768
|
+
return scopeApproved;
|
|
2769
|
+
});
|
|
2770
|
+
if (!both.ok) {
|
|
2771
|
+
if (both.reason === "second-approver")
|
|
2772
|
+
return approvalProblem(gateWords({ verdict: "vote", have: both.have, need: 2, already: both.already }), 200);
|
|
2773
|
+
if (both.reason === "policy")
|
|
2774
|
+
return approvalProblem(both.message, 403);
|
|
2775
|
+
const status = both.reason === "changed" || both.reason === "unrouted" ? 409 : 403;
|
|
2776
|
+
return approvalProblem(approveRefusalWords(both.reason), status);
|
|
2777
|
+
}
|
|
2778
|
+
return redirect(response, approvalBack);
|
|
2779
|
+
}
|
|
2780
|
+
const approved = approveScope(store, taskId, who.name, now, scopeRow.digest, token);
|
|
2781
|
+
if (!approved.ok) {
|
|
2782
|
+
// v102: a first yes on protected work is recorded, not refused — the page says who else is needed.
|
|
2783
|
+
if (approved.reason === "second-approver")
|
|
2784
|
+
return approvalProblem(gateWords({ verdict: "vote", have: approved.have, need: 2, already: approved.already }), 200);
|
|
2785
|
+
if (approved.reason === "policy")
|
|
2786
|
+
return approvalProblem(approved.message, 403);
|
|
2787
|
+
const status = approved.reason === "changed" || approved.reason === "unrouted" ? 409 : 403;
|
|
2788
|
+
return approvalProblem(approveRefusalWords(approved.reason), status);
|
|
2789
|
+
}
|
|
2790
|
+
return redirect(response, approvalBack);
|
|
2791
|
+
}
|
|
2792
|
+
case "accept-proof": {
|
|
2793
|
+
const body = readForm(posted, CONSOLE_FORMS.taskAcceptProof);
|
|
2794
|
+
// Accepting is a person's act, like approving a scope (Priority
|
|
2795
|
+
// 2): a cookie session only, never a bearer credential.
|
|
2796
|
+
if (who.via !== "cookie") {
|
|
2797
|
+
return refuse(response, who, 403, "accepting a proof is a browser session's act");
|
|
2798
|
+
}
|
|
2799
|
+
// v40 fix: a reviewer run is never the attempt whose proof is
|
|
2800
|
+
// being accepted.
|
|
2801
|
+
const latest = store.runsFor(ref.id).find(runIsTaskResult);
|
|
2802
|
+
if (latest === undefined) {
|
|
2803
|
+
return taskScreen(response, who, taskId, "this task has no finished attempt to accept", 404);
|
|
2804
|
+
}
|
|
2805
|
+
const family = store.taskFamilyOf(taskId, admissionList(), false);
|
|
2806
|
+
if (Number(body.get("run")) !== latest.id || family?.current.id !== taskId || family.problem !== null) {
|
|
2807
|
+
return taskScreen(response, who, taskId, "This result changed. Review the current result before accepting.", 409);
|
|
2808
|
+
}
|
|
2809
|
+
const rawNote = (body.get("note") ?? "").trim();
|
|
2810
|
+
let note = null;
|
|
2811
|
+
if (rawNote !== "") {
|
|
2812
|
+
const validated = validateNote(rawNote);
|
|
2813
|
+
if (!validated.ok) {
|
|
2814
|
+
return taskScreen(response, who, taskId, validated.problem, 400);
|
|
2815
|
+
}
|
|
2816
|
+
note = validated.note;
|
|
2817
|
+
}
|
|
2818
|
+
// A failed task's result is accepted only anyway, on purpose: with a reason.
|
|
2819
|
+
if (note === null && store.getTask(taskId)?.state === "failed") {
|
|
2820
|
+
// Posted from the result page: back there, where the reason field is, with the refusal said on it.
|
|
2821
|
+
const back = body.get("return") === null ? null : safeReturn(body.get("return"));
|
|
2822
|
+
if (back !== null && back.startsWith("/review?"))
|
|
2823
|
+
return redirect(response, withRefusal(back, "reason"));
|
|
2824
|
+
return taskScreen(response, who, taskId, ACCEPT_ANYWAY_NEEDS_REASON, 400);
|
|
2825
|
+
}
|
|
2826
|
+
store.acceptProof(latest.id, verifiedAuthor(who.name), note, now);
|
|
2827
|
+
return redirect(response, body.get("return") === null ? taskHref(taskId) : safeReturn(body.get("return")));
|
|
2828
|
+
}
|
|
2829
|
+
case "complete": {
|
|
2830
|
+
const body = readForm(posted, CONSOLE_FORMS.taskComplete);
|
|
2831
|
+
if (who.via !== "cookie" || who.role !== "approver")
|
|
2832
|
+
return refuse(response, who, 403, "Only an approver can mark a result complete.");
|
|
2833
|
+
const principal = matePrincipal(who);
|
|
2834
|
+
if (principal === null)
|
|
2835
|
+
return refuse(response, who, 403, "Your access changed. Sign in again.");
|
|
2836
|
+
const digest = body.get("receipt") ?? "";
|
|
2837
|
+
const namedRun = body.get("run") ?? "";
|
|
2838
|
+
const current = assignmentOf(store, taskId, now, { principal: "operator", repos: principal.repos }, evidenceRoot);
|
|
2839
|
+
if (current === null || current.receipt?.taskId !== taskId || !/^[0-9]{1,15}$/.test(namedRun) || current.receipt.runId !== Number(namedRun)) {
|
|
2840
|
+
return taskScreen(response, who, taskId, "This result changed. Open the current result before marking it complete.", 409);
|
|
2841
|
+
}
|
|
2842
|
+
if (body.get("accept") === "1") {
|
|
2843
|
+
// Accept and finish: the person's acceptance and the completion of what it accepted, in one request
|
|
2844
|
+
// and one transaction. The receipt named is the one read, before accepting.
|
|
2845
|
+
const rawNote = (body.get("note") ?? "").trim();
|
|
2846
|
+
let note = null;
|
|
2847
|
+
if (rawNote !== "") {
|
|
2848
|
+
const validated = validateNote(rawNote);
|
|
2849
|
+
if (!validated.ok)
|
|
2850
|
+
return taskScreen(response, who, taskId, validated.problem, 400);
|
|
2851
|
+
note = validated.note;
|
|
2852
|
+
}
|
|
2853
|
+
const publish = body.get("publish") === "1";
|
|
2854
|
+
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);
|
|
2855
|
+
if (!finished.ok)
|
|
2856
|
+
return taskScreen(response, who, taskId, finished.message, finished.reason === "needs-reason" ? 400 : 409);
|
|
2857
|
+
return redirect(response, publish ? `${taskHref(familyOf(taskId)?.root.id ?? taskId)}#merge` : body.get("return") === null ? reviewHref(taskId, Number(namedRun)) : safeReturn(body.get("return")));
|
|
2858
|
+
}
|
|
2859
|
+
if (body.get("publish") === "1") {
|
|
2860
|
+
// "Complete and open a pull request": the same exact-receipt completion, plus the owed PR for this
|
|
2861
|
+
// exact commit in one transaction. The watch process pushes and opens it; the task shows its progress.
|
|
2862
|
+
const opened = completeAndOpenPullRequest(store, { taskId, digest, runId: Number(namedRun), who: principal, root: evidenceRoot }, now);
|
|
2863
|
+
if (!opened.ok)
|
|
2864
|
+
return taskScreen(response, who, taskId, opened.message, 409);
|
|
2865
|
+
return redirect(response, `${taskHref(familyOf(taskId)?.root.id ?? taskId)}#merge`);
|
|
2866
|
+
}
|
|
2867
|
+
const completed = checkAssignmentAsOperator(store, taskId, digest, principal, now, evidenceRoot);
|
|
2868
|
+
if (!completed.ok)
|
|
2869
|
+
return taskScreen(response, who, taskId, completed.message, 409);
|
|
2870
|
+
return redirect(response, reviewHref(taskId, Number(namedRun)));
|
|
2871
|
+
}
|
|
2872
|
+
case "retry-review": {
|
|
2873
|
+
return taskScreen(response, who, taskId, "Separate agent reviews have retired. Open the saved result to mark it complete or request a revision.", 410);
|
|
2874
|
+
}
|
|
2875
|
+
case "stop": {
|
|
2876
|
+
const body = readForm(posted, CONSOLE_FORMS.taskStop);
|
|
2877
|
+
// The exact-run stop (v52): a browser session's act, an approver's
|
|
2878
|
+
// act, naming ONE run. The store's transaction proves the run is
|
|
2879
|
+
// this task's current live attempt and records who asked before
|
|
2880
|
+
// any process is signalled; the answer is "stopping", never
|
|
2881
|
+
// "stopped". A repeated post is the same request; a stale one (a
|
|
2882
|
+
// finished run, a successor) is refused in words.
|
|
2883
|
+
if (who.via !== "cookie")
|
|
2884
|
+
return refuse(response, who, 403, "stopping a task is a browser session's act");
|
|
2885
|
+
if (who.role !== "approver")
|
|
2886
|
+
return taskScreen(response, who, taskId, "your login can watch — stopping an attempt is an approver's act", 403);
|
|
2887
|
+
if (store.isDemo())
|
|
2888
|
+
return taskScreen(response, who, taskId, "the demo authorizes nothing", 403);
|
|
2889
|
+
const named = (body.get("run") ?? "").trim();
|
|
2890
|
+
if (!/^[0-9]{1,15}$/.test(named))
|
|
2891
|
+
return taskScreen(response, who, taskId, "which attempt? the stop names one exact run", 400);
|
|
2892
|
+
const asked = requestTaskStop(store, { taskId, runId: Number(named), by: verifiedAuthor(who.name), via: "web", held: options.attended?.coordinator }, now);
|
|
2893
|
+
if (!asked.ok) {
|
|
2894
|
+
const words = {
|
|
2895
|
+
"no-run": `there is no run #${escape(named)}`,
|
|
2896
|
+
"wrong-task": `run #${escape(named)} is not one of this task's attempts`,
|
|
2897
|
+
finished: `run #${escape(named)} already ended before the stop — nothing rewrites a finished attempt; reload to see the current state`,
|
|
2898
|
+
"not-live": `run #${escape(named)} is not the attempt holding this task now — reload and decide against the current attempt`,
|
|
2899
|
+
tournament: "this run is a racing lane — stop the tournament through its own pick or abandon controls",
|
|
2900
|
+
publication: `run #${escape(named)} already admitted its publication — an external request in flight is not recalled by a stop`,
|
|
2901
|
+
};
|
|
2902
|
+
return taskScreen(response, who, taskId, words[asked.reason] ?? asked.detail, 409);
|
|
2903
|
+
}
|
|
2904
|
+
const back = body.get("return") ?? "";
|
|
2905
|
+
return redirect(response, back === "chat" ? `${taskChatHref(taskId)}#task-control` : `${taskHref(taskId)}#task-control`);
|
|
2906
|
+
}
|
|
2907
|
+
case "resume-arm": {
|
|
2908
|
+
const body = readForm(posted, CONSOLE_FORMS.taskResume);
|
|
2909
|
+
return armTaskResume(response, who, taskId, (body.get("run") ?? "").trim(), body.get("return") === "chat" ? "chat" : "task", now);
|
|
2910
|
+
}
|
|
2911
|
+
case "resume": {
|
|
2912
|
+
const body = readForm(posted, CONSOLE_FORMS.taskResume);
|
|
2913
|
+
// The resume itself (v52): password typed again + the durable nonce
|
|
2914
|
+
// consumed INSIDE the store's resume transaction against the digest
|
|
2915
|
+
// re-derived from live state. A replay finds the nonce spent; a
|
|
2916
|
+
// changed approval finds the digest moved; a successor attempt finds
|
|
2917
|
+
// the run superseded — each refuses without starting work; a submitted nonce remains one-shot.
|
|
2918
|
+
if (who.via !== "cookie")
|
|
2919
|
+
return refuse(response, who, 403, "resuming a task is a browser session's act");
|
|
2920
|
+
if (who.role !== "approver")
|
|
2921
|
+
return taskScreen(response, who, taskId, "your login can watch — resuming an attempt is an approver's act", 403);
|
|
2922
|
+
if (store.isDemo())
|
|
2923
|
+
return taskScreen(response, who, taskId, "the demo authorizes nothing", 403);
|
|
2924
|
+
const named = (body.get("run") ?? "").trim();
|
|
2925
|
+
if (!/^[0-9]{1,15}$/.test(named))
|
|
2926
|
+
return taskScreen(response, who, taskId, "which attempt? the resume names the exact stopped run", 400);
|
|
2927
|
+
const runId = Number(named);
|
|
2928
|
+
const token = body.get("token") ?? "";
|
|
2929
|
+
if (!authenticateApprover(who, token).ok) {
|
|
2930
|
+
return taskScreen(response, who, taskId, "resuming takes your password, typed again", 403);
|
|
2931
|
+
}
|
|
2932
|
+
const nonceValue = body.get("nonce") ?? "";
|
|
2933
|
+
if (nonceValue === "")
|
|
2934
|
+
return taskScreen(response, who, taskId, "that confirmation form was incomplete — start again", 400);
|
|
2935
|
+
const resumed = store.transact(() => {
|
|
2936
|
+
const stop = store.stopOf(runId);
|
|
2937
|
+
if (stop === null || stop.taskRef !== ref.id)
|
|
2938
|
+
return { ok: false, reason: "no-stop", detail: `run #${runId} is not a stopped attempt of this task` };
|
|
2939
|
+
const digest = resumeDigestOf(taskId, runId, stop.settledAt ?? "", store.getScope(taskId));
|
|
2940
|
+
if (!store.consumeCeremonyNonce(nonceHashOf(nonceValue), who.name, "run-resume", runId, digest, now)) {
|
|
2941
|
+
return { ok: false, reason: "stale", detail: "that confirmation is stale, already used, or the approval changed since you read it — start again from the task" };
|
|
2942
|
+
}
|
|
2943
|
+
return resumeTaskStop(store, { taskId, runId, by: verifiedAuthor(who.name), via: "web" }, now);
|
|
2944
|
+
});
|
|
2945
|
+
if (!resumed.ok) {
|
|
2946
|
+
const words = {
|
|
2947
|
+
stale: resumed.detail,
|
|
2948
|
+
"no-stop": resumed.detail,
|
|
2949
|
+
stopping: `not yet — ${resumed.detail}`,
|
|
2950
|
+
occupied: `not yet — ${resumed.detail}`,
|
|
2951
|
+
"already-resumed": resumed.detail,
|
|
2952
|
+
superseded: resumed.detail,
|
|
2953
|
+
busy: resumed.detail,
|
|
2954
|
+
review: `run #${runId} is a review — use Review again, which keeps its bounded attempts`,
|
|
2955
|
+
};
|
|
2956
|
+
return taskScreen(response, who, taskId, words[resumed.reason] ?? resumed.detail, 409);
|
|
2957
|
+
}
|
|
2958
|
+
const back = body.get("return") ?? "";
|
|
2959
|
+
return redirect(response, back === "chat" ? `${taskChatHref(taskId)}#task-control` : `${taskHref(taskId)}#task-control`);
|
|
2960
|
+
}
|
|
2961
|
+
default:
|
|
2962
|
+
return refuse(response, who, 404, "That action isn't available here.", taskHref(taskId));
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2965
|
+
/** The queue as a region: dispatch order per column, drag handles and
|
|
2966
|
+
* all — the /queue page's body, and the board's "order" view (the one
|
|
2967
|
+
* place dragging exists, because reordering and reserving are scheduling,
|
|
2968
|
+
* never authority). */
|
|
2969
|
+
function queueRegionFor(project, csrf, revision, now) {
|
|
2970
|
+
const tasks = store.queueScoped(project, now).map(one => ({
|
|
2971
|
+
...one,
|
|
2972
|
+
dispatch: diagnoseTaskDispatch(store, one.id, now),
|
|
2973
|
+
}));
|
|
2974
|
+
const owned = new Set(tasks.map(one => one.assignedRunner).filter((one) => one !== null));
|
|
2975
|
+
const building = new Map();
|
|
2976
|
+
for (const claim of store.liveClaims(project, now)) {
|
|
2977
|
+
building.set(claim.runner, (building.get(claim.runner) ?? 0) + 1);
|
|
2978
|
+
}
|
|
2979
|
+
const workers = store
|
|
2980
|
+
.listRunners()
|
|
2981
|
+
.filter(one => one.retiredAt === null || owned.has(one.name))
|
|
2982
|
+
.map(one => ({
|
|
2983
|
+
name: one.name,
|
|
2984
|
+
retired: one.retiredAt !== null,
|
|
2985
|
+
note: one.queueNote ?? null,
|
|
2986
|
+
capacity: one.capacity,
|
|
2987
|
+
building: building.get(one.name) ?? 0,
|
|
2988
|
+
}));
|
|
2989
|
+
return queueBody(tasks, workers, csrf, revision, store.queueRevision());
|
|
2990
|
+
}
|
|
2991
|
+
/** An OBSERVED CI failure on a completed row's PR — the open episode the
|
|
2992
|
+
* watcher recorded for exactly that repository and number; never a guess
|
|
2993
|
+
* from quiet. One indexed publication read per row that has a PR. */
|
|
2994
|
+
function ciFailingFor(runId, prNumber) {
|
|
2995
|
+
if (runId === null || prNumber === null)
|
|
2996
|
+
return false;
|
|
2997
|
+
const publication = store.publicationForRun(runId);
|
|
2998
|
+
return publication !== null && publication.prNumber === prNumber && store.hasOpenCiEpisode(publication.githubRepo, prNumber);
|
|
2999
|
+
}
|
|
3000
|
+
/**
|
|
3001
|
+
* One completed row by task id, for a `?result=` deep link the bounded
|
|
3002
|
+
* queue could not show — the done list's own predicate, re-proved per
|
|
3003
|
+
* read: the task is done, sits in this project (or in none), and its
|
|
3004
|
+
* repository passes the ceiling; the result run is the newest finished
|
|
3005
|
+
* built/no-change builder or scout attempt. Anything else is
|
|
3006
|
+
* null, and null reads exactly like a task that never existed.
|
|
3007
|
+
*/
|
|
3008
|
+
function completedRowFor(taskId, project) {
|
|
3009
|
+
const family = familyOf(taskId);
|
|
3010
|
+
if (family === null)
|
|
3011
|
+
return null;
|
|
3012
|
+
const task = store.getTask(taskId);
|
|
3013
|
+
const ref = store.lookupRef(taskId);
|
|
3014
|
+
if (task === null || ref === null || task.state !== "done")
|
|
3015
|
+
return null;
|
|
3016
|
+
if (!(project === null || ref.repo === null || ref.repo === project) || !visible(ref.repo))
|
|
3017
|
+
return null;
|
|
3018
|
+
const run = store.runsFor(ref.id).find(one => (one.outcome === "built" || one.outcome === "no-change") &&
|
|
3019
|
+
one.finishedAt !== null &&
|
|
3020
|
+
(one.role === "builder" || one.role === "scout")) ?? null;
|
|
3021
|
+
return resultRowOf(task, ref.repo, family.problem, run);
|
|
3022
|
+
}
|
|
3023
|
+
/**
|
|
3024
|
+
* A build attempt that didn't deliver a result (failed, refused, stopped,
|
|
3025
|
+
* never finished) still has a result page: its changes, its checks and what
|
|
3026
|
+
* went wrong. The row for one: the run named by `runId` when it is an ended
|
|
3027
|
+
* builder or scout attempt of this task that isn't a delivered result, or a
|
|
3028
|
+
* failed task's latest attempt; with no run named, that latest attempt. The
|
|
3029
|
+
* same admission as a completed row: in this project (or none) and inside
|
|
3030
|
+
* the ceiling. Null otherwise, which reads like a task that never existed.
|
|
3031
|
+
*/
|
|
3032
|
+
function attemptRowFor(taskId, runId, project) {
|
|
3033
|
+
const family = familyOf(taskId);
|
|
3034
|
+
const task = store.getTask(taskId);
|
|
3035
|
+
const ref = store.lookupRef(taskId);
|
|
3036
|
+
if (family === null || task === null || ref === null)
|
|
3037
|
+
return null;
|
|
3038
|
+
if (!(project === null || ref.repo === null || ref.repo === project) || !visible(ref.repo))
|
|
3039
|
+
return null;
|
|
3040
|
+
const attempts = store.runsFor(ref.id).filter(one => (one.role === "builder" || one.role === "scout") && (one.finishedAt !== null || one.outcome !== null));
|
|
3041
|
+
const latest = task.state === "failed" ? latestFinishedAttempt(attempts) : null;
|
|
3042
|
+
const run = runId !== null ? attempts.find(one => one.id === runId) ?? null : latest;
|
|
3043
|
+
// A delivered result has its completed row; a failed task's latest attempt is what failed, whatever it ended as.
|
|
3044
|
+
if (run === null || runIsLive(run) || ((run.outcome === "built" || run.outcome === "no-change") && run.id !== latest?.id))
|
|
3045
|
+
return null;
|
|
3046
|
+
return resultRowOf(task, ref.repo, family.problem, run);
|
|
3047
|
+
}
|
|
3048
|
+
function resultRowOf(task, repo, historyProblem, run) {
|
|
3049
|
+
const ref = { repo };
|
|
3050
|
+
const family = { problem: historyProblem };
|
|
3051
|
+
const publication = run === null ? null : store.publicationForRun(run.id);
|
|
3052
|
+
const verdict = run === null ? null : store.proofVerdictFor(run.id);
|
|
3053
|
+
return {
|
|
3054
|
+
taskId: task.id,
|
|
3055
|
+
title: task.title,
|
|
3056
|
+
repo: ref.repo,
|
|
3057
|
+
completedAt: task.updatedAt,
|
|
3058
|
+
historyProblem: family.problem,
|
|
3059
|
+
outcome: run?.outcome ?? null,
|
|
3060
|
+
handoff: run?.handoff ?? null,
|
|
3061
|
+
costUsd: run?.costUsd ?? null,
|
|
3062
|
+
provider: run?.provider ?? null,
|
|
3063
|
+
authMode: run?.authMode === "subscription" || run?.authMode === "api-key" ? run.authMode : null,
|
|
3064
|
+
ranMinutes: run === null || run.finishedAt === null
|
|
3065
|
+
? null
|
|
3066
|
+
: Math.max(1, Math.round((new Date(run.finishedAt).getTime() - new Date(run.startedAt).getTime()) / 60_000)),
|
|
3067
|
+
prNumber: publication?.prNumber ?? null,
|
|
3068
|
+
prUrl: publication?.prUrl ?? null,
|
|
3069
|
+
publicationState: publication?.state ?? null,
|
|
3070
|
+
runId: run?.id ?? null,
|
|
3071
|
+
proofVerdict: verdict?.verdict ?? null,
|
|
3072
|
+
proofMatrix: verdict?.matrix ?? [],
|
|
3073
|
+
proofAccepted: run !== null && store.proofAcceptance(run.id) !== null,
|
|
3074
|
+
};
|
|
3075
|
+
}
|
|
3076
|
+
function reviewCockpitViewOf(row, who, now) {
|
|
3077
|
+
const ref = store.lookupRef(row.taskId);
|
|
3078
|
+
const scope = store.getScope(row.taskId);
|
|
3079
|
+
// The result run, read once: the contest road and the shared result
|
|
3080
|
+
// detail below both hang off this one record.
|
|
3081
|
+
const run = row.runId === null ? null : store.getRun(row.runId);
|
|
3082
|
+
const intent = scope === null
|
|
3083
|
+
? null
|
|
3084
|
+
: {
|
|
3085
|
+
goal: scope.goal,
|
|
3086
|
+
outOfScope: scope.outOfScope,
|
|
3087
|
+
touches: scope.touches,
|
|
3088
|
+
acceptance: scope.acceptance,
|
|
3089
|
+
approval: approvalOf(scope),
|
|
3090
|
+
approvedBy: scope.approvedBy,
|
|
3091
|
+
};
|
|
3092
|
+
const plan = (() => {
|
|
3093
|
+
if (ref === null)
|
|
3094
|
+
return null;
|
|
3095
|
+
const current = revisionLedgerOf(ref.id).current;
|
|
3096
|
+
if (current === null)
|
|
3097
|
+
return null;
|
|
3098
|
+
const parsed = parseExecutionPlanDocument(current.document);
|
|
3099
|
+
return {
|
|
3100
|
+
revision: current.revision,
|
|
3101
|
+
sha256: current.sha256,
|
|
3102
|
+
approach: parsed.ok ? oneLineOf(parsed.document.approach, 600) : null,
|
|
3103
|
+
};
|
|
3104
|
+
})();
|
|
3105
|
+
const contestOf = () => {
|
|
3106
|
+
// Either road to a tournament: the result run was one of its
|
|
3107
|
+
// contestants, or the task still owes an operator a comparison.
|
|
3108
|
+
const viaRun = run?.contestant === null || run?.contestant === undefined ? null : store.getContestant(run.contestant);
|
|
3109
|
+
const contest = viaRun === null ? (ref === null ? null : store.contestNeedingOperator(ref.id)) : store.getContest(viaRun.contest);
|
|
3110
|
+
return contest === null ? null : { id: contest.id, state: contest.state, kind: contest.kind, agents: store.contestants(contest.id).length };
|
|
3111
|
+
};
|
|
3112
|
+
const base = {
|
|
3113
|
+
taskId: row.taskId,
|
|
3114
|
+
title: row.title,
|
|
3115
|
+
repo: row.repo,
|
|
3116
|
+
completedAt: row.completedAt,
|
|
3117
|
+
historyProblem: row.historyProblem ?? null,
|
|
3118
|
+
priority: row.priority,
|
|
3119
|
+
assignment: row.assignment ?? null,
|
|
3120
|
+
intent,
|
|
3121
|
+
plan,
|
|
3122
|
+
contest: contestOf(),
|
|
3123
|
+
...(store.isDemo() ? { demo: true } : {}),
|
|
3124
|
+
};
|
|
3125
|
+
if (run === null) {
|
|
3126
|
+
return { ...base, run: null, detail: null, reviewRetry: null, review: null, notes: [] };
|
|
3127
|
+
}
|
|
3128
|
+
const detail = resultDetailOf(run, who, now);
|
|
3129
|
+
return {
|
|
3130
|
+
...base,
|
|
3131
|
+
run: {
|
|
3132
|
+
id: run.id,
|
|
3133
|
+
role: run.role,
|
|
3134
|
+
outcome: run.outcome,
|
|
3135
|
+
runner: run.runner,
|
|
3136
|
+
provider: run.provider,
|
|
3137
|
+
model: run.model,
|
|
3138
|
+
finishedAt: run.finishedAt,
|
|
3139
|
+
branch: run.branch,
|
|
3140
|
+
headRevision: run.headRevision,
|
|
3141
|
+
ranMinutes: row.ranMinutes,
|
|
3142
|
+
cost: runCostWords(run, false),
|
|
3143
|
+
summary: detail.handoff?.conclusion ?? run.handoff,
|
|
3144
|
+
},
|
|
3145
|
+
detail,
|
|
3146
|
+
reviewRetry: store.reviewRetryStateOf(run.id),
|
|
3147
|
+
review: reviewFactsFor(run.id),
|
|
3148
|
+
notes: store.notesForRun(run.id),
|
|
3149
|
+
};
|
|
3150
|
+
}
|
|
3151
|
+
// ---- evidence ------------------------------------------------------------
|
|
3152
|
+
function decisionEvidence(response, decisionId, artifactId) {
|
|
3153
|
+
// The ceiling applies to decision evidence exactly as to run evidence
|
|
3154
|
+
// (v2 review, finding 2): a decision whose task belongs to a repo this
|
|
3155
|
+
// server may not serve does not exist here, and neither do its bytes.
|
|
3156
|
+
const decision = store.getDecision(decisionId);
|
|
3157
|
+
const decisionRun = decision === null ? null : store.getRun(decision.run);
|
|
3158
|
+
if (decisionRun !== null && !visible(taskRepoOf(decisionRun.taskRef))) {
|
|
3159
|
+
return respond(response, 404, "text/plain; charset=utf-8", "no such evidence");
|
|
3160
|
+
}
|
|
3161
|
+
// Only through the decision's own relation — an artifact id from another
|
|
3162
|
+
// run simply is not in this list, whatever the URL claims.
|
|
3163
|
+
const linked = store.evidenceFor(decisionId).find(one => one.id === artifactId);
|
|
3164
|
+
if (linked === undefined) {
|
|
3165
|
+
return respond(response, 404, "text/plain; charset=utf-8", "no such evidence");
|
|
3166
|
+
}
|
|
3167
|
+
return sendArtifact(response, linked);
|
|
3168
|
+
}
|
|
3169
|
+
function runEvidence(response, runId, artifactId) {
|
|
3170
|
+
const run = store.getRun(runId);
|
|
3171
|
+
if (run === null || !runVisible(run)) {
|
|
3172
|
+
return respond(response, 404, "text/plain; charset=utf-8", "no such run");
|
|
3173
|
+
}
|
|
3174
|
+
// Membership is the lookup's own predicate — there is no way to check
|
|
3175
|
+
// the run and fetch the artifact as two separate acts here.
|
|
3176
|
+
const linked = store.artifactForRun(runId, artifactId);
|
|
3177
|
+
if (linked === null) {
|
|
3178
|
+
return respond(response, 404, "text/plain; charset=utf-8", "no such evidence");
|
|
3179
|
+
}
|
|
3180
|
+
return sendArtifact(response, linked);
|
|
3181
|
+
}
|
|
3182
|
+
function sendArtifact(response, linked) {
|
|
3183
|
+
const read = readVerifiedArtifact(evidenceRoot, linked);
|
|
3184
|
+
if (!read.ok) {
|
|
3185
|
+
return respond(response, 410, "text/plain; charset=utf-8", "the evidence no longer matches its record");
|
|
3186
|
+
}
|
|
3187
|
+
// A validated screenshot is the one evidence kind meant to render
|
|
3188
|
+
// inline (thumbnails, full-image links) — every other kind stays a
|
|
3189
|
+
// downloaded text record, exactly as before.
|
|
3190
|
+
if (linked.kind === "screenshot") {
|
|
3191
|
+
const imageType = linked.key.endsWith(".png") ? "image/png" : linked.key.endsWith(".jpg") ? "image/jpeg" : null;
|
|
3192
|
+
if (imageType !== null) {
|
|
3193
|
+
response.writeHead(200, { ...SAFETY, "Content-Type": imageType, "Cache-Control": "private, max-age=31536000, immutable" });
|
|
3194
|
+
response.end(read.content);
|
|
3195
|
+
return;
|
|
3196
|
+
}
|
|
3197
|
+
}
|
|
3198
|
+
response.writeHead(200, {
|
|
3199
|
+
...SAFETY,
|
|
3200
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
3201
|
+
"Content-Disposition": `attachment; filename="evidence-${linked.id}.txt"`,
|
|
3202
|
+
});
|
|
3203
|
+
response.end(read.content);
|
|
3204
|
+
}
|
|
3205
|
+
/** The routine screen: the standing order restated, its verbs, its ledger. */
|
|
3206
|
+
function routinePage(response, who, routineId, problem, status) {
|
|
3207
|
+
const routine = store.getRoutine(routineId);
|
|
3208
|
+
if (routine === null || !visible(routine.repo)) {
|
|
3209
|
+
return refuse(response, who, 404, "no such routine", "/routines");
|
|
3210
|
+
}
|
|
3211
|
+
// Same rule as scope approval: the nonce exists only where the exact
|
|
3212
|
+
// terms are restated, bound to who saw which digest of which order —
|
|
3213
|
+
// and only where a yes could bind them (v48): an order whose agents
|
|
3214
|
+
// are unresolved, unreadable, or never frozen gets the recovery road,
|
|
3215
|
+
// never a password field.
|
|
3216
|
+
const nonce = who.via === "cookie" && routineAgentsState(routine).approvable
|
|
3217
|
+
? mintApprovalNonce(who.name, `routine:${routine.id}`, routine.digest)
|
|
3218
|
+
: "";
|
|
3219
|
+
const paneProject = restricted() ? routine.repo : who.via === "cookie" ? who.session.project : null;
|
|
3220
|
+
return sendScreen(response, status, routineScreenPage(chromeFor(paneProject, "routines"), {
|
|
3221
|
+
routine,
|
|
3222
|
+
fires: store.routineFires(routine.id, 14),
|
|
3223
|
+
spend: store.routineSpend(routine.id, new Date(clock().getTime() - 7 * 24 * 60 * 60_000).toISOString()),
|
|
3224
|
+
blocker: store.routineBlocker(routine.id, clock()),
|
|
3225
|
+
csrf: who.via === "cookie" ? who.session.csrf : "",
|
|
3226
|
+
nonce,
|
|
3227
|
+
problem,
|
|
3228
|
+
now: clock(),
|
|
3229
|
+
}));
|
|
3230
|
+
}
|
|
3231
|
+
/** The full guard list (v3 §4) — run on every request, hit or miss. */
|
|
3232
|
+
function peekGuards(runId) {
|
|
3233
|
+
if (options.localRunner === undefined || options.poolRoot === undefined) {
|
|
3234
|
+
return { ok: false, message: "live peek is off — start serve with --runner <name> naming this machine's runner", final: true };
|
|
3235
|
+
}
|
|
3236
|
+
const run = store.getRun(runId);
|
|
3237
|
+
if (run === null || !runVisible(run))
|
|
3238
|
+
return { ok: false, message: "no such build", final: true };
|
|
3239
|
+
if (run.outcome !== null)
|
|
3240
|
+
return { ok: false, message: "this build has finished — the final diff below is the record", final: true };
|
|
3241
|
+
// The reviewer role (v29): artifact-only, honestly — there is no
|
|
3242
|
+
// checkout anywhere to watch, and there never was.
|
|
3243
|
+
if (run.worktree === null) {
|
|
3244
|
+
return { ok: false, message: "this run reviewed the sealed diff — no workspace existed to watch", final: true };
|
|
3245
|
+
}
|
|
3246
|
+
if (run.baseRevision === null)
|
|
3247
|
+
return { ok: false, message: "the build has not settled its starting point yet" };
|
|
3248
|
+
// The run's lease must be the task's CURRENT live lease — max generation,
|
|
3249
|
+
// unreleased, strictly unexpired. liveClaimByLease proves only that the
|
|
3250
|
+
// lease exists; a superseded lease would still pass it (round-4
|
|
3251
|
+
// finding 15), and superseded is forever, so the poller may stop.
|
|
3252
|
+
if (store.currentLiveLease(run.taskRef, clock()) !== run.leaseId) {
|
|
3253
|
+
return { ok: false, message: "the build is not actively running right now", final: true };
|
|
3254
|
+
}
|
|
3255
|
+
const row = store.getWorktree(run.worktree);
|
|
3256
|
+
if (row === null || row.taskRef !== run.taskRef)
|
|
3257
|
+
return { ok: false, message: "the checkout is not where the record says" };
|
|
3258
|
+
if (row.runner !== options.localRunner) {
|
|
3259
|
+
return { ok: false, message: "this build runs on another machine — open the console there to watch it", final: true };
|
|
3260
|
+
}
|
|
3261
|
+
// Adoption-path rows can carry no epoch (round-3 finding 37): no fence,
|
|
3262
|
+
// no peek — never a guess.
|
|
3263
|
+
if (row.leaseEpoch === null || row.leaseEpoch === undefined) {
|
|
3264
|
+
return { ok: false, message: "this checkout was set up before live watching existed — the next fresh build can be watched", final: true };
|
|
3265
|
+
}
|
|
3266
|
+
try {
|
|
3267
|
+
const real = realpathSync(run.worktree);
|
|
3268
|
+
const pool = realpathSync(options.poolRoot);
|
|
3269
|
+
if (real !== run.worktree || !(real === pool || real.startsWith(`${pool}/`)) || !lstatSync(run.worktree).isDirectory()) {
|
|
3270
|
+
return { ok: false, message: "the checkout is not inside this machine's pool" };
|
|
3271
|
+
}
|
|
3272
|
+
}
|
|
3273
|
+
catch {
|
|
3274
|
+
return { ok: false, message: "the checkout could not be found on this machine" };
|
|
3275
|
+
}
|
|
3276
|
+
// The snapshot: exactly one successful, untruncated base-tree artifact
|
|
3277
|
+
// whose envelope binds THIS run and THIS base (round-3 finding 42).
|
|
3278
|
+
const artifact = store.artifactsFor(runId).find(one => one.kind === "base-tree");
|
|
3279
|
+
if (artifact === undefined)
|
|
3280
|
+
return { ok: false, message: "no base snapshot was captured for this build" };
|
|
3281
|
+
if (artifact.captureStatus !== "ok" || artifact.truncated) {
|
|
3282
|
+
return { ok: false, message: "the base snapshot did not capture cleanly — this build cannot be watched live" };
|
|
3283
|
+
}
|
|
3284
|
+
const read = readVerifiedArtifact(evidenceRoot, artifact);
|
|
3285
|
+
if (!read.ok)
|
|
3286
|
+
return { ok: false, message: "the base snapshot no longer verifies" };
|
|
3287
|
+
const snapshot = parseBaseTreeSnapshot(read.content.toString("utf8"));
|
|
3288
|
+
if (snapshot === null || snapshot.run !== runId || snapshot.base !== run.baseRevision) {
|
|
3289
|
+
return { ok: false, message: "the base snapshot does not match this build" };
|
|
3290
|
+
}
|
|
3291
|
+
return { ok: true, admit: { run, worktree: run.worktree, epoch: row.leaseEpoch, entries: snapshot } };
|
|
3292
|
+
}
|
|
3293
|
+
/**
|
|
3294
|
+
* The tournament's comparison data: the pick view plus everything the
|
|
3295
|
+
* page states — the task behind it, per-agent question counts, and the
|
|
3296
|
+
* money totals. The ceiling is proved here, on the server-resolved repo,
|
|
3297
|
+
* whatever the request named.
|
|
3298
|
+
*/
|
|
3299
|
+
function contestData(contestId) {
|
|
3300
|
+
const view = buildPickView(store, evidenceRoot, contestId);
|
|
3301
|
+
if (view === null)
|
|
3302
|
+
return null;
|
|
3303
|
+
const ref = store.refForId(view.contest.taskRef);
|
|
3304
|
+
if (ref === null || (ref.repo !== null && !visible(ref.repo)))
|
|
3305
|
+
return null;
|
|
3306
|
+
const found = store.getTask(ref.externalId);
|
|
3307
|
+
if (found === null)
|
|
3308
|
+
return null;
|
|
3309
|
+
// Question counts follow run lineage: every run belonging to the agent,
|
|
3310
|
+
// not just its final one — a parked question is part of its story.
|
|
3311
|
+
const byRun = new Map();
|
|
3312
|
+
for (const one of store.runsFor(view.contest.taskRef)) {
|
|
3313
|
+
if (one.contestant !== null)
|
|
3314
|
+
byRun.set(one.id, one.contestant);
|
|
3315
|
+
}
|
|
3316
|
+
const questions = new Map();
|
|
3317
|
+
for (const decision of store.decisionsForTask(view.contest.taskRef)) {
|
|
3318
|
+
const owner = byRun.get(decision.run);
|
|
3319
|
+
if (owner !== undefined)
|
|
3320
|
+
questions.set(owner, (questions.get(owner) ?? 0) + 1);
|
|
3321
|
+
}
|
|
3322
|
+
return {
|
|
3323
|
+
rollups: new Map(view.agents.map(agent => [agent.contestant.id, store.contestantSpendRollup(agent.contestant.id)])),
|
|
3324
|
+
view,
|
|
3325
|
+
taskId: ref.externalId,
|
|
3326
|
+
taskTitle: found.title,
|
|
3327
|
+
repo: ref.repo,
|
|
3328
|
+
refOrigin: ref.origin,
|
|
3329
|
+
questions,
|
|
3330
|
+
// An interrupted tournament's agents are STOPPED, not "still
|
|
3331
|
+
// working" — their run records stay unfinished, so the card must
|
|
3332
|
+
// prove liveness the same way every other surface does (round-4
|
|
3333
|
+
// finding 16).
|
|
3334
|
+
liveRuns: liveRunIds(view.agents.flatMap(agent => (agent.run === null ? [] : [agent.run]))),
|
|
3335
|
+
totalMicrousd: view.agents.reduce((sum, agent) => sum + agent.contestant.accountedMicrousd, 0),
|
|
3336
|
+
anyUnknown: view.agents.some(agent => agent.contestant.unknownSpend),
|
|
3337
|
+
};
|
|
3338
|
+
}
|
|
3339
|
+
/**
|
|
3340
|
+
* Assemble the LIVE rendered terms of one watched attempt, or say in
|
|
3341
|
+
* words why there are none (Phase 2E, ruling 12). Everything here is
|
|
3342
|
+
* re-read at confirm time — the digest is the proof the world held.
|
|
3343
|
+
*/
|
|
3344
|
+
async function liveAttendedTerms(taskId, inputs, now) {
|
|
3345
|
+
if (options.attended === undefined)
|
|
3346
|
+
return { ok: false, problem: "this console cannot hold a session — start it with `toolroll up`" };
|
|
3347
|
+
const ref = store.lookupRef(taskId);
|
|
3348
|
+
if (ref === null)
|
|
3349
|
+
return { ok: false, problem: "no such task" };
|
|
3350
|
+
const scope = store.getScope(taskId);
|
|
3351
|
+
if (scope === null)
|
|
3352
|
+
return { ok: false, problem: "file a scope first — the terms come from it" };
|
|
3353
|
+
// CONTINUATION (A4): the parent attempt and the follow-up enter the
|
|
3354
|
+
// SIGNED terms; the head is the parent's accepted commit per outcome;
|
|
3355
|
+
// a moving publication blocks; failed parents are refused outright for
|
|
3356
|
+
// now (a dirty preserved tree cannot promise a clean continuation) —
|
|
3357
|
+
// stated, not hidden.
|
|
3358
|
+
let continuation = null;
|
|
3359
|
+
if (inputs.parent !== undefined) {
|
|
3360
|
+
const parent = store.getRun(inputs.parent);
|
|
3361
|
+
if (parent === null || parent.taskRef !== ref.id)
|
|
3362
|
+
return { ok: false, problem: "no such finished attempt on this task" };
|
|
3363
|
+
const followup = (inputs.followup ?? "").trim();
|
|
3364
|
+
if (followup === "" || followup.length > 2000) {
|
|
3365
|
+
return { ok: false, problem: "say what to do next — 1 to 2000 characters" };
|
|
3366
|
+
}
|
|
3367
|
+
if (parent.outcome !== "built" && parent.outcome !== "no-change") {
|
|
3368
|
+
return { ok: false, problem: "only a built or no-change attempt can be continued — for a failed one, file a follow-up task" };
|
|
3369
|
+
}
|
|
3370
|
+
const accepted = parent.outcome === "built" ? parent.headRevision : parent.baseRevision;
|
|
3371
|
+
if (accepted === null)
|
|
3372
|
+
return { ok: false, problem: "the attempt's accepted head was never recorded — file a follow-up task" };
|
|
3373
|
+
const blocked = store.continuationBlockOf(parent.id);
|
|
3374
|
+
if (blocked !== null)
|
|
3375
|
+
return { ok: false, problem: blocked };
|
|
3376
|
+
continuation = { parentRun: parent.id, followup, head: accepted };
|
|
3377
|
+
}
|
|
3378
|
+
else if (approvalOf(scope).approved) {
|
|
3379
|
+
return { ok: false, problem: "the scope is approved — it already dispatches unattended" };
|
|
3380
|
+
}
|
|
3381
|
+
let pinned = scope.profileState === "resolved" ? (scope.profile ?? null) : null;
|
|
3382
|
+
if (pinned === null) {
|
|
3383
|
+
return { ok: false, problem: "the scope cannot say exactly what runs — name a model (task scope --model, or config set build)" };
|
|
3384
|
+
}
|
|
3385
|
+
if (pinned.provider !== "claude") {
|
|
3386
|
+
return { ok: false, problem: `${pinned.provider} cannot hold a watched session yet — this road is claude-only for now` };
|
|
3387
|
+
}
|
|
3388
|
+
// THE MINT PICKER (P1/C7): a chosen model or posture rebuilds the
|
|
3389
|
+
// WHOLE ClaudeProfile — the digest signs the profile that actually
|
|
3390
|
+
// runs, never a partial edit. Absent choices keep the scope's pin.
|
|
3391
|
+
const chosenModel = (inputs.model ?? "").trim();
|
|
3392
|
+
const chosenPosture = inputs.posture === "bypassPermissions"
|
|
3393
|
+
? "bypassPermissions"
|
|
3394
|
+
: inputs.posture === "auto"
|
|
3395
|
+
? "auto"
|
|
3396
|
+
: inputs.posture === "acceptEdits"
|
|
3397
|
+
? "acceptEdits"
|
|
3398
|
+
: null;
|
|
3399
|
+
if (chosenModel !== "" || chosenPosture !== null) {
|
|
3400
|
+
pinned = {
|
|
3401
|
+
...pinned,
|
|
3402
|
+
...(chosenModel === "" ? {} : { model: chosenModel }),
|
|
3403
|
+
...(chosenPosture === null ? {} : { permissionArgv: chosenPosture }),
|
|
3404
|
+
};
|
|
3405
|
+
}
|
|
3406
|
+
if (store.activeTournamentTerms(ref.id) !== null) {
|
|
3407
|
+
return { ok: false, problem: "this task races a tournament — one attempt cannot authorize N racers" };
|
|
3408
|
+
}
|
|
3409
|
+
const repo = ref.repo;
|
|
3410
|
+
if (repo === null)
|
|
3411
|
+
return { ok: false, problem: "place the task in a repository first — the terms pin the exact head" };
|
|
3412
|
+
const head = continuation !== null ? continuation.head : await options.attended.headOf(repo);
|
|
3413
|
+
if (head === null)
|
|
3414
|
+
return { ok: false, problem: `the head of ${repo} cannot be read right now` };
|
|
3415
|
+
const minutes = Math.max(5, Math.min(240, Math.floor(inputs.minutes) || 60));
|
|
3416
|
+
const turns = Math.max(1, Math.min(100, Math.floor(inputs.turns) || 20));
|
|
3417
|
+
const budget = Math.max(100_000, Math.min(50_000_000, Math.floor(inputs.budgetMicrousd) || 2_000_000));
|
|
3418
|
+
return {
|
|
3419
|
+
ok: true,
|
|
3420
|
+
terms: {
|
|
3421
|
+
taskId,
|
|
3422
|
+
attentionMode: "console-visible",
|
|
3423
|
+
scopeDigest: scope.digest,
|
|
3424
|
+
profileDigest: profileDigestOf(pinned),
|
|
3425
|
+
profileJson: canonicalProfileJson(pinned),
|
|
3426
|
+
repo,
|
|
3427
|
+
runner: options.attended.runner,
|
|
3428
|
+
runnerGeneration: 0,
|
|
3429
|
+
head,
|
|
3430
|
+
maxSessionTurns: turns,
|
|
3431
|
+
budgetMicrousd: budget,
|
|
3432
|
+
turnTimeoutSeconds: pinned.timeoutSeconds,
|
|
3433
|
+
...(continuation === null ? {} : { parentRun: continuation.parentRun, followup: continuation.followup }),
|
|
3434
|
+
// The expiry is FIXED at preview and carried through the confirm —
|
|
3435
|
+
// a timestamp recomputed at signing time would change the digest
|
|
3436
|
+
// every millisecond and make the proof unmatchable. The confirm's
|
|
3437
|
+
// bound check keeps a stale form honest.
|
|
3438
|
+
absoluteExpiry: inputs.expiry !== undefined &&
|
|
3439
|
+
Date.parse(inputs.expiry) > now.getTime() &&
|
|
3440
|
+
Date.parse(inputs.expiry) <= now.getTime() + 241 * 60_000
|
|
3441
|
+
? inputs.expiry
|
|
3442
|
+
: new Date(now.getTime() + minutes * 60_000).toISOString(),
|
|
3443
|
+
},
|
|
3444
|
+
};
|
|
3445
|
+
}
|
|
3446
|
+
/** The signed form, rendered from EXACT terms — what you read is what the password signs. */
|
|
3447
|
+
function attendConfirmScreen(response, who, terms, inputs, digest, nonce, csrf) {
|
|
3448
|
+
const scope = store.getScope(terms.taskId);
|
|
3449
|
+
// Quick mint (M4): the mode signature substitutes for the password —
|
|
3450
|
+
// the confirm screen still shows EVERY term; only the credential line
|
|
3451
|
+
// changes. The mint transaction re-proves the mode either way.
|
|
3452
|
+
const quickRef = store.lookupRef(terms.taskId);
|
|
3453
|
+
const quickMode = quickRef?.repo == null ? null : store.activeMode(quickRef.repo, clock());
|
|
3454
|
+
const quickTerms = quickMode === null ? null : modeTermsFromJson(quickMode.termsJson);
|
|
3455
|
+
const quick = quickTerms?.quickMint === true && quickMode !== null && quickMode.signedBy === who.name;
|
|
3456
|
+
const body = `<h1>Run ${escape(terms.taskId)} once, while you watch</h1>` +
|
|
3457
|
+
`<form method="post" action="${taskHref(terms.taskId)}/attend" class="card approve-form">` +
|
|
3458
|
+
`<input type="hidden" name="csrf" value="${escape(csrf)}">` +
|
|
3459
|
+
`<input type="hidden" name="nonce" value="${escape(nonce)}">` +
|
|
3460
|
+
`<input type="hidden" name="digest" value="${escape(digest)}">` +
|
|
3461
|
+
`<input type="hidden" name="minutes" value="${inputs.minutes}">` +
|
|
3462
|
+
`<input type="hidden" name="turns" value="${inputs.turns}">` +
|
|
3463
|
+
`<input type="hidden" name="budget" value="${inputs.budgetMicrousd}">` +
|
|
3464
|
+
`<input type="hidden" name="expiry" value="${escape(terms.absoluteExpiry)}">` +
|
|
3465
|
+
(inputs.model === undefined ? "" : `<input type="hidden" name="model" value="${escape(inputs.model)}">`) +
|
|
3466
|
+
(inputs.posture === undefined ? "" : `<input type="hidden" name="posture" value="${escape(inputs.posture)}">`) +
|
|
3467
|
+
(terms.parentRun == null ? "" : `<input type="hidden" name="parent" value="${terms.parentRun}">`) +
|
|
3468
|
+
(terms.followup == null ? "" : `<input type="hidden" name="followup" value="${escape(terms.followup)}">`) +
|
|
3469
|
+
`<p><strong>Your password signs exactly this:</strong></p>` +
|
|
3470
|
+
`<p class="meta">Goal</p><p class="recap" style="margin-top:0">${escape(scope?.goal ?? "")}</p>` +
|
|
3471
|
+
`<p class="meta">Not this</p><p class="recap" style="margin-top:0">${scope?.outOfScope == null ? "<em>no exclusions</em>" : escape(scope.outOfScope)}</p>` +
|
|
3472
|
+
`<p class="meta">Touches · ${scope === null || scope.touches.length === 0 ? "anything" : scope.touches.map(one => escape(one)).join(", ")}</p>` +
|
|
3473
|
+
(() => {
|
|
3474
|
+
const profile = JSON.parse(terms.profileJson).profile;
|
|
3475
|
+
const posture = profile?.permissionArgv === "bypassPermissions"
|
|
3476
|
+
? "FULL permissions — claude runs with --dangerously-skip-permissions; nothing asks"
|
|
3477
|
+
: profile?.permissionArgv === "auto"
|
|
3478
|
+
? "safe unattended permissions — routine project commands and edits proceed; risky acts stop"
|
|
3479
|
+
: "legacy acceptEdits — edits proceed, commands that ask are denied unattended";
|
|
3480
|
+
return `<p class="meta">Runs on</p><p class="recap" style="margin-top:0">claude · ${escape(String(profile?.model ?? ""))} — ${posture}</p>`;
|
|
3481
|
+
})() +
|
|
3482
|
+
(terms.parentRun == null
|
|
3483
|
+
? ""
|
|
3484
|
+
: `<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>` +
|
|
3485
|
+
`<p class="meta">The follow-up — this is the instruction</p><p class="recap" style="margin-top:0">${escape(terms.followup ?? "")}</p>`) +
|
|
3486
|
+
`<p class="meta">Repository · head</p><p class="recap mono" style="margin-top:0">${escape(terms.repo)} @ ${escape(terms.head.slice(0, 12))}</p>` +
|
|
3487
|
+
`<p class="meta">Worker</p><p class="recap" style="margin-top:0">${escape(terms.runner)} (this machine)</p>` +
|
|
3488
|
+
`<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>` +
|
|
3489
|
+
`<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>` +
|
|
3490
|
+
`<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>` +
|
|
3491
|
+
(quick
|
|
3492
|
+
? `<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>`
|
|
3493
|
+
: `<label>Your password, typed again — a signed-in session alone cannot authorize work<input type="password" name="token" autocomplete="current-password"></label>`) +
|
|
3494
|
+
`<div class="sticky-actions"><button type="submit">Run it while I watch</button></div>` +
|
|
3495
|
+
`</form>` +
|
|
3496
|
+
`<p class="meta"><a href="${taskHref(terms.taskId)}">back to the task</a></p>`;
|
|
3497
|
+
// The chrome binds to the AUTHORITY repository (surfaces round 1,
|
|
3498
|
+
// finding 5): the banner on this screen is the mode that would cover
|
|
3499
|
+
// a quick mint here, never the session's open-project filter.
|
|
3500
|
+
return sendScreen(response, 200, screen(`attend \u00b7 ${terms.taskId}`, body, { chrome: chromeFor(terms.repo, "tasks") }));
|
|
3501
|
+
}
|
|
3502
|
+
/** The live subset of a bounded run page \u2014 one indexed lookup per row. */
|
|
3503
|
+
function liveRunIds(rows) {
|
|
3504
|
+
const live = new Set();
|
|
3505
|
+
for (const run of rows)
|
|
3506
|
+
if (runIsLive(run))
|
|
3507
|
+
live.add(run.id);
|
|
3508
|
+
return live;
|
|
3509
|
+
}
|
|
3510
|
+
const registrations = [
|
|
3511
|
+
{ id: "code.page", domain: "tasks", stage: "console", method: "GET", handle: get },
|
|
3512
|
+
{ id: "inbox", domain: "tasks", stage: "console", method: "GET", handle: get },
|
|
3513
|
+
{ id: "work", domain: "tasks", stage: "console", method: "GET", handle: get },
|
|
3514
|
+
{ id: "next", domain: "tasks", stage: "console", method: "GET", handle: get },
|
|
3515
|
+
{ id: "board", domain: "tasks", stage: "console", method: "GET", handle: get },
|
|
3516
|
+
{ id: "review", domain: "tasks", stage: "console", method: "GET", handle: get },
|
|
3517
|
+
{ id: "done", domain: "tasks", stage: "console", method: "GET", handle: get },
|
|
3518
|
+
{ id: "tasks", domain: "tasks", stage: "console", method: "GET", handle: get },
|
|
3519
|
+
{ id: "queue", domain: "tasks", stage: "console", method: "GET", handle: get },
|
|
3520
|
+
{ id: "tasks.new", domain: "tasks", stage: "console", method: "GET", handle: get },
|
|
3521
|
+
{ id: "task.live", domain: "tasks", stage: "console", method: "GET", handle: get },
|
|
3522
|
+
{ id: "task.page", domain: "tasks", stage: "console", method: "GET", handle: get },
|
|
3523
|
+
{ id: "task.evidence", domain: "tasks", stage: "console", method: "GET", handle: get },
|
|
3524
|
+
{ id: "runs", domain: "tasks", stage: "console", method: "GET", handle: get },
|
|
3525
|
+
{ id: "run.page", domain: "tasks", stage: "console", method: "GET", handle: get },
|
|
3526
|
+
{ id: "run.evidence", domain: "tasks", stage: "console", method: "GET", handle: get },
|
|
3527
|
+
{ id: "contest.page", domain: "tasks", stage: "console", method: "GET", handle: get },
|
|
3528
|
+
{ id: "routines", domain: "tasks", stage: "console", method: "GET", handle: get },
|
|
3529
|
+
{ id: "routine.page", domain: "tasks", stage: "console", method: "GET", handle: get },
|
|
3530
|
+
{ id: "decision.page", domain: "tasks", stage: "console", method: "GET", handle: get },
|
|
3531
|
+
{ id: "decision.evidence", domain: "tasks", stage: "console", method: "GET", handle: get },
|
|
3532
|
+
{ id: "session.attended-beats", domain: "tasks", stage: "console", method: "POST", handle: post },
|
|
3533
|
+
{ id: "code.act", domain: "tasks", stage: "console", method: "POST", handle: post },
|
|
3534
|
+
{ id: "tasks.add", domain: "tasks", stage: "console", method: "POST", handle: post },
|
|
3535
|
+
{ id: "queue.move", domain: "tasks", stage: "console", method: "POST", handle: post },
|
|
3536
|
+
{ id: "queue.note", domain: "tasks", stage: "console", method: "POST", handle: post },
|
|
3537
|
+
{ id: "decision.answer", domain: "tasks", stage: "console", method: "POST", handle: post },
|
|
3538
|
+
{ id: "contest.act.arm", domain: "tasks", stage: "console", method: "POST", handle: post },
|
|
3539
|
+
{ id: "contest.act.pick", domain: "tasks", stage: "console", method: "POST", handle: post },
|
|
3540
|
+
{ id: "contest.act.abandon", domain: "tasks", stage: "console", method: "POST", handle: post },
|
|
3541
|
+
{ id: "task.attend.attend-preview", domain: "tasks", stage: "console", method: "POST", handle: post },
|
|
3542
|
+
{ id: "task.attend.attend", domain: "tasks", stage: "console", method: "POST", handle: post },
|
|
3543
|
+
{ id: "task.attend.attend-revoke", domain: "tasks", stage: "console", method: "POST", handle: post },
|
|
3544
|
+
{ id: "task.act.hold", domain: "tasks", stage: "console", method: "POST", handle: post },
|
|
3545
|
+
{ id: "task.act.unhold", domain: "tasks", stage: "console", method: "POST", handle: post },
|
|
3546
|
+
{ id: "task.act.requeue", domain: "tasks", stage: "console", method: "POST", handle: post },
|
|
3547
|
+
{ id: "task.act.cancel", domain: "tasks", stage: "console", method: "POST", handle: post },
|
|
3548
|
+
{ id: "task.act.scope", domain: "tasks", stage: "console", method: "POST", handle: post },
|
|
3549
|
+
{ id: "task.act.approve", domain: "tasks", stage: "console", method: "POST", handle: post },
|
|
3550
|
+
{ id: "task.act.plan", domain: "tasks", stage: "console", method: "POST", handle: post },
|
|
3551
|
+
{ id: "task.act.plan-edit", domain: "tasks", stage: "console", method: "POST", handle: post },
|
|
3552
|
+
{ id: "task.act.next", domain: "tasks", stage: "console", method: "POST", handle: post },
|
|
3553
|
+
{ id: "task.act.reopen", domain: "tasks", stage: "console", method: "POST", handle: post },
|
|
3554
|
+
{ id: "task.act.steer", domain: "tasks", stage: "console", method: "POST", handle: post },
|
|
3555
|
+
{ id: "task.act.accept-proof", domain: "tasks", stage: "console", method: "POST", handle: post },
|
|
3556
|
+
{ id: "task.act.accept-revision", domain: "tasks", stage: "console", method: "POST", handle: post },
|
|
3557
|
+
{ id: "task.act.reject-revision", domain: "tasks", stage: "console", method: "POST", handle: post },
|
|
3558
|
+
{ id: "task.act.route", domain: "tasks", stage: "console", method: "POST", handle: post },
|
|
3559
|
+
{ id: "task.act.retry-review", domain: "tasks", stage: "console", method: "POST", handle: post },
|
|
3560
|
+
{ id: "task.act.complete", domain: "tasks", stage: "console", method: "POST", handle: post },
|
|
3561
|
+
{ id: "task.act.merge", domain: "tasks", stage: "console", method: "POST", handle: post },
|
|
3562
|
+
{ id: "task.act.confirm-stopped", domain: "tasks", stage: "console", method: "POST", handle: post },
|
|
3563
|
+
{ id: "task.act.stop", domain: "tasks", stage: "console", method: "POST", handle: post },
|
|
3564
|
+
{ id: "task.act.resume-arm", domain: "tasks", stage: "console", method: "POST", handle: post },
|
|
3565
|
+
{ id: "task.act.resume", domain: "tasks", stage: "console", method: "POST", handle: post },
|
|
3566
|
+
{ id: "task.instance-act", domain: "tasks", stage: "console", method: "POST", handle: post },
|
|
3567
|
+
{ id: "routines.add", domain: "tasks", stage: "console", method: "POST", handle: post },
|
|
3568
|
+
{ id: "routine.act.approve", domain: "tasks", stage: "console", method: "POST", handle: post },
|
|
3569
|
+
{ id: "routine.act.refresh", domain: "tasks", stage: "console", method: "POST", handle: post },
|
|
3570
|
+
{ id: "routine.act.pause", domain: "tasks", stage: "console", method: "POST", handle: post },
|
|
3571
|
+
{ id: "routine.act.resume", domain: "tasks", stage: "console", method: "POST", handle: post },
|
|
3572
|
+
{ id: "routine.act.run-now", domain: "tasks", stage: "console", method: "POST", handle: post },
|
|
3573
|
+
{ id: "run.act", domain: "tasks", stage: "console", method: "POST", handle: post },
|
|
3574
|
+
{ id: "session.editor-links", domain: "tasks", stage: "console", method: "POST", handle: post },
|
|
3575
|
+
{ id: "run.turn", domain: "tasks", stage: "console", method: "POST", handle: post },
|
|
3576
|
+
{ id: "incident.resolve", domain: "tasks", stage: "console", method: "POST", handle: post },
|
|
3577
|
+
];
|
|
3578
|
+
return { registrations, get, post, runListPane, peekFragment, contestScreen, routineMutation, attendedBeats, attendMutation, taskMutation, queueRegionFor, ciFailingFor, completedRowFor, attemptRowFor, resultRowOf, reviewCockpitViewOf, decisionEvidence, runEvidence, sendArtifact, routinePage, peekGuards, contestData, liveAttendedTerms, attendConfirmScreen, liveRunIds };
|
|
3579
|
+
}
|