toolroll 0.9.51 → 0.9.53
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 +20 -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/backup.d.ts +8 -0
- package/dist/backup.js +10 -1
- package/dist/board.d.ts +3 -19
- package/dist/board.js +4 -42
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +18 -18
- package/dist/browser-workspace.d.ts +3 -1
- package/dist/builder.d.ts +9 -40
- package/dist/builder.js +50 -257
- package/dist/chat-actions.js +3 -14
- package/dist/chat-controls.d.ts +0 -4
- package/dist/chat-controls.js +0 -1
- package/dist/chat-decide.js +1 -3
- package/dist/chat-delivery.d.ts +0 -2
- package/dist/chat-delivery.js +0 -1
- package/dist/chat-quiet.js +2 -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/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/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/store-json.d.ts +1 -25
- package/dist/contracts/store-json.js +3 -13
- 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/database-reclaim.d.ts +4 -0
- package/dist/database-reclaim.js +13 -0
- package/dist/demo.d.ts +1 -1
- package/dist/demo.js +17 -27
- package/dist/desktop-update-gate.js +0 -1
- package/dist/desktop-update.d.ts +1 -1
- package/dist/desktop-update.js +1 -3
- 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 +4 -4
- 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/live-bus.d.ts +3 -3
- package/dist/live-bus.js +3 -3
- 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 +10 -22
- package/dist/modes.d.ts +6 -7
- package/dist/modes.js +7 -13
- package/dist/monitoring.js +32 -8
- package/dist/notification-delivery.d.ts +19 -0
- package/dist/notification-delivery.js +37 -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 +283 -2378
- package/dist/phase-routing.d.ts +13 -23
- package/dist/phase-routing.js +13 -35
- package/dist/plan-auto.js +2 -2
- 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/process-recovery-settlement.js +1 -0
- package/dist/project-delete.js +3 -15
- 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/retention.js +6 -5
- 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 +16 -25
- 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 +4 -9
- package/dist/server/shared.d.ts +29 -212
- package/dist/server/shared.js +73 -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-chat.d.ts +0 -2
- package/dist/slack-chat.js +0 -1
- package/dist/store.d.ts +142 -1382
- package/dist/store.js +900 -4083
- package/dist/surface.js +10 -25
- 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/teammates-ui.js +1 -1
- package/dist/telegram-mate.d.ts +0 -3
- package/dist/telegram-mate.js +0 -1
- package/dist/templates.d.ts +11 -7
- package/dist/templates.js +12 -9
- package/dist/toolroll-update.d.ts +24 -3
- package/dist/toolroll-update.js +159 -16
- package/dist/webhooks.d.ts +10 -22
- package/dist/webhooks.js +43 -81
- package/dist/work-index.js +3 -7
- package/dist/workspace-revision.d.ts +28 -1
- package/dist/workspace-revision.js +276 -49
- package/dist/workspace-ui.js +1 -1
- package/package.json +1 -3
- 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/supervisor.mjs +0 -297
package/dist/server/shared.js
CHANGED
|
@@ -28,7 +28,6 @@ import {} from '../cli-http.js';
|
|
|
28
28
|
import { CODING_SHIPPING_CSS } from '../coding-shipping-ui.js';
|
|
29
29
|
import { CODING_CSS } from '../coding-ui.js';
|
|
30
30
|
import { CodingWorkspace } from '../coding-workspace.js';
|
|
31
|
-
import { contestNoun } from "../contest.js";
|
|
32
31
|
import {} from "../contracts/console-api.js";
|
|
33
32
|
import { connectionWords, hiddenFields } from "../control-ui.js";
|
|
34
33
|
import { isSubscriptionChatProvider, PRICED_MODELS } from "../converse.js";
|
|
@@ -61,7 +60,6 @@ import { ABOUT_YOU_LINE_MAX, ABOUT_YOU_MAX_LINES } from "../lead-about.js";
|
|
|
61
60
|
import { LEAD_CONTEXT_CSS } from '../lead-context.js';
|
|
62
61
|
import { LEAD_NAME_MAX, LEAD_PERSONA_MAX } from "../lead-identity.js";
|
|
63
62
|
import { LIMITS_CSS, limitsHtml } from "../limits-ui.js";
|
|
64
|
-
import { attendedLivenessState } from "../liveness.js";
|
|
65
63
|
import { PROPOSAL_WAIT_REASON, proposalActGate } from "../mate-doors.js";
|
|
66
64
|
import { MATE_MESSAGE_MAX_CHARS } from "../mate.js";
|
|
67
65
|
import { MOBILE_VIEWPORT_SCRIPT } from "../mobile-viewport.js";
|
|
@@ -73,17 +71,16 @@ import { cloneGithubRepo, listGithubRepos, previewGithubRepo } from "../onboard.
|
|
|
73
71
|
import {} from "../operate-remote.js";
|
|
74
72
|
import { parseBaseTreeSnapshot } from "../peek.js";
|
|
75
73
|
import { PEOPLE_AUDIT_CSS } from "../people-audit-ui.js";
|
|
76
|
-
import { agentsSummary, chosenWords,
|
|
74
|
+
import { agentsSummary, chosenWords, makesNoPlan, postureWords, projectRoute, PHASES as ROUTE_PHASES, TASK_SIZES } from "../phase-routing.js";
|
|
77
75
|
import { parseExecutionPlanDocument } from "../plan.js";
|
|
78
76
|
import {} from "../planner-source.js";
|
|
79
77
|
import { POLICY_CSS } from "../policy-ui.js";
|
|
80
|
-
import { PRICED_BUILD_MODELS } from "../pricing.js";
|
|
81
78
|
import { PROJECT_DELETE_CSS } from "../project-delete-ui.js";
|
|
82
79
|
import { projectName } from "../project.js";
|
|
83
80
|
import { coverageStateWords, coverageWords, dispatchStatusToken, GOAL_ASSESSMENT_PENDING, manualReviewCriterionOf, manualReviewOnly, passFraction, personCheckWords, plainReasonWords, reviewConflict, semanticCoverage } from "../proof.js";
|
|
84
81
|
import { providerName } from "../provider-auth.js";
|
|
85
82
|
import {} from "../provider-connection.js";
|
|
86
|
-
import {
|
|
83
|
+
import {} from "../provider.js";
|
|
87
84
|
import {} from '../pull-request-flow.js';
|
|
88
85
|
import { PULL_REQUEST_SETTINGS_CSS } from '../pull-request-ui.js';
|
|
89
86
|
import { qualityModeTitle } from "../quality.js";
|
|
@@ -98,9 +95,9 @@ import { evidenceHealthOf, evidenceProblemDetailsOf, evidenceResultStatusOf, evi
|
|
|
98
95
|
import { RESULT_SHOT_CHOICES } from "../result-shots.js";
|
|
99
96
|
import { RETENTION_CSS } from "../retention-ui.js";
|
|
100
97
|
import { findingWords } from "../review-switch.js";
|
|
101
|
-
import { describeSchedule, parseSchedule
|
|
98
|
+
import { describeSchedule, parseSchedule } from "../flow-schedule.js";
|
|
102
99
|
import { isAlive as runnerAlive } from "../runner.js";
|
|
103
|
-
import { acceptanceToLines, approvalOf,
|
|
100
|
+
import { acceptanceToLines, approvalOf, scopeAuthorityOf } from "../scope.js";
|
|
104
101
|
import { parseReport } from "../scout-report.js";
|
|
105
102
|
import { ASSISTANTS } from "../setup-guide.js";
|
|
106
103
|
import { SKILLS_CSS } from "../skills-ui.js";
|
|
@@ -711,7 +708,7 @@ export function approvalAmendmentHtml(view) {
|
|
|
711
708
|
* post to the same route, and the task page and chat card share it.
|
|
712
709
|
*/
|
|
713
710
|
export function approvalSheetHtml(input) {
|
|
714
|
-
const { scope, route
|
|
711
|
+
const { scope, route } = input;
|
|
715
712
|
// A machine-drafted repair appends its brief to the signed goal; the row
|
|
716
713
|
// shows the goal and one sentence says what the repair fixes. The whole
|
|
717
714
|
// signed goal stays in Details.
|
|
@@ -742,14 +739,12 @@ export function approvalSheetHtml(input) {
|
|
|
742
739
|
const legOf = (phase) => legs.find(one => one.phase === phase);
|
|
743
740
|
const who = [];
|
|
744
741
|
const builder = legOf("build");
|
|
745
|
-
if (
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
who.push(`Builder ${agentNameWords(scope.profile.provider, scope.profile.model)}`);
|
|
752
|
-
}
|
|
742
|
+
if (builder !== undefined)
|
|
743
|
+
who.push(`Builder ${agentNameWords(builder.provider, builder.model)}`);
|
|
744
|
+
else if (route?.legacy != null)
|
|
745
|
+
who.push(`Builder ${agentNameWords(route.legacy.provider, route.legacy.model)}`);
|
|
746
|
+
else if (scope.profile != null)
|
|
747
|
+
who.push(`Builder ${agentNameWords(scope.profile.provider, scope.profile.model)}`);
|
|
753
748
|
const planner = legOf("plan");
|
|
754
749
|
// A small change makes no plan: no planner is named (a person's chosen planner still is).
|
|
755
750
|
const sized = route?.projection?.size ?? null;
|
|
@@ -762,26 +757,17 @@ export function approvalSheetHtml(input) {
|
|
|
762
757
|
const allowed = permissionPlainWords(scope.profile);
|
|
763
758
|
if (allowed !== null)
|
|
764
759
|
allowing.push(allowed);
|
|
765
|
-
|
|
766
|
-
allowing.push(scope.budgetMicrousd === null ? "no attempt limit" : `up to ${money(scope.budgetMicrousd)} per attempt`);
|
|
760
|
+
allowing.push(scope.budgetMicrousd === null ? "no attempt limit" : `up to ${money(scope.budgetMicrousd)} per attempt`);
|
|
767
761
|
const earlier = earlierVersionsWords(input.earlier.active, input.earlier.running);
|
|
768
762
|
if (earlier !== null)
|
|
769
763
|
allowing.push(earlier);
|
|
770
764
|
const yoursToCheck = scope.acceptance.filter(one => one.evidence.includes("manual-review")).map(one => one.statement.trim().replace(/[.\s]+$/, ""));
|
|
771
765
|
const consent = (allowing.length === 0 ? "" : `<p class="approval-allowing" data-approval-allowing>You’re allowing: ${allowing.map(escape).join(" · ")}</p>`) +
|
|
772
766
|
(yoursToCheck.length === 0 ? "" : `<p class="approval-you-check" data-approval-you-check>You’ll check: ${yoursToCheck.map(escape).join("; ")}</p>`);
|
|
773
|
-
const race = raceTerms === null
|
|
774
|
-
? ""
|
|
775
|
-
: `<p class="approval-race">${raceTerms.n} agents build this separately: ${raceTerms.agents.map(one => escape(agentNameWords(one.provider, one.model))).join(" and ")}. ` +
|
|
776
|
-
(raceTerms.kind === "comparison"
|
|
777
|
-
? "No dollar cap; you compare the results and pick one.</p>"
|
|
778
|
-
: `Each may spend ${money(raceTerms.perAgentBudgetMicrousd)} plus a ${money(raceTerms.overrunReserveMicrousd)} reserve, ${money(raceTerms.totalBudgetMicrousd)} in all; you compare the results and pick one.</p>`);
|
|
779
767
|
const after = input.deliverable === "report"
|
|
780
768
|
? "An agent investigates without changing the repository. You'll hear when its report is ready."
|
|
781
|
-
:
|
|
782
|
-
|
|
783
|
-
: "An agent starts in its own branch. You'll hear when it's ready to review.";
|
|
784
|
-
const submit = raceTerms === null ? "Approve & start" : raceTerms.kind === "comparison" ? "Approve comparison" : "Approve tournament";
|
|
769
|
+
: "An agent starts in its own branch. You'll hear when it's ready to review.";
|
|
770
|
+
const submit = "Approve & start";
|
|
785
771
|
// Everything else the digest binds, one tap away.
|
|
786
772
|
const detail = (title, body, attrs = "") => body === "" ? "" : `<section class="approval-detail"${attrs}><h3>${title}</h3>${body}</section>`;
|
|
787
773
|
const revisionDetail = input.revision === null ? "" : detail("Earlier build", `<div class="revision-card" data-revision-feedback>` +
|
|
@@ -802,7 +788,7 @@ export function approvalSheetHtml(input) {
|
|
|
802
788
|
: projection === null
|
|
803
789
|
? route.legacy === null ? "" : `<p>${escape(agentsSummaryWords(route))}</p>`
|
|
804
790
|
: `<p>${escape(projection.summary)}</p>` +
|
|
805
|
-
`<p class="meta"
|
|
791
|
+
`<p class="meta">Uses ${escape(projection.postureWords)}.</p>` +
|
|
806
792
|
(projection.demands.length === 0 ? "" : `<ul class="meta">${projection.demands.map(one => `<li>${escape(one)}</li>`).join("")}</ul>`) +
|
|
807
793
|
`<dl class="approval-roles">${projection.legs.map(leg => `<div><dt>${escape(ROLE_NOUN[leg.phase])}</dt><dd><span class="mono">${escape(leg.provider)} · ${escape(leg.model)}</span> <span class="meta">${escape(chosenWords(leg))}</span>` +
|
|
808
794
|
`<ul>${leg.reasons.map(reason => `<li>${escape(reason)}</li>`).join("")}${leg.problem === null ? "" : `<li><strong>${escape(leg.problem)}</strong></li>`}</ul></dd></div>`).join("")}</dl>` +
|
|
@@ -884,7 +870,6 @@ export function approvalSheetHtml(input) {
|
|
|
884
870
|
row("Done when", doneWhen) +
|
|
885
871
|
`</dl>` +
|
|
886
872
|
approvalAmendmentHtml(contract) +
|
|
887
|
-
race +
|
|
888
873
|
// The size, said once and plainly beside who builds: "Small change: fast model, no plan".
|
|
889
874
|
(projection === null ? "" : sizeLineHtml(projection)) +
|
|
890
875
|
(who.length === 0 ? "" : `<p class="approval-who">${who.map(escape).join(" · ")}</p>`) +
|
|
@@ -1013,17 +998,7 @@ export function profileWords(scope) {
|
|
|
1013
998
|
: profile.provider === "gemini"
|
|
1014
999
|
? `<p class="meta">Runs on <span class="mono">gemini · ${escape(profile.model)}</span> — ${profile.approvalArgv === "yolo" ? "Full access via --approval-mode yolo; every tool auto-approved" : "Auto via --approval-mode auto_edit; edits auto-approved, other tools refused"}, no turn limit (${Math.round(profile.timeoutSeconds / 60)} min ${profile.timeoutKind === "idle" ? "without progress" : "per attempt"}), spend reported in tokens only; repairs on ${escape(repair)}, ${Math.round(profile.repairTimeoutSeconds / 60)} min</p>`
|
|
1015
1000
|
: `<p class="meta">Runs on <span class="mono">${escape(profile.provider)} · ${escape(profile.model)}</span> — ${profile.sandboxMode === "danger-full-access" ? "FULL permissions via --dangerously-bypass-approvals-and-sandbox; nothing asks" : "workspace-write sandbox"}, no turn limit (${Math.round(profile.timeoutSeconds / 60)} min ${profile.timeoutKind === "idle" ? "without progress" : "per attempt"}); repairs on ${escape(repair)}, ${Math.round(profile.repairTimeoutSeconds / 60)} min</p>`;
|
|
1016
|
-
|
|
1017
|
-
// finding 2): the digest binds the whole chain, so the password form —
|
|
1018
|
-
// task page and /next alike — states every entry, credential included.
|
|
1019
|
-
const chain = chainFromJson(scope.proposedChainJson ?? null);
|
|
1020
|
-
const chainLine = chain === null || chain.length < 2
|
|
1021
|
-
? ""
|
|
1022
|
-
: `<p class="meta">If its subscription runs out: ${chain
|
|
1023
|
-
.slice(1)
|
|
1024
|
-
.map(one => `falls back to <span class="mono">${escape(one.profile.provider)} · ${escape(one.profile.model)}</span> — ${one.authMode === "api-key" ? "your API key; spend moves to that account" : "its subscription login"}`)
|
|
1025
|
-
.join("; ")}</p>`;
|
|
1026
|
-
return base + chainLine;
|
|
1001
|
+
return base;
|
|
1027
1002
|
}
|
|
1028
1003
|
export const ROLE_NOUN = { plan: "Planner", build: "Builder", repair: "Repair", review: "Reviewer" };
|
|
1029
1004
|
export function agentsStandingWords(view) {
|
|
@@ -1060,16 +1035,15 @@ export function agentsSummaryWords(view) {
|
|
|
1060
1035
|
return `${view.legacy.provider} · ${view.legacy.model} builds and repairs (repair model ${view.legacy.repairModel}); the planner and reviewer come from configuration at run time`;
|
|
1061
1036
|
return view.problem ?? "the agents cannot be read";
|
|
1062
1037
|
}
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
return `${projection.riskTitle}: ${consequence}`;
|
|
1038
|
+
/** A risk badge only when there is something to say: a risky change, or an
|
|
1039
|
+
* older route sealed at elevated or high risk. */
|
|
1040
|
+
export function riskBadgeHtml(projection) {
|
|
1041
|
+
return projection.riskTitle === "Routine" ? "" : `<span class="badge">${escape(sentenceCase(projection.riskTitle))}</span>`;
|
|
1068
1042
|
}
|
|
1069
|
-
/** Neutral metadata badges: risk, posture, and standing. */
|
|
1043
|
+
/** Neutral metadata badges: risk (when not routine), posture, and standing. */
|
|
1070
1044
|
export function agentsBadgesHtml(view) {
|
|
1071
1045
|
const p = view.projection;
|
|
1072
|
-
return (
|
|
1046
|
+
return ((p === null ? "" : riskBadgeHtml(p)) +
|
|
1073
1047
|
(p === null ? "" : `<span class="badge">${escape(sentenceCase(p.postureWords))}</span>`) +
|
|
1074
1048
|
`<span class="badge">${escape(sentenceCase(agentsStandingWords(view)))}</span>`);
|
|
1075
1049
|
}
|
|
@@ -1118,39 +1092,12 @@ export function agentsCeremonyHtml(view) {
|
|
|
1118
1092
|
return (`<div class="agents-ceremony"><p class="approval-label">agents</p>` +
|
|
1119
1093
|
sizeLineHtml(view.projection) +
|
|
1120
1094
|
`<p class="agents-summary">${escape(view.projection.summary)}</p>` +
|
|
1121
|
-
`<div class="agents-badges"
|
|
1122
|
-
`<p class="meta agents-risk-line">${escape(agentsRiskWords(view.projection))}.</p>` +
|
|
1095
|
+
`<div class="agents-badges">${riskBadgeHtml(view.projection)}<span class="badge">${escape(sentenceCase(view.projection.postureWords))}</span></div>` +
|
|
1123
1096
|
agentsWhyHtml(view.projection) +
|
|
1124
1097
|
`<p class="meta">These exact agents are part of what you approve; changing any of them asks for a fresh approval.</p></div>`);
|
|
1125
1098
|
}
|
|
1126
|
-
/** A standing order's agents (v48): the four-role route its approval
|
|
1127
|
-
* froze (or will freeze), in the same concise block a task's ceremony
|
|
1128
|
-
* shows — a firing can never be re-routed by a later configuration
|
|
1129
|
-
* change, and the page says so. */
|
|
1130
|
-
export function routineAgentsHtml(routine, approved, ceremony = false) {
|
|
1131
|
-
const agents = routineAgentsState(routine);
|
|
1132
|
-
const route = (approved ? routine.approvedRoute : routine.route) ?? null;
|
|
1133
|
-
if (route === null || agents.state !== "frozen" && agents.state !== "pending") {
|
|
1134
|
-
// The closed door, with its one road: no agents to restate means no
|
|
1135
|
-
// yes to give — the words say which fact closed it and what opens it.
|
|
1136
|
-
const words = agents.state === "unreadable"
|
|
1137
|
-
? `the agents on file cannot be read back (${agents.problem ?? "corrupt snapshot"}) — refresh the agents from today's configuration, read them, and approve this standing order again; until then nothing fires`
|
|
1138
|
-
: agents.state === "unfrozen"
|
|
1139
|
-
? "approved before agents were frozen — refresh the agents, read the exact planner, builder, repair, and reviewer it then names, and approve this standing order again; until then nothing fires"
|
|
1140
|
-
: agents.state === "unverified"
|
|
1141
|
-
? `the frozen agents do not verify (${agents.problem ?? "the approved snapshot is not whole"}) — refresh the agents from today's configuration, read them, and approve this standing order again; until then nothing fires`
|
|
1142
|
-
: `not resolved — ${agents.problem ?? "this standing order does not name an exact agent for every role"}; configure the project's agents, then refresh and approve again`;
|
|
1143
|
-
return `<div class="agents-ceremony agents-closed"><p class="approval-label">agents</p><p class="agents-summary">${escape(words)}</p></div>`;
|
|
1144
|
-
}
|
|
1145
|
-
const projection = projectRoute(route, () => null);
|
|
1146
|
-
return (`<div class="agents-ceremony"><p class="approval-label">agents</p>` +
|
|
1147
|
-
`<p class="agents-summary">${escape(agentsSummary(route))}</p>` +
|
|
1148
|
-
`<div class="agents-badges"><span class="badge">${escape(sentenceCase(projection.riskTitle))}</span><span class="badge">${escape(sentenceCase(postureWords(route)))}</span><span class="badge">${approved ? "frozen by the approval" : "frozen when you approve"}</span></div>` +
|
|
1149
|
-
(ceremony ? agentsWhyHtml(projection) : "") +
|
|
1150
|
-
`<p class="meta">${approved ? "Every firing runs on exactly these agents; a configuration change cannot re-route it." : "Approving freezes exactly these agents for every firing; a configuration change afterwards cannot re-route one."}</p></div>`);
|
|
1151
|
-
}
|
|
1152
1099
|
/** The runtime limits the sealed profile binds — permissions, turn and
|
|
1153
|
-
* time bounds, repairs
|
|
1100
|
+
* time bounds, repairs — restated on the ceremony as
|
|
1154
1101
|
* a CLOSED disclosure (v48): a term the yes covers, one tap away, never a
|
|
1155
1102
|
* wall of switches between the reader and the password. An unresolved
|
|
1156
1103
|
* profile still speaks in the open: that is a refusal, not a detail. */
|
|
@@ -1178,11 +1125,10 @@ export function agentsCardHtml(taskId, view, csrf, canEdit) {
|
|
|
1178
1125
|
(canEdit && view.editable ? `<form method="post" action="${taskHref(taskId)}/route" class="agents-clear">${hidden}<input type="hidden" name="clear-phase" value="${escape(one.phase)}"><button type="submit" class="secondary" aria-label="clear the ${escape(ROLE_NOUN[one.phase].toLowerCase())} choice">Clear</button></form>` : "") +
|
|
1179
1126
|
`</li>`)
|
|
1180
1127
|
.join("")}</ul>`;
|
|
1181
|
-
// The controls (v48): a
|
|
1128
|
+
// The controls (v48): a size choice that says what each size does, and
|
|
1182
1129
|
// — per role — ONLY the configured, role-valid agents the operator may
|
|
1183
1130
|
// pick from. Nothing is typed free-hand, no command line is quoted; the
|
|
1184
1131
|
// reasons above already say why the current agents were chosen.
|
|
1185
|
-
const riskGuide = `<dl class="agents-risk-guide">${RISK_CHOICES.map(one => `<div><dt>${escape(one.title)}</dt><dd>${escape(one.consequence)}</dd></div>`).join("")}</dl>`;
|
|
1186
1132
|
const roleForms = ROUTE_PHASES.filter(phase => phase !== "review").map(phase => {
|
|
1187
1133
|
// Selectable choices are the role's own configured agents; a current
|
|
1188
1134
|
// agent the configuration no longer names is DISPLAY-ONLY — said
|
|
@@ -1204,15 +1150,11 @@ export function agentsCardHtml(taskId, view, csrf, canEdit) {
|
|
|
1204
1150
|
: !view.editable
|
|
1205
1151
|
? `<p class="meta">${escape(view.editableWhy ?? "the agents cannot change right now")}</p>`
|
|
1206
1152
|
: `<details class="agents-change"><summary>Change agents</summary>` +
|
|
1207
|
-
`<form method="post" action="${taskHref(taskId)}/route" class="agents-form-risk">${hidden}` +
|
|
1208
|
-
`<label>Risk<select name="risk" aria-label="declared risk">${RISK_LEVELS.map(one => `<option value="${one}"${one === view.riskLevel ? " selected" : ""}>${escape(riskTitle(one))}</option>`).join("")}</select></label>` +
|
|
1209
|
-
`<button type="submit" class="secondary">Set risk</button></form>` +
|
|
1210
1153
|
`<form method="post" action="${taskHref(taskId)}/route" class="agents-form-risk">${hidden}` +
|
|
1211
1154
|
`<label>Size<select name="size" aria-label="task size">${TASK_SIZES.map(one => `<option value="${one}"${one === (p?.size?.size ?? "medium") ? " selected" : ""}>${escape(sizeConsequence(one, false))}</option>`).join("")}</select></label>` +
|
|
1212
1155
|
// The hidden "no" says the form showed the box: unticked means not risky; a request without either keeps the flag.
|
|
1213
1156
|
`<input type="hidden" name="risky" value="no"><label class="agents-risky"><input type="checkbox" name="risky" value="yes"${p?.size?.risky === true ? " checked" : ""}> Risky</label>` +
|
|
1214
1157
|
`<button type="submit" class="secondary">Set size</button></form>` +
|
|
1215
|
-
riskGuide +
|
|
1216
1158
|
`<div class="agents-role-forms">${roleForms.join("")}</div>` +
|
|
1217
1159
|
`<p class="meta">Only agents you have configured are offered; each choice is recorded as you. ${view.replanOnPlanChange ? "Changing the planner asks for a new plan — the drafted one is not relabeled. " : ""}An approval given under the earlier agents needs renewing.</p>` +
|
|
1218
1160
|
overrides +
|
|
@@ -1986,8 +1928,6 @@ ${THEME_DARK}
|
|
|
1986
1928
|
.filters strong { background: var(--muted); color: var(--foreground); }
|
|
1987
1929
|
@media (hover: hover) and (pointer: fine) { .filters a:hover { color: var(--foreground); } }
|
|
1988
1930
|
|
|
1989
|
-
/* A picked tournament result: marked in the built green, not magenta. */
|
|
1990
|
-
.card.picked { border-color: color-mix(in srgb, var(--success) 40%, var(--border)); }
|
|
1991
1931
|
.seal {
|
|
1992
1932
|
display: inline-block; font-family: var(--font-mono); font-size: .75rem;
|
|
1993
1933
|
background: var(--muted); border: 1px solid var(--border);
|
|
@@ -2483,13 +2423,8 @@ ${THEME_DARK}
|
|
|
2483
2423
|
.agents-clear button { padding-inline: .8rem; font-size: .72rem; }
|
|
2484
2424
|
.agents-ceremony { margin: .5rem 0 0; }
|
|
2485
2425
|
.agents-ceremony .agents-badges { margin-top: .35rem; }
|
|
2486
|
-
.agents-risk-line { margin: .4rem 0 0; }
|
|
2487
2426
|
.agents-runtime { margin-top: .5rem; }
|
|
2488
2427
|
.agents-runtime > summary { display: flex; align-items: center; min-height: 2.75rem; padding: .35rem .2rem; cursor: pointer; font-size: .8rem; font-weight: 600; }
|
|
2489
|
-
.agents-risk-guide { display: grid; gap: .3rem; margin: .5rem 0 0; font-size: .76rem; line-height: 1.45; }
|
|
2490
|
-
.agents-risk-guide div { display: grid; grid-template-columns: 6rem minmax(0, 1fr); gap: .5rem; }
|
|
2491
|
-
.agents-risk-guide dt { color: var(--muted-foreground); font: 500 .66rem/1.8 var(--font-mono); letter-spacing: .06em; text-transform: uppercase; }
|
|
2492
|
-
.agents-risk-guide dd { margin: 0; color: var(--muted-foreground); overflow-wrap: anywhere; }
|
|
2493
2428
|
.agents-role-forms { display: grid; gap: .45rem; margin-top: .6rem; }
|
|
2494
2429
|
.agents-role-row { display: grid; grid-template-columns: 6rem minmax(0, 1fr); gap: .5rem; align-items: center; }
|
|
2495
2430
|
.agents-role-name { color: var(--muted-foreground); font: 500 .66rem/1.8 var(--font-mono); letter-spacing: .06em; text-transform: uppercase; }
|
|
@@ -2517,7 +2452,7 @@ ${THEME_DARK}
|
|
|
2517
2452
|
.approval-sheet { display: grid; gap: 16px; margin: 0; padding: 20px; border: 1px solid var(--border); border-radius: 10px; background: var(--card); }
|
|
2518
2453
|
.approval-sheet.approve-form > :not(input) { margin: 0; }
|
|
2519
2454
|
.approval-sheet-title { margin: 0; font-size: 15px; font-weight: 600; line-height: 1.4; letter-spacing: -.01em; }
|
|
2520
|
-
.approval-revision, .approval-note
|
|
2455
|
+
.approval-revision, .approval-note { max-width: 75ch; font-size: 14px; line-height: 1.6; overflow-wrap: anywhere; }
|
|
2521
2456
|
.approval-note { color: var(--muted-foreground); }
|
|
2522
2457
|
.approval-rows { display: grid; gap: 14px; margin: 0; }
|
|
2523
2458
|
.approval-row { display: grid; grid-template-columns: 7.5rem minmax(0, 1fr); gap: 16px; }
|
|
@@ -2686,7 +2621,6 @@ ${THEME_DARK}
|
|
|
2686
2621
|
}
|
|
2687
2622
|
details.section[open] > summary::after { transform: rotate(225deg) translateY(-.125rem); }
|
|
2688
2623
|
@media (hover: hover) and (pointer: fine) { details.section > summary:hover h2 { color: var(--foreground); } }
|
|
2689
|
-
details.more-agents { margin: .75rem 0 0; }
|
|
2690
2624
|
.task-layout > .task-main { min-width: 0; }
|
|
2691
2625
|
.task-rail { position: sticky; top: 1rem; }
|
|
2692
2626
|
.task-rail .card { margin-top: .75rem; }
|
|
@@ -3812,7 +3746,7 @@ ${THEME_DARK}
|
|
|
3812
3746
|
.task-options-grid .wide, .task-options-grid .permission-field { grid-column: auto; }
|
|
3813
3747
|
.agents-form { grid-template-columns: 1fr; }
|
|
3814
3748
|
.agents-roles { grid-template-columns: 1fr; }
|
|
3815
|
-
.agents-
|
|
3749
|
+
.agents-role-row { grid-template-columns: 1fr; }
|
|
3816
3750
|
.approval-card { padding: 1rem; }
|
|
3817
3751
|
/* The orientation block says the wait on a phone; the kicker would say it twice. */
|
|
3818
3752
|
.approval-card .approval-kicker { display: none; }
|
|
@@ -4059,19 +3993,6 @@ button { min-height: 44px; }
|
|
|
4059
3993
|
}
|
|
4060
3994
|
}
|
|
4061
3995
|
|
|
4062
|
-
/* The tournament comparison: an at-a-glance table and side-by-side cards. */
|
|
4063
|
-
.scroll-x { overflow-x: auto; max-width: 100%; }
|
|
4064
|
-
.contest-glance { border-collapse: collapse; font-size: .8125rem; min-width: 34rem; margin: .75rem 0; }
|
|
4065
|
-
.contest-glance th, .contest-glance td { text-align: left; padding: .375rem .75rem .375rem 0; vertical-align: top; border-bottom: 1px solid var(--border); }
|
|
4066
|
-
.contest-glance th { font-weight: 600; }
|
|
4067
|
-
.contest-glance tr:last-child td { border-bottom: none; }
|
|
4068
|
-
.contest-compare { display: grid; grid-template-columns: 1fr; gap: .75rem; align-items: start; }
|
|
4069
|
-
@media (min-width: 1100px) {
|
|
4070
|
-
.content > main:has(.contest-compare) { max-width: none; }
|
|
4071
|
-
.contest-compare { grid-template-columns: repeat(auto-fit, minmax(24rem, 1fr)); }
|
|
4072
|
-
}
|
|
4073
|
-
.contest-compare .card { margin: 0; }
|
|
4074
|
-
|
|
4075
3996
|
/* The per-file comment button: a small real button beside a diff row. */
|
|
4076
3997
|
button.pick-file { min-height: 1.75rem; padding: 0 .55rem; font-size: .75rem; }
|
|
4077
3998
|
|
|
@@ -4221,22 +4142,11 @@ export function transcriptScript(path, elementId = "live-transcript") {
|
|
|
4221
4142
|
`function later(){setTimeout(go,2000);}go();})();`);
|
|
4222
4143
|
}
|
|
4223
4144
|
/**
|
|
4224
|
-
* The
|
|
4145
|
+
* The chrome layer: the jump palette and elapsed tickers. Pure
|
|
4225
4146
|
* navigation — no key ever posts, so the palette cannot approve anything;
|
|
4226
4147
|
* ceremonies stay POST + password + CSRF, untouched. Reads its index from
|
|
4227
4148
|
* a non-executable JSON script tag rendered by the same authorized page.
|
|
4228
4149
|
*/
|
|
4229
|
-
/** The attended beat (v28): parameterless, cookie + same-origin proven
|
|
4230
|
-
* server-side, renewal-only — any console page keeps the signed-in
|
|
4231
|
-
* approver's own sessions live; a hidden tab pauses honestly. Cheap
|
|
4232
|
-
* no-op when nothing is open. Shipped ALONE on sensitive pages. */
|
|
4233
|
-
export function beatScript(enabled = true) {
|
|
4234
|
-
if (!enabled)
|
|
4235
|
-
return "";
|
|
4236
|
-
return (`(function(){var beat=function(){if(document.hidden)return;` +
|
|
4237
|
-
`fetch("/session/attended-beats",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:""}).catch(function(){});};` +
|
|
4238
|
-
`beat();setInterval(beat,15000);})();`);
|
|
4239
|
-
}
|
|
4240
4150
|
/** The rail's presentation-only state is safe beside a password ceremony:
|
|
4241
4151
|
* it never reads a field or sends a request, and keeps the chat collapsible
|
|
4242
4152
|
* from its very first screen while the palette and global keys remain absent. */
|
|
@@ -4250,9 +4160,8 @@ export function sidebarScript() {
|
|
|
4250
4160
|
`setSide(sideCollapsed);sideToggle.addEventListener("click",function(){var next=!app.classList.contains("sidebar-collapsed");setSide(next);` +
|
|
4251
4161
|
`try{localStorage.setItem("standing-orders:sidebar-collapsed",next?"1":"0");}catch(e){}});}})();`);
|
|
4252
4162
|
}
|
|
4253
|
-
export function chromeScript(
|
|
4254
|
-
return (
|
|
4255
|
-
sidebarScript() +
|
|
4163
|
+
export function chromeScript() {
|
|
4164
|
+
return (sidebarScript() +
|
|
4256
4165
|
`(function(){` +
|
|
4257
4166
|
// The app-icon badge (Phase 2E): the page's server-rendered waiting
|
|
4258
4167
|
// count is authoritative over any stale push — synced on every chrome
|
|
@@ -4338,7 +4247,7 @@ export function chromeScript(beats = true) {
|
|
|
4338
4247
|
`if(ev.key==="?"){toggleHelp();ev.preventDefault();return;}` +
|
|
4339
4248
|
`if(ev.key==="j"||ev.key==="k"){rove(ev.key==="j"?1:-1,ev);return;}` +
|
|
4340
4249
|
`if(pending==="g"){pending=null;` +
|
|
4341
|
-
`var map={b:"/board",i:"/",w:"/workbench",
|
|
4250
|
+
`var map={b:"/board",i:"/",w:"/workbench",d:"/done",q:"/board?view=order",f:"/fleet",t:"/tasks",a:"/activity",p:"/projects"};` +
|
|
4342
4251
|
`if(map[ev.key]){go(map[ev.key]);ev.preventDefault();}return;}` +
|
|
4343
4252
|
`if(ev.key==="g"){pending="g";setTimeout(function(){pending=null;},800);}});` +
|
|
4344
4253
|
`})();`);
|
|
@@ -4376,7 +4285,6 @@ export const KBD_HELP = `<div class="kbd-help" hidden role="dialog" aria-label="
|
|
|
4376
4285
|
`<tr><td><kbd>g</kbd> then <kbd>q</kbd></td><td>go to the queue</td></tr>` +
|
|
4377
4286
|
`<tr><td><kbd>g</kbd> then <kbd>f</kbd></td><td>go to the fleet</td></tr>` +
|
|
4378
4287
|
`<tr><td><kbd>g</kbd> then <kbd>w</kbd></td><td>go to the workbench</td></tr>` +
|
|
4379
|
-
`<tr><td><kbd>g</kbd> then <kbd>r</kbd></td><td>go to the routines</td></tr>` +
|
|
4380
4288
|
`<tr><td><kbd>g</kbd> then <kbd>t</kbd></td><td>go to the task list</td></tr>` +
|
|
4381
4289
|
`<tr><td><kbd>g</kbd> then <kbd>a</kbd></td><td>go to the activity view</td></tr>` +
|
|
4382
4290
|
`<tr><td><kbd>g</kbd> then <kbd>p</kbd></td><td>go to the projects</td></tr>` +
|
|
@@ -4903,9 +4811,8 @@ export function systemPage(chrome, data) {
|
|
|
4903
4811
|
const dot = age < 5 * 60_000 ? "dot-ok" : age < 60 * 60_000 ? "dot-warn" : "dot-off";
|
|
4904
4812
|
const said = age < 5 * 60_000 ? "alive" : age < 60 * 60_000 ? `quiet ${Math.round(age / 60_000)}m` : "not heard from";
|
|
4905
4813
|
const busy = data.building.filter(claim => claim.runner === one.name).length;
|
|
4906
|
-
const sessions = data.heldSessions?.get(one.name) ?? 0;
|
|
4907
4814
|
return (`<div class="stat-card"><span class="k"><span class="dot ${dot}"></span>${escape(one.name)}</span>` +
|
|
4908
|
-
`<span class="v">builder \u00b7 ${said} \u00b7 ${busy}/${one.capacity} building
|
|
4815
|
+
`<span class="v">builder \u00b7 ${said} \u00b7 ${busy}/${one.capacity} building</span></div>`);
|
|
4909
4816
|
});
|
|
4910
4817
|
const worktreeCards = data.worktrees.map(tree => {
|
|
4911
4818
|
const leased = tree.leasedAt !== null && tree.releasedAt === null;
|
|
@@ -4948,7 +4855,7 @@ export function systemPage(chrome, data) {
|
|
|
4948
4855
|
const agentsCard = `<h2>Agents</h2>` +
|
|
4949
4856
|
`<p class="meta">Which AI provider runs each phase — changed from the terminal with your credentials (<code>toolroll config</code>), never by a browser click</p>` +
|
|
4950
4857
|
`<div class="card">${agentLines}` +
|
|
4951
|
-
`<p class="meta">Repair always stays on the provider that built — only its model can differ. A
|
|
4858
|
+
`<p class="meta">Repair always stays on the provider that built — only its model can differ. A schedule's task filed under an approval moved from a routine is pinned to the agents approved then.</p>` +
|
|
4952
4859
|
`</div>`;
|
|
4953
4860
|
return screen("system", [
|
|
4954
4861
|
`<h1>System</h1>`,
|
|
@@ -5023,7 +4930,7 @@ export function boardBody(data, ciRed) {
|
|
|
5023
4930
|
};
|
|
5024
4931
|
// The card's facts: mono key–value pairs under the title (board pass) —
|
|
5025
4932
|
// task, worker, runtime — the same grammar on every lane, so the eye
|
|
5026
|
-
// learns one card. Chips carry the words (project,
|
|
4933
|
+
// learns one card. Chips carry the words (project, reservation).
|
|
5027
4934
|
const facts = (rows) => rows.length === 0
|
|
5028
4935
|
? ""
|
|
5029
4936
|
: `<span class="facts">${rows
|
|
@@ -5041,7 +4948,6 @@ export function boardBody(data, ciRed) {
|
|
|
5041
4948
|
...(card.stalledSince === null ? [] : [["waiting", age(card.stalledSince)]]),
|
|
5042
4949
|
]) +
|
|
5043
4950
|
chips([
|
|
5044
|
-
card.routineName === null ? "" : `<span class="badge">${escape(card.routineName)}</span>`,
|
|
5045
4951
|
chip(card.repo).trim(),
|
|
5046
4952
|
]) +
|
|
5047
4953
|
`</a>`;
|
|
@@ -5070,7 +4976,6 @@ export function boardBody(data, ciRed) {
|
|
|
5070
4976
|
? `<span class="badge">${card.assignedRunner === null ? "next up" : `next for ${escape(card.assignedRunner)}`}</span>`
|
|
5071
4977
|
: "",
|
|
5072
4978
|
card.assignedRunner === null ? "" : `<span class="badge">Reserved</span>`,
|
|
5073
|
-
card.routineName === null ? "" : `<span class="badge">${escape(card.routineName)}</span>`,
|
|
5074
4979
|
chip(card.repo).trim(),
|
|
5075
4980
|
]) +
|
|
5076
4981
|
`</a>`;
|
|
@@ -5136,15 +5041,6 @@ export function boardBody(data, ciRed) {
|
|
|
5136
5041
|
` \u2014 every project this server serves, each card wearing its project`
|
|
5137
5042
|
: `<strong>${escape(projectName(data.project))}</strong> \u00b7 <a href="/board?scope=all">all projects</a>`) +
|
|
5138
5043
|
`</p>`;
|
|
5139
|
-
// The tracks: standing orders as rows under the lanes \u2014 the heartbeat
|
|
5140
|
-
// below the one-off pipeline. Only routines with something to say render;
|
|
5141
|
-
// the full list lives at /routines.
|
|
5142
|
-
const tracksSection = data.tracks.length === 0
|
|
5143
|
-
? ""
|
|
5144
|
-
: `<section class="tracks"><h2><a href="/routines">routines</a></h2>` +
|
|
5145
|
-
`<p class="hint">each dot is one firing, oldest first \u2014 the full list is under routines</p>` +
|
|
5146
|
-
data.tracks.map(track => trackRow(track, data.all)).join("\n") +
|
|
5147
|
-
`</section>`;
|
|
5148
5044
|
const ago = (minutes) => minutes < 60 ? `${minutes}m` : minutes < 48 * 60 ? `${Math.round(minutes / 60)}h` : `${Math.round(minutes / (24 * 60))}d`;
|
|
5149
5045
|
const deltaLine = data.delta === null
|
|
5150
5046
|
? ""
|
|
@@ -5169,7 +5065,6 @@ export function boardBody(data, ciRed) {
|
|
|
5169
5065
|
lane("building", "building", "one agent per card, in its own workspace", building),
|
|
5170
5066
|
`<details class="lane lane-done"${data.done.length === 0 ? "" : " open"}><summary><h2><a href="/done">done recently</a></h2></summary><p class="hint">the most recent \u2014 the full list is under done</p>${doneCards}</details>`,
|
|
5171
5067
|
`</div>`,
|
|
5172
|
-
tracksSection,
|
|
5173
5068
|
].join("\n");
|
|
5174
5069
|
}
|
|
5175
5070
|
/** Completed work: one row per done task, its final run and PR attached. */
|
|
@@ -5201,72 +5096,13 @@ export function donePage(chrome, rows, ciRed) {
|
|
|
5201
5096
|
list,
|
|
5202
5097
|
].join("\n"), { chrome });
|
|
5203
5098
|
}
|
|
5204
|
-
export const routineHref = (id) => `/routines/${id}`;
|
|
5205
|
-
export function routineStatus(routine) {
|
|
5206
|
-
const approved = routine.approvedAt !== null && routine.approvedDigest === routine.digest;
|
|
5207
|
-
if (routine.paused)
|
|
5208
|
-
return { text: "paused", badge: "badge" };
|
|
5209
|
-
if (!approved) {
|
|
5210
|
-
return {
|
|
5211
|
-
text: routine.approvedAt === null ? "awaiting approval" : "edited — approve again",
|
|
5212
|
-
badge: "badge badge-failed",
|
|
5213
|
-
};
|
|
5214
|
-
}
|
|
5215
|
-
// An approval that froze no readable agents is not live (v48): its
|
|
5216
|
-
// firings wait until the agents are refreshed and approved again.
|
|
5217
|
-
const agents = routineAgentsState(routine);
|
|
5218
|
-
if (agents.state !== "frozen")
|
|
5219
|
-
return { text: "agents not frozen — refresh and approve again", badge: "badge badge-failed" };
|
|
5220
|
-
return { text: "live", badge: "badge badge-running" };
|
|
5221
|
-
}
|
|
5222
|
-
/**
|
|
5223
|
-
* The run-history strip: the last firings as dots, oldest on the left like
|
|
5224
|
-
* a CI history. Fired slots wear their instance's fate; skipped slots are
|
|
5225
|
-
* hollow — recorded absence, not silence.
|
|
5226
|
-
*/
|
|
5227
|
-
export function trackStrip(fires) {
|
|
5228
|
-
const dots = [...fires].reverse().map(fire => {
|
|
5229
|
-
const slot = fire.scheduledFor.replace(/^manual:/, "");
|
|
5230
|
-
if (fire.outcome === "skipped") {
|
|
5231
|
-
return `<span class="fire fire-skip" title="${escape(slot)} — skipped: ${escape(fire.reason ?? "")}"></span>`;
|
|
5232
|
-
}
|
|
5233
|
-
const state = fire.instanceState;
|
|
5234
|
-
const kind = state === "done" ? "fire-ok" : state === "failed" || state === "cancelled" ? "fire-bad" : "fire-live";
|
|
5235
|
-
const title = `${slot} — ${fire.instanceTaskId ?? "instance"}${state === null ? "" : ` (${state})`}`;
|
|
5236
|
-
return fire.instanceTaskId === null
|
|
5237
|
-
? `<span class="fire ${kind}" title="${escape(title)}"></span>`
|
|
5238
|
-
: `<a class="fire ${kind}" href="${taskHref(fire.instanceTaskId)}" title="${escape(title)}"></a>`;
|
|
5239
|
-
});
|
|
5240
|
-
return `<span class="track-strip">${dots.join("")}</span>`;
|
|
5241
|
-
}
|
|
5242
|
-
/** One track row — shared by the board's tracks section and /routines. */
|
|
5243
|
-
export function trackRow(track, all) {
|
|
5244
|
-
const { routine, fires, spend, blocker } = track;
|
|
5245
|
-
const status = routineStatus(routine);
|
|
5246
|
-
const schedule = parseSchedule(routine.schedule);
|
|
5247
|
-
const latest = fires.find(fire => fire.outcome === "fired");
|
|
5248
|
-
return (`<div class="card track-row">` +
|
|
5249
|
-
`<p><a href="${routineHref(routine.id)}"><strong>${escape(routine.name)}</strong></a> ` +
|
|
5250
|
-
`<span class="${status.badge}">${escape(status.text)}</span>` +
|
|
5251
|
-
`${all ? ` <span class="badge">${escape(projectName(routine.repo))}</span>` : ""}` +
|
|
5252
|
-
`<span class="right meta">${escape(schedule === null ? routine.schedule : describeSchedule(schedule))}</span></p>` +
|
|
5253
|
-
`<p class="meta">${escape(routine.goal.length > 110 ? routine.goal.slice(0, 110) + "…" : routine.goal)}</p>` +
|
|
5254
|
-
`<p>${trackStrip(fires)}` +
|
|
5255
|
-
`<span class="right meta">$${spend.costUsd.toFixed(2)} this week${spend.unmeasuredRuns > 0 ? ` — measured on ${spend.totalRuns - spend.unmeasuredRuns} of ${spend.totalRuns} builds` : ""}${routine.costCeilingUsd === null ? "" : ` of $${routine.costCeilingUsd.toFixed(2)}`}</span></p>` +
|
|
5256
|
-
(blocker !== null
|
|
5257
|
-
? `<p class="meta">Stopped behind <a href="${taskHref(blocker.taskId)}" class="mono">${escape(blocker.taskId)}</a> (${escape(blocker.state)})</p>`
|
|
5258
|
-
: latest?.instanceTaskId !== undefined && latest.instanceTaskId !== null
|
|
5259
|
-
? `<p class="meta">Latest: <a href="${taskHref(latest.instanceTaskId)}" class="mono">${escape(latest.instanceTaskId)}</a>${latest.instanceState === null ? "" : ` (${escape(latest.instanceState)})`}</p>`
|
|
5260
|
-
: "") +
|
|
5261
|
-
`</div>`);
|
|
5262
|
-
}
|
|
5263
5099
|
export function chatMoney(microusd) {
|
|
5264
5100
|
return microusd === null ? "unknown" : `$${(microusd / 1_000_000).toFixed(2)}`;
|
|
5265
5101
|
}
|
|
5266
|
-
/**
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5102
|
+
/** A ceremony nonce as stored: the sha256 hex of the value the form carried. */
|
|
5103
|
+
export function nonceHashOf(value) {
|
|
5104
|
+
return createHash("sha256").update(value).digest("hex");
|
|
5105
|
+
}
|
|
5270
5106
|
/** The approve POST's refusal, in words a person can act on. */
|
|
5271
5107
|
export function approveRefusalWords(reason) {
|
|
5272
5108
|
if (reason === "requester" || reason === "person-required")
|
|
@@ -5277,14 +5113,19 @@ export function approveRefusalWords(reason) {
|
|
|
5277
5113
|
return "not approved: the scope cannot name an exact agent for every role — fix the agent setup and re-file it";
|
|
5278
5114
|
return `not approved: ${reason}`;
|
|
5279
5115
|
}
|
|
5280
|
-
|
|
5281
|
-
|
|
5116
|
+
/** The browser's one-click approval binds not only the signed scope but the
|
|
5117
|
+
* exact advisory plan revision shown beside it. The scope digest remains the
|
|
5118
|
+
* durable authority; this composite makes a stale open tab fail when somebody
|
|
5119
|
+
* edits the plan before approval. The empty middle slot keeps the bytes of
|
|
5120
|
+
* forms already open (it once held other terms, always empty here). */
|
|
5121
|
+
export function approvalFormDigest(scopeDigest, planSha) {
|
|
5122
|
+
if (planSha === null)
|
|
5282
5123
|
return scopeDigest;
|
|
5283
5124
|
return createHash("sha256")
|
|
5284
5125
|
.update("standing-orders/browser-approval/v1\0", "utf8")
|
|
5285
5126
|
.update(scopeDigest, "utf8")
|
|
5286
5127
|
.update("\0", "utf8")
|
|
5287
|
-
.update(
|
|
5128
|
+
.update("", "utf8")
|
|
5288
5129
|
.update("\0", "utf8")
|
|
5289
5130
|
.update(planSha ?? "", "utf8")
|
|
5290
5131
|
.digest("hex");
|
|
@@ -5431,7 +5272,7 @@ export function resumeCeremonyPage(chrome, data) {
|
|
|
5431
5272
|
`<div class="card resume-ceremony">`,
|
|
5432
5273
|
`<p class="row">Lifts this stop’s hold. Other holds stay in place.</p>`,
|
|
5433
5274
|
`<p class="row">Continues saved work${control.committed ? " and its commit" : ""} under the current approved scope. The next attempt needs fresh evidence; the earlier handoff cannot count as a new result.</p>`,
|
|
5434
|
-
`<p class="row">Resuming grants no new approval or publishing permission. Scope, budget, agents,
|
|
5275
|
+
`<p class="row">Resuming grants no new approval or publishing permission. Scope, budget, agents, verification, and review limits still apply.</p>`,
|
|
5435
5276
|
`<details><summary>Stop record and saved work</summary><p>Run #${control.run} was stopped by <span class="mono">${escape(control.stop.requestedBy)}</span> at ${escape(stopWhen(control.stop.requestedAt))}${control.stop.settledAt === null ? "" : ` and settled ${escape(stopWhen(control.stop.settledAt))} (${escape(control.stop.settlement ?? "?")})`}.</p>${control.worktree === null ? "" : `<p class="mono">${escape(control.worktree)}</p>`}</details>`,
|
|
5436
5277
|
data.approved ? "" : `<p class="row problem-words"><strong>The scope is not approved as it stands</strong> — resuming lifts the pause, but no worker spends until the scope is approved again</p>`,
|
|
5437
5278
|
data.gate === null ? "" : `<p class="row"><strong>Before work starts:</strong> ${escape(data.gate.summary)} — ${escape(data.gate.detail)}</p>`,
|
|
@@ -5505,12 +5346,11 @@ export function taskChatApproval(focus, csrf) {
|
|
|
5505
5346
|
revision: approval.revision,
|
|
5506
5347
|
revisionSourceHref: approval.revision === null ? "" : chatResultHref(focus.id, approval.revision.sourceRun),
|
|
5507
5348
|
repairChain: approval.repairChain,
|
|
5508
|
-
raceTerms: approval.raceTerms,
|
|
5509
5349
|
route: focus.route,
|
|
5510
5350
|
coordinator: approval.coordinator,
|
|
5511
5351
|
deliverable: approval.deliverable,
|
|
5512
5352
|
// Edit plan opens the task page's sheet with its fields open for editing.
|
|
5513
|
-
editHref:
|
|
5353
|
+
editHref: `${taskHref(focus.id)}?edit=plan#plan-editor`,
|
|
5514
5354
|
notNowHref: "/chat",
|
|
5515
5355
|
sticky: false,
|
|
5516
5356
|
earlier: { active: focus.assignment?.earlierActive ?? 0, running: focus.assignment?.earlierRunning ?? 0 },
|
|
@@ -5978,12 +5818,11 @@ export function chatPage(chrome, data) {
|
|
|
5978
5818
|
for (const one of candidates) {
|
|
5979
5819
|
const draft = one.draft;
|
|
5980
5820
|
parts.push(`<div class="card">` +
|
|
5981
|
-
`<p><strong>${
|
|
5821
|
+
`<p><strong>${escape(draft.title)}</strong> <span class="badge">Draft ${escape(draft.kind)}</span></p>` +
|
|
5982
5822
|
`<p class="meta">Drafted by the model from fleet context — nothing is filed; drafts do not survive a restart</p>` +
|
|
5983
5823
|
`<p style="white-space:pre-wrap">${escape(draft.goal)}</p>` +
|
|
5984
5824
|
(draft.outOfScope === null ? "" : `<p class="meta">Not: ${escape(draft.outOfScope)}</p>`) +
|
|
5985
5825
|
(draft.touches.length > 0 ? `<p class="meta">Touches: ${escape(draft.touches.join(", "))}</p>` : "") +
|
|
5986
|
-
(draft.kind === "routine" ? `<p class="meta">Schedule: ${escape(draft.schedule)}</p>` : "") +
|
|
5987
5826
|
`<p class="meta">Repo: <span class="mono">${escape(projectName(one.repoPath))}</span></p>` +
|
|
5988
5827
|
`<form method="post" action="/chat/file/${escape(one.key)}" class="inline">` +
|
|
5989
5828
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
|
|
@@ -6173,11 +6012,10 @@ export function proposalCardParts(view, csrf, inert, decision, returnTo = null)
|
|
|
6173
6012
|
}
|
|
6174
6013
|
else if (view.kind === "agents") {
|
|
6175
6014
|
// The agents card (v48): exactly what changes, in the same words the
|
|
6176
|
-
// task page uses — the role, the exact agent,
|
|
6177
|
-
//
|
|
6015
|
+
// task page uses — the role, the exact agent, the size — and the
|
|
6016
|
+
// consequence for the standing approval.
|
|
6178
6017
|
const taskTitle = text("taskTitle") || task;
|
|
6179
6018
|
const role = text("role");
|
|
6180
|
-
const risk = text("risk");
|
|
6181
6019
|
const clear = payload["clear"] === true;
|
|
6182
6020
|
const agentWords = text("provider") === "" ? "" : `${text("provider")} · ${text("model")}`;
|
|
6183
6021
|
const size = text("size");
|
|
@@ -6186,15 +6024,13 @@ export function proposalCardParts(view, csrf, inert, decision, returnTo = null)
|
|
|
6186
6024
|
? `Run <a href="${taskHref(task)}">${escape(taskTitle)}</a>'s ${escape(role)} on <span class="mono">${escape(agentWords)}</span>`
|
|
6187
6025
|
: role !== "" && clear
|
|
6188
6026
|
? `Let the recommended ${escape(role)} stand for <a href="${taskHref(task)}">${escape(taskTitle)}</a>`
|
|
6189
|
-
:
|
|
6027
|
+
: size !== ""
|
|
6190
6028
|
? `Treat <a href="${taskHref(task)}">${escape(taskTitle)}</a> as a ${escape(sizeLine)}`
|
|
6191
|
-
: `
|
|
6192
|
-
const riskWords = isRiskLevel(risk) ? `${riskTitle(risk)}: ${riskConsequence(risk)}.` : "";
|
|
6029
|
+
: `Change the agents for <a href="${taskHref(task)}">${escape(taskTitle)}</a>`;
|
|
6193
6030
|
what =
|
|
6194
6031
|
`<h3>${heading}</h3>` +
|
|
6195
6032
|
(text("why") === "" ? "" : `<p class="proposal-summary">${escape(text("why"))}</p>`) +
|
|
6196
|
-
facts(["agents now", escape(text("before"))], ["
|
|
6197
|
-
(role !== "" && riskWords !== "" ? `<p class="meta">${escape(riskWords)}</p>` : "") +
|
|
6033
|
+
facts(["agents now", escape(text("before"))], ["size", size === "" ? "" : escape(sizeConsequence(size, payload["risky"] === true))], ["role", role === "" ? "" : `${escape(role)} → ${clear ? "the recommendation" : `<span class="mono">${escape(agentWords)}</span>`}`], ["project", `<span class="mono">${escape(repoId)}</span>`]) +
|
|
6198
6034
|
`<p class="meta proposal-disclosure">Recorded under your name when you confirm. ${text("approval") === "approved" ? "The current approval no longer covers the task afterwards — approve it again on the task." : "The next approval seals these agents."}</p>`;
|
|
6199
6035
|
}
|
|
6200
6036
|
else if (view.kind === "scope") {
|
|
@@ -6593,149 +6429,6 @@ export function matePage(chrome, data) {
|
|
|
6593
6429
|
},
|
|
6594
6430
|
});
|
|
6595
6431
|
}
|
|
6596
|
-
export function routinesPage(chrome, tracks, form) {
|
|
6597
|
-
const fill = form.values === undefined ? form.prefill ?? null : { name: form.values.get("name") ?? "", goal: form.values.get("goal") ?? "", not: form.values.get("not") ?? "", touches: form.values.get("touches") ?? "", schedule: form.values.get("schedule") ?? "", acceptance: form.values.get("acceptance") ?? "" };
|
|
6598
|
-
const capture = chrome.project === null
|
|
6599
|
-
? ""
|
|
6600
|
-
: [
|
|
6601
|
-
`<h2>File a standing order</h2>`,
|
|
6602
|
-
form.problem === null ? "" : `<div class="problem">${escape(form.problem)}</div>`,
|
|
6603
|
-
`<form method="post" action="/routines/add" class="card">`,
|
|
6604
|
-
`<input type="hidden" name="csrf" value="${escape(form.csrf)}">`,
|
|
6605
|
-
`<input type="hidden" name="projectRevision" value="${form.revision}">`,
|
|
6606
|
-
fill === null
|
|
6607
|
-
? `<p class="meta">Start from a template: ${TEMPLATES.filter(one => one.kind === "routine")
|
|
6608
|
-
.map(one => `<a href="/routines?template=${escape(one.name)}">${escape(one.name)}</a>`)
|
|
6609
|
-
.join(" · ")}</p>`
|
|
6610
|
-
: `<p class="meta">${form.values === undefined ? "pre-filled from a template — edit anything" : "Your entries are kept below"}; nothing fires until you approve the standing order</p>`,
|
|
6611
|
-
`<label>Name <span class="meta">(lowercase-with-dashes — it names each instance)</span><input type="text" name="name" placeholder="nightly-deps" maxlength="41" value="${fill === null ? "" : escape(fill.name)}"></label>`,
|
|
6612
|
-
`<label>Goal <span class="meta">(what every firing is allowed to do)</span><textarea name="goal" rows="2">${fill === null ? "" : escape(fill.goal)}</textarea></label>`,
|
|
6613
|
-
`<label>Not this <span class="meta">(optional)</span><input type="text" name="not" value="${fill === null ? "" : escape(fill.not)}"></label>`,
|
|
6614
|
-
`<label>Touches <span class="meta">(paths, comma-separated, optional)</span><input type="text" name="touches" value="${fill === null ? "" : escape(fill.touches)}"></label>`,
|
|
6615
|
-
`<label>Acceptance <span class="meta">(required — one criterion per line: <code>statement | evidence,kinds | how</code>; evidence kinds are check, screenshot, changed-path, manual-review; id is optional and auto-numbered)</span><textarea name="acceptance" rows="3" placeholder="The full test suite passes | check">${fill === null ? "" : escape(fill.acceptance)}</textarea></label>`,
|
|
6616
|
-
scheduleEditorHtml(fill?.schedule ?? null, form.values),
|
|
6617
|
-
`<label>Budget <span class="meta">(dollars per rolling 7 days, optional — needs a provider that reports cost)</span><input type="text" name="ceiling" inputmode="decimal" style="width:8rem" value="${escape(form.values?.get("ceiling") ?? "")}"></label>`,
|
|
6618
|
-
`<button type="submit">File it \u2192 approve the standing order next</button>`,
|
|
6619
|
-
`<p class="meta">Filing is cheap — nothing fires until you approve the template on the next screen, password and all</p>`,
|
|
6620
|
-
`</form>`,
|
|
6621
|
-
].join("\n");
|
|
6622
|
-
const list = tracks.length === 0
|
|
6623
|
-
? `<p class="meta">No routines${chrome.project === null ? " — open a project to file one" : " in this project yet — file one below; nothing fires until you approve it"}.</p>`
|
|
6624
|
-
: tracks.map(track => trackRow(track, chrome.project === null)).join("\n");
|
|
6625
|
-
return screen("routines", [
|
|
6626
|
-
`<h1>Routines</h1>`,
|
|
6627
|
-
`<p class="hint">scheduled work — anything needing a person appears in the inbox</p><p><a class="new-task" href="/recipes">Create a workflow from a recipe →</a></p>`,
|
|
6628
|
-
list,
|
|
6629
|
-
capture,
|
|
6630
|
-
].join("\n"), { chrome, functional: { script: scheduleEditorScript() } });
|
|
6631
|
-
}
|
|
6632
|
-
export function routineScreenPage(chrome, data) {
|
|
6633
|
-
const { routine, fires } = data;
|
|
6634
|
-
const status = routineStatus(routine);
|
|
6635
|
-
const approved = routine.approvedAt !== null && routine.approvedDigest === routine.digest;
|
|
6636
|
-
const schedule = parseSchedule(routine.schedule);
|
|
6637
|
-
const scheduleSaid = schedule === null ? routine.schedule : describeSchedule(schedule);
|
|
6638
|
-
const terms = `<div class="card">` +
|
|
6639
|
-
`<p class="meta">Goal</p><p class="recap" style="margin-top:0">${escape(routine.goal)}</p>` +
|
|
6640
|
-
`<p class="meta">Not this</p><p class="recap" style="margin-top:0">${routine.outOfScope === null ? "<em>no exclusions</em>" : escape(routine.outOfScope)}</p>` +
|
|
6641
|
-
`<p class="meta">Touches · ${routine.touches.length === 0 ? "anything" : routine.touches.map(one => escape(one)).join(", ")}</p>` +
|
|
6642
|
-
(routine.acceptance.length === 0
|
|
6643
|
-
? ""
|
|
6644
|
-
: `<p class="meta">Acceptance</p><ul class="recap">${routine.acceptance
|
|
6645
|
-
.map(c => `<li><code>${escape(c.id)}</code> ${escape(c.statement)} <span class="meta">[requires: ${c.evidence.map(escape).join(", ")}]</span></li>`)
|
|
6646
|
-
.join("")}</ul>`) +
|
|
6647
|
-
`<p class="meta">Needs · ${routine.requirements.length === 0 ? "nothing beyond the repository" : routine.requirements.map(one => escape(one)).join(", ")}</p>` +
|
|
6648
|
-
routineAgentsHtml(routine, approved) +
|
|
6649
|
-
`<p class="meta">Schedule · ${escape(scheduleSaid)}</p>` +
|
|
6650
|
-
`<p class="meta">Budget · ${routine.costCeilingUsd === null ? "no ceiling" : `$${routine.costCeilingUsd.toFixed(2)} per rolling 7 days`}</p>` +
|
|
6651
|
-
`<p class="meta">One at a time — a firing skips while the previous instance is unfinished</p>` +
|
|
6652
|
-
`</div>`;
|
|
6653
|
-
// THE CONSENT DOOR (v48): the password and the approve act exist only
|
|
6654
|
-
// where an exact, readable route can be restated — never over agents
|
|
6655
|
-
// that are unresolved, unreadable, or never frozen. Those states get the
|
|
6656
|
-
// one recovery act instead: refresh the agents, then approve again.
|
|
6657
|
-
const agents = routineAgentsState(routine);
|
|
6658
|
-
// One plain-language act with accessible, neutral controls: the form is
|
|
6659
|
-
// named by its title, the button described by the reason — no password,
|
|
6660
|
-
// no danger verb, nothing to type.
|
|
6661
|
-
const refreshForm = `<form method="post" action="${routineHref(routine.id)}/refresh" class="card approve-form agents-recovery" id="agents-recovery" aria-labelledby="agents-recovery-title">` +
|
|
6662
|
-
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
|
|
6663
|
-
`<p id="agents-recovery-title"><strong>${escape(agents.state === "unfrozen"
|
|
6664
|
-
? "This standing order was approved before its agents were frozen — nothing fires until you approve it again."
|
|
6665
|
-
: agents.state === "unreadable"
|
|
6666
|
-
? "The agents on file for this standing order cannot be read — nothing approves or fires until they are refreshed."
|
|
6667
|
-
: agents.state === "unverified"
|
|
6668
|
-
? "The agents this standing order's approval froze do not verify — nothing fires until they are refreshed and approved again."
|
|
6669
|
-
: "This standing order cannot be approved yet: it does not name an exact agent for every role.")}</strong></p>` +
|
|
6670
|
-
`<p id="agents-recovery-why" class="recap">${escape(agents.state === "unresolved" && agents.problem !== null
|
|
6671
|
-
? `${agents.problem}. Configure the project's agents (config set <phase> --provider … --model …), then refresh.`
|
|
6672
|
-
: "Refreshing reads today's configured agents into this order and shows you exactly who would plan, build, repair, and review each firing. Nothing is approved by refreshing — the password step comes after, on this page.")}</p>` +
|
|
6673
|
-
`<button type="submit" aria-describedby="agents-recovery-why">Refresh agents</button></form>`;
|
|
6674
|
-
const approveForm = approved
|
|
6675
|
-
? agents.refresh ? refreshForm : ""
|
|
6676
|
-
: !agents.approvable
|
|
6677
|
-
? refreshForm
|
|
6678
|
-
: [
|
|
6679
|
-
`<form method="post" action="${routineHref(routine.id)}/approve" class="card approve-form">`,
|
|
6680
|
-
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
|
|
6681
|
-
`<input type="hidden" name="nonce" value="${escape(data.nonce)}">`,
|
|
6682
|
-
`<input type="hidden" name="digest" value="${escape(routine.digest)}">`,
|
|
6683
|
-
`<p><strong>Approve this standing order:</strong></p>`,
|
|
6684
|
-
`<p class="recap">Each firing creates a task under exactly the terms above and BUILDS IT WITHOUT ASKING — ${escape(scheduleSaid)}, until you pause it. Questions and failures still reach you like any other work.</p>`,
|
|
6685
|
-
// The agents the yes freezes (v48): the same concise block every
|
|
6686
|
-
// approval shows, before the password.
|
|
6687
|
-
routineAgentsHtml(routine, false, true),
|
|
6688
|
-
`<label>Your password, typed again — a signed-in session alone cannot agree to standing work<input type="password" name="token" autocomplete="current-password"></label>` +
|
|
6689
|
-
`<button type="submit">Approve this routine</button>`,
|
|
6690
|
-
`</form>`,
|
|
6691
|
-
].join("\n");
|
|
6692
|
-
const verb = (name, label, danger = false) => `<form method="post" action="${routineHref(routine.id)}/${name}" class="inline">` +
|
|
6693
|
-
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
|
|
6694
|
-
`<button type="submit"${danger ? ' class="danger"' : ""}>${label}</button></form>`;
|
|
6695
|
-
const runNowForm = approved && !routine.paused && agents.state === "frozen"
|
|
6696
|
-
? `<form method="post" action="${routineHref(routine.id)}/run-now" class="inline">` +
|
|
6697
|
-
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
|
|
6698
|
-
`<input type="password" name="token" autocomplete="current-password" class="inline" placeholder="your password" aria-label="password for run now" style="width:11rem"> ` +
|
|
6699
|
-
`<button type="submit">Run now</button></form>`
|
|
6700
|
-
: "";
|
|
6701
|
-
const acts = `<div class="card">` +
|
|
6702
|
-
(routine.paused ? verb("resume", "resume") : verb("pause", "pause")) +
|
|
6703
|
-
(runNowForm === "" ? "" : " " + runNowForm) +
|
|
6704
|
-
`<p class="meta">${routine.paused ? "resuming fires again at the next due slot" : "pausing stops firing instantly; a running instance finishes"}${runNowForm === "" ? "" : " · run now spawns an extra instance without touching the schedule — spend outside the schedule takes your password again"}</p>` +
|
|
6705
|
-
`</div>`;
|
|
6706
|
-
const ledger = fires.length === 0
|
|
6707
|
-
? `<p class="meta">No firings yet${approved && routine.nextFireAt !== null ? ` — first at ${whenTime(routine.nextFireAt)}` : ""}.</p>`
|
|
6708
|
-
: fires
|
|
6709
|
-
.map(fire => {
|
|
6710
|
-
const said = fire.outcome === "fired"
|
|
6711
|
-
? fire.instanceTaskId === null
|
|
6712
|
-
? "fired"
|
|
6713
|
-
: `<a href="${taskHref(fire.instanceTaskId)}" class="mono">${escape(fire.instanceTaskId)}</a>${fire.instanceState === null ? "" : ` <span class="badge badge-${escape(fire.instanceState)}">${escape(sentenceCase(fire.instanceState))}</span>`}`
|
|
6714
|
-
: `<span class="meta">skipped — ${escape(fire.reason ?? "")}</span>`;
|
|
6715
|
-
const slot = fire.scheduledFor.replace(/^manual:/, "");
|
|
6716
|
-
return `<p class="row">${said}<span class="right meta mono">${fire.reason === "manual" ? "run now · " : ""}${whenTime(slot)}</span></p>`;
|
|
6717
|
-
})
|
|
6718
|
-
.join("\n");
|
|
6719
|
-
return screen(`routine · ${routine.name}`, [
|
|
6720
|
-
`<h1>${escape(routine.name)} <span class="${status.badge}">${escape(status.text)}</span>` +
|
|
6721
|
-
`<span class="meta"> · ${escape(projectName(routine.repo))}</span></h1>`,
|
|
6722
|
-
data.problem === null ? "" : `<div class="problem">${escape(data.problem)}</div>`,
|
|
6723
|
-
`<p>${trackStrip(fires)}<span class="right meta">$${data.spend.costUsd.toFixed(2)} this week${data.spend.unmeasuredRuns > 0 ? ` — measured on ${data.spend.totalRuns - data.spend.unmeasuredRuns} of ${data.spend.totalRuns} builds` : ""}</span></p>`,
|
|
6724
|
-
data.blocker !== null
|
|
6725
|
-
? `<div class="problem">stopped behind <a href="${taskHref(data.blocker.taskId)}" class="mono">${escape(data.blocker.taskId)}</a> (${escape(data.blocker.state)}) — the track resumes when it finishes or is cancelled</div>`
|
|
6726
|
-
: "",
|
|
6727
|
-
approved && !routine.paused && routine.nextFireAt !== null
|
|
6728
|
-
? `<p class="meta">Next fire ${whenTime(routine.nextFireAt)}</p>`
|
|
6729
|
-
: "",
|
|
6730
|
-
"<h2>The standing order</h2>",
|
|
6731
|
-
terms,
|
|
6732
|
-
approveForm,
|
|
6733
|
-
"<h2>Acts</h2>",
|
|
6734
|
-
acts,
|
|
6735
|
-
"<h2>Firings</h2>",
|
|
6736
|
-
ledger,
|
|
6737
|
-
].join("\n"), { chrome });
|
|
6738
|
-
}
|
|
6739
6432
|
export function homePage(chrome, data) {
|
|
6740
6433
|
const { summary } = data;
|
|
6741
6434
|
// The night's harvest is the page's reason to exist — strong figures in a
|
|
@@ -6803,9 +6496,8 @@ export function homePage(chrome, data) {
|
|
|
6803
6496
|
const dot = age < 5 * 60_000 ? "dot-ok" : age < 60 * 60_000 ? "dot-warn" : "dot-off";
|
|
6804
6497
|
const said = age < 5 * 60_000 ? "alive" : age < 60 * 60_000 ? `quiet ${Math.round(age / 60_000)}m` : "not heard from";
|
|
6805
6498
|
const busy = data.building.filter(claim => claim.runner === one.name).length;
|
|
6806
|
-
const sessions = data.heldSessions?.get(one.name) ?? 0;
|
|
6807
6499
|
return (`<div class="stat-card"><span class="k"><span class="dot ${dot}"></span>${escape(one.name)}</span>` +
|
|
6808
|
-
`<span class="v">builder \u00b7 ${said} \u00b7 ${busy}/${one.capacity} building
|
|
6500
|
+
`<span class="v">builder \u00b7 ${said} \u00b7 ${busy}/${one.capacity} building</span></div>`);
|
|
6809
6501
|
});
|
|
6810
6502
|
const worktreeCards = data.worktrees.map(tree => {
|
|
6811
6503
|
const leased = tree.leasedAt !== null && tree.releasedAt === null;
|
|
@@ -7005,9 +6697,9 @@ export function workPage(chrome, data) {
|
|
|
7005
6697
|
(data.previous ? `<a class="button-link" href="${escape(href(data.view))}">First page</a>` : '') +
|
|
7006
6698
|
(data.work.nextCursor === null ? '' : `<a class="button-link" rel="next" href="${escape(href(data.view, data.work.nextCursor))}">Next page</a>`) + `</nav>`;
|
|
7007
6699
|
const tools = `<details class="work-tools"><summary>Work tools${CHEVRON_ICON}</summary><nav class="work-tools-menu">` +
|
|
7008
|
-
[['/inbox', 'Inbox'], ...(chrome.code ? [['/code', 'Coding sessions']] : []), ['/board', 'Board'], ['/board?view=order', 'Order'], ['/tasks', 'Task list'], ['/recipes', 'Recipes'],
|
|
6700
|
+
[['/inbox', 'Inbox'], ...(chrome.code ? [['/code', 'Coding sessions']] : []), ['/board', 'Board'], ['/board?view=order', 'Order'], ['/tasks', 'Task list'], ['/recipes', 'Recipes'], ...(chrome.projectScoped ? [] : [['/workbench', 'Portfolio']]), ['/ledger', 'Action ledger']]
|
|
7009
6701
|
.map(([path, label]) => `<a href="${path}">${label}</a>`).join('') + `</nav></details>`;
|
|
7010
|
-
const toolLinks = [['/inbox', 'Inbox'], ...(chrome.code ? [['/code', 'Coding sessions']] : []), ['/board', 'Board'], ['/board?view=order', 'Order'], ['/tasks', 'Task list'], ['/recipes', 'Recipes'],
|
|
6702
|
+
const toolLinks = [['/inbox', 'Inbox'], ...(chrome.code ? [['/code', 'Coding sessions']] : []), ['/board', 'Board'], ['/board?view=order', 'Order'], ['/tasks', 'Task list'], ['/recipes', 'Recipes'], ...(chrome.projectScoped ? [] : [['/workbench', 'Portfolio']])];
|
|
7011
6703
|
const view = {
|
|
7012
6704
|
kind: 'tasks',
|
|
7013
6705
|
tabs: WORK_VIEWS.map(one => ({ label: one.label, href: href(one.key), count: data.work.totals[one.key], active: one.key === data.view })),
|
|
@@ -7379,228 +7071,6 @@ export function portfolioOverview(data) {
|
|
|
7379
7071
|
data.ledger.length === 0 ? `<p class="meta">None yet</p>` : ledgerRows,
|
|
7380
7072
|
].join("\n");
|
|
7381
7073
|
}
|
|
7382
|
-
/** Dollars for a screen: micro-USD stated as money, unknowables in words. */
|
|
7383
|
-
export function contestDollars(microusd) {
|
|
7384
|
-
return `$${(microusd / 1_000_000).toFixed(2)}`;
|
|
7385
|
-
}
|
|
7386
|
-
export const CONTEST_STATE_WORDS = {
|
|
7387
|
-
dispatching: "the agents are being set up",
|
|
7388
|
-
racing: "the agents are working right now",
|
|
7389
|
-
"pick-wait": "every agent has finished — compare the results and pick one",
|
|
7390
|
-
"decision-wait": "an agent asked a question — answer it from the task screen and the tournament continues",
|
|
7391
|
-
picked: "decided — one result was picked",
|
|
7392
|
-
abandoned: "abandoned — nothing was picked; every agent's work is kept",
|
|
7393
|
-
interrupted: "interrupted — the machine running it went away; decide what happens next",
|
|
7394
|
-
exhausted: "finished with nothing to pick — decide what happens next",
|
|
7395
|
-
};
|
|
7396
|
-
/**
|
|
7397
|
-
* The comparison screen: every agent's result side by side — outcome, cost,
|
|
7398
|
-
* questions, conclusion, and the verified diff — with a pick button only on
|
|
7399
|
-
* results the evidence supports. Plain words throughout: tournament,
|
|
7400
|
-
* agents, results. The pick itself happens on a separate confirmation
|
|
7401
|
-
* screen whose form this page can only reach through a POST.
|
|
7402
|
-
*/
|
|
7403
|
-
export function contestPage(chrome, data) {
|
|
7404
|
-
const { contest, agents } = data.view;
|
|
7405
|
-
const picking = contest.state === "pick-wait";
|
|
7406
|
-
const abandonable = ["pick-wait", "exhausted", "interrupted", "decision-wait"].includes(contest.state);
|
|
7407
|
-
// ONE summary per agent (arc 6, finding 7): the at-a-glance table and the
|
|
7408
|
-
// cards below both render from this object, so the two can never tell a
|
|
7409
|
-
// pick two different stories. Every diff state keeps its own words —
|
|
7410
|
-
// verified-zero, changes, missing, and capture problems are not the same
|
|
7411
|
-
// fact and are never collapsed into "no diff".
|
|
7412
|
-
const summarize = (agent) => {
|
|
7413
|
-
const { contestant, run } = agent;
|
|
7414
|
-
const outcome = run === null
|
|
7415
|
-
? "never produced a finished attempt"
|
|
7416
|
-
: run.outcome === "built"
|
|
7417
|
-
? "finished with changes"
|
|
7418
|
-
: run.outcome === "no-change"
|
|
7419
|
-
? "concluded no change was needed"
|
|
7420
|
-
: run.outcome === "parked"
|
|
7421
|
-
? "waiting on an answer"
|
|
7422
|
-
: run.outcome === null
|
|
7423
|
-
? (data.liveRuns?.has(run.id) === true ? "still working" : "stopped without finishing")
|
|
7424
|
-
: run.outcome === "failed"
|
|
7425
|
-
? "failed"
|
|
7426
|
-
: run.outcome === "refused"
|
|
7427
|
-
? "refused — a gate said no"
|
|
7428
|
-
: "stopped";
|
|
7429
|
-
const minutes = run === null || run.finishedAt === null
|
|
7430
|
-
? null
|
|
7431
|
-
: Math.max(1, Math.round((new Date(run.finishedAt).getTime() - new Date(run.startedAt).getTime()) / 60_000));
|
|
7432
|
-
const diff = data.diffs.get(contestant.id) ?? null;
|
|
7433
|
-
const diffWords = diff === null || diff.stat === null
|
|
7434
|
-
? "no change summary"
|
|
7435
|
-
: "problem" in diff.stat
|
|
7436
|
-
? "summary capture failed"
|
|
7437
|
-
: diff.stat.fileCount === 0
|
|
7438
|
-
? "no changes, verified"
|
|
7439
|
-
: `${diff.stat.fileCount} file(s) · +${diff.stat.additions} −${diff.stat.deletions}`;
|
|
7440
|
-
return {
|
|
7441
|
-
agent,
|
|
7442
|
-
contestant,
|
|
7443
|
-
run,
|
|
7444
|
-
winner: contest.winnerContestant === contestant.id,
|
|
7445
|
-
outcome,
|
|
7446
|
-
minutes,
|
|
7447
|
-
asked: data.questions.get(contestant.id) ?? 0,
|
|
7448
|
-
// Money words are KIND words (slice B, E3): a comparison lane never
|
|
7449
|
-
// had a reservation, so reservation language would lie in both
|
|
7450
|
-
// directions — unmeasured lanes say tokens instead.
|
|
7451
|
-
cost: (() => {
|
|
7452
|
-
if (contest.kind !== "comparison") {
|
|
7453
|
-
return contestant.unknownSpend
|
|
7454
|
-
? `${contestDollars(contestant.accountedMicrousd)} — the exact figure was unknowable, so the full reservation was charged`
|
|
7455
|
-
: contestDollars(contestant.accountedMicrousd);
|
|
7456
|
-
}
|
|
7457
|
-
// The WHOLE lineage speaks (Codex slice-B finding 6): main attempt,
|
|
7458
|
-
// resumes, and repairs — a newest-run read under-reports every
|
|
7459
|
-
// park cycle.
|
|
7460
|
-
const rollup = data.rollups.get(contestant.id) ?? { costMicrousd: 0, tokensIn: 0, tokensOut: 0, measuredRuns: 0, totalRuns: 0 };
|
|
7461
|
-
return contestant.unknownSpend
|
|
7462
|
-
? `tokens only${rollup.tokensIn + rollup.tokensOut > 0 ? ` (${(rollup.tokensIn + rollup.tokensOut).toLocaleString()} across ${rollup.totalRuns} run${rollup.totalRuns === 1 ? "" : "s"})` : ""} — this harness reports no dollars`
|
|
7463
|
-
: `${contestDollars(rollup.costMicrousd)} measured${rollup.measuredRuns < rollup.totalRuns ? ` on ${rollup.measuredRuns} of ${rollup.totalRuns} runs` : ""}`;
|
|
7464
|
-
})(),
|
|
7465
|
-
diff,
|
|
7466
|
-
diffWords,
|
|
7467
|
-
};
|
|
7468
|
-
};
|
|
7469
|
-
const summaries = agents.map(summarize);
|
|
7470
|
-
// The at-a-glance table: one COLUMN per agent, the same derived facts as
|
|
7471
|
-
// the cards. Wide content scrolls in its own box (the arc-4 rule).
|
|
7472
|
-
const glance = summaries.length < 2
|
|
7473
|
-
? ""
|
|
7474
|
-
: `<div class="scroll-x"><table class="contest-glance">` +
|
|
7475
|
-
`<tr><td></td>${summaries.map(one => `<th>Agent ${one.contestant.ordinal}<span class="meta"> · ${escape(one.contestant.provider)} · ${escape(one.contestant.model)}</span>${one.winner ? ` <span class="badge badge-done">Picked</span>` : ""}</th>`).join("")}</tr>` +
|
|
7476
|
-
`<tr><td class="meta">outcome</td>${summaries.map(one => `<td>${escape(one.outcome)}</td>`).join("")}</tr>` +
|
|
7477
|
-
`<tr><td class="meta">changed</td>${summaries.map(one => `<td>${escape(one.diffWords)}</td>`).join("")}</tr>` +
|
|
7478
|
-
`<tr><td class="meta">time</td>${summaries.map(one => `<td>${one.minutes === null ? "—" : `${one.minutes} min`}</td>`).join("")}</tr>` +
|
|
7479
|
-
`<tr><td class="meta">questions</td>${summaries.map(one => `<td>${one.asked}</td>`).join("")}</tr>` +
|
|
7480
|
-
`<tr><td class="meta">cost</td>${summaries.map(one => `<td>${escape(one.cost)}</td>`).join("")}</tr>` +
|
|
7481
|
-
`<tr><td></td>${summaries.map(one => `<td>${one.run === null ? "" : `<a href="/r/${one.run.id}">the build</a>`}</td>`).join("")}</tr>` +
|
|
7482
|
-
`</table></div>`;
|
|
7483
|
-
const agentCard = (summary) => {
|
|
7484
|
-
const { contestant, run, winner, outcome, minutes, asked, cost, diff } = summary;
|
|
7485
|
-
const agent = summary.agent;
|
|
7486
|
-
const parts = [
|
|
7487
|
-
`<div class="card${winner ? " picked" : ""}">`,
|
|
7488
|
-
`<p><strong>Agent ${contestant.ordinal}</strong> <span class="meta">${escape(contestant.provider)} · ${escape(contestant.model)}</span>` +
|
|
7489
|
-
`${winner ? ` <span class="badge badge-done">Picked</span>` : ""}</p>`,
|
|
7490
|
-
`<p class="row">${escape(outcome)}` +
|
|
7491
|
-
`${minutes === null ? "" : ` <span class="meta">· ${minutes} min</span>`}` +
|
|
7492
|
-
`${asked > 0 ? ` <span class="meta">· asked ${asked} question${asked > 1 ? "s" : ""}</span>` : ""}` +
|
|
7493
|
-
`${run === null ? "" : ` <span class="meta">· <a href="/r/${run.id}">the build</a></span>`}</p>`,
|
|
7494
|
-
`<p class="row"><strong>Cost</strong> ${escape(cost)}</p>`,
|
|
7495
|
-
run === null || run.handoff === null ? "" : `<p><strong>Its own conclusion</strong></p><p class="recap">${escape(run.handoff)}</p>`,
|
|
7496
|
-
diff === null ? `<p class="meta">No diff was captured</p>` : terminalDiffCard(diff, run === null ? 0 : run.id),
|
|
7497
|
-
];
|
|
7498
|
-
if (picking) {
|
|
7499
|
-
if (agent.pickable) {
|
|
7500
|
-
parts.push(`<form method="post" action="/contest/${contest.id}/arm" class="inline">`, `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`, `<input type="hidden" name="choice" value="${contestant.id}">`, `<button type="submit">Pick this result…</button>`, `</form>`, `<p class="meta">Picking continues to a confirmation screen — nothing happens yet</p>`);
|
|
7501
|
-
}
|
|
7502
|
-
else {
|
|
7503
|
-
parts.push(`<p class="meta">Cannot be picked — ${escape(agent.unpickableReason ?? "")}</p>`);
|
|
7504
|
-
}
|
|
7505
|
-
}
|
|
7506
|
-
parts.push(`</div>`);
|
|
7507
|
-
return parts.filter(one => one !== "").join("\n");
|
|
7508
|
-
};
|
|
7509
|
-
return screen("tournament", [
|
|
7510
|
-
`<h1>${contestNoun(contest.kind)}</h1>`,
|
|
7511
|
-
`<p class="meta">${agents.length} agents raced on <a href="${taskHref(data.taskId)}">${escape(data.taskTitle)}</a> — ` +
|
|
7512
|
-
`only one result will be kept as the task's outcome; the rest stay archived with their evidence</p>`,
|
|
7513
|
-
data.problem === null ? "" : `<div class="problem">${escape(data.problem)}</div>`,
|
|
7514
|
-
`<p class="row"><strong>${escape((CONTEST_STATE_WORDS[contest.state] ?? "the tournament is in an unexpected state — the records have the detail").replace(/tournament/g, contestNoun(contest.kind)))}</strong></p>`,
|
|
7515
|
-
contest.pickedBy === null ? "" : `<p class="meta">Picked by ${escape(contest.pickedBy)} at ${whenTime(contest.pickedAt ?? "")}</p>`,
|
|
7516
|
-
contest.kind === "comparison"
|
|
7517
|
-
? `<p class="row"><strong>Spend</strong> ${escape(contestDollars(data.totalMicrousd))} measured on the lanes that report dollars` +
|
|
7518
|
-
`${data.anyUnknown ? ` <span class="meta">— the rest report tokens only</span>` : ""}</p>`
|
|
7519
|
-
: `<p class="row"><strong>Charged so far</strong> ${escape(contestDollars(data.totalMicrousd))}` +
|
|
7520
|
-
`${data.anyUnknown ? ` <span class="meta">— includes at least one agent charged its full reservation because the exact figure was unknowable</span>` : ""}</p>`,
|
|
7521
|
-
glance,
|
|
7522
|
-
`<div class="contest-compare">${summaries.map(agentCard).join("\n")}</div>`,
|
|
7523
|
-
abandonable
|
|
7524
|
-
? [
|
|
7525
|
-
`<div class="card">`,
|
|
7526
|
-
`<form method="post" action="/contest/${contest.id}/arm" class="inline">`,
|
|
7527
|
-
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
|
|
7528
|
-
`<input type="hidden" name="act" value="abandon">`,
|
|
7529
|
-
`<button type="submit">Abandon the ${contestNoun(contest.kind)}…</button>`,
|
|
7530
|
-
`</form>`,
|
|
7531
|
-
`<p class="meta">Abandoning picks nothing: the task is marked failed (it can be re-queued), and every agent's branch and evidence is kept</p>`,
|
|
7532
|
-
`</div>`,
|
|
7533
|
-
].join("\n")
|
|
7534
|
-
: "",
|
|
7535
|
-
].filter(one => one !== "").join("\n"), { chrome });
|
|
7536
|
-
}
|
|
7537
|
-
/**
|
|
7538
|
-
* The confirmation screen a POST minted: it restates, in full, exactly what
|
|
7539
|
-
* the password will authorize — the identified result, the money, and the
|
|
7540
|
-
* one publication consequence — over a single-use nonce bound to that
|
|
7541
|
-
* restatement. If anything shifts underneath before the yes, the pick
|
|
7542
|
-
* refuses rather than landing on the moved thing.
|
|
7543
|
-
*/
|
|
7544
|
-
export function contestCeremonyPage(chrome, data) {
|
|
7545
|
-
const back = `<p class="meta"><a href="/contest/${data.contestId}">back — decide nothing</a></p>`;
|
|
7546
|
-
if (data.kind === "abandon") {
|
|
7547
|
-
return screen("tournament", [
|
|
7548
|
-
`<h1>Abandon this ${contestNoun(data.contestKind)}?</h1>`,
|
|
7549
|
-
`<div class="card">`,
|
|
7550
|
-
`<p class="row">${data.agents} agents ${data.contestKind === "comparison" ? "built independently" : "raced"} on <strong>${escape(data.taskTitle)}</strong>. Abandoning picks nothing:</p>`,
|
|
7551
|
-
`<p class="row">— the task is marked <strong>failed</strong> and can be re-queued later</p>`,
|
|
7552
|
-
`<p class="row">— every agent's branch and evidence is kept; nothing is deleted and nothing is published</p>`,
|
|
7553
|
-
data.contestKind === "comparison"
|
|
7554
|
-
? `<p class="row">— the ${escape(contestDollars(data.totalMicrousd))} measured so far stays on the record</p>`
|
|
7555
|
-
: `<p class="row">— the ${escape(contestDollars(data.totalMicrousd))} already charged stays charged</p>`,
|
|
7556
|
-
`</div>`,
|
|
7557
|
-
`<form method="post" action="/contest/${data.contestId}/abandon" class="card">`,
|
|
7558
|
-
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
|
|
7559
|
-
`<input type="hidden" name="nonce" value="${escape(data.nonceValue)}">`,
|
|
7560
|
-
`<label>Your password, typed again<input type="password" name="token" autocomplete="current-password"></label>`,
|
|
7561
|
-
`<button type="submit" class="danger">Abandon the ${contestNoun(data.contestKind)}</button>`,
|
|
7562
|
-
`</form>`,
|
|
7563
|
-
back,
|
|
7564
|
-
].join("\n"), { chrome });
|
|
7565
|
-
}
|
|
7566
|
-
const agent = data.chosen;
|
|
7567
|
-
if (agent === undefined || agent.run === null)
|
|
7568
|
-
return screen("tournament", `<p class="meta">Nothing to confirm</p>`, { chrome });
|
|
7569
|
-
const run = agent.run;
|
|
7570
|
-
return screen("tournament", [
|
|
7571
|
-
`<h1>Pick agent ${agent.contestant.ordinal}'s result?</h1>`,
|
|
7572
|
-
`<div class="card">`,
|
|
7573
|
-
`<p class="row"><strong>Agent ${agent.contestant.ordinal}</strong> — ${escape(agent.contestant.provider)} · ${escape(agent.contestant.model)}</p>`,
|
|
7574
|
-
`<p class="row">its result becomes the outcome of <strong>${escape(data.taskTitle)}</strong> — the task is marked done, keyed to <a href="/r/${run.id}">this build</a></p>`,
|
|
7575
|
-
run.outcome === "no-change"
|
|
7576
|
-
? `<p class="row">the result is a verified <strong>no change</strong> — the agent concluded nothing needed doing, and its checkout still matches the starting point</p>`
|
|
7577
|
-
: `<p class="row">the changes live on branch <span class="mono">${escape(run.branch ?? "?")}</span>` +
|
|
7578
|
-
`${run.headRevision === null ? "" : `, ending at <span class="mono">${escape(run.headRevision.slice(0, 12))}</span>`}</p>`,
|
|
7579
|
-
agent.diff === null
|
|
7580
|
-
? ""
|
|
7581
|
-
: `<p class="row">the diff being picked: <span class="mono">${escape(agent.diff.sha256.slice(0, 12))}</span> · ${agent.diff.bytesStored} bytes, verified</p>`,
|
|
7582
|
-
data.publication === null || data.publication === undefined
|
|
7583
|
-
? `<p class="row"><strong>Nothing is published</strong> — the branch stays local to this machine</p>`
|
|
7584
|
-
: `<p class="row"><strong>A ${data.publication.draft ? "draft " : ""}pull request will be opened</strong> on ` +
|
|
7585
|
-
`<span class="mono">${escape(data.publication.githubRepo)}</span> from <span class="mono">${escape(data.publication.branch)}</span></p>`,
|
|
7586
|
-
data.contestKind === "comparison"
|
|
7587
|
-
? `<p class="row">this agent's spend: ${agent.contestant.unknownSpend ? "unmeasured — its harness reports tokens, not dollars" : `${escape(contestDollars(agent.contestant.measuredMicrousd))} measured`}` +
|
|
7588
|
-
`; the comparison measured ${escape(contestDollars(data.totalMicrousd))} across the lanes that report dollars</p>`
|
|
7589
|
-
: `<p class="row">this agent was charged ${escape(contestDollars(agent.contestant.accountedMicrousd))}` +
|
|
7590
|
-
`${agent.contestant.unknownSpend ? " (its full reservation — the exact figure was unknowable)" : ""}` +
|
|
7591
|
-
`; the tournament charged ${escape(contestDollars(data.totalMicrousd))} in total</p>`,
|
|
7592
|
-
`<p class="row">the other ${data.agents - 1} agent${data.agents - 1 === 1 ? "'s result is" : "s' results are"} not used — their branches and evidence are kept for reference</p>`,
|
|
7593
|
-
`</div>`,
|
|
7594
|
-
`<form method="post" action="/contest/${data.contestId}/pick" class="card approve-form">`,
|
|
7595
|
-
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
|
|
7596
|
-
`<input type="hidden" name="nonce" value="${escape(data.nonceValue)}">`,
|
|
7597
|
-
`<input type="hidden" name="choice" value="${agent.contestant.id}">`,
|
|
7598
|
-
`<label>Your password, typed again<input type="password" name="token" autocomplete="current-password"></label>`,
|
|
7599
|
-
`<div class="sticky-actions"><button type="submit">Yes — pick this result</button></div>`,
|
|
7600
|
-
`</form>`,
|
|
7601
|
-
back,
|
|
7602
|
-
].filter(one => one !== "").join("\n"), { chrome });
|
|
7603
|
-
}
|
|
7604
7074
|
/**
|
|
7605
7075
|
* owner/name from a git remote URL — FULLY ANCHORED https/ssh github.com
|
|
7606
7076
|
* forms only (ghlist review, finding 2): a foreign host carrying
|
|
@@ -8037,7 +7507,7 @@ export const NAV_ICONS = {
|
|
|
8037
7507
|
* The workspace shell (package 1): three primary rows (chat, work,
|
|
8038
7508
|
* projects) above two accordion groups. Work tools is where work is
|
|
8039
7509
|
* arranged, planned, and scheduled — the board, its order view, the
|
|
8040
|
-
* flat task list, recipes,
|
|
7510
|
+
* flat task list, recipes, the portfolio, and the action
|
|
8041
7511
|
* ledger. Settings is who and what the fleet runs on — fleet,
|
|
8042
7512
|
* requirements, people, operating mode, system — plus the agent-defaults
|
|
8043
7513
|
* page where the console offers it. Both draw from the same two lists on
|
|
@@ -8052,7 +7522,6 @@ export function workToolRows(scoped = false, _offersChat = false, offersCode = f
|
|
|
8052
7522
|
{ key: "board", href: "/board", label: "Board", hint: "lanes by state, with the order view" },
|
|
8053
7523
|
{ key: "tasks", href: "/tasks", label: "Task list", hint: "everything, filterable by state" },
|
|
8054
7524
|
{ key: "recipes", href: "/recipes", label: "Recipes", hint: "choose, customize, and reuse a workflow" },
|
|
8055
|
-
{ key: "routines", href: "/routines", label: "Routines", hint: "scheduled tracks and their firings" },
|
|
8056
7525
|
{ key: "workbench", href: "/workbench", label: "Portfolio", hint: "every project and live build in one place" },
|
|
8057
7526
|
{ key: "ledger", href: "/ledger", label: "Action ledger", hint: "who acted, what happened, and the result" },
|
|
8058
7527
|
{ key: "spend", href: "/spend", label: "Spend", hint: "what agent work cost, and monthly budgets" },
|
|
@@ -8071,7 +7540,7 @@ export function settingsRows(scoped = false, offersSettings = false) {
|
|
|
8071
7540
|
return scoped ? rows.filter(row => row.key === "people" || row.key === "settings") : rows;
|
|
8072
7541
|
}
|
|
8073
7542
|
/** Which accordion group opens by default for a given active page. */
|
|
8074
|
-
export const TOOL_KEYS = new Set(["code", "inbox", "board", "queue", "tasks", "workbench", "recipes", "
|
|
7543
|
+
export const TOOL_KEYS = new Set(["code", "inbox", "board", "queue", "tasks", "workbench", "recipes", "ledger", "spend"]);
|
|
8075
7544
|
export const SETTINGS_KEYS = new Set(["settings", "fleet", "caps", "people", "mode", "system"]);
|
|
8076
7545
|
/** The builds screen's views (reduction pass §1): done, the review queue,
|
|
8077
7546
|
* and activity are ways of looking at builds, not destinations. */
|
|
@@ -8469,14 +7938,14 @@ export function revisionLineageWords(lineage) {
|
|
|
8469
7938
|
}
|
|
8470
7939
|
else if (lineage.terms !== null) {
|
|
8471
7940
|
const terms = lineage.terms;
|
|
8472
|
-
words.push(`inherited terms, as they stand now: ${
|
|
7941
|
+
words.push(`inherited terms, as they stand now: ${qualityModeTitle(terms.qualityMode)} quality · ` +
|
|
8473
7942
|
`${terms.permissionMode === "bypassPermissions" ? "full access" : "auto permissions"} · ` +
|
|
8474
7943
|
`${terms.budgetMicrousd === null ? "no attempt cap" : `$${(terms.budgetMicrousd / 1_000_000).toFixed(2)} attempt cap`} · ` +
|
|
8475
7944
|
`${terms.exclusions ? "its exclusions" : "no exclusions"} · ${terms.touches === 0 ? "any path" : `${terms.touches} path limit${terms.touches === 1 ? "" : "s"}`} · ` +
|
|
8476
7945
|
`${terms.criteria} criteri${terms.criteria === 1 ? "on" : "a"}${terms.routeOverrides === 0 ? "" : ` · ${terms.routeOverrides} agent override${terms.routeOverrides === 1 ? "" : "s"}`}`);
|
|
8477
7946
|
}
|
|
8478
|
-
words.push("re-resolved for this approval: the agents route
|
|
8479
|
-
words.push("never inherited: the source's approval,
|
|
7947
|
+
words.push("re-resolved for this approval: the agents route — a yes on the source never covers it");
|
|
7948
|
+
words.push("never inherited: the source's approval, publication and merge grants");
|
|
8480
7949
|
if (lineage.repair !== null) {
|
|
8481
7950
|
const repair = lineage.repair;
|
|
8482
7951
|
words.push(`repair chain rooted at ${repair.rootTask}: ${repair.attemptsUsed} attempt${repair.attemptsUsed === 1 ? "" : "s"} used` +
|
|
@@ -8745,16 +8214,6 @@ export function taskBodyParts(data) {
|
|
|
8745
8214
|
}
|
|
8746
8215
|
return box("problem", "Dispatch unknown", "Refresh this task before relying on its scheduler state.", "unknown");
|
|
8747
8216
|
})();
|
|
8748
|
-
const contest = data.contest ?? null;
|
|
8749
|
-
const contestCard = contest === null
|
|
8750
|
-
? ""
|
|
8751
|
-
: [
|
|
8752
|
-
`<div class="card">`,
|
|
8753
|
-
`<p><strong>${contestNoun(contest.kind)}</strong> <span class="meta">${contest.agents} agents on this task</span></p>`,
|
|
8754
|
-
`<p class="row">${escape((CONTEST_STATE_WORDS[contest.state] ?? "the tournament is in an unexpected state — the records have the detail").replace(/tournament/g, contestNoun(contest.kind)))}</p>`,
|
|
8755
|
-
`<p class="row"><a href="/contest/${contest.id}">${contest.state === "pick-wait" ? "compare the results and pick one" : `see the ${contestNoun(contest.kind)}`}</a></p>`,
|
|
8756
|
-
`</div>`,
|
|
8757
|
-
].join("\n");
|
|
8758
8217
|
// Operator steering (arc 1): notes for the agent, each wearing exactly
|
|
8759
8218
|
// where it stands — waiting, attached, proven delivered, or superseded.
|
|
8760
8219
|
const steering = data.steering ?? [];
|
|
@@ -8771,7 +8230,7 @@ export function taskBodyParts(data) {
|
|
|
8771
8230
|
.map(one => `<p class="row"><span class="meta">${escape(one.author)} · ${whenTime(one.createdAt)} · ${escape(steerState(one))}</span> ` +
|
|
8772
8231
|
`${escape(one.note)}</p>`)
|
|
8773
8232
|
.join("\n");
|
|
8774
|
-
const steerForm = data.csrf === "" || task.state === "done" || task.state === "cancelled"
|
|
8233
|
+
const steerForm = data.csrf === "" || task.state === "done" || task.state === "cancelled"
|
|
8775
8234
|
? ""
|
|
8776
8235
|
: `<form method="post" action="${taskHref(task.id)}/steer" class="row">` +
|
|
8777
8236
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">` +
|
|
@@ -8800,18 +8259,6 @@ export function taskBodyParts(data) {
|
|
|
8800
8259
|
scope.outOfScope === null ? "" : `<p><strong>Not this</strong></p><p class="recap">${escape(scope.outOfScope)}</p>`,
|
|
8801
8260
|
scope.touches.length === 0 ? "" : `<p class="scope-paths"><strong>Touches</strong> ${scope.touches.map(one => escape(one)).join(", ")}</p>`,
|
|
8802
8261
|
`<p><strong>Quality</strong> ${escape(qualityModeTitle(scope.qualityMode ?? "default"))}</p>`,
|
|
8803
|
-
// The fallback chain, on the card the yes reads (Layer F): the
|
|
8804
|
-
// digest binds the WHOLE chain, so every entry — credential
|
|
8805
|
-
// included — is said before anyone signs.
|
|
8806
|
-
(() => {
|
|
8807
|
-
const chain = chainFromJson(scope.proposedChainJson ?? null);
|
|
8808
|
-
if (chain === null || chain.length < 2)
|
|
8809
|
-
return "";
|
|
8810
|
-
return `<p><strong>If its subscription runs out</strong></p><p class="recap">${chain
|
|
8811
|
-
.slice(1)
|
|
8812
|
-
.map(one => `falls back to ${escape(one.profile.provider)} (${escape(one.profile.model)}) — ${one.authMode === "api-key" ? "your API key; spend moves to that account" : "its subscription login"}`)
|
|
8813
|
-
.join("; ")}</p>`;
|
|
8814
|
-
})(),
|
|
8815
8262
|
acceptanceCeremonyHtml(scope.acceptance),
|
|
8816
8263
|
approval.approved
|
|
8817
8264
|
? `<p class="meta scope-seal">Approved by ${escape(approval.by)} · ${whenTime(approval.at)} · <span class="seal">signs ${shortDigest(scope.digest)}</span><span class="so-sr-only"> — approval binds to this exact wording</span></p>`
|
|
@@ -8943,7 +8390,6 @@ export function taskBodyParts(data) {
|
|
|
8943
8390
|
revision: data.revision == null || "problem" in data.revision ? null : data.revision,
|
|
8944
8391
|
revisionSourceHref: data.revision == null || "problem" in data.revision ? "" : `/r/${data.revision.sourceRun}`,
|
|
8945
8392
|
repairChain: data.repairChain ?? null,
|
|
8946
|
-
raceTerms: data.raceTerms ?? null,
|
|
8947
8393
|
route: data.route,
|
|
8948
8394
|
coordinator: data.coordinator ?? null,
|
|
8949
8395
|
deliverable: data.deliverable ?? "branch",
|
|
@@ -8951,55 +8397,10 @@ export function taskBodyParts(data) {
|
|
|
8951
8397
|
notNowHref: "/work",
|
|
8952
8398
|
sticky: data.dispatch?.action === "approve-scope",
|
|
8953
8399
|
earlier: { active: data.assignment?.earlierActive ?? 0, running: data.assignment?.earlierRunning ?? 0 },
|
|
8954
|
-
|
|
8955
|
-
edit: data.csrf === "" || (data.raceTerms ?? null) !== null ? null
|
|
8400
|
+
edit: data.csrf === "" ? null
|
|
8956
8401
|
: { action: `${taskHref(task.id)}/scope`, draft: inlineScopeDraft ? data.scopeDraft ?? null : null, problem: inlineScopeDraft ? data.problem : null,
|
|
8957
8402
|
open: data.editPlan === true, stepsHref: data.planSha != null && data.planDocument !== null ? "#plan-edit" : null },
|
|
8958
8403
|
});
|
|
8959
|
-
// The attended road (Phase 2E): beside the approval, never replacing it.
|
|
8960
|
-
// The mint button leads to the CONFIRM screen where every term renders
|
|
8961
|
-
// and the password signs; an open authorization shows its state, its
|
|
8962
|
-
// revoke, and — through the page script — the liveness beat that IS
|
|
8963
|
-
// "while you watch".
|
|
8964
|
-
const attended = data.attended ?? null;
|
|
8965
|
-
const attendedCard = attended === null
|
|
8966
|
-
? ""
|
|
8967
|
-
: attended.open !== null
|
|
8968
|
-
? [
|
|
8969
|
-
`<div class="card" data-attended="${escape(attended.open.id)}">`,
|
|
8970
|
-
`<p><strong>Attended session</strong> <span class="meta">${escape(attended.open.state)}</span></p>`,
|
|
8971
|
-
`<p class="meta">${attended.open.running ? "the agent is running — your open console keeps it live" : "waiting to dispatch to this machine"} · ${attended.open.turnsUsed}/${attended.open.cap} messages · $${(attended.open.spentMicrousd / 1_000_000).toFixed(2)} of $${(attended.open.budgetMicrousd / 1_000_000).toFixed(2)} · everything ends by ${whenTime(attended.open.expiresAt)}</p>`,
|
|
8972
|
-
`<p class="meta">This page being open keeps it alive — close it and the session winds down within a minute</p>`,
|
|
8973
|
-
`<form method="post" action="${taskHref(task.id)}/attend-revoke" class="inline">`,
|
|
8974
|
-
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
|
|
8975
|
-
`<button type="submit">Revoke — stop the session</button>`,
|
|
8976
|
-
`</form>`,
|
|
8977
|
-
`</div>`,
|
|
8978
|
-
].join("\n")
|
|
8979
|
-
: attended.canMint
|
|
8980
|
-
? [
|
|
8981
|
-
`<form method="post" action="${taskHref(task.id)}/attend-preview" class="card">`,
|
|
8982
|
-
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
|
|
8983
|
-
`<p><strong>Or run it once while you watch</strong></p>`,
|
|
8984
|
-
`<p class="meta">No approval filed: one attempt, on this machine, only while this page is open. You will read every term before ${attended.mint?.quick === true ? "you confirm it" : "your password signs it"}.</p>`,
|
|
8985
|
-
attended.mint === undefined || attended.mint.models.length <= 1
|
|
8986
|
-
? ""
|
|
8987
|
-
: `<label>Model <span class="meta">(watched sessions run claude only)</span><select name="model">` +
|
|
8988
|
-
attended.mint.models
|
|
8989
|
-
.map(model => `<option value="${escape(model)}"${model === attended.mint?.pinnedModel ? " selected" : ""}>${escape(model)}</option>`)
|
|
8990
|
-
.join("") +
|
|
8991
|
-
`</select></label>`,
|
|
8992
|
-
attended.mint === undefined
|
|
8993
|
-
? ""
|
|
8994
|
-
: `<label>Permissions<select name="posture">` +
|
|
8995
|
-
`<option value="auto"${attended.mint.posture === "auto" ? " selected" : ""}>safe unattended — routine project commands and edits proceed</option>` +
|
|
8996
|
-
`<option value="acceptEdits"${attended.mint.posture === "acceptEdits" ? " selected" : ""}>legacy acceptEdits — commands that ask are denied unattended</option>` +
|
|
8997
|
-
`<option value="bypassPermissions"${attended.mint.posture === "bypassPermissions" ? " selected" : ""}>full permissions — nothing asks</option>` +
|
|
8998
|
-
`</select></label>`,
|
|
8999
|
-
`<button type="submit">Read the terms</button>`,
|
|
9000
|
-
`</form>`,
|
|
9001
|
-
].join("\n")
|
|
9002
|
-
: "";
|
|
9003
8404
|
const scopeForm = [
|
|
9004
8405
|
`<details${scope === null || scopeDraft !== undefined ? " open" : ""}><summary>${scope === null ? "Write the scope" : "Edit the scope"}${approval.approved ? " (editing voids the approval)" : ""}</summary>`,
|
|
9005
8406
|
`<form method="post" action="${taskHref(task.id)}/scope" class="scope-editor">`,
|
|
@@ -9040,50 +8441,6 @@ export function taskBodyParts(data) {
|
|
|
9040
8441
|
qualityModeChoices("quality-mode", selected) +
|
|
9041
8442
|
`<p class="meta permission-note">This choice is signed into the scope. Inspect the saved work and actual check results when it is ready. Publication and deployment need their own authorization.</p></fieldset>`;
|
|
9042
8443
|
})(),
|
|
9043
|
-
(() => {
|
|
9044
|
-
// The tournament controls (operator request): how many agents compete,
|
|
9045
|
-
// on which model, under which dollars. "One agent" is the ordinary
|
|
9046
|
-
// path; anything more files race terms beside the scope, and the ONE
|
|
9047
|
-
// approval above restates and covers both.
|
|
9048
|
-
const defaults = data.spendDefaults ?? null;
|
|
9049
|
-
const terms = data.raceTerms ?? null;
|
|
9050
|
-
const selectedCount = scopeDraft === undefined ? terms !== null ? terms.n : defaults?.raceAgents ?? 0 : Number(scopeDraft.get("race-count") ?? "0");
|
|
9051
|
-
const selectedModel = scopeDraft?.get("race-model") ?? terms?.agents[0]?.model ?? "claude-sonnet-5";
|
|
9052
|
-
const perPrefill = terms !== null
|
|
9053
|
-
? (terms.perAgentBudgetMicrousd / 1_000_000).toFixed(2)
|
|
9054
|
-
: defaults?.racePerAgentMicrousd != null
|
|
9055
|
-
? (defaults.racePerAgentMicrousd / 1_000_000).toFixed(2)
|
|
9056
|
-
: "";
|
|
9057
|
-
const totalPrefill = terms !== null
|
|
9058
|
-
? (terms.totalBudgetMicrousd / 1_000_000).toFixed(2)
|
|
9059
|
-
: defaults?.raceTotalMicrousd != null
|
|
9060
|
-
? (defaults.raceTotalMicrousd / 1_000_000).toFixed(2)
|
|
9061
|
-
: "";
|
|
9062
|
-
return [
|
|
9063
|
-
`<details class="more-agents"${selectedCount > 0 ? " open" : ""}><summary>More than one agent (optional)</summary>`,
|
|
9064
|
-
`<label>How many agents compete <span class="meta">(a tournament builds the task independently N times — you compare and pick one)</span>` +
|
|
9065
|
-
`<select name="race-count">` +
|
|
9066
|
-
`<option value=""${selectedCount === 0 ? " selected" : ""}>one agent — no tournament</option>` +
|
|
9067
|
-
[2, 3, 4].map(count => `<option value="${count}"${selectedCount === count ? " selected" : ""}>${count} agents</option>`).join("") +
|
|
9068
|
-
`</select></label>`,
|
|
9069
|
-
`<label>Competing model <select name="race-model">` +
|
|
9070
|
-
PRICED_BUILD_MODELS.map(model => `<option value="${escape(model)}"${model === selectedModel ? " selected" : ""}>${escape(model)}</option>`).join("") +
|
|
9071
|
-
`</select></label>`,
|
|
9072
|
-
`<label>Each competing agent may spend ($)<input type="number" name="race-per-usd" step="0.01" min="0.01" value="${escape(scopeDraft?.get("race-per-usd") ?? perPrefill)}"></label>`,
|
|
9073
|
-
`<label>The whole tournament may spend ($)<input type="number" name="race-total-usd" step="0.01" min="0.01" value="${escape(scopeDraft?.get("race-total-usd") ?? totalPrefill)}"></label>`,
|
|
9074
|
-
// The comparison lanes (Phase 3 slice B): 2-4 rows, any registered
|
|
9075
|
-
// provider, exact model required — no dollar fields exist. Blank
|
|
9076
|
-
// rows are unused; filling any row files a comparison INSTEAD of a
|
|
9077
|
-
// tournament, and mixing the two refuses in words.
|
|
9078
|
-
`<p class="meta" style="margin-top:.75rem">or compare different agents side by side — no dollar caps; each agent's clock is its bound, and spend lands measured only where the harness reports dollars:</p>`,
|
|
9079
|
-
...[1, 2, 3, 4].map(lane => `<div class="row"><label>Agent ${lane} <select name="compare-provider-${lane}">` +
|
|
9080
|
-
`<option value="">—</option>` +
|
|
9081
|
-
PROVIDER_IDS.map(provider => `<option value="${escape(provider)}"${scopeDraft?.get(`compare-provider-${lane}`) === provider ? " selected" : ""}>${escape(provider)}</option>`).join("") +
|
|
9082
|
-
`</select></label>` +
|
|
9083
|
-
`<label>Its exact model<input type="text" name="compare-model-${lane}" value="${escape(scopeDraft?.get(`compare-model-${lane}`) ?? "")}" placeholder="e.g. gemini-2.5-pro"></label></div>`),
|
|
9084
|
-
`</details>`,
|
|
9085
|
-
].join("\n");
|
|
9086
|
-
})(),
|
|
9087
8444
|
`<button type="submit">Save scope</button>`,
|
|
9088
8445
|
`</form></details>`,
|
|
9089
8446
|
].join("\n");
|
|
@@ -9509,7 +8866,6 @@ export function taskBodyParts(data) {
|
|
|
9509
8866
|
// (slice 1c) rides beside the main column on wide screens and above it
|
|
9510
8867
|
// on narrow ones.
|
|
9511
8868
|
`<div class="task-layout"><div class="task-main">`,
|
|
9512
|
-
contestCard,
|
|
9513
8869
|
attemptPanel,
|
|
9514
8870
|
data.coordinatorProposals == null || data.coordinatorProposals.rows.length === 0
|
|
9515
8871
|
? ""
|
|
@@ -9531,7 +8887,7 @@ export function taskBodyParts(data) {
|
|
|
9531
8887
|
section("scope",
|
|
9532
8888
|
// The recipe road rides with the scope it reuses (UI polish
|
|
9533
8889
|
// 2026-09-13), off the title-to-action path.
|
|
9534
|
-
["<h2>Scope</h2>", scopeCard, data.repo !== null && data.scope !== null ? `<p class="meta"><a href="/recipes/from-task?task=${encodeURIComponent(task.id)}">Reuse this scope as a recipe →</a></p>` : "", agentsCardHtml(task.id, data.route, data.csrf, data.canEditRoute === true), revisionInApproval ? "" : revisionCard, data.completion != null ? "" : repairChainHtml(data.repairChain ?? null),
|
|
8890
|
+
["<h2>Scope</h2>", scopeCard, data.repo !== null && data.scope !== null ? `<p class="meta"><a href="/recipes/from-task?task=${encodeURIComponent(task.id)}">Reuse this scope as a recipe →</a></p>` : "", agentsCardHtml(task.id, data.route, data.csrf, data.canEditRoute === true), revisionInApproval ? "" : revisionCard, data.completion != null ? "" : repairChainHtml(data.repairChain ?? null), scopeForm].join("\n"), scopeDraft !== undefined || (data.plan !== "requested" && approveForm === "" && !(scope === null && canPlan))),
|
|
9535
8891
|
dependencyChoiceNeeded ? "" : section("waits for", waitsForCard, (data.waitsFor ?? []).length > 0, (data.waitsFor ?? []).length),
|
|
9536
8892
|
section("holds", holds, true, data.holds.length),
|
|
9537
8893
|
cancelAct,
|
|
@@ -9754,7 +9110,7 @@ export function taskBodyParts(data) {
|
|
|
9754
9110
|
{ key: "history", html: data.history ?? "" }, { key: "control", html: stop === null ? controlHtml : taskControlDetailsHtml(data.control, task.id, data.csrf, "task", false, true) }, { key: "problem", html: problemHtml },
|
|
9755
9111
|
{ key: "pull-request", html: pullRequestCard },
|
|
9756
9112
|
{ key: "needs-scope", html: needsScopeCard },
|
|
9757
|
-
{ key: "mirror", html: mirrorCard }, { key: "
|
|
9113
|
+
{ key: "mirror", html: mirrorCard }, { key: "attempt", html: attemptPanel },
|
|
9758
9114
|
].filter(one => one.html !== ""),
|
|
9759
9115
|
// Open questions are answered in the thread.
|
|
9760
9116
|
questions: "",
|
|
@@ -9789,11 +9145,7 @@ export function taskBody(data) {
|
|
|
9789
9145
|
return taskBodyParts(data).html;
|
|
9790
9146
|
}
|
|
9791
9147
|
export function taskPage(chrome, data) {
|
|
9792
|
-
// The
|
|
9793
|
-
// approver's sessions live; this page no longer carries its own.
|
|
9794
|
-
//
|
|
9795
|
-
// The sensitive-page composition guard (slice 1c): a live attended
|
|
9796
|
-
// attempt can coexist with an unapproved scope, so this page may carry a
|
|
9148
|
+
// The sensitive-page composition guard (slice 1c): this page may carry a
|
|
9797
9149
|
// password ceremony. sendScreen() would keep a functional script on such
|
|
9798
9150
|
// a page — so the decision is made HERE, from the rendered body itself:
|
|
9799
9151
|
// when the body (or the chrome's list pane) shows a password input, the
|
|
@@ -10238,7 +9590,7 @@ export function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab,
|
|
|
10238
9590
|
taskHref: taskHref(view.taskId), chatHref: taskChatHref(view.taskId),
|
|
10239
9591
|
status: { label: status.label, tone: status.tone, token: status.token },
|
|
10240
9592
|
problem: view.detail === null ? view.historyProblem : null,
|
|
10241
|
-
next: reviewNextActionOf(view, csrf), complete: null, decision: null, checks: null, intent: intentView, noRun: null, panel: null,
|
|
9593
|
+
next: reviewNextActionOf(view, csrf), complete: null, decision: null, checks: null, intent: intentView, noRun: null, panel: null,
|
|
10242
9594
|
notes: view.notes.map(one => ({ author: one.author, at: one.createdAt, note: one.note })),
|
|
10243
9595
|
acts: { primary: null, secondary: null, line: null }, runChecks: null, mismatch: null,
|
|
10244
9596
|
// The raw run record lives under Details now (2026-10-02): /r/<id> for this result redirects here.
|
|
@@ -10282,8 +9634,7 @@ export function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab,
|
|
|
10282
9634
|
headStatus: false,
|
|
10283
9635
|
action: false,
|
|
10284
9636
|
});
|
|
10285
|
-
|
|
10286
|
-
parts.push(`<div id="verification" data-cockpit-section="result">` + panel.html + contest + `</div>`);
|
|
9637
|
+
parts.push(`<div id="verification" data-cockpit-section="result">` + panel.html + `</div>`);
|
|
10287
9638
|
const blocked = cantAcceptYetOf(proof?.verdict ?? null, proof?.reasons ?? [], accepted);
|
|
10288
9639
|
const youCheck = panel.panel.youCheck;
|
|
10289
9640
|
// Accept and finish posts the completion; when an acceptance is owed (the person's own checks, or a reason a
|
|
@@ -10378,7 +9729,7 @@ export function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab,
|
|
|
10378
9729
|
return {
|
|
10379
9730
|
html: `<section class="cockpit-detail">${parts.join("\n")}</section>`,
|
|
10380
9731
|
selected: {
|
|
10381
|
-
...selected, complete: failure === null ? complete : null, decision: failure === null ? decision : null,
|
|
9732
|
+
...selected, complete: failure === null ? complete : null, decision: failure === null ? decision : null, acts, actFacts, runChecks, mismatch, failure,
|
|
10382
9733
|
// A failed build reads Failed, whatever its task has done since; its outcome is what went wrong.
|
|
10383
9734
|
// Run checks is the decision's own act on this page; a status row never sends the person to Chat for it.
|
|
10384
9735
|
panel: (() => {
|
|
@@ -10461,9 +9812,6 @@ export function reviewNextActionOf(view, csrf) {
|
|
|
10461
9812
|
if (run === null || detail === null) {
|
|
10462
9813
|
return card("inspect-task", "No build to review", "This task was marked complete without a build record.", `<a class="button-link" href="${taskHref(view.taskId)}">Open the task</a>`);
|
|
10463
9814
|
}
|
|
10464
|
-
if (view.contest !== null && view.contest.state === "pick-wait") {
|
|
10465
|
-
return card("compare-contest", `Compare the ${contestNoun(view.contest.kind)}`, `${view.contest.agents} agents finished — compare their results side by side and pick one.`, `<a class="button-link" href="/contest/${view.contest.id}">Compare results</a>`);
|
|
10466
|
-
}
|
|
10467
9815
|
if (detail.ciFailing && csrf !== "") {
|
|
10468
9816
|
return card("draft-repair", "CI is failing on its pull request", "Toolroll confirmed the failure. Draft one repair task, then approve it before it runs.", `<form method="post" action="/r/${run.id}/draft-repair"><input type="hidden" name="csrf" value="${escape(csrf)}"><button type="submit">Draft a repair task</button></form>`);
|
|
10469
9817
|
}
|
|
@@ -11773,42 +11121,8 @@ export function runFactsRows(run, taskId, live, route = null) {
|
|
|
11773
11121
|
? `<p class="row"><span class="meta" style="min-width:8.5rem">recorded error</span></p><pre class="mono" id="run-reason-detail" style="white-space:pre-wrap;overflow-wrap:anywhere">${escape(run.reason)}</pre>`
|
|
11774
11122
|
: ""));
|
|
11775
11123
|
}
|
|
11776
|
-
export function runPage(chrome, run, taskId, artifacts, terminal = null, notes = [], csrf = "", comments = [], ciRepair = null, liveScript, peekable = false, running = false, editor = null, editorToggle = null, noted = false,
|
|
11124
|
+
export function runPage(chrome, run, taskId, artifacts, terminal = null, notes = [], csrf = "", comments = [], ciRepair = null, liveScript, peekable = false, running = false, editor = null, editorToggle = null, noted = false, structuredHandoff = null, proofBundle = null, route = null, publicationRow = null, review = null, result = null, sourceDigest = null) {
|
|
11777
11125
|
const rows = runFactsRows(run, taskId, running, route);
|
|
11778
|
-
// The conversation (Phase 2E, v2 S1g): every stdin injection as the
|
|
11779
|
-
// ledger records it — author named for operator turns, machine turns
|
|
11780
|
-
// say so — and the TURN BOX while the session is held. An unconfirmed
|
|
11781
|
-
// turn says honestly that the agent may or may not have seen it.
|
|
11782
|
-
const turnWords = (turn) => turn.state === "settled"
|
|
11783
|
-
? ""
|
|
11784
|
-
: turn.state === "uncertain"
|
|
11785
|
-
? " · unconfirmed — the agent may or may not have seen this; its cost is counted at worst case"
|
|
11786
|
-
: turn.state === "cancelled"
|
|
11787
|
-
? " · never reached the agent"
|
|
11788
|
-
: " · the agent is working on this";
|
|
11789
|
-
const conversation = heldTurns === null
|
|
11790
|
-
? ""
|
|
11791
|
-
: `<h2>Conversation</h2>` +
|
|
11792
|
-
(heldTurns.turns.length === 0
|
|
11793
|
-
? `<p class="meta">Nothing said yet</p>`
|
|
11794
|
-
: heldTurns.turns
|
|
11795
|
-
.map(turn => `<p class="row"><span class="meta">${turn.sourceKind === "operator"
|
|
11796
|
-
? escape(turn.author ?? "operator")
|
|
11797
|
-
: turn.sourceKind === "brief"
|
|
11798
|
-
? "the brief"
|
|
11799
|
-
: turn.sourceKind === "answer"
|
|
11800
|
-
? "your answer"
|
|
11801
|
-
: "repair (machine)"} · ${whenTime(turn.recordedAt)}${turnWords(turn)}</span> ${escape(oneLineOf(turn.text, 240))}</p>`)
|
|
11802
|
-
.join("\n")) +
|
|
11803
|
-
(heldTurns.open && csrf !== ""
|
|
11804
|
-
? `<form method="post" action="/r/${run.id}/turn" class="row">` +
|
|
11805
|
-
`<input type="hidden" name="csrf" value="${escape(csrf)}">` +
|
|
11806
|
-
`<input type="text" name="text" maxlength="500" placeholder="say something to the agent — it reads this as its next instruction, inside the approved scope" aria-label="turn" style="width:100%;max-width:34rem">` +
|
|
11807
|
-
`<button type="submit">Send</button></form>` +
|
|
11808
|
-
`<p class="meta">${escape(heldTurns.state)} · ${heldTurns.turns.length}/${heldTurns.cap} messages · a waiting question must be answered before free-form messages</p>`
|
|
11809
|
-
: heldTurns.open
|
|
11810
|
-
? ""
|
|
11811
|
-
: `<p class="meta">The session has ended — the record above is complete</p>`);
|
|
11812
11126
|
// The live peek region (A2): the poller fills it only on a serve that
|
|
11813
11127
|
// asserted its runner. Without the assertion the section still appears
|
|
11814
11128
|
// for a running build and says honestly why it is empty \u2014 a page that
|
|
@@ -11927,19 +11241,6 @@ export function runPage(chrome, run, taskId, artifacts, terminal = null, notes =
|
|
|
11927
11241
|
`<input type="text" name="note" placeholder="a note for whoever reads this run next" aria-label="run note" style="width:100%;max-width:28rem">` +
|
|
11928
11242
|
`<button type="submit">Add note</button></form>`;
|
|
11929
11243
|
const notesCard = noteRows === "" && noteForm === "" ? "" : `<h2>Operator notes</h2>${noteRows}${noteForm}`;
|
|
11930
|
-
// Continuation (Phase 2E, A4): a finished attempt offers a watched
|
|
11931
|
-
// follow-up — the text you type here enters the SIGNED terms on the
|
|
11932
|
-
// confirm screen; nothing runs until your password agrees to exactly it.
|
|
11933
|
-
const continueCard = continueOffer === null || csrf === ""
|
|
11934
|
-
? ""
|
|
11935
|
-
: `<details class="result-request-open continue-watch"><summary>Continue while you watch</summary>` +
|
|
11936
|
-
`<form method="post" action="${taskHref(continueOffer.taskId)}/attend-preview" class="card">` +
|
|
11937
|
-
`<input type="hidden" name="csrf" value="${escape(csrf)}">` +
|
|
11938
|
-
`<input type="hidden" name="parent" value="${run.id}">` +
|
|
11939
|
-
`<p class="meta">Picks up from where this attempt stopped.</p>` +
|
|
11940
|
-
`<label>What next<textarea name="followup" rows="2" maxlength="2000" placeholder="What should the agent do next, within the same scope…"></textarea></label>` +
|
|
11941
|
-
`<button type="submit">Review the terms</button>` +
|
|
11942
|
-
`</form></details>`;
|
|
11943
11244
|
// Outcome first (UI polish 2026-09-13): a finished build leads with its
|
|
11944
11245
|
// result, proof, and diff; the machine facts fold under "Build details".
|
|
11945
11246
|
// A live build keeps the facts open — they are what a watcher polls.
|
|
@@ -11966,7 +11267,6 @@ export function runPage(chrome, run, taskId, artifacts, terminal = null, notes =
|
|
|
11966
11267
|
return screen(`build #${run.id}`, [
|
|
11967
11268
|
`<h1>Build #${run.id} <span class="meta"><a href="${taskHref(taskId)}">${escape(taskId)}</a></span></h1>`,
|
|
11968
11269
|
running ? facts : "",
|
|
11969
|
-
conversation,
|
|
11970
11270
|
transcript,
|
|
11971
11271
|
peek,
|
|
11972
11272
|
...(resultPanel === null
|
|
@@ -11977,7 +11277,6 @@ export function runPage(chrome, run, taskId, artifacts, terminal = null, notes =
|
|
|
11977
11277
|
reviewCard,
|
|
11978
11278
|
]
|
|
11979
11279
|
: [resultPanel, editorToggleForm]),
|
|
11980
|
-
continueCard,
|
|
11981
11280
|
running ? "" : facts,
|
|
11982
11281
|
evidence,
|
|
11983
11282
|
notesCard,
|
|
@@ -12313,6 +11612,7 @@ export function settingsPage(chrome, existing, hasEnv, csrf, problem, messaging
|
|
|
12313
11612
|
kind: "settings",
|
|
12314
11613
|
said: problem,
|
|
12315
11614
|
groups: settingsGroups(messaging, telegramFailing),
|
|
11615
|
+
...(messaging?.legacyWarning ? { legacyWebhookWarning: messaging.legacyWarning } : {}),
|
|
12316
11616
|
theme,
|
|
12317
11617
|
accent: requestContext.getStore()?.accent ?? DEFAULT_ACCENT,
|
|
12318
11618
|
accentPresets: ACCENT_PRESETS,
|
|
@@ -12346,6 +11646,7 @@ export function settingsPage(chrome, existing, hasEnv, csrf, problem, messaging
|
|
|
12346
11646
|
};
|
|
12347
11647
|
return screen("Settings", [
|
|
12348
11648
|
"<h1>Settings</h1>",
|
|
11649
|
+
messaging?.legacyWarning ? `<p data-legacy-webhooks>Legacy webhooks are deprecated. Connect <a href="/settings/slack" style="display:inline-flex;min-height:44px;align-items:center">Slack</a> or <a href="/settings/discord" style="display:inline-flex;min-height:44px;align-items:center">Discord</a> in Chat settings.</p>` : "",
|
|
12349
11650
|
settingsTiles(view.groups),
|
|
12350
11651
|
phone === null ? "" : phoneSetupHtml(phone),
|
|
12351
11652
|
appearanceCard(csrf),
|
|
@@ -12592,9 +11893,6 @@ export function nextPage(chrome, data) {
|
|
|
12592
11893
|
`<p class="meta">Not this</p><p class="recap" style="margin-top:0">${scope?.outOfScope == null ? "<em>no exclusions</em>" : escape(scope.outOfScope)}</p>` +
|
|
12593
11894
|
`<p class="meta">Touches · ${scope === null || scope.touches.length === 0 ? "anything" : scope.touches.map(one => escape(one)).join(", ")}</p>` +
|
|
12594
11895
|
(scope === null ? "" : acceptanceCeremonyHtml(scope.acceptance)) +
|
|
12595
|
-
(data.raceTerms === null
|
|
12596
|
-
? ""
|
|
12597
|
-
: `<p><strong>${data.raceTerms.kind === "comparison" ? "comparison" : "tournament"}</strong></p><p class="meta">${data.raceTerms.n} agents build independently: ${data.raceTerms.agents.map(one => `${escape(one.provider)} · ${escape(one.model)}`).join(" vs ")}.</p>`) +
|
|
12598
11896
|
// The AGENTS the yes freezes (v48): the same concise block the task
|
|
12599
11897
|
// page and chat sign under, before the password — runtime limits one
|
|
12600
11898
|
// tap away, never in the way.
|
|
@@ -12696,11 +11994,6 @@ export async function form(request, cap = BODY_CAP) {
|
|
|
12696
11994
|
}
|
|
12697
11995
|
return new URLSearchParams(Buffer.concat(chunks).toString("utf8"));
|
|
12698
11996
|
}
|
|
12699
|
-
/** Attended liveness in words for the task card — reads the durable clock. */
|
|
12700
|
-
export function attendedWatchWords(lastBeatAt, now, absoluteExpiry) {
|
|
12701
|
-
const state = attendedLivenessState(lastBeatAt === null ? null : Date.parse(lastBeatAt), now.getTime(), Date.parse(absoluteExpiry));
|
|
12702
|
-
return state === "live" ? "watching" : state === "grace" ? "watching (a beat behind)" : state === "expired" ? "expired" : "not watching — reopen this page to resume";
|
|
12703
|
-
}
|
|
12704
11997
|
/** One display line, bounded — turn text is data, never layout. */
|
|
12705
11998
|
export function oneLineOf(text, cap) {
|
|
12706
11999
|
const flat = text.replace(/\s+/g, " ").trim();
|