toolroll 0.9.91 → 0.9.93
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 +41 -0
- package/dist/approval-authority.d.ts +9 -1
- package/dist/approval-authority.js +15 -2
- package/dist/assignment-status.d.ts +1 -1
- package/dist/assignment-ui.js +2 -2
- package/dist/assignment.js +11 -3
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +30 -30
- package/dist/browser-crew.js +2 -2
- package/dist/browser-workspace.d.ts +3 -5
- package/dist/build-review.js +2 -0
- package/dist/builder.js +21 -2
- package/dist/chat-actions.d.ts +11 -3
- package/dist/chat-actions.js +47 -2
- package/dist/chat-decide.js +2 -0
- package/dist/chat-review.d.ts +2 -1
- package/dist/chat-review.js +2 -2
- package/dist/check-facts.d.ts +124 -0
- package/dist/check-facts.js +272 -0
- package/dist/check-report.d.ts +5 -0
- package/dist/check-report.js +4 -3
- package/dist/cli.js +5 -0
- package/dist/contracts/chat-actions.d.ts +10 -0
- package/dist/contracts/chat-actions.js +4 -0
- package/dist/contracts/console-api.d.ts +3 -2
- package/dist/contracts/console-api.js +5 -2
- package/dist/contracts/release-check.d.ts +25 -1
- package/dist/contracts/release-check.js +19 -0
- package/dist/contracts/route-results.d.ts +121 -0
- package/dist/contracts/route-results.js +71 -0
- package/dist/contracts/route.d.ts +343 -5
- package/dist/contracts/route.js +36 -7
- package/dist/contracts/store-json.d.ts +2 -0
- package/dist/desktop-update.d.ts +2 -2
- package/dist/desktop-update.js +13 -9
- package/dist/dispose.js +6 -0
- package/dist/eval-hooks.d.ts +55 -0
- package/dist/eval-hooks.js +227 -4
- package/dist/flaky.d.ts +17 -0
- package/dist/flaky.js +9 -0
- package/dist/flow-engine.js +1 -1
- package/dist/flow-pull-request.js +1 -1
- package/dist/lead-doors.js +4 -2
- package/dist/lead-tools.js +10 -2
- package/dist/local-owner.d.ts +1 -1
- package/dist/local-owner.js +11 -0
- package/dist/memory-cli.d.ts +3 -0
- package/dist/memory-cli.js +3 -3
- package/dist/memory-pass.js +5 -1
- package/dist/model-choices.d.ts +7 -3
- package/dist/model-choices.js +26 -11
- package/dist/models-cli.d.ts +205 -0
- package/dist/models-cli.js +143 -4
- package/dist/models-ui.d.ts +57 -0
- package/dist/models-ui.js +44 -1
- package/dist/needs-you.d.ts +7 -7
- package/dist/needs-you.js +12 -10
- package/dist/operate-remote-arguments.js +4 -4
- package/dist/operate-remote.js +1 -1
- package/dist/operate.js +191 -23
- package/dist/phase-routing.d.ts +82 -2
- package/dist/phase-routing.js +132 -18
- package/dist/project-delete.d.ts +4 -0
- package/dist/project-delete.js +16 -4
- package/dist/project-memory.d.ts +8 -0
- package/dist/project-memory.js +20 -1
- package/dist/pull-request-flow.js +1 -1
- package/dist/push.d.ts +10 -3
- package/dist/push.js +22 -6
- package/dist/release-adapters.js +11 -1
- package/dist/release-approval.d.ts +191 -0
- package/dist/release-approval.js +382 -0
- package/dist/release-auto.js +22 -4
- package/dist/release-card.d.ts +73 -4
- package/dist/release-card.js +168 -23
- package/dist/release-cli.js +9 -1
- package/dist/release.d.ts +30 -0
- package/dist/release.js +42 -3
- package/dist/result-actions.d.ts +4 -0
- package/dist/result-actions.js +3 -0
- package/dist/result-acts.d.ts +3 -3
- package/dist/result-acts.js +1 -1
- package/dist/route-hooks.d.ts +54 -0
- package/dist/route-hooks.js +179 -0
- package/dist/route-results.d.ts +70 -0
- package/dist/route-results.js +340 -0
- package/dist/route-rules.d.ts +324 -0
- package/dist/route-rules.js +781 -0
- package/dist/route-savings.d.ts +136 -0
- package/dist/route-savings.js +193 -0
- package/dist/route-store.d.ts +300 -0
- package/dist/route-store.js +598 -0
- package/dist/scope.d.ts +3 -1
- package/dist/scope.js +4 -1
- package/dist/server/guards.js +1 -0
- package/dist/server/pages-assets.js +1 -1
- package/dist/server/render-chat.js +2 -2
- package/dist/server/render-release.d.ts +17 -0
- package/dist/server/render-release.js +84 -0
- package/dist/server/render-results.d.ts +0 -2
- package/dist/server/render-results.js +4 -6
- package/dist/server/render-tasks.d.ts +7 -0
- package/dist/server/render-tasks.js +16 -3
- package/dist/server/route-table.d.ts +1 -1
- package/dist/server/route-table.js +19 -1
- package/dist/server/settings.d.ts +2 -1
- package/dist/server/settings.js +87 -1
- package/dist/server/task-views.js +6 -0
- package/dist/server/tasks.d.ts +15 -0
- package/dist/server/tasks.js +105 -20
- package/dist/settings-proofs.js +2 -0
- package/dist/spend-ui.d.ts +11 -0
- package/dist/spend-ui.js +49 -2
- package/dist/stop-proof.d.ts +67 -0
- package/dist/stop-proof.js +293 -0
- package/dist/store.d.ts +106 -31
- package/dist/store.js +653 -128
- package/dist/surface.js +17 -5
- package/dist/task-outcome-cli.js +24 -2
- package/dist/toolroll-update-ui.js +2 -2
- package/dist/toolroll-update.js +4 -2
- package/dist/work-index.js +7 -9
- package/dist/work-summary.d.ts +2 -2
- package/package.json +1 -1
package/dist/server/guards.js
CHANGED
|
@@ -176,6 +176,7 @@ export function createSharedGuards(runtime) {
|
|
|
176
176
|
const task = matchTaskPath(url.pathname, '(?:/[a-z-]+)?$');
|
|
177
177
|
return task === null ? null : runtime.store.lookupRef(task.taskId)?.repo ?? null;
|
|
178
178
|
}
|
|
179
|
+
case 'task-ref': return Number.isSafeInteger(id) && id > 0 ? runtime.store.refForId(id)?.repo ?? null : null;
|
|
179
180
|
case 'run': return runRepo(id);
|
|
180
181
|
case 'decision': return runRepo(runtime.store.getDecision(id)?.run);
|
|
181
182
|
case 'incident': return runRepo(runtime.store.openIncidents().find(one => one.id === id)?.run);
|
|
@@ -39,7 +39,7 @@ const PWA_MANIFEST = JSON.stringify({
|
|
|
39
39
|
// paths — the payload URL is data, revalidated, never handed raw to the
|
|
40
40
|
// browser (arc 3 finding 5).
|
|
41
41
|
const PWA_WORKER = `// Toolroll — push only; deliberately NO fetch handler (no offline cache of an authenticated console).
|
|
42
|
-
const SHAPES = [/^\\/next$/, /^\\/review$/, /^\\/system$/, /^\\/routines$/, /^\\/routines\\/[0-9]+$/, /^\\/d\\/[0-9]+$/, /^\\/r\\/[0-9]+$/];
|
|
42
|
+
const SHAPES = [/^\\/next$/, /^\\/review$/, /^\\/system$/, /^\\/routines$/, /^\\/routines\\/[0-9]+$/, /^\\/d\\/[0-9]+$/, /^\\/r\\/[0-9]+$/, /^\\/approve\\/[0-9]+$/];
|
|
43
43
|
self.addEventListener("push", function (event) {
|
|
44
44
|
var data = {};
|
|
45
45
|
try { data = event.data ? event.data.json() : {}; } catch (e) {}
|
|
@@ -34,7 +34,7 @@ import { chatMoney, oneLineOf, relativeAge, safePrUrl, screen, sentenceCase, str
|
|
|
34
34
|
import { chatResultHref, projectChatHref, taskChatHref, taskHref } from "./http.js";
|
|
35
35
|
import {} from "./render-pages.js";
|
|
36
36
|
import { completionReceiptCard } from "./render-results.js";
|
|
37
|
-
import { agentsAvailabilityHtml, agentsBadgesHtml, agentsStripHtml, agentsSummaryWords, agentsWhyHtml, approvalSheetHtml, checkProgressHtml, consentClosedHtml, consentDoorOf, decisionAnswerCard, dispatchHeadline, milestoneProgressHtml, planRevisionLedgerHtml, profileWords, sizeConsequence, taskControlDetailsHtml, taskStatusCard, taskViewSwitch } from "./render-tasks.js";
|
|
37
|
+
import { routeEvidenceHtml, agentsAvailabilityHtml, agentsBadgesHtml, agentsStripHtml, agentsSummaryWords, agentsWhyHtml, approvalSheetHtml, checkProgressHtml, consentClosedHtml, consentDoorOf, decisionAnswerCard, dispatchHeadline, milestoneProgressHtml, planRevisionLedgerHtml, profileWords, sizeConsequence, taskControlDetailsHtml, taskStatusCard, taskViewSwitch } from "./render-tasks.js";
|
|
38
38
|
/** The installation fact that the lead was turned on by default, once (its value: the provider). */
|
|
39
39
|
export const LEAD_BY_DEFAULT_FACT = "lead-on-by-default";
|
|
40
40
|
/** The installation fact that the phone card was put away. */
|
|
@@ -51,7 +51,7 @@ export function taskChatContext(focus) {
|
|
|
51
51
|
// conversation never hides which agent runs.
|
|
52
52
|
focus.route === null
|
|
53
53
|
? ""
|
|
54
|
-
: html `<div class="task-chat-agents-aside"><span class="eyebrow">agents</span><p class="agents-summary">${agentsSummaryWords(focus.route)}</p
|
|
54
|
+
: html `<div class="task-chat-agents-aside"><span class="eyebrow">agents</span><p class="agents-summary">${agentsSummaryWords(focus.route)}</p>${routeEvidenceHtml(focus.route)}<div class="agents-badges">${agentsBadgesHtml(focus.route)}</div>${focus.route.projection === null ? "" : html `${agentsAvailabilityHtml(focus.route.projection)}${agentsWhyHtml(focus.route.projection)}`}<p class="meta"><a href="${taskHref(focus.id)}#agents">Change agents on the task →</a></p></div>`}<div class="task-chat-overview-actions"><a class="task-chat-overview-link" href="${taskHref(focus.id)}">Open full overview →</a></div></aside>`;
|
|
55
55
|
}
|
|
56
56
|
export function taskChatApproval(focus, csrf) {
|
|
57
57
|
const approval = focus.approval;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ApprovalFacts } from "../design/kit.js";
|
|
2
|
+
import { type ReleaseApprovalCard } from "../release-approval.js";
|
|
3
|
+
import { type Chrome, type Screen } from "./chrome.js";
|
|
4
|
+
export type ReleaseSheetData = {
|
|
5
|
+
card: ReleaseApprovalCard;
|
|
6
|
+
/** The single-use approval nonce for this person, task and card digest ("" when nothing can be approved here). */
|
|
7
|
+
nonce: string;
|
|
8
|
+
facts: ApprovalFacts;
|
|
9
|
+
/** This computer's local owner: releases need a full sign-in first (scope.ts localApprovalEligible). */
|
|
10
|
+
local: {
|
|
11
|
+
message: string;
|
|
12
|
+
action: string;
|
|
13
|
+
href: string;
|
|
14
|
+
} | null;
|
|
15
|
+
problem: string | null;
|
|
16
|
+
};
|
|
17
|
+
export declare function releaseSheetPage(chrome: Chrome, data: ReleaseSheetData): Screen;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The release approval sheet (docs/plans/trusted-release-reports.md, slice A, A7): one screen with what the person is
|
|
3
|
+
* approving, GitHub CI now, what approving does, what changed and every term, then one Approve release button. The
|
|
4
|
+
* page only shows: the approval POST (`/t/<id>/approve`, `sheet=release`) proves the person, the card and the scope
|
|
5
|
+
* again (release-approval.ts approveReleaseCard).
|
|
6
|
+
*/
|
|
7
|
+
import { html, joinHtml } from "../html.js";
|
|
8
|
+
import { ciWords, releaseConsequence } from "../release-approval.js";
|
|
9
|
+
import { checkAccessWords } from "../check-access.js";
|
|
10
|
+
import { projectName } from "../project.js";
|
|
11
|
+
import { screen } from "./chrome.js";
|
|
12
|
+
import { approvalTerms, approveForm, chip, disclose, doc, docHeader, facts, infoLine, note, notice, slots, btn } from "./kit.js";
|
|
13
|
+
import { taskHref } from "./http.js";
|
|
14
|
+
/** The change list, folded: every commit when it is whole, or why it isn't. */
|
|
15
|
+
function changesHtml(card) {
|
|
16
|
+
const changes = card.card.changes;
|
|
17
|
+
const id = card.seal.identity;
|
|
18
|
+
if (changes.commits === null) {
|
|
19
|
+
return note(changes.baselineUnreadable === true ? "What changed couldn't be read: the last verified release's commit can't be read."
|
|
20
|
+
: `What changed couldn't be read${changes.why === undefined ? "" : ` (${changes.why})`}. See pull request #${id.pr} on GitHub.`);
|
|
21
|
+
}
|
|
22
|
+
const head = changes.baseline === null ? `Pull request #${id.pr}'s commits` : `Since the last verified release (${changes.baseline.slice(0, 7)})`;
|
|
23
|
+
const list = changes.commits.length === 0 ? note("No commits.")
|
|
24
|
+
: html `<ul class="k-list">${changes.commits.map(one => html `<li><code class="k-mono">${one.sha.slice(0, 7)}</code> ${one.subject}</li>`)}</ul>`;
|
|
25
|
+
return disclose(`What changed: ${changes.commits.length} commit${changes.commits.length === 1 ? "" : "s"}`, html `<p class="k-note">${head}</p>${list}`);
|
|
26
|
+
}
|
|
27
|
+
/** Every signed term, before the button. */
|
|
28
|
+
function termsHtml(card) {
|
|
29
|
+
return disclose("Every term", facts(card.terms.map(line => [line.label, html `<span class="k-mono">${line.value}</span>`])), true);
|
|
30
|
+
}
|
|
31
|
+
export function releaseSheetPage(chrome, data) {
|
|
32
|
+
const card = data.card;
|
|
33
|
+
const id = card.seal.identity;
|
|
34
|
+
const title = `Release ${id.packageVersion}`;
|
|
35
|
+
const summary = facts([
|
|
36
|
+
["Pull request", `#${id.pr}`],
|
|
37
|
+
["Branch", html `<code class="k-mono">${id.branch}</code>`],
|
|
38
|
+
["Commit", html `<code class="k-mono">${id.approvedSha}</code>`],
|
|
39
|
+
["Project", projectName(id.repo)],
|
|
40
|
+
["Checks", checkAccessWords(id.access)],
|
|
41
|
+
]);
|
|
42
|
+
const consequence = releaseConsequence(card);
|
|
43
|
+
const ci = ciWords(card.ci, id.approvedSha);
|
|
44
|
+
const notRunning = card.stopped === null ? "" : infoLine(html `The release isn't running (it stopped at ${card.stopped.step}). After you approve, run <code class="k-mono">toolroll release ${id.branch}</code>.`);
|
|
45
|
+
let status, text, acts, terms, wait = false;
|
|
46
|
+
if (card.approved !== null) {
|
|
47
|
+
status = html `${chip("Approved", "done")}<h2>Release approved</h2>`;
|
|
48
|
+
text = `Approved by ${card.approved.by}. ${ci}`;
|
|
49
|
+
acts = btn("Open the task", { variant: "outline", href: taskHref(card.taskId) });
|
|
50
|
+
terms = "";
|
|
51
|
+
}
|
|
52
|
+
else if (card.refusal !== null) {
|
|
53
|
+
status = html `${chip("Can't approve", "failed")}<h2>Can't approve this release now</h2>`;
|
|
54
|
+
text = card.refusal;
|
|
55
|
+
acts = btn("Open the task", { variant: "outline", href: taskHref(card.taskId) });
|
|
56
|
+
terms = "";
|
|
57
|
+
}
|
|
58
|
+
else if (data.local !== null) {
|
|
59
|
+
wait = true;
|
|
60
|
+
status = html `${chip("Needs you", "needs")}<h2>Ready for your approval</h2>`;
|
|
61
|
+
text = `${ci} Releases need a full sign-in on this computer.`;
|
|
62
|
+
acts = btn(data.local.action, { variant: "primary", href: data.local.href });
|
|
63
|
+
terms = data.local.message;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
wait = true;
|
|
67
|
+
status = html `${chip("Needs you", "needs")}<h2>Ready for your approval</h2>`;
|
|
68
|
+
text = ci;
|
|
69
|
+
acts = data.nonce === "" ? "" : approveForm({
|
|
70
|
+
action: `${taskHref(card.taskId)}/approve`, kind: "standard", facts: data.facts, label: "Approve release", primary: true,
|
|
71
|
+
before: html `<section class="k-sec"><h3>What approving does</h3>${joinHtml(consequence.map(line => html `<p class="k-note">${line}</p>`))}${termsHtml(card)}</section>`,
|
|
72
|
+
form: { hidden: { nonce: data.nonce, digest: card.digest, sheet: "release" }, attrs: { class: "k-form", "data-release-approve": card.digest } },
|
|
73
|
+
});
|
|
74
|
+
terms = approvalTerms("standard", data.facts);
|
|
75
|
+
}
|
|
76
|
+
// What, then what changed, then the one decision: its status, CI now, what approving does, every term, the button.
|
|
77
|
+
const formShown = acts !== "" && data.nonce !== "" && card.approved === null && card.refusal === null && data.local === null;
|
|
78
|
+
const body = doc(docHeader({ title, back: [taskHref(card.taskId), "Task"], meta: `${projectName(id.repo)} · pull request #${id.pr}` }),
|
|
79
|
+
// A refused approval says why first, above the card it now shows.
|
|
80
|
+
data.problem === null ? "" : notice(data.problem), summary, changesHtml(card), slots({ wait, attrs: { "data-release-sheet": card.taskId }, status, text, acts, terms }), notRunning,
|
|
81
|
+
// When the form isn't shown, the consequence and the terms are still on the page.
|
|
82
|
+
formShown ? "" : html `<section class="k-sec"><h3>What approving does</h3>${joinHtml(consequence.map(line => html `<p class="k-note">${line}</p>`))}</section>${termsHtml(card)}`);
|
|
83
|
+
return screen(title, body, { chrome });
|
|
84
|
+
}
|
|
@@ -707,8 +707,6 @@ export declare function needActionOf(assignment: AssignmentSnapshot | null, stat
|
|
|
707
707
|
rebuildable?: boolean;
|
|
708
708
|
refuted?: boolean;
|
|
709
709
|
}): BrowserNeedAction | null;
|
|
710
|
-
/** A build Toolroll can't check at all: the approver ticks that they checked before confirming. */
|
|
711
|
-
export declare const needsCheck: (assignment: AssignmentSnapshot) => boolean;
|
|
712
710
|
/** The server page's form of the same: the need's action first, Request changes as the quiet second. */
|
|
713
711
|
export declare function resultNeedAction(need: BrowserNeedAction, o: ResultPanelOptions, canRequest: boolean): Html;
|
|
714
712
|
/** The outcome in one bounded sentence (repair 2026-09-14): the handoff's
|
|
@@ -731,7 +731,7 @@ export function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab,
|
|
|
731
731
|
checksRunning,
|
|
732
732
|
blocked,
|
|
733
733
|
canRequest: panel.panel.canRequest && panel.panel.request !== null,
|
|
734
|
-
need: need === null || need.accept != null ? null : need.rebuild != null ? "rebuild" : need.confirm !== null ? "
|
|
734
|
+
need: need === null || need.accept != null ? null : need.rebuild != null ? "rebuild" : need.confirm !== null ? "recheck-stopped" : null,
|
|
735
735
|
next: nextKind === "revise" || nextKind === "draft-repair" ? nextKind : null,
|
|
736
736
|
unanswered: settled === null ? 0 : unanswered.length,
|
|
737
737
|
notRight: 0,
|
|
@@ -2009,12 +2009,10 @@ export function needActionOf(assignment, status, csrf, returnTo, result) {
|
|
|
2009
2009
|
if (status.need.key === "review-result" || here)
|
|
2010
2010
|
return null;
|
|
2011
2011
|
}
|
|
2012
|
-
const confirm = action?.code === "
|
|
2013
|
-
? { action: `${taskHref(assignment.rootId)}/
|
|
2012
|
+
const confirm = action?.code === "recheck-stopped" && action.target.runId !== null && csrf !== ""
|
|
2013
|
+
? { action: `${taskHref(assignment.rootId)}/recheck-stopped`, run: action.target.runId, returnTo } : null;
|
|
2014
2014
|
return { label: status.need.action.label, href: assignmentActionHref(assignment) ?? taskHref(assignment.rootId), confirm };
|
|
2015
2015
|
}
|
|
2016
|
-
/** A build Toolroll can't check at all: the approver ticks that they checked before confirming. */
|
|
2017
|
-
export const needsCheck = (assignment) => assignment.need != null && "key" in assignment.need && assignment.need.key === "check-stopped";
|
|
2018
2016
|
/** The server page's form of the same: the need's action first, Request changes as the quiet second. */
|
|
2019
2017
|
export function resultNeedAction(need, o, canRequest) {
|
|
2020
2018
|
const control = need.accept != null
|
|
@@ -2022,7 +2020,7 @@ export function resultNeedAction(need, o, canRequest) {
|
|
|
2022
2020
|
: need.rebuild != null
|
|
2023
2021
|
? postForm(need.rebuild.action, html `<button type="submit" data-primary-action data-rebuild style="min-height:44px">${need.label}</button>`, { attrs: { class: "rebuild" } })
|
|
2024
2022
|
: need.confirm !== null
|
|
2025
|
-
? postForm(need.confirm.action, html
|
|
2023
|
+
? postForm(need.confirm.action, html `<button type="submit" data-primary-action style="min-height:44px">${need.label}</button>`, { attrs: { id: "recheck-stopped", class: "recheck-stopped" }, returnTo: need.confirm.returnTo, hidden: { run: need.confirm.run } })
|
|
2026
2024
|
: html `<a class="button-link" href="${need.href ?? "#"}" data-primary-action>${need.label}</a>`;
|
|
2027
2025
|
return html `<div class="result-action" data-result-action="need">${control}${canRequest && o.csrf !== "" ? html `<a class="result-feedback-link" href="#request-changes">Request changes</a>` : ""}</div>`;
|
|
2028
2026
|
}
|
|
@@ -220,7 +220,12 @@ export type RouteView = {
|
|
|
220
220
|
* every option an exact pair the operator named once in configuration.
|
|
221
221
|
* The form offers these and nothing else. */
|
|
222
222
|
choices: Record<Phase, AgentChoice[]>;
|
|
223
|
+
/** Results routing (slice 2): "Why this model: …" when an earned rule selected the build leg (route-rules.ts
|
|
224
|
+
* selectionEvidenceWords), else absent. The selection is sealed; the counts read "so far". */
|
|
225
|
+
evidence?: string | null;
|
|
223
226
|
};
|
|
227
|
+
/** The evidence line beside the agents, only when an earned rule chose the builder. */
|
|
228
|
+
export declare function routeEvidenceHtml(view: RouteView | null | undefined, className?: string): Html;
|
|
224
229
|
export declare const ROLE_NOUN: Record<RouteProjection["legs"][number]["phase"], string>;
|
|
225
230
|
export declare function agentsStandingWords(view: RouteView): string;
|
|
226
231
|
/** The size line an approval leads its agents with — "Small change: fast
|
|
@@ -609,6 +614,8 @@ export declare function taskBodyParts(data: {
|
|
|
609
614
|
qualityMode?: QualityMode | null;
|
|
610
615
|
/** The phase route (v47) and whether this session may edit it. */
|
|
611
616
|
route?: RouteView | null;
|
|
617
|
+
/** Results routing (slice 1): this task family's result record in words; null when it has none yet. */
|
|
618
|
+
resultRecord?: string | null;
|
|
612
619
|
canEditRoute?: boolean;
|
|
613
620
|
runs: Run[];
|
|
614
621
|
/** Proof produced by the newest finished attempt. The two booleans are
|
|
@@ -50,7 +50,7 @@ import { isOverdue, oneLineOf, projectChip, regionScript, safePrUrl, screen, SEN
|
|
|
50
50
|
import { steerDestinationWords, steerStandingParts } from "../steer-status.js";
|
|
51
51
|
import { reviewHref, taskChatHref, taskHref } from "./http.js";
|
|
52
52
|
import { coordinatorProposalsSection } from "./render-chat.js";
|
|
53
|
-
import { completionReceiptCard, criterionMatrixHtml, evidenceWords, homePhaseWords, incidentWords,
|
|
53
|
+
import { completionReceiptCard, criterionMatrixHtml, evidenceWords, homePhaseWords, incidentWords, pullRequestCardHtml, reasonWords, receiptStatusOf, repairChainHtml, reportItemsHtml, reviewRetryPanel, runNoun, runOutcomeBadge, semanticCoverageHtml, verificationExplanation, verificationRecovered } from "./render-results.js";
|
|
54
54
|
import {} from "./session.js";
|
|
55
55
|
import { linkWords, originNote, requestName, requestWords } from "../task-source.js";
|
|
56
56
|
export function contractChangeHtml(change) {
|
|
@@ -439,6 +439,7 @@ export function approvalSheetParts(input) {
|
|
|
439
439
|
// The size, said once and plainly beside who builds: "Small change: fast model, no plan".
|
|
440
440
|
projection === null ? "" : sizeLineHtml(projection),
|
|
441
441
|
who.length === 0 ? "" : html `<p class="approval-who">${who.join(" · ")}</p>`,
|
|
442
|
+
routeEvidenceHtml(route, "approval-evidence"),
|
|
442
443
|
consent,
|
|
443
444
|
]);
|
|
444
445
|
return { lead, rows, editor, editorForm, details, submit, label: rebuild !== null ? "Approve and build again" : "Approve and start", after, allowing, passwordNote };
|
|
@@ -565,6 +566,14 @@ export function profileWords(scope) {
|
|
|
565
566
|
const terms = sandboxTermsWords(profile);
|
|
566
567
|
return html `${base}<p class="meta" data-scope-network>${networkWords(profile.network ?? null)}</p><p class="meta" data-scope-writes>Writes: ${writesWords(profile.writes)}.</p>${profile.checkAccess === undefined ? "" : html `<p class="meta" data-scope-checks>Checks: ${checkAccessWords(profile.checkAccess)}.</p>`}<p class="meta" data-scope-reads>Reads: ${terms.reads}.</p>${terms.mach === null ? "" : html `<p class="meta" data-scope-mach>${terms.mach.charAt(0).toUpperCase()}${terms.mach.slice(1)}.</p>`}`;
|
|
567
568
|
}
|
|
569
|
+
/** The evidence line beside the agents, only when an earned rule chose the builder. */
|
|
570
|
+
export function routeEvidenceHtml(view, className = "meta") {
|
|
571
|
+
const words = view?.evidence ?? null;
|
|
572
|
+
if (words === null)
|
|
573
|
+
return html ``;
|
|
574
|
+
const split = words.indexOf(": ");
|
|
575
|
+
return html `<p class="${className} route-evidence" data-route-evidence><strong>${words.slice(0, split + 1)}</strong>${words.slice(split + 1)}</p>`;
|
|
576
|
+
}
|
|
568
577
|
export const ROLE_NOUN = { plan: "Planner", build: "Builder", repair: "Repair", review: "Reviewer" };
|
|
569
578
|
export function agentsStandingWords(view) {
|
|
570
579
|
return view.kind === "legacy"
|
|
@@ -658,6 +667,7 @@ export function agentsCeremonyHtml(view) {
|
|
|
658
667
|
html `<div class="agents-ceremony"><p class="approval-label">agents</p>`,
|
|
659
668
|
sizeLineHtml(view.projection),
|
|
660
669
|
html `<p class="agents-summary">${view.projection.summary}</p>`,
|
|
670
|
+
routeEvidenceHtml(view),
|
|
661
671
|
html `<div class="agents-badges">${riskBadgeHtml(view.projection)}<span class="badge">${sentenceCase(view.projection.postureWords)}</span></div>`,
|
|
662
672
|
agentsWhyHtml(view.projection),
|
|
663
673
|
html `<p class="meta">These exact agents are part of what you approve; changing any of them asks for a fresh approval.</p></div>`,
|
|
@@ -735,6 +745,7 @@ export function agentsCardHtml(taskId, view, canEdit) {
|
|
|
735
745
|
html `<section class="card agents-card" id="agents" aria-label="agents">`,
|
|
736
746
|
html `<div class="agents-head"><h3>Agents</h3><div class="agents-badges">${agentsBadgesHtml(view)}</div></div>`,
|
|
737
747
|
html `<p class="agents-summary">${agentsSummaryWords(view)}</p>`,
|
|
748
|
+
routeEvidenceHtml(view),
|
|
738
749
|
p === null
|
|
739
750
|
? view.kind === "unreadable"
|
|
740
751
|
? html `<p class="agents-halted">Nothing runs for this task until its scope is filed again and approved.</p>`
|
|
@@ -2280,6 +2291,8 @@ export function taskBodyParts(data) {
|
|
|
2280
2291
|
review.push({ ...approvedScope, label: approvedScope.label === "Approved scope" ? "Approvals" : "Scope" });
|
|
2281
2292
|
review.push({ label: "Who reviews", parts: [facts.find(one => one.label === "Approval rules")?.parts[0] ?? "Any approver in this project"] });
|
|
2282
2293
|
review.push({ label: "Publishes as", parts: [publishesAs] });
|
|
2294
|
+
if (data.resultRecord != null)
|
|
2295
|
+
review.push({ label: "Result record", parts: [data.resultRecord] });
|
|
2283
2296
|
const about = [];
|
|
2284
2297
|
const filedFact = facts.find(one => one.label === "Filed by");
|
|
2285
2298
|
about.push(filedFact ?? { label: "Filed by", parts: [filedBy ?? (data.origin != null ? "An automation" : data.filedVia == null ? "Not recorded" : `via ${data.filedVia}`)] });
|
|
@@ -2395,8 +2408,8 @@ export function taskBodyParts(data) {
|
|
|
2395
2408
|
approve: data.archived != null || sheetInput === null || dependencyChoiceNeeded || data.csrf === "" ? null : approveKitOf(sheetInput, data.approvalFresh ?? null),
|
|
2396
2409
|
approved: scope !== null && approval.approved ? approvedKitOf(scope, approval, data.approvalProof ?? null) : null,
|
|
2397
2410
|
more,
|
|
2398
|
-
|
|
2399
|
-
? { action: `${taskHref(data.assignment.rootId)}/
|
|
2411
|
+
recheckStopped: data.csrf !== "" && data.assignment?.primaryAction?.code === "recheck-stopped" && data.assignment.primaryAction.target.runId !== null
|
|
2412
|
+
? { action: `${taskHref(data.assignment.rootId)}/recheck-stopped`, run: data.assignment.primaryAction.target.runId } : null,
|
|
2400
2413
|
rebuild,
|
|
2401
2414
|
// The plan, progress and plan changes are thread entries now; the rest still needs a person here.
|
|
2402
2415
|
lead: [
|
|
@@ -22,7 +22,7 @@ export type Caller = "anonymous" | "service" | "cookie" | "bearer" | "oauth" | "
|
|
|
22
22
|
/** What the caller must hold: nothing, read, act (an approver or an `act` token), or a fresh password/provider check on top. */
|
|
23
23
|
export type RouteScope = "none" | "read" | "act" | "step-up";
|
|
24
24
|
/** Where the request's project comes from. */
|
|
25
|
-
export type ProjectResolver = "none" | "session" | "form" | "form-path" | "task" | "run" | "decision" | "incident" | "flow" | "proposal" | "conversation" | "coding" | "adapter";
|
|
25
|
+
export type ProjectResolver = "none" | "session" | "form" | "form-path" | "task" | "task-ref" | "run" | "decision" | "incident" | "flow" | "proposal" | "conversation" | "coding" | "adapter";
|
|
26
26
|
/**
|
|
27
27
|
* What a project-limited account may do here:
|
|
28
28
|
* - `deny`: instance access only.
|
|
@@ -91,6 +91,11 @@ const LOCAL_POSTS = Object.freeze({
|
|
|
91
91
|
"settings.lead-on": "choose-default-agent",
|
|
92
92
|
// Choosing which open project the pages show: a view filter, never authorization.
|
|
93
93
|
"projects.select": "read",
|
|
94
|
+
// Results routing: lowering only.
|
|
95
|
+
"settings.models-turn-off": "routing-turn-off",
|
|
96
|
+
"settings.models-not-now": "dismiss-suggestion",
|
|
97
|
+
"settings.models-suggest-off": "dismiss-suggestion",
|
|
98
|
+
"settings.models-stop": "trial-stop",
|
|
94
99
|
"agents.seen": "mark-seen",
|
|
95
100
|
"agents.watch": "watch-agent",
|
|
96
101
|
});
|
|
@@ -281,6 +286,10 @@ get("tasks", "welcome", "/welcome", { limited: "collection", ...free });
|
|
|
281
286
|
post("tasks", "welcome.sample", "/welcome/sample", { callers: ["cookie"], project: "none", ...approver("An approver adds projects.", "/welcome") });
|
|
282
287
|
get("tasks", "task.page", `^\\/t\\/${TASK}$`, { project: "task", limited: "resource", ...free, sample: "/t/one" });
|
|
283
288
|
get("tasks", "task.evidence", `^\\/t\\/${TASK}\\/evidence$`, { project: "task", limited: "resource", ...free, sample: "/t/one/evidence" });
|
|
289
|
+
// One-tap release approval (trusted-release-reports.md A6/A7): the sheet, and the push's opaque locator, which only
|
|
290
|
+
// redirects to it once the plane's own state says the task ref is a release in the person's projects.
|
|
291
|
+
get("tasks", "task.release", `^\\/t\\/${TASK}\\/release$`, { project: "task", limited: "resource", ...free, sample: "/t/one/release" });
|
|
292
|
+
get("tasks", "release.locate", `^\\/approve\\/${ID}$`, { project: "task-ref", limited: "resource", ...free, sample: "/approve/1" });
|
|
284
293
|
get("pages", "ledger.export", "/ledger/export", { limited: "unscoped", ...free });
|
|
285
294
|
get("tasks", "runs", "/runs", { limited: "collection", ...free }); // a redirect to /agents now
|
|
286
295
|
get("pages", "menu", "/menu", { limited: "collection", ...free });
|
|
@@ -483,6 +492,13 @@ for (const one of ["check", "watch", "update"])
|
|
|
483
492
|
// Passwordless session; model-choices.ts re-proves the session and standing inside its one transaction).
|
|
484
493
|
for (const one of ["agent", "apply"])
|
|
485
494
|
post("settings", `settings.models-${one}`, `/settings/models/${one}`, { scope: "step-up", ...operator("An instance operator changes models.", "/settings/models") });
|
|
495
|
+
// Earned rules (results routing): accepting, keeping a paused rule and switching suggestions on are an instance
|
|
496
|
+
// operator's browser step-up; turning a rule off, Not now and switching suggestions off only take authority away (a
|
|
497
|
+
// signed-in operator, or the local owner in exactly that act: route-store.ts proveRuleDoor re-checks it in the write).
|
|
498
|
+
for (const one of ["accept", "keep", "suggest-on", "try"])
|
|
499
|
+
post("settings", `settings.models-${one}`, `/settings/models/${one}`, { scope: "step-up", ...operator("An instance operator changes which models run.", "/settings/models") });
|
|
500
|
+
for (const one of ["turn-off", "not-now", "suggest-off", "stop"])
|
|
501
|
+
post("settings", `settings.models-${one}`, `/settings/models/${one}`, operator("An instance operator changes which models run.", "/settings/models"));
|
|
486
502
|
// Any other models action answers the models area's own refusal.
|
|
487
503
|
post("settings", "settings.models-act", "^\\/settings\\/models\\/.*$", { sample: "/settings/models/unknown", ...MODELS });
|
|
488
504
|
post("settings", "settings.knowledge-refresh", "/settings/knowledge/refresh", { project: "form", limited: "unscoped", ...approver("Sign in as an approver to refresh project context.", "/settings/knowledge") });
|
|
@@ -544,7 +560,9 @@ post("tasks", "task.act.route", `^\\/t\\/${TASK}\\/route$`, { project: "task", l
|
|
|
544
560
|
post("tasks", "task.act.retry-review", `^\\/t\\/${TASK}\\/retry-review$`, { project: "task", limited: "resource", sample: "/t/one/retry-review" });
|
|
545
561
|
post("tasks", "task.act.complete", `^\\/t\\/${TASK}\\/complete$`, { project: "task", limited: "resource", sample: "/t/one/complete", ...approver("Only an approver can mark a result complete.") });
|
|
546
562
|
post("tasks", "task.act.merge", `^\\/t\\/${TASK}\\/merge$`, { project: "task", limited: "resource", sample: "/t/one/merge", ...approver("Only an approver can merge.") });
|
|
547
|
-
|
|
563
|
+
// Check again: Toolroll looks at this computer's processes for a finished build once more (stop-proof.ts). Nobody
|
|
564
|
+
// vouches that a build stopped, so no password: it only asks for the look the worker makes on its own anyway.
|
|
565
|
+
post("tasks", "task.act.recheck-stopped", `^\\/t\\/${TASK}\\/recheck-stopped$`, { project: "task", limited: "resource", sample: "/t/one/recheck-stopped", ...approver("Only an approver can have Toolroll check a build again.") });
|
|
548
566
|
post("tasks", "task.act.stop-carrying", `^\\/t\\/${TASK}\\/stop-carrying$`, { project: "task", limited: "resource", sample: "/t/one/stop-carrying", ...approver("Only an approver can stop carrying a release.") });
|
|
549
567
|
post("tasks", "task.act.stop", `^\\/t\\/${TASK}\\/stop$`, { project: "task", limited: "resource", sample: "/t/one/stop" });
|
|
550
568
|
// Putting one task away and bringing it back (task-tidy.md): a person's act, so only a browser session.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ServerRuntime } from './runtime.js';
|
|
2
|
-
import { type RoleView } from "../models-ui.js";
|
|
2
|
+
import { type RoleView, type SuggestedView } from "../models-ui.js";
|
|
3
3
|
export declare function createSettingsHandlers(runtime: ServerRuntime): {
|
|
4
4
|
registrations: import("./handler-registry.js").Registration[];
|
|
5
5
|
modelsView: (canManage: boolean, csrf: string, now: Date, said: string | null, problem: string | null, query?: URLSearchParams) => {
|
|
@@ -71,5 +71,6 @@ export declare function createSettingsHandlers(runtime: ServerRuntime): {
|
|
|
71
71
|
taskId: string;
|
|
72
72
|
title: string;
|
|
73
73
|
}[];
|
|
74
|
+
suggested: SuggestedView;
|
|
74
75
|
};
|
|
75
76
|
};
|
package/dist/server/settings.js
CHANGED
|
@@ -76,6 +76,7 @@ import { startConnect } from "../mcp-connect.js";
|
|
|
76
76
|
import { decideProposal, listProposals, memoryStatus } from "../memory-pass.js";
|
|
77
77
|
import { checkModels, isNewModel, modelOptions, modelWords, RUNTIME_TOOLS, runtimeStates, seenModels, setWatch, updateRuntime, watchState } from "../model-catalog.js";
|
|
78
78
|
import { modelsHtml, modelsScript } from "../models-ui.js";
|
|
79
|
+
import { acceptSuggestion, dismissSuggestion, grantWords, keepRule, modelName, ruleWords, rulesOf, setSuggestModels, startTrial, stopTrial, suggestionsFor, suggestionWords, suggestModelsOn, trialOffersFor, trialOfferWords, trialsOf, trialWords, turnOffRule } from "../route-rules.js";
|
|
79
80
|
import { ALL_SLOTS, applyModelChoices, browserWords, checkScope, DEFAULT_PRESET, defaultAgentOf, fillDefaultRouting, isPresetName, newerModels, PRESETS, presetChange, presetInUse, previewModelChange, recentRuns, rowAt, slotKey, SLOTS, slotWords, stateDigest, switchChange } from "../model-choices.js";
|
|
80
81
|
import { HEADROOM_FIVE_HOUR_LIMIT, HEADROOM_WEEKLY_LIMIT, exactModelId } from "../phase-routing.js";
|
|
81
82
|
import { RISKY_MEANS, SIZE_MEANS, SIZING_CLAUDE_MODEL } from "../task-sizing.js";
|
|
@@ -118,6 +119,7 @@ import { slackSettingsHtml } from "../slack-settings.js";
|
|
|
118
119
|
import { spendHtml, spendScript } from "../spend-ui.js";
|
|
119
120
|
import { approvalFactsNow } from "./kit.js";
|
|
120
121
|
import { spendReport, spendReportCsv, spendRows } from "../spend-report.js";
|
|
122
|
+
import { savingsReportOrError } from "../route-savings.js";
|
|
121
123
|
import { limitsView } from "../limits-ui.js";
|
|
122
124
|
import { budgetStates, monthNamed, spendItems, usd as spendUsd, subagentNames as subagentNamesOf } from "../spend.js";
|
|
123
125
|
import { removeSsoSettings, saveSsoSettings, SSO_CALLBACK, ssoChangeWords } from "../sso-settings.js";
|
|
@@ -185,7 +187,9 @@ export function createSettingsHandlers(runtime) {
|
|
|
185
187
|
};
|
|
186
188
|
const windows = limitsView(store.providerLimits(), [], { project: projectName, subagent: id => subagentNames.get(id) ?? `Subagent ${id}` }, now);
|
|
187
189
|
const view = { month: month.name, previous: shift(-1), next: month.name === current.name ? null : shift(1), current: current.name, report: spendReport(rows),
|
|
188
|
-
budgets, unlimited, targets, windows, subagentNames, edit, facts: approvalFactsNow()
|
|
190
|
+
budgets, unlimited, targets, windows, subagentNames, edit, facts: approvalFactsNow(),
|
|
191
|
+
// Suggestions and rules (results-routing slice 4): read-only, for every project this console shows.
|
|
192
|
+
savings: savingsReportOrError(store, consoleProjects(), now) };
|
|
189
193
|
// A refusal for the open form shows in the form; anything else under the limits' heading.
|
|
190
194
|
const said = { said: url.searchParams.get("said"), problem: edit === null ? url.searchParams.get("problem") : null };
|
|
191
195
|
return sendScreen(response, 200, screen("Spend", spendHtml(view, said), { chrome: chromeFor(null, "spend"), functional: { script: spendScript() } }));
|
|
@@ -2565,6 +2569,7 @@ ${postForm("/settings/chat-approval/save", html `\
|
|
|
2565
2569
|
chosen: one.chosen === null ? "its default" : /^(opus|sonnet|haiku|fable)$/.test(one.chosen) ? `floating ${one.chosen}` : one.chosen,
|
|
2566
2570
|
ranOn: one.ranOn ?? "not reported" })),
|
|
2567
2571
|
waiting: canManage ? store.scopesAwaitingApproval(null, 10, admissionList()).map(one => ({ taskId: one.taskId, title: one.title })) : [],
|
|
2572
|
+
suggested: suggestedView(repo, projects, query.get("suggestion"), now),
|
|
2568
2573
|
};
|
|
2569
2574
|
}
|
|
2570
2575
|
/** Google's redirect back: the code becomes a saved refresh token, and a page sends the person back to Settings. */
|
|
@@ -2739,6 +2744,14 @@ ${postForm("/settings/chat-approval/save", html `\
|
|
|
2739
2744
|
"settings.models-agent": modelsSave,
|
|
2740
2745
|
"settings.models-apply": modelsApply,
|
|
2741
2746
|
"settings.models-act": modelsAct("other"),
|
|
2747
|
+
"settings.models-accept": modelsRuleAct("accept"),
|
|
2748
|
+
"settings.models-keep": modelsRuleAct("keep"),
|
|
2749
|
+
"settings.models-suggest-on": modelsRuleAct("suggest-on"),
|
|
2750
|
+
"settings.models-turn-off": modelsRuleAct("turn-off"),
|
|
2751
|
+
"settings.models-not-now": modelsRuleAct("not-now"),
|
|
2752
|
+
"settings.models-suggest-off": modelsRuleAct("suggest-off"),
|
|
2753
|
+
"settings.models-try": modelsRuleAct("try"),
|
|
2754
|
+
"settings.models-stop": modelsRuleAct("stop"),
|
|
2742
2755
|
"settings.knowledge-refresh": knowledgeRefresh,
|
|
2743
2756
|
"settings.knowledge-proposal": knowledgeProposal,
|
|
2744
2757
|
"settings.knowledge-decision": knowledgeDecision,
|
|
@@ -3005,5 +3018,78 @@ ${postForm("/settings/chat-approval/save", html `\
|
|
|
3005
3018
|
return back("problem", turned.reason === "not-on" ? "Local mode is already off." : "Only an instance operator can turn off local mode.");
|
|
3006
3019
|
return back("said", "Local mode is off. This computer's browser signs in with your password now.");
|
|
3007
3020
|
}
|
|
3021
|
+
/** Settings → Models → Suggested: one project's offers, rules and switch; on the installation scope, every
|
|
3022
|
+
* project's offers and rules, each reviewed on its own project's page. */
|
|
3023
|
+
function suggestedView(repo, projects, asked, now) {
|
|
3024
|
+
const repos = repo === null ? projects : [repo];
|
|
3025
|
+
const offers = repos.flatMap(one => suggestionsFor(store, one, now));
|
|
3026
|
+
const sizeTitle = (size, name) => `${name} for ${size === "small" ? "small" : "routine"} tasks`;
|
|
3027
|
+
const reviewed = asked === null || repo === null ? null : offers.find(one => one.id === asked && one.repo === repo) ?? null;
|
|
3028
|
+
return {
|
|
3029
|
+
repo,
|
|
3030
|
+
offers: offers.map(one => ({ id: one.id, repo: one.repo, project: projectName(one.repo), title: sizeTitle(one.cohortSize, one.name), words: suggestionWords(one) })),
|
|
3031
|
+
review: asked === null || repo === null ? null : reviewed === null ? { stale: true, repo } : {
|
|
3032
|
+
id: reviewed.id, repo: reviewed.repo, title: `${sizeTitle(reviewed.cohortSize, reviewed.name)} instead of ${reviewed.configured.name}`,
|
|
3033
|
+
evidence: [suggestionWords(reviewed), reviewed.configuredEvidence.decided === 0 ? `${reviewed.configured.name}: no decided similar tasks here in the last 90 days.` : `${reviewed.configured.name}: ${reviewed.configuredEvidence.mature} of its last ${reviewed.configuredEvidence.decided} decided similar tasks accepted with checks passing.`],
|
|
3034
|
+
prices: `${reviewed.name} $${reviewed.prices.model.in} / $${reviewed.prices.model.out}, ${reviewed.configured.name} $${reviewed.prices.configured.in} / $${reviewed.prices.configured.out} per 1M tokens (listed now)`,
|
|
3035
|
+
terms: grantWords(reviewed.grant), save: `Use ${reviewed.name} for ${reviewed.cohortSize === "small" ? "small" : "routine"} tasks`,
|
|
3036
|
+
},
|
|
3037
|
+
rules: repos.flatMap(one => rulesOf(store, one, now)).map(one => ({ id: one.id, repo: one.repo, project: projectName(one.repo), title: sizeTitle(one.cohortSize, modelName(store, one.model)), words: ruleWords(store, one),
|
|
3038
|
+
state: one.state === "active" ? "active" : one.state === "paused" ? "paused" : "ended" })),
|
|
3039
|
+
suggest: repo === null ? null : { on: suggestModelsOn(store, repo) },
|
|
3040
|
+
trialOffers: repos.flatMap(one => trialOffersFor(store, one, now)).map(one => ({ id: one.id, repo: one.repo, project: projectName(one.repo),
|
|
3041
|
+
title: `Try ${sizeTitle(one.cohortSize, one.name)}`, words: trialOfferWords(one), save: `Try it on the next ${one.places}` })),
|
|
3042
|
+
trials: repos.flatMap(one => trialsOf(store, one, now)).map(one => ({ id: one.id, repo: one.repo, project: projectName(one.repo),
|
|
3043
|
+
title: `Trying ${sizeTitle(one.cohortSize, modelName(store, one.model))}`, words: trialWords(store, one), live: one.state === "trialling" || one.state === "paused" })),
|
|
3044
|
+
};
|
|
3045
|
+
}
|
|
3046
|
+
/**
|
|
3047
|
+
* Settings → Models → Suggested (results routing, slice 2): the earned-rule doors (route-rules.ts), re-proved inside
|
|
3048
|
+
* their own transaction. Accepting, keeping a paused rule and switching suggestions on are an instance operator's
|
|
3049
|
+
* step-up (the password, or a Passwordless session); turning a rule off or dropping it, Not now, and switching
|
|
3050
|
+
* suggestions off only take authority away, so a signed-in session does — or this computer's local owner, whose
|
|
3051
|
+
* route row narrows it to exactly that act.
|
|
3052
|
+
*/
|
|
3053
|
+
function modelsRuleAct(kind) {
|
|
3054
|
+
return async (ctx) => {
|
|
3055
|
+
const { response, now, posted } = ctx;
|
|
3056
|
+
const who = browserCaller(ctx.who);
|
|
3057
|
+
const body = readForm(posted, CONSOLE_FORMS.modelsRules);
|
|
3058
|
+
const repo = body.get("scope") ?? "";
|
|
3059
|
+
const back = (key, words) => redirect(response, `/settings/models?scope=${encodeURIComponent(repo)}&${key}=${encodeURIComponent(words)}#models-suggested`);
|
|
3060
|
+
if (["scope", "password", "id", "rule"].some(field => body.sent.getAll(field).length > 1))
|
|
3061
|
+
return back("problem", "That form was sent twice over. Nothing changed.");
|
|
3062
|
+
if (!consoleProjects().includes(repo) || !store.accountCanAccess(who.name, repo))
|
|
3063
|
+
return refuse(response, who, 403, "That project is outside your access.", "/settings/models");
|
|
3064
|
+
let door;
|
|
3065
|
+
if (kind === "accept" || kind === "keep" || kind === "suggest-on" || kind === "try") {
|
|
3066
|
+
const proved = authenticateApprover(who, body.get("password") ?? "");
|
|
3067
|
+
if (!proved.ok)
|
|
3068
|
+
return back("problem", "Enter your Toolroll password to change which models run.");
|
|
3069
|
+
door = { kind: "person", person: modelsAuthority(who, proved.method) };
|
|
3070
|
+
}
|
|
3071
|
+
else {
|
|
3072
|
+
door = who.authority === "local-owner" ? { kind: "local-owner", actor: who.name } : { kind: "person", person: modelsAuthority(who, "session") };
|
|
3073
|
+
}
|
|
3074
|
+
const ruleId = /^[1-9][0-9]{0,9}$/.test(body.get("rule") ?? "") ? Number(body.get("rule")) : -1;
|
|
3075
|
+
const id = body.get("id") ?? "";
|
|
3076
|
+
const result = kind === "accept" ? acceptSuggestion(store, { repo, id }, door, now)
|
|
3077
|
+
: kind === "try" ? startTrial(store, { repo, id }, door, now)
|
|
3078
|
+
: kind === "stop" ? stopTrial(store, ruleId, door, now)
|
|
3079
|
+
: kind === "keep" ? keepRule(store, ruleId, door, now)
|
|
3080
|
+
: kind === "turn-off" ? turnOffRule(store, ruleId, door, now)
|
|
3081
|
+
: kind === "not-now" ? dismissSuggestion(store, { repo, id }, door, now)
|
|
3082
|
+
: setSuggestModels(store, repo, kind === "suggest-on", door);
|
|
3083
|
+
if (!result.ok)
|
|
3084
|
+
return back("problem", result.problem);
|
|
3085
|
+
return back("said", kind === "try" ? "Trial started. The next tasks of that size try the cheaper model; each waits for your approval."
|
|
3086
|
+
: kind === "stop" ? "Trial stopped. New tasks build on the configured model; tasks already approved keep their model."
|
|
3087
|
+
: kind === "accept" ? "Saved. New tasks of that size build on the suggested model; each is still approved by a person."
|
|
3088
|
+
: kind === "keep" ? "Kept. The rule is on again with its original terms."
|
|
3089
|
+
: kind === "turn-off" ? "Turned off. New tasks build on the configured model; tasks already approved keep their model."
|
|
3090
|
+
: kind === "not-now" ? "Not now. This suggestion stays quiet for 30 days."
|
|
3091
|
+
: kind === "suggest-on" ? "Suggestions are on for this project." : "Suggestions are off for this project.");
|
|
3092
|
+
};
|
|
3093
|
+
}
|
|
3008
3094
|
return { registrations, modelsView };
|
|
3009
3095
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { resultRecordWordsOf } from "../route-results.js";
|
|
2
|
+
import { selectionEvidenceWords } from "../route-rules.js";
|
|
1
3
|
import { feedbackImageProblem } from "../result-actions.js";
|
|
2
4
|
import { parseFeedbackImage } from "../result-review.js";
|
|
3
5
|
import { releaseViewOf } from "../release-view.js";
|
|
@@ -473,6 +475,8 @@ export function createTaskViews(runtime) {
|
|
|
473
475
|
// and the focused chat, with the readiness this task's runners report.
|
|
474
476
|
route: routeView,
|
|
475
477
|
canEditRoute: who.role === "approver",
|
|
478
|
+
// Results routing (slice 1): the family's saved record, or the one its rows give now; never a reason to fail the page.
|
|
479
|
+
resultRecord: resultRecordWordsOf(runtime.store, taskId, runtime.evidenceRoot, now),
|
|
476
480
|
spendDefaults: runtime.store.getSpendDefaults(),
|
|
477
481
|
permissionDefault: runtime.store.permissionDefault().mode,
|
|
478
482
|
permissionMode: ref?.permissionMode ?? null,
|
|
@@ -571,6 +575,8 @@ export function createTaskViews(runtime) {
|
|
|
571
575
|
replanOnPlanChange: ref.plan === "drafted" && scope !== null && !approvalOf(scope).approved,
|
|
572
576
|
digest: scope?.digest ?? null,
|
|
573
577
|
choices: agentChoicesFor(runtime.store, ref.repo, routed.kind === "route" ? routed.route : null),
|
|
578
|
+
// Results routing: why an earned rule's model builds it, from the records as they stand now.
|
|
579
|
+
evidence: routed.kind === "route" ? selectionEvidenceWords(runtime.store, routed.route, now) : null,
|
|
574
580
|
};
|
|
575
581
|
if (routed.kind === "route") {
|
|
576
582
|
return { kind: "route", source: routed.source, projection: projectRoute(routed.route, runtime.store.readinessLookupFor(ref.repo, ref.assignedRunner, now)), legacy: null, problem: null, ...shared };
|
package/dist/server/tasks.d.ts
CHANGED
|
@@ -64,3 +64,18 @@ export declare function createAgentSignalHandlers(runtime: ServerRuntime): {
|
|
|
64
64
|
"agents.seen": (ctx: HandlerContext) => Promise<void>;
|
|
65
65
|
"agents.watch": (ctx: HandlerContext) => Promise<void>;
|
|
66
66
|
};
|
|
67
|
+
/**
|
|
68
|
+
* One-tap release approval (docs/plans/trusted-release-reports.md, slice A): the sheet (`/t/<id>/release`), the push's
|
|
69
|
+
* opaque locator (`/approve/<task ref>`, a GET that only redirects to the sheet) and the sheet's approval, which the
|
|
70
|
+
* task approval POST hands here once its shared checks (cookie session, CSRF and the browser key at admission, the
|
|
71
|
+
* single-use nonce, a password or a standing proof) passed. The approval itself is the shared card wrapper around the
|
|
72
|
+
* unchanged approve() (release-approval.ts approveReleaseCard).
|
|
73
|
+
*/
|
|
74
|
+
export declare function createReleaseSheetHandlers(runtime: ServerRuntime): {
|
|
75
|
+
routes: {
|
|
76
|
+
"task.release": (ctx: HandlerContext) => Promise<void>;
|
|
77
|
+
"release.locate": (ctx: HandlerContext) => Promise<void>;
|
|
78
|
+
};
|
|
79
|
+
sheetScreen: (ctx: Pick<HandlerContext, "who" | "response" | "project">, taskId: string, problem: string | null, status: number) => void;
|
|
80
|
+
approveFromSheet: (ctx: Pick<HandlerContext, "who" | "response" | "project">, taskId: string, cardDigest: string, token: string) => void;
|
|
81
|
+
};
|