toolroll 0.9.52 → 0.9.54
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 +16 -0
- package/README.md +55 -80
- package/dist/agentconfig.d.ts +11 -36
- package/dist/agentconfig.js +3 -62
- package/dist/approval-policy.d.ts +1 -1
- package/dist/approval-policy.js +1 -1
- package/dist/board.d.ts +3 -19
- package/dist/board.js +4 -42
- package/dist/browser/workspace.js +18 -18
- package/dist/browser-workspace.d.ts +2 -1
- package/dist/builder.d.ts +9 -40
- package/dist/builder.js +50 -257
- package/dist/chat-actions.js +3 -14
- package/dist/chat-ask.js +8 -8
- package/dist/chat-controls.d.ts +0 -4
- package/dist/chat-controls.js +0 -1
- package/dist/chat-core.d.ts +171 -0
- package/dist/chat-core.js +288 -0
- package/dist/chat-decide.d.ts +10 -8
- package/dist/chat-decide.js +27 -24
- package/dist/chat-delivery-state.d.ts +51 -75
- package/dist/chat-delivery-state.js +140 -170
- package/dist/chat-delivery.d.ts +21 -4
- package/dist/chat-delivery.js +313 -195
- package/dist/chat-flow.d.ts +29 -1
- package/dist/chat-flow.js +24 -50
- package/dist/chat-messages.d.ts +319 -0
- package/dist/chat-messages.js +841 -0
- package/dist/chat-migration.d.ts +30 -0
- package/dist/chat-migration.js +802 -0
- package/dist/chat-quiet.d.ts +3 -0
- package/dist/chat-quiet.js +7 -2
- package/dist/chat-task-actions.d.ts +1 -1
- package/dist/chat-task-actions.js +0 -2
- package/dist/claim.d.ts +79 -244
- package/dist/claim.js +688 -1023
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +10 -2
- package/dist/contracts/chat-content.d.ts +71 -2
- package/dist/contracts/chat-content.js +22 -3
- package/dist/contracts/chat-tables.d.ts +11 -4
- package/dist/contracts/chat-tables.js +165 -87
- package/dist/contracts/cli.d.ts +0 -13
- package/dist/contracts/cli.js +0 -2
- package/dist/contracts/console-api.d.ts +5 -10
- package/dist/contracts/console-api.js +4 -10
- package/dist/contracts/discord-callback.js +2 -1
- package/dist/contracts/flow.d.ts +50 -0
- package/dist/contracts/flow.js +26 -1
- package/dist/contracts/lead-tools.d.ts +0 -13
- package/dist/contracts/lead-tools.js +3 -3
- package/dist/contracts/slack-callback.js +3 -2
- package/dist/contracts/store-json.d.ts +1 -29
- package/dist/contracts/store-json.js +4 -17
- package/dist/contracts/teams-callback.js +2 -1
- package/dist/converse.d.ts +1 -11
- package/dist/converse.js +6 -38
- package/dist/coordinator.d.ts +0 -1
- package/dist/coordinator.js +3 -5
- package/dist/demo.js +16 -26
- package/dist/desktop-update-gate.js +0 -1
- package/dist/desktop-update.js +1 -3
- package/dist/discord-chat.js +19 -38
- package/dist/discord-settings.js +4 -13
- package/dist/discord.js +4 -14
- package/dist/dispatch.d.ts +1 -1
- package/dist/dispatch.js +1 -1
- package/dist/dispose.d.ts +2 -3
- package/dist/dispose.js +17 -92
- package/dist/envelope.js +0 -3
- package/dist/exec.d.ts +0 -64
- package/dist/exec.js +3 -356
- package/dist/exhaustion.d.ts +22 -76
- package/dist/exhaustion.js +14 -140
- package/dist/export.js +8 -8
- package/dist/flow-engine.js +1 -1
- package/dist/flow-schedule.d.ts +154 -0
- package/dist/flow-schedule.js +321 -0
- package/dist/flow-share.js +1 -1
- package/dist/flow-triggers.d.ts +6 -1
- package/dist/flow-triggers.js +65 -10
- package/dist/flows-cli.js +8 -4
- package/dist/flows-ui.js +2 -0
- package/dist/flows.js +1 -1
- package/dist/guides.js +9 -29
- package/dist/invoke.d.ts +1 -24
- package/dist/invoke.js +17 -259
- package/dist/lead-status.js +2 -4
- package/dist/lead-voice.js +1 -1
- package/dist/mate-doors.d.ts +1 -2
- package/dist/mate-doors.js +12 -25
- package/dist/mate-tools.d.ts +2 -2
- package/dist/mate-tools.js +13 -24
- package/dist/modes.d.ts +19 -8
- package/dist/modes.js +24 -14
- package/dist/notification-rows.d.ts +30 -0
- package/dist/notification-rows.js +102 -0
- package/dist/operate-remote-arguments.js +2 -6
- package/dist/operate-remote.js +6 -7
- package/dist/operate.d.ts +0 -6
- package/dist/operate.js +226 -2248
- package/dist/phase-routing.d.ts +13 -23
- package/dist/phase-routing.js +13 -35
- package/dist/plan-auto.js +2 -2
- package/dist/plane-review.js +5 -11
- package/dist/planner-source.js +2 -1
- package/dist/policy.d.ts +1 -4
- package/dist/policy.js +0 -8
- package/dist/process-recovery-eligibility.js +2 -3
- package/dist/project-delete.js +24 -41
- package/dist/proposal.d.ts +4 -24
- package/dist/proposal.js +3 -88
- package/dist/provider-auth.d.ts +3 -3
- package/dist/provider-auth.js +4 -6
- package/dist/provider.d.ts +11 -25
- package/dist/provider.js +4 -36
- package/dist/push.js +1 -1
- package/dist/recipe-ui.js +6 -6
- package/dist/recipes.d.ts +3 -3
- package/dist/recipes.js +27 -23
- package/dist/result-shots.js +2 -2
- package/dist/retention.js +1 -1
- package/dist/run-closeout.d.ts +1 -1
- package/dist/run-closeout.js +1 -4
- package/dist/scope.d.ts +11 -65
- package/dist/scope.js +17 -82
- package/dist/serve.js +15 -24
- package/dist/server/chat.js +7 -30
- package/dist/server/flows.js +11 -10
- package/dist/server/guards.js +0 -1
- package/dist/server/pages-assets.js +1 -1
- package/dist/server/pages.js +0 -2
- package/dist/server/people-tokens.js +0 -4
- package/dist/server/route-table.d.ts +1 -1
- package/dist/server/route-table.js +2 -17
- package/dist/server/settings.js +6 -12
- package/dist/server/shared.d.ts +28 -212
- package/dist/server/shared.js +71 -780
- package/dist/server/task-views.js +9 -76
- package/dist/server/tasks.d.ts +1 -50
- package/dist/server/tasks.js +36 -946
- package/dist/setup-guide.d.ts +2 -2
- package/dist/slack-api.d.ts +5 -1
- package/dist/slack-api.js +2 -2
- package/dist/slack-chat.d.ts +9 -6
- package/dist/slack-chat.js +29 -53
- package/dist/slack-settings.js +6 -15
- package/dist/slack.js +6 -16
- package/dist/store.d.ts +200 -1702
- package/dist/store.js +811 -5439
- package/dist/surface.js +10 -24
- package/dist/task-composer.d.ts +1 -1
- package/dist/task-composer.js +1 -1
- package/dist/task-control.d.ts +3 -10
- package/dist/task-control.js +2 -7
- package/dist/teammate-desk.js +1 -1
- package/dist/teammate-question.js +12 -12
- package/dist/teammates-ui.js +1 -1
- package/dist/teams-chat.js +12 -9
- package/dist/teams-settings.js +2 -2
- package/dist/teams.js +4 -5
- package/dist/telegram-flow.js +5 -5
- package/dist/telegram-mate.d.ts +0 -3
- package/dist/telegram-mate.js +0 -1
- package/dist/telegram.js +4 -4
- package/dist/templates.d.ts +11 -7
- package/dist/templates.js +12 -9
- package/dist/toolroll-update.d.ts +20 -0
- package/dist/toolroll-update.js +104 -5
- package/dist/work-index.js +2 -6
- package/dist/workspace-revision.js +3 -1
- package/dist/workspace-ui.js +1 -1
- package/package.json +1 -1
- package/scripts/recovery-canary.mjs +3 -3
- package/dist/contest.d.ts +0 -318
- package/dist/contest.js +0 -753
- package/dist/held.d.ts +0 -150
- package/dist/held.js +0 -799
- package/dist/liveness.d.ts +0 -30
- package/dist/liveness.js +0 -42
- package/dist/pricing.d.ts +0 -45
- package/dist/pricing.js +0 -77
- package/dist/routine.d.ts +0 -228
- package/dist/routine.js +0 -764
- package/dist/slack-state.d.ts +0 -13
- package/dist/slack-state.js +0 -10
- package/dist/supervisor.mjs +0 -297
package/dist/server/tasks.js
CHANGED
|
@@ -10,7 +10,6 @@ import { codingHandoffPreview, createCodingHandoff } from '../coding-handoff.js'
|
|
|
10
10
|
import { codingShippingHtml } from '../coding-shipping-ui.js';
|
|
11
11
|
import { codingWorkspaceHtml, codingWorkspaceScript } from '../coding-ui.js';
|
|
12
12
|
import { CodingActionError } from '../coding-workspace.js';
|
|
13
|
-
import { abandonContest, buildPickView, computePickPlan, finalizeContestPick, nonceHashOf, pickTupleDigest, planComparison, planTournament } from "../contest.js";
|
|
14
13
|
import { checkResponse, CONSOLE_FORMS, readForm } from "../contracts/console-api.js";
|
|
15
14
|
import { hasForbiddenControls, validateNote } from "../decision.js";
|
|
16
15
|
import { diagnoseTaskDispatch } from "../dispatch.js";
|
|
@@ -19,16 +18,15 @@ import { readVerifiedArtifact, readVerifiedReport, writeEvidenceFile } from "../
|
|
|
19
18
|
import { computeGaps } from "../gaps.js";
|
|
20
19
|
import { limitsView } from "../limits-ui.js";
|
|
21
20
|
import { readLiveWindow } from "../live.js";
|
|
22
|
-
import { modeTermsFromJson } from "../modes.js";
|
|
23
21
|
import { latestFinishedAttempt, retryNoteOf } from '../needs-you.js';
|
|
24
22
|
import { REMOTE_MESSAGES } from "../operate-remote.js";
|
|
25
23
|
import { aggregateNewNames, observeWorktree, parseBaseTreeSnapshot, PEEK_LIMITS } from "../peek.js";
|
|
26
|
-
import {
|
|
24
|
+
import { isTaskSize, PHASES as ROUTE_PHASES } from "../phase-routing.js";
|
|
27
25
|
import { applyModeToNewFiling, authorizePlanUnderMode } from "../plan-auto.js";
|
|
28
26
|
import { milestonesOf, parseExecutionPlanDocument } from "../plan.js";
|
|
29
27
|
import { levelOfProfile } from "../policy.js";
|
|
30
28
|
import { authorizedProject, canonicalProject, projectName, rowVisible } from "../project.js";
|
|
31
|
-
import {
|
|
29
|
+
import { fileTaskProposal } from "../proposal.js";
|
|
32
30
|
import { isProviderId, validateSpec } from "../provider.js";
|
|
33
31
|
import { completeAndOpenPullRequest, mergeAsPerson } from '../pull-request-flow.js';
|
|
34
32
|
import { isQualityMode } from "../quality.js";
|
|
@@ -36,21 +34,19 @@ import { createResultRevision, requestResultChanges } from "../result-actions.js
|
|
|
36
34
|
import { structuredHandoffView, terminalDiffView } from "../result-evidence-readers.js";
|
|
37
35
|
import { fileAddTestsTask, requestFollowUpChecks } from '../result-follow-ups.js';
|
|
38
36
|
import { commentSourceKey, isRevisionFeedback, parseResultTab, resultReturnTarget } from "../result-review.js";
|
|
39
|
-
import { approveRoutine, fireRoutine, refreshRoutineAgents, routineAgentsState } from "../routine.js";
|
|
40
37
|
import { isAlive as runnerAlive } from "../runner.js";
|
|
41
|
-
import { acceptanceLinesToInput, acceptanceToLines, approvalOf, approve as approveScope,
|
|
38
|
+
import { acceptanceLinesToInput, acceptanceToLines, approvalOf, approve as approveScope, hasFreshIdentitySignIn, modeFilingCoverage, PLACEHOLDER_RUBRIC, proposeGuarded } from "../scope.js";
|
|
42
39
|
import { teammateNames as teammateNamesOf } from "../spend.js";
|
|
43
40
|
import { verifiedAuthor } from "../store.js";
|
|
44
41
|
import { runCostWords } from "../summary.js";
|
|
45
|
-
import { composerSchedule } from "../task-composer.js";
|
|
46
42
|
import { requestTaskStop, resumeTaskStop } from "../task-control.js";
|
|
47
43
|
import { templateByName } from "../templates.js";
|
|
48
44
|
import { TEXT_LIMITS } from "../text-limits.js";
|
|
49
45
|
import { WorkIndexCursorError, workIndexPage } from "../work-index.js";
|
|
50
46
|
import { buildProgressOf, parseWorkView } from "../workspace-ui.js";
|
|
51
|
-
import { ACCEPT_ANYWAY_NEEDS_REASON, approvalFormDigest,
|
|
47
|
+
import { ACCEPT_ANYWAY_NEEDS_REASON, approvalFormDigest, nonceHashOf, approveRefusalWords, boardBody, chatReturnWithSaid, checkProgressHtml, consentDoorOf, 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, runFactsFragment, runOutcomeBadge, runPage, RUNS_PAGE, runsPage, safeChatReturn, safeReturn, SAFETY, screen, TASK_STATES, taskChatHref, taskHref, taskOf, tasksPage, transcriptScript, whenTime, withRefusal, workPage } from "./shared.js";
|
|
52
48
|
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,
|
|
49
|
+
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, 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
50
|
async function get(ctx) {
|
|
55
51
|
const { url, who, request, response, now, project, chosenProject, posted, route } = ctx;
|
|
56
52
|
if (url.pathname === '/code' || url.pathname.startsWith('/code/')) {
|
|
@@ -273,8 +269,7 @@ export function createTasksHandlers(runtime) {
|
|
|
273
269
|
const ref = store.lookupRef(item.approval.taskId);
|
|
274
270
|
const scope = store.getScope(item.approval.taskId);
|
|
275
271
|
const planView = ref !== null && ref.plan === "drafted" ? planViewOf(ref.id) : null;
|
|
276
|
-
const
|
|
277
|
-
const approvalDigest = approvalFormDigest(item.approval.digest, raceTerms?.raceDigest ?? null, planView?.sha256 ?? null);
|
|
272
|
+
const approvalDigest = approvalFormDigest(item.approval.digest, planView?.sha256 ?? null);
|
|
278
273
|
// The same agents block the task page and chat sign under (v48),
|
|
279
274
|
// and the same consent door: closed, it mints no nonce.
|
|
280
275
|
const route = routeViewOf(item.approval.taskId, ref, scope, now, who);
|
|
@@ -284,7 +279,6 @@ export function createTasksHandlers(runtime) {
|
|
|
284
279
|
planDocument: planView?.document ?? null,
|
|
285
280
|
planContract: ref === null || planView === null ? null : planContractViewOf(ref.id, scope),
|
|
286
281
|
approvalDigest,
|
|
287
|
-
raceTerms,
|
|
288
282
|
deliverable: ref?.deliverable ?? "branch",
|
|
289
283
|
route,
|
|
290
284
|
csrf, nonce, remaining: remaining.length, skipped: [...skipped], now,
|
|
@@ -292,7 +286,7 @@ export function createTasksHandlers(runtime) {
|
|
|
292
286
|
}
|
|
293
287
|
return sendScreen(response, 200, nextPage(chromeFor(project, "inbox"), {
|
|
294
288
|
item, scope: null, planDocument: null, planContract: null, csrf, nonce: "",
|
|
295
|
-
approvalDigest: null,
|
|
289
|
+
approvalDigest: null, route: null,
|
|
296
290
|
remaining: remaining.length, skipped: [...skipped], now,
|
|
297
291
|
}));
|
|
298
292
|
}
|
|
@@ -373,14 +367,8 @@ export function createTasksHandlers(runtime) {
|
|
|
373
367
|
repairChain,
|
|
374
368
|
});
|
|
375
369
|
});
|
|
376
|
-
|
|
377
|
-
|
|
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));
|
|
370
|
+
const laneCards = [...cards, ...unverifiedCards];
|
|
371
|
+
const body = boardBody({ cards: laneCards, done: verifiedDone, saturated: snapshot.saturated, now, all, project, delta }, pr => store.ciFailureObserved(pr));
|
|
384
372
|
if (url.searchParams.get("fragment") === "1") {
|
|
385
373
|
// The live region alone — the in-page swapper's diet. Same auth,
|
|
386
374
|
// same ceiling, no shell, no scripts (finding 2).
|
|
@@ -411,7 +399,7 @@ export function createTasksHandlers(runtime) {
|
|
|
411
399
|
// The review cockpit (Priority 5): every visible COMPLETED task,
|
|
412
400
|
// ranked by review priority, with one selected result projected
|
|
413
401
|
// from the records the task, run, and done pages already read —
|
|
414
|
-
// scope, plan, run, artifacts, verdict,
|
|
402
|
+
// scope, plan, run, artifacts, verdict, publication. The
|
|
415
403
|
// ranking is a labeled presentation aid: it never rewrites the
|
|
416
404
|
// stored verdict, and the sealed patch downloads exactly as stored.
|
|
417
405
|
// Admission binds BEFORE the SQL limit (the done page's own rule),
|
|
@@ -527,8 +515,8 @@ export function createTasksHandlers(runtime) {
|
|
|
527
515
|
const csrf = who.via === "cookie" ? who.session.csrf : "";
|
|
528
516
|
const revision = who.via === "cookie" ? who.session.projectRevision : 0;
|
|
529
517
|
// Column headers read one thing beyond the queue snapshot: live claims
|
|
530
|
-
// in THIS project, per worker (the capacity is global
|
|
531
|
-
//
|
|
518
|
+
// in THIS project, per worker (the capacity is global), so the header
|
|
519
|
+
// names both and never a ratio.
|
|
532
520
|
if (url.searchParams.get("fragment") === "1") {
|
|
533
521
|
return respond(response, 200, "text/html; charset=utf-8", queueRegionFor(project, csrf, revision, clock()));
|
|
534
522
|
}
|
|
@@ -722,26 +710,7 @@ export function createTasksHandlers(runtime) {
|
|
|
722
710
|
found.runner === options.localRunner &&
|
|
723
711
|
who.via === "cookie"
|
|
724
712
|
? { 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),
|
|
713
|
+
: null, url.searchParams.get("noted") !== null, structuredHandoffView(artifacts, evidenceRoot), proofBundleView(store, found, artifacts, evidenceRoot), store.runRoute(found.id), store.publicationForRun(found.id), reviewFactsFor(found.id),
|
|
745
714
|
// The shared result presentation (package 3) for a finished result.
|
|
746
715
|
!running && (found.outcome === "built" || found.outcome === "no-change")
|
|
747
716
|
? { detail: resultDetailOf(found, who, now), tab: parseResultTab(url.searchParams.get("tab")), user: who.name, requestToken: randomBytes(16).toString("hex") }
|
|
@@ -751,44 +720,9 @@ export function createTasksHandlers(runtime) {
|
|
|
751
720
|
if (runArtifact !== null) {
|
|
752
721
|
return runEvidence(response, Number(runArtifact[1]), Number(runArtifact[2]));
|
|
753
722
|
}
|
|
754
|
-
|
|
755
|
-
if (
|
|
756
|
-
return
|
|
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
|
-
}
|
|
723
|
+
// v115: routines became scheduled flows; an old link to the list or to one routine lands on the flows page.
|
|
724
|
+
if (url.pathname === "/routines" || /^\/routines\/[0-9]{1,15}$/.test(url.pathname))
|
|
725
|
+
return redirect(response, "/flows");
|
|
792
726
|
const one = /^\/d\/([0-9]{1,15})$/.exec(url.pathname);
|
|
793
727
|
if (one !== null) {
|
|
794
728
|
const decision = store.getDecision(Number(one[1]));
|
|
@@ -811,17 +745,6 @@ export function createTasksHandlers(runtime) {
|
|
|
811
745
|
}
|
|
812
746
|
async function post(ctx) {
|
|
813
747
|
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
748
|
if (url.pathname === '/code' || url.pathname.startsWith('/code/')) {
|
|
826
749
|
const body = readForm(posted, CONSOLE_FORMS.code);
|
|
827
750
|
const wantsJson = request.headers.accept?.includes('application/json') === true;
|
|
@@ -1060,7 +983,7 @@ export function createTasksHandlers(runtime) {
|
|
|
1060
983
|
};
|
|
1061
984
|
const moveReason = (reason) => reason === "stale"
|
|
1062
985
|
? "the queue moved underneath you — it just reloaded"
|
|
1063
|
-
: reason === "claimed"
|
|
986
|
+
: reason === "claimed"
|
|
1064
987
|
? "that task is being taken right now — it keeps its claim"
|
|
1065
988
|
: reason === "worker-retired"
|
|
1066
989
|
? "that worker is retired — drag its work elsewhere, or register the name again"
|
|
@@ -1098,11 +1021,11 @@ export function createTasksHandlers(runtime) {
|
|
|
1098
1021
|
const now = clock();
|
|
1099
1022
|
const snapshot = store.queueScoped(ref.repo, now);
|
|
1100
1023
|
const self = snapshot.find(one => one.id === taskId);
|
|
1101
|
-
// A claim
|
|
1024
|
+
// A claim can land after the form rendered WITHOUT
|
|
1102
1025
|
// bumping queueRevision, so the snapshot — not the form — decides
|
|
1103
1026
|
// whether the card is still free. A taken or vanished card is the
|
|
1104
1027
|
// typed refusal, never a silent no-op that would skip moveTask()'s
|
|
1105
|
-
// own claimed
|
|
1028
|
+
// own claimed recheck.
|
|
1106
1029
|
if (self === undefined)
|
|
1107
1030
|
return respondMove(409, moveReason("unknown-task"));
|
|
1108
1031
|
if (self.taken)
|
|
@@ -1173,87 +1096,6 @@ export function createTasksHandlers(runtime) {
|
|
|
1173
1096
|
}
|
|
1174
1097
|
return redirect(response, decisionBack);
|
|
1175
1098
|
}
|
|
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
1099
|
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
1100
|
if (act !== null && act.verb === "confirm-stopped") {
|
|
1259
1101
|
const body = readForm(posted, CONSOLE_FORMS.confirmStopped);
|
|
@@ -1312,79 +1154,6 @@ export function createTasksHandlers(runtime) {
|
|
|
1312
1154
|
if (act !== null) {
|
|
1313
1155
|
return taskMutation(response, who, act.taskId, act.verb, posted, now);
|
|
1314
1156
|
}
|
|
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
1157
|
const runNote = /^\/r\/([0-9]{1,15})\/note$/.exec(url.pathname);
|
|
1389
1158
|
if (runNote !== null) {
|
|
1390
1159
|
const body = readForm(posted, CONSOLE_FORMS.runNote);
|
|
@@ -1505,43 +1274,6 @@ export function createTasksHandlers(runtime) {
|
|
|
1505
1274
|
const back = body.get("return") ?? "/";
|
|
1506
1275
|
return redirect(response, /^\/[a-z0-9/_-]*$/i.test(back) ? back : "/");
|
|
1507
1276
|
}
|
|
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
1277
|
const revise = /^\/r\/([0-9]{1,15})\/revise$/.exec(url.pathname);
|
|
1546
1278
|
if (revise !== null) {
|
|
1547
1279
|
const body = readForm(posted, CONSOLE_FORMS.revise);
|
|
@@ -1806,259 +1538,7 @@ export function createTasksHandlers(runtime) {
|
|
|
1806
1538
|
peekBySession.set(sessionKey, left);
|
|
1807
1539
|
}
|
|
1808
1540
|
}
|
|
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
1541
|
// ---- 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
1542
|
function taskMutation(response, who, taskId, verb, posted, now) {
|
|
2063
1543
|
const ref = store.lookupRef(taskId);
|
|
2064
1544
|
if (ref === null || store.getTask(taskId) === null) {
|
|
@@ -2079,13 +1559,11 @@ export function createTasksHandlers(runtime) {
|
|
|
2079
1559
|
// The session IS the verified principal here — cookie + CSRF proved it.
|
|
2080
1560
|
const filed = store.fileSteerNote(taskId, verifiedAuthor(who.name), body.get("note") ?? "", now);
|
|
2081
1561
|
if (!filed.ok) {
|
|
2082
|
-
const said = filed.reason === "
|
|
2083
|
-
? "
|
|
2084
|
-
: filed.reason === "
|
|
2085
|
-
?
|
|
2086
|
-
:
|
|
2087
|
-
? (filed.problem ?? "that note will not store")
|
|
2088
|
-
: "no such task";
|
|
1562
|
+
const said = filed.reason === "task-finished"
|
|
1563
|
+
? "this task is finished — a note has no next attempt to reach"
|
|
1564
|
+
: filed.reason === "invalid-note"
|
|
1565
|
+
? (filed.problem ?? "that note will not store")
|
|
1566
|
+
: "no such task";
|
|
2089
1567
|
return taskScreen(response, who, taskId, said, 400);
|
|
2090
1568
|
}
|
|
2091
1569
|
return redirect(response, taskHref(taskId));
|
|
@@ -2107,7 +1585,7 @@ export function createTasksHandlers(runtime) {
|
|
|
2107
1585
|
// Chains are scheduling, not authority (chains-and-next review,
|
|
2108
1586
|
// finding 2): the edge decides WHEN the ready set admits the task;
|
|
2109
1587
|
// approval still decides WHAT may build. Both ends are re-proved
|
|
2110
|
-
// here — existence
|
|
1588
|
+
// here — existence and ceiling — and the
|
|
2111
1589
|
// cycle refusal comes from the store's own closure check.
|
|
2112
1590
|
const on = (body.get("on") ?? "").trim();
|
|
2113
1591
|
const blocker = on === "" ? null : store.getTask(on);
|
|
@@ -2115,9 +1593,6 @@ export function createTasksHandlers(runtime) {
|
|
|
2115
1593
|
if (blocker === null || blockerRef === null || !visible(blockerRef.repo)) {
|
|
2116
1594
|
return taskScreen(response, who, taskId, "that task to wait for does not exist here", 404);
|
|
2117
1595
|
}
|
|
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
1596
|
const added = store.addEdge(taskId, on);
|
|
2122
1597
|
if (!added.ok) {
|
|
2123
1598
|
return taskScreen(response, who, taskId, `could not make ${taskId} wait for ${on} — ${added.reason}`, 409);
|
|
@@ -2127,9 +1602,6 @@ export function createTasksHandlers(runtime) {
|
|
|
2127
1602
|
case "unblock": {
|
|
2128
1603
|
const body = readForm(posted, CONSOLE_FORMS.taskBlock);
|
|
2129
1604
|
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
1605
|
const removed = store.removeEdge(taskId, on);
|
|
2134
1606
|
if (!removed.ok) {
|
|
2135
1607
|
return taskScreen(response, who, taskId, `${taskId} was not waiting on ${on}`, 409);
|
|
@@ -2144,9 +1616,6 @@ export function createTasksHandlers(runtime) {
|
|
|
2144
1616
|
if (blockerTask === null || (blockerTask.state !== "failed" && blockerTask.state !== "cancelled")) {
|
|
2145
1617
|
return taskScreen(response, who, taskId, "what this task waits for changed — refresh the page and choose again", 409);
|
|
2146
1618
|
}
|
|
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
1619
|
if (operation === "retry") {
|
|
2151
1620
|
const blockerRef = store.lookupRef(blocker);
|
|
2152
1621
|
if (blockerTask.state !== "failed" || blockerRef === null || !visible(blockerRef.repo)) {
|
|
@@ -2194,9 +1663,7 @@ export function createTasksHandlers(runtime) {
|
|
|
2194
1663
|
? "only queued work can move up — this task is not waiting in the queue"
|
|
2195
1664
|
: moved.reason === "claimed"
|
|
2196
1665
|
? "this task is being built right now — it needs no place in line"
|
|
2197
|
-
:
|
|
2198
|
-
? "a tournament is running on this task — let it finish, then pick or abandon it"
|
|
2199
|
-
: "the queue rank could not be raised";
|
|
1666
|
+
: "the queue rank could not be raised";
|
|
2200
1667
|
return taskScreen(response, who, taskId, said, 409);
|
|
2201
1668
|
}
|
|
2202
1669
|
return redirect(response, taskHref(taskId));
|
|
@@ -2217,7 +1684,6 @@ export function createTasksHandlers(runtime) {
|
|
|
2217
1684
|
"not-latched": "the tracker never closed this — there is nothing to reopen",
|
|
2218
1685
|
"not-seen-open": "the tracker has not been seen open again since the close — reopen it there first; the next sync notices",
|
|
2219
1686
|
claimed: "this task is being built right now",
|
|
2220
|
-
"contest-open": "a tournament is open on this task — decide it first",
|
|
2221
1687
|
held: "a hold stands — lift it first",
|
|
2222
1688
|
"question-open": "an unanswered question stands — answer or close it first",
|
|
2223
1689
|
"incident-open": "an unresolved incident stands — resolve it first",
|
|
@@ -2414,16 +1880,15 @@ export function createTasksHandlers(runtime) {
|
|
|
2414
1880
|
}
|
|
2415
1881
|
case "route": {
|
|
2416
1882
|
const body = readForm(posted, CONSOLE_FORMS.taskRoute);
|
|
2417
|
-
// THE AGENTS EDIT (v47): an approver's session
|
|
1883
|
+
// THE AGENTS EDIT (v47): an approver's session sets the size,
|
|
2418
1884
|
// overrides one role to an exact agent, or clears an override — ONE
|
|
2419
1885
|
// authenticated transaction in the store that CAS-checks the scope
|
|
2420
1886
|
// digest the form rendered (empty = "I saw no scope"), records the
|
|
2421
1887
|
// change under this name, reconciles the planner pin, re-files the
|
|
2422
1888
|
// scope so a sealed route goes visibly stale, and re-requests a
|
|
2423
1889
|
// drafted plan when the planner changed. The viewer gate above
|
|
2424
|
-
// already refused a watcher; a live claim
|
|
1890
|
+
// already refused a watcher; a live claim refuses
|
|
2425
1891
|
// inside the transaction.
|
|
2426
|
-
const riskGiven = body.get("risk");
|
|
2427
1892
|
const sizeGiven = body.get("size");
|
|
2428
1893
|
const riskyGiven = body.getAll("risky");
|
|
2429
1894
|
const phaseGiven = body.get("phase");
|
|
@@ -2436,8 +1901,6 @@ export function createTasksHandlers(runtime) {
|
|
|
2436
1901
|
const providerGiven = agentGiven === "" ? body.get("provider") : agentGiven.slice(0, agentGiven.indexOf("|") === -1 ? agentGiven.length : agentGiven.indexOf("|"));
|
|
2437
1902
|
const modelGiven = agentGiven === "" ? (body.get("model") ?? "").trim() : agentGiven.indexOf("|") === -1 ? "" : agentGiven.slice(agentGiven.indexOf("|") + 1).trim();
|
|
2438
1903
|
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
1904
|
const phase = phaseGiven ?? clearGiven;
|
|
2442
1905
|
if (phase !== null && !ROUTE_PHASES.includes(phase))
|
|
2443
1906
|
return taskScreen(response, who, taskId, "the role is planner, builder, or revision", 400);
|
|
@@ -2450,7 +1913,7 @@ export function createTasksHandlers(runtime) {
|
|
|
2450
1913
|
size: sizeGiven !== null && isTaskSize(sizeGiven) ? sizeGiven : ref.sizing?.size ?? "medium",
|
|
2451
1914
|
risky: riskyGiven.includes("yes") ? true : riskyGiven.includes("no") ? false : ref.sizing?.risky ?? false,
|
|
2452
1915
|
};
|
|
2453
|
-
if (
|
|
1916
|
+
if (phase === null && sizeEdit === undefined)
|
|
2454
1917
|
return taskScreen(response, who, taskId, "nothing to change about the agents", 400);
|
|
2455
1918
|
if (phaseGiven !== null) {
|
|
2456
1919
|
if (providerGiven === null || !isProviderId(providerGiven))
|
|
@@ -2473,7 +1936,6 @@ export function createTasksHandlers(runtime) {
|
|
|
2473
1936
|
const account = store.accountOf(who.name);
|
|
2474
1937
|
return account !== null && account.revokedAt === null && account.role === "approver" ? { ok: true } : { ok: false, reason: "your login is no longer an approver" };
|
|
2475
1938
|
},
|
|
2476
|
-
...(riskGiven === null ? {} : { risk: riskGiven }),
|
|
2477
1939
|
// A person's size replaces the classifier's; the risky flag rides with it.
|
|
2478
1940
|
...(sizeEdit === undefined ? {} : { size: sizeEdit }),
|
|
2479
1941
|
...(phase === null
|
|
@@ -2532,75 +1994,17 @@ export function createTasksHandlers(runtime) {
|
|
|
2532
1994
|
if (Number.isNaN(exactBudget))
|
|
2533
1995
|
return taskScreen(response, who, taskId, "the dollar cap is a positive amount", 400);
|
|
2534
1996
|
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
1997
|
let saved;
|
|
2590
1998
|
try {
|
|
2591
1999
|
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
2000
|
// C1/M3: the signer's own credentialed filing auto-approves —
|
|
2596
2001
|
// coverage is asked INSIDE this transaction, the escalated
|
|
2597
2002
|
// default and budget ride the filing, and the seal commits
|
|
2598
|
-
// atomically with it.
|
|
2599
|
-
// comparison keeps its own human ceremony.
|
|
2003
|
+
// atomically with it.
|
|
2600
2004
|
// COOKIE ONLY (C1's channel table; surfaces round 1, finding 1):
|
|
2601
2005
|
// bearer credentials are for machines, and a machine road must
|
|
2602
2006
|
// never spend the signer's mode.
|
|
2603
|
-
const coverage = who.via === "cookie"
|
|
2007
|
+
const coverage = who.via === "cookie"
|
|
2604
2008
|
? modeFilingCoverage(store, ref.repo, who.name, now)
|
|
2605
2009
|
: null;
|
|
2606
2010
|
const before = store.getScope(taskId);
|
|
@@ -2653,37 +2057,6 @@ export function createTasksHandlers(runtime) {
|
|
|
2653
2057
|
}
|
|
2654
2058
|
}
|
|
2655
2059
|
}
|
|
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
2060
|
return { ok: true };
|
|
2688
2061
|
});
|
|
2689
2062
|
}
|
|
@@ -2737,9 +2110,8 @@ export function createTasksHandlers(runtime) {
|
|
|
2737
2110
|
const scopeRow = store.getScope(taskId);
|
|
2738
2111
|
if (scopeRow === null)
|
|
2739
2112
|
return approvalProblem("this task has no scope to approve", 409);
|
|
2740
|
-
const raceTerms = store.activeTournamentTerms(ref.id);
|
|
2741
2113
|
const planView = planViewOf(ref.id);
|
|
2742
|
-
const expectedDigest = approvalFormDigest(scopeRow.digest,
|
|
2114
|
+
const expectedDigest = approvalFormDigest(scopeRow.digest, planView?.sha256 ?? null);
|
|
2743
2115
|
if (digest !== expectedDigest) {
|
|
2744
2116
|
return approvalProblem("the scope or plan changed while this form was open — read the latest version and approve again", 409);
|
|
2745
2117
|
}
|
|
@@ -2754,29 +2126,6 @@ export function createTasksHandlers(runtime) {
|
|
|
2754
2126
|
return approvalProblem(`approval is blocked: ${view.problem}`, 409);
|
|
2755
2127
|
}
|
|
2756
2128
|
}
|
|
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
2129
|
const approved = approveScope(store, taskId, who.name, now, scopeRow.digest, token);
|
|
2781
2130
|
if (!approved.ok) {
|
|
2782
2131
|
// v102: a first yes on protected work is recorded, not refused — the page says who else is needed.
|
|
@@ -2889,14 +2238,13 @@ export function createTasksHandlers(runtime) {
|
|
|
2889
2238
|
const named = (body.get("run") ?? "").trim();
|
|
2890
2239
|
if (!/^[0-9]{1,15}$/.test(named))
|
|
2891
2240
|
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"
|
|
2241
|
+
const asked = requestTaskStop(store, { taskId, runId: Number(named), by: verifiedAuthor(who.name), via: "web" }, now);
|
|
2893
2242
|
if (!asked.ok) {
|
|
2894
2243
|
const words = {
|
|
2895
2244
|
"no-run": `there is no run #${escape(named)}`,
|
|
2896
2245
|
"wrong-task": `run #${escape(named)} is not one of this task's attempts`,
|
|
2897
2246
|
finished: `run #${escape(named)} already ended before the stop — nothing rewrites a finished attempt; reload to see the current state`,
|
|
2898
2247
|
"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
2248
|
publication: `run #${escape(named)} already admitted its publication — an external request in flight is not recalled by a stop`,
|
|
2901
2249
|
};
|
|
2902
2250
|
return taskScreen(response, who, taskId, words[asked.reason] ?? asked.detail, 409);
|
|
@@ -3076,8 +2424,8 @@ export function createTasksHandlers(runtime) {
|
|
|
3076
2424
|
function reviewCockpitViewOf(row, who, now) {
|
|
3077
2425
|
const ref = store.lookupRef(row.taskId);
|
|
3078
2426
|
const scope = store.getScope(row.taskId);
|
|
3079
|
-
// The result run, read once: the
|
|
3080
|
-
//
|
|
2427
|
+
// The result run, read once: the shared result detail below hangs off
|
|
2428
|
+
// this one record.
|
|
3081
2429
|
const run = row.runId === null ? null : store.getRun(row.runId);
|
|
3082
2430
|
const intent = scope === null
|
|
3083
2431
|
? null
|
|
@@ -3102,13 +2450,6 @@ export function createTasksHandlers(runtime) {
|
|
|
3102
2450
|
approach: parsed.ok ? oneLineOf(parsed.document.approach, 600) : null,
|
|
3103
2451
|
};
|
|
3104
2452
|
})();
|
|
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
2453
|
const base = {
|
|
3113
2454
|
taskId: row.taskId,
|
|
3114
2455
|
title: row.title,
|
|
@@ -3119,7 +2460,6 @@ export function createTasksHandlers(runtime) {
|
|
|
3119
2460
|
assignment: row.assignment ?? null,
|
|
3120
2461
|
intent,
|
|
3121
2462
|
plan,
|
|
3122
|
-
contest: contestOf(),
|
|
3123
2463
|
...(store.isDemo() ? { demo: true } : {}),
|
|
3124
2464
|
};
|
|
3125
2465
|
if (run === null) {
|
|
@@ -3202,32 +2542,6 @@ export function createTasksHandlers(runtime) {
|
|
|
3202
2542
|
});
|
|
3203
2543
|
response.end(read.content);
|
|
3204
2544
|
}
|
|
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
2545
|
/** The full guard list (v3 §4) — run on every request, hit or miss. */
|
|
3232
2546
|
function peekGuards(runId) {
|
|
3233
2547
|
if (options.localRunner === undefined || options.poolRoot === undefined) {
|
|
@@ -3290,215 +2604,6 @@ export function createTasksHandlers(runtime) {
|
|
|
3290
2604
|
}
|
|
3291
2605
|
return { ok: true, admit: { run, worktree: run.worktree, epoch: row.leaseEpoch, entries: snapshot } };
|
|
3292
2606
|
}
|
|
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
2607
|
/** The live subset of a bounded run page \u2014 one indexed lookup per row. */
|
|
3503
2608
|
function liveRunIds(rows) {
|
|
3504
2609
|
const live = new Set();
|
|
@@ -3524,23 +2629,15 @@ export function createTasksHandlers(runtime) {
|
|
|
3524
2629
|
{ id: "runs", domain: "tasks", stage: "console", method: "GET", handle: get },
|
|
3525
2630
|
{ id: "run.page", domain: "tasks", stage: "console", method: "GET", handle: get },
|
|
3526
2631
|
{ id: "run.evidence", domain: "tasks", stage: "console", method: "GET", handle: get },
|
|
3527
|
-
{ id: "contest.page", domain: "tasks", stage: "console", method: "GET", handle: get },
|
|
3528
2632
|
{ id: "routines", domain: "tasks", stage: "console", method: "GET", handle: get },
|
|
3529
2633
|
{ id: "routine.page", domain: "tasks", stage: "console", method: "GET", handle: get },
|
|
3530
2634
|
{ id: "decision.page", domain: "tasks", stage: "console", method: "GET", handle: get },
|
|
3531
2635
|
{ 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
2636
|
{ id: "code.act", domain: "tasks", stage: "console", method: "POST", handle: post },
|
|
3534
2637
|
{ id: "tasks.add", domain: "tasks", stage: "console", method: "POST", handle: post },
|
|
3535
2638
|
{ id: "queue.move", domain: "tasks", stage: "console", method: "POST", handle: post },
|
|
3536
2639
|
{ id: "queue.note", domain: "tasks", stage: "console", method: "POST", handle: post },
|
|
3537
2640
|
{ 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
2641
|
{ id: "task.act.hold", domain: "tasks", stage: "console", method: "POST", handle: post },
|
|
3545
2642
|
{ id: "task.act.unhold", domain: "tasks", stage: "console", method: "POST", handle: post },
|
|
3546
2643
|
{ id: "task.act.requeue", domain: "tasks", stage: "console", method: "POST", handle: post },
|
|
@@ -3564,16 +2661,9 @@ export function createTasksHandlers(runtime) {
|
|
|
3564
2661
|
{ id: "task.act.resume-arm", domain: "tasks", stage: "console", method: "POST", handle: post },
|
|
3565
2662
|
{ id: "task.act.resume", domain: "tasks", stage: "console", method: "POST", handle: post },
|
|
3566
2663
|
{ 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
2664
|
{ id: "run.act", domain: "tasks", stage: "console", method: "POST", handle: post },
|
|
3574
2665
|
{ 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
2666
|
{ id: "incident.resolve", domain: "tasks", stage: "console", method: "POST", handle: post },
|
|
3577
2667
|
];
|
|
3578
|
-
return { registrations, get, post, runListPane, peekFragment,
|
|
2668
|
+
return { registrations, get, post, runListPane, peekFragment, taskMutation, queueRegionFor, ciFailingFor, completedRowFor, attemptRowFor, resultRowOf, reviewCockpitViewOf, decisionEvidence, runEvidence, sendArtifact, peekGuards, liveRunIds };
|
|
3579
2669
|
}
|