toolroll 0.9.74 → 0.9.76
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 +80 -0
- package/dist/agent-fence.js +8 -0
- package/dist/approval-authority.js +6 -2
- package/dist/approval-rules-ui.d.ts +11 -0
- package/dist/approval-rules-ui.js +13 -1
- package/dist/assignment.d.ts +3 -0
- package/dist/assignment.js +27 -8
- package/dist/batch-checks.js +3 -1
- package/dist/browser/workspace.js +27 -27
- package/dist/browser-workspace.d.ts +1 -1
- package/dist/builder.js +1 -1
- package/dist/chat-acceptance.d.ts +4 -1
- package/dist/chat-acceptance.js +13 -3
- package/dist/cli.js +1 -1
- package/dist/coding-handoff.js +4 -3
- package/dist/coding-ui.js +2 -2
- package/dist/coding-workspace.js +23 -9
- package/dist/contracts/console-api.d.ts +6 -1
- package/dist/contracts/console-api.js +11 -0
- package/dist/control-ui.d.ts +3 -2
- package/dist/control-ui.js +7 -2
- package/dist/demo.js +5 -3
- package/dist/deploy-gate.d.ts +3 -0
- package/dist/deploy-gate.js +3 -2
- package/dist/desktop-update.d.ts +4 -1
- package/dist/desktop-update.js +43 -9
- package/dist/discover.js +6 -1
- package/dist/first-run.d.ts +8 -5
- package/dist/first-run.js +42 -7
- package/dist/flow-card-task.js +3 -2
- package/dist/flow-unattended.d.ts +2 -0
- package/dist/flow-unattended.js +12 -1
- package/dist/git-incident.d.ts +49 -0
- package/dist/git-incident.js +69 -0
- package/dist/git-views.d.ts +20 -0
- package/dist/git-views.js +120 -0
- package/dist/grant.js +3 -1
- package/dist/integrations.d.ts +4 -3
- package/dist/integrations.js +5 -6
- package/dist/lead-contract.d.ts +1 -1
- package/dist/lead-contract.js +2 -2
- package/dist/lead-follow.js +3 -1
- package/dist/openrouter-models.d.ts +5 -2
- package/dist/openrouter-models.js +7 -4
- package/dist/operate.js +9 -1
- package/dist/planner.js +5 -1
- package/dist/project-knowledge.js +1 -1
- package/dist/project-learning.js +65 -7
- package/dist/project-tools.js +19 -1
- package/dist/pull-request-flow.js +4 -0
- package/dist/pull-request-ui.d.ts +1 -1
- package/dist/pull-request-ui.js +5 -1
- package/dist/push.d.ts +2 -0
- package/dist/push.js +24 -19
- package/dist/read-only-request.d.ts +4 -0
- package/dist/read-only-request.js +15 -0
- package/dist/release-adapters.d.ts +24 -2
- package/dist/release-adapters.js +136 -10
- package/dist/release-authority.d.ts +46 -0
- package/dist/release-authority.js +64 -0
- package/dist/release-auto.d.ts +153 -0
- package/dist/release-auto.js +496 -0
- package/dist/release-cli.js +56 -2
- package/dist/release-coverage.js +1 -1
- package/dist/release.d.ts +105 -1
- package/dist/release.js +229 -9
- package/dist/repository-context.js +5 -2
- package/dist/result-follow-ups.js +23 -2
- package/dist/result-review.js +5 -1
- package/dist/scope.d.ts +31 -2
- package/dist/scope.js +44 -3
- package/dist/scout.js +5 -1
- package/dist/serve.js +162 -6
- package/dist/server/agents-live-filed.d.ts +17 -0
- package/dist/server/agents-live-filed.js +97 -0
- package/dist/server/agents-live-room.js +4 -1
- package/dist/server/chat.js +31 -23
- package/dist/server/http.d.ts +5 -0
- package/dist/server/last-checks.d.ts +17 -0
- package/dist/server/last-checks.js +22 -0
- package/dist/server/pages.js +18 -4
- package/dist/server/people-tokens.js +7 -4
- package/dist/server/render-pages.d.ts +2 -1
- package/dist/server/render-pages.js +28 -24
- package/dist/server/render-results.d.ts +11 -2
- package/dist/server/render-results.js +54 -12
- package/dist/server/render-settings.js +3 -3
- package/dist/server/render-tasks.d.ts +10 -0
- package/dist/server/render-tasks.js +30 -17
- package/dist/server/route-table.js +17 -1
- package/dist/server/runtime.d.ts +16 -1
- package/dist/server/settings.js +111 -31
- package/dist/server/task-views.js +8 -1
- package/dist/server/tasks.js +11 -2
- package/dist/storage-ui.d.ts +2 -0
- package/dist/storage-ui.js +1 -1
- package/dist/storage.js +2 -1
- package/dist/store.d.ts +4 -1
- package/dist/store.js +52 -13
- package/dist/subagent-work.js +3 -0
- package/dist/task-base.js +1 -1
- package/dist/task-status.d.ts +28 -1
- package/dist/task-status.js +93 -11
- package/dist/teardown-git.d.ts +59 -5
- package/dist/teardown-git.js +1022 -90
- package/dist/telegram-progress.js +4 -4
- package/dist/telegram-status.js +8 -3
- package/dist/test-infra.d.ts +26 -0
- package/dist/test-infra.js +321 -0
- package/dist/tool-secret-store.js +8 -0
- package/dist/toolroll-update-ui.d.ts +2 -0
- package/dist/toolroll-update-ui.js +4 -3
- package/dist/toolroll-update.js +10 -9
- package/dist/work-index.js +29 -12
- package/dist/workspace-ui.d.ts +5 -0
- package/dist/workspace-ui.js +38 -10
- package/dist/worktree.d.ts +4 -0
- package/dist/worktree.js +13 -7
- package/package.json +1 -1
|
@@ -232,6 +232,11 @@ export declare function agentsStripHtml(view: RouteView | null, taskId: string):
|
|
|
232
232
|
/** A ceremony nonce as stored: the sha256 hex of the value the form carried. */
|
|
233
233
|
export declare function nonceHashOf(value: string): string;
|
|
234
234
|
/** The approve POST's refusal, in words a person can act on. */
|
|
235
|
+
/** A release task's automatic approval, in words (docs/plans/auto-approve-green.md). */
|
|
236
|
+
export declare function autoReleaseWords(auto: {
|
|
237
|
+
kind: "checking" | "approved";
|
|
238
|
+
candidate: string;
|
|
239
|
+
}): string;
|
|
235
240
|
/** " (password remembered from 10:42)" when the approval stood on a remembered password or sign-in; nothing otherwise. */
|
|
236
241
|
export declare function approvalProofWords(proof: {
|
|
237
242
|
method: string;
|
|
@@ -523,6 +528,11 @@ export declare function taskBodyParts(data: {
|
|
|
523
528
|
method: string;
|
|
524
529
|
at: string;
|
|
525
530
|
} | null;
|
|
531
|
+
/** A release task the plane approved (or is checking) itself (docs/plans/auto-approve-green.md): its candidate. */
|
|
532
|
+
autoRelease?: {
|
|
533
|
+
kind: "checking" | "approved";
|
|
534
|
+
candidate: string;
|
|
535
|
+
} | null;
|
|
526
536
|
/** One approval per release (release-once.md): the release this task belongs to, and whether this reader may stop it. */
|
|
527
537
|
release?: ReleaseView | null;
|
|
528
538
|
canStopRelease?: boolean;
|
|
@@ -746,6 +746,11 @@ export function nonceHashOf(value) {
|
|
|
746
746
|
return createHash("sha256").update(value).digest("hex");
|
|
747
747
|
}
|
|
748
748
|
/** The approve POST's refusal, in words a person can act on. */
|
|
749
|
+
/** A release task's automatic approval, in words (docs/plans/auto-approve-green.md). */
|
|
750
|
+
export function autoReleaseWords(auto) {
|
|
751
|
+
return auto.kind === "approved" ? `Approved automatically: CI and release check passed on ${auto.candidate.slice(0, 7)}`
|
|
752
|
+
: `Checking automatically: approves itself once CI and the release check pass on ${auto.candidate.slice(0, 7)}`;
|
|
753
|
+
}
|
|
749
754
|
/** " (password remembered from 10:42)" when the approval stood on a remembered password or sign-in; nothing otherwise. */
|
|
750
755
|
export function approvalProofWords(proof) {
|
|
751
756
|
if (proof == null)
|
|
@@ -1437,6 +1442,11 @@ export function taskBodyParts(data) {
|
|
|
1437
1442
|
if (resultStatus.token === "evidence-damaged") {
|
|
1438
1443
|
return withReview(box("problem", resultStatus.label, html `<a href="/r/${proof.runId}">Build #${proof.runId}</a> finished. ${resultStatus.detail}${publicationWords}`, "evidence-damaged"), html `<div class="proof-review-actions"><a class="button-link" href="/r/${proof.runId}">Review the evidence problems</a></div>`, proofDetails);
|
|
1439
1444
|
}
|
|
1445
|
+
// A recorded pass the check read now doesn't stand behind (unreadable, failed since, or not read here) never
|
|
1446
|
+
// reads "passed": the box says what holds now (task-status.ts approvedCheckOf).
|
|
1447
|
+
if (proof.proofVerdict === "verified" && !accepted && !(proof.receipt?.checks !== undefined && ["ready-to-review", "pr-opened", "merge-observed", "pr-closed"].includes(resultStatus.token))) {
|
|
1448
|
+
return withReview(box(resultStatus.tone === "problem" ? "problem" : "ok", resultStatus.label, html `<a href="/r/${proof.runId}">Build #${proof.runId}</a> finished. ${resultStatus.detail}${publicationWords}`, resultStatus.token), proofDetails);
|
|
1449
|
+
}
|
|
1440
1450
|
if (proof.proofVerdict === "verified" && !accepted) {
|
|
1441
1451
|
const recovered = verificationRecovered(proof.proofReasons);
|
|
1442
1452
|
return withReview(box("ok", resultStatus.label, recovered
|
|
@@ -1492,11 +1502,13 @@ export function taskBodyParts(data) {
|
|
|
1492
1502
|
html `<p><strong>Quality</strong> ${qualityModeTitle(scope.qualityMode ?? "default")}</p>`,
|
|
1493
1503
|
acceptanceCeremonyHtml(scope.acceptance),
|
|
1494
1504
|
scope.releaseTerms == null ? "" : html `<p><strong>Release</strong></p><p class="recap">${releaseTermsSentence(scope.releaseTerms)}</p>${scope.releaseTerms.policy.carry ? html `<details><summary>What asks again</summary><ul>${joinHtml(releaseAsksAgainWords(scope.releaseTerms).map(line => html `<li>${line}</li>`), "")}</ul></details>` : ""}`,
|
|
1495
|
-
approval.approved && data.
|
|
1496
|
-
? html `<p class="meta scope-seal"
|
|
1497
|
-
: approval.approved
|
|
1498
|
-
? html `<p class="meta scope-seal">
|
|
1499
|
-
:
|
|
1505
|
+
approval.approved && data.autoRelease != null
|
|
1506
|
+
? html `<p class="meta scope-seal">${autoReleaseWords(data.autoRelease)} · <span class="seal">signs ${shortDigest(scope.digest)}</span></p>`
|
|
1507
|
+
: approval.approved && data.release?.kind === "carried"
|
|
1508
|
+
? html `<p class="meta scope-seal">Carried from ${data.release.approver ?? "a person"}'s release approval · <a href="${taskHref(data.release.anchorTask)}">Release ${data.release.version}</a> · <span class="seal">signs ${shortDigest(scope.digest)}</span></p>`
|
|
1509
|
+
: approval.approved
|
|
1510
|
+
? html `<p class="meta scope-seal">Approved by ${approval.by}${approvalProofWords(data.approvalProof)} · ${whenTime(approval.at)} · <span class="seal">signs ${shortDigest(scope.digest)}</span><span class="so-sr-only"> — approval binds to this exact wording</span></p>`
|
|
1511
|
+
: html `<p class="meta"><span class="seal">signs ${shortDigest(scope.digest)}</span> — approval binds to this exact wording</p><p class="meta">Not approved${approval.reason === "changed" ? " — approved once, then rewritten" : ""}</p>`,
|
|
1500
1512
|
html `</div>`,
|
|
1501
1513
|
], "\n");
|
|
1502
1514
|
// The scout's report (mate arc §10): title, summary, the document inert,
|
|
@@ -1787,11 +1799,13 @@ export function taskBodyParts(data) {
|
|
|
1787
1799
|
: "";
|
|
1788
1800
|
const scopeRow = scope === null
|
|
1789
1801
|
? prop("scope", "none yet")
|
|
1790
|
-
: approval.approved && data.
|
|
1791
|
-
? prop("approved scope", html `<span class="seal">signs ${shortDigest(scope.digest)}</span> ·
|
|
1792
|
-
: approval.approved
|
|
1793
|
-
? prop("approved scope", html `<span class="seal">signs ${shortDigest(scope.digest)}</span> · ${
|
|
1794
|
-
:
|
|
1802
|
+
: approval.approved && data.autoRelease != null
|
|
1803
|
+
? prop("approved scope", html `<span class="seal">signs ${shortDigest(scope.digest)}</span> · ${autoReleaseWords(data.autoRelease)}`)
|
|
1804
|
+
: approval.approved && data.release?.kind === "carried"
|
|
1805
|
+
? prop("approved scope", html `<span class="seal">signs ${shortDigest(scope.digest)}</span> · carried from ${data.release.approver ?? "a person"}'s release approval · <a href="${taskHref(data.release.anchorTask)}">Release ${data.release.version}</a>`)
|
|
1806
|
+
: approval.approved
|
|
1807
|
+
? prop("approved scope", html `<span class="seal">signs ${shortDigest(scope.digest)}</span> · ${qualityModeTitle(scope.qualityMode ?? "default")} · approved by ${approval.by}${approvalProofWords(data.approvalProof)} · ${whenTime(approval.at)}`)
|
|
1808
|
+
: prop("scope", approval.reason === "changed" ? "rewritten since its approval — needs a new yes" : "not approved");
|
|
1795
1809
|
const strikesRow = data.strikes > 0 ? prop("strikes", `${data.strikes} failed attempt(s)`) : "";
|
|
1796
1810
|
const propsCard = html `<div class="card props">${workerRow}${queueRow}${scopeRow}${publishesRow}${economics}${strikesRow}</div>`;
|
|
1797
1811
|
// The same facts for the rebuilt page, row for row.
|
|
@@ -1806,9 +1820,10 @@ export function taskBodyParts(data) {
|
|
|
1806
1820
|
facts.push({ label: "Queue", parts: [`${data.position.position} of ${data.position.total}${data.position.column === null ? " in the shared queue" : ` in ${data.position.column}'s queue`} · `, { label: "Reorder", href: "/board?view=order" }] });
|
|
1807
1821
|
}
|
|
1808
1822
|
facts.push(scope === null ? { label: "Scope", parts: ["none yet"] }
|
|
1809
|
-
: approval.approved && data.
|
|
1810
|
-
: approval.approved ? { label: "Approved scope", parts: [
|
|
1811
|
-
: { label: "
|
|
1823
|
+
: approval.approved && data.autoRelease != null ? { label: "Approved scope", parts: [autoReleaseWords(data.autoRelease)] }
|
|
1824
|
+
: approval.approved && data.release?.kind === "carried" ? { label: "Approved scope", parts: [`Carried from ${data.release.approver ?? "a person"}'s release approval · `, { label: `Release ${data.release.version}`, href: taskHref(data.release.anchorTask) }] }
|
|
1825
|
+
: approval.approved ? { label: "Approved scope", parts: [{ seal: `signs ${scope.digest.length <= 12 ? scope.digest : `${scope.digest.slice(0, 12)}…`}` }, ` · ${qualityModeTitle(scope.qualityMode ?? "default")} · approved by ${approval.by}${approvalProofWords(data.approvalProof)}`, ...(approval.at === null ? [] : [" · ", { at: approval.at }])] }
|
|
1826
|
+
: { label: "Scope", parts: [approval.reason === "changed" ? "rewritten since its approval — needs a new yes" : "not approved"] });
|
|
1812
1827
|
if (data.release?.kind === "anchor")
|
|
1813
1828
|
facts.push({ label: "Release", parts: [`Approved for release ${data.release.version}`, ...(data.release.landed.length === 0 ? [] : [` · later commits: ${data.release.landed.map(one => one.sha.slice(0, 7)).join(", ")}`]), ...(data.release.carrying ? [] : [` · later commits ask again`])] });
|
|
1814
1829
|
facts.push({ label: "Publishes as", parts: [publishesAs] });
|
|
@@ -2364,10 +2379,8 @@ export function taskBodyParts(data) {
|
|
|
2364
2379
|
journey: task.state === "cancelled" || data.status === undefined ? null : (() => {
|
|
2365
2380
|
const planning = data.plan === "requested" && approveForm === "";
|
|
2366
2381
|
const read = data.assignment != null ? assignmentStageOf(data.assignment, { token: data.status.token }, planning) : stageOfCode(data.status.token, { planning });
|
|
2367
|
-
// The
|
|
2368
|
-
const
|
|
2369
|
-
const checks = shown === null ? null : shown.mark === "ok" ? "passed" : shown.mark === "running" ? "running"
|
|
2370
|
-
: shown.mark === "failed" || /\bfailed\b/i.test(shown.text) ? "failed" : shown.mark === "none" ? "not-run" : "unread";
|
|
2382
|
+
// The one answer the status card's headline and Project checks row read (task-status.ts approvedCheckOf).
|
|
2383
|
+
const checks = statusCard?.status.check ?? null;
|
|
2371
2384
|
const report = data.deliverable === "report" || data.report != null;
|
|
2372
2385
|
return taskRoad(read, scope !== null && approvalOf(scope).approved, task.state === "done", { checks, report });
|
|
2373
2386
|
})(),
|
|
@@ -237,6 +237,10 @@ for (const one of ["close", "remove"])
|
|
|
237
237
|
post("tasks", `term.${one}`, `^\\/term\\/${TERMINAL}\\/${one}$`, { ...TERM, sample: `/term/${"a".repeat(32)}/${one}` });
|
|
238
238
|
for (const one of ["send", "stop", "resume", "recover", "continue", "ship", "answer"])
|
|
239
239
|
post("tasks", `code.${one}-send`, `^\\/code\\/${SESSION}\\/${one}$`, { ...CODING_ACT, sample: `/code/${"a".repeat(32)}/${one}` });
|
|
240
|
+
// Reading a coding checkout runs git in a folder an agent writes, so it is a POST with the form token, never a page
|
|
241
|
+
// visit (docs/plans/get-side-effects.md): the session's changes, and the review-for-shipping preview.
|
|
242
|
+
post("tasks", "code.changes-read", `^\\/code\\/${SESSION}\\/changes$`, { project: "coding", limited: "self", callers: ["cookie"], sample: `/code/${"a".repeat(32)}/changes` });
|
|
243
|
+
post("tasks", "code.ship-preview", `^\\/code\\/${SESSION}\\/ship\\/preview$`, { project: "coding", limited: "self", callers: ["cookie"], sample: `/code/${"a".repeat(32)}/ship/preview` });
|
|
240
244
|
// Any other coding action answers "not found" after the same access check.
|
|
241
245
|
post("tasks", "code.act-other", "^\\/code(\\/.*)?$", { ...CODING_ACT, sample: "/code/unknown" });
|
|
242
246
|
post("settings", "settings.skills-revise", "/settings/skills/revise", { project: "form", limited: "unscoped", ...approver("Sign in to revise a test.", "/settings/skills") });
|
|
@@ -299,6 +303,9 @@ post("tasks", "sweep.cancel-rest", "^\\/sweeps\\/[1-9][0-9]{0,9}\\/cancel-rest$"
|
|
|
299
303
|
// (the handler proves it as a credential act); turning it off is one click, so the row itself is not a step-up.
|
|
300
304
|
get("settings", "settings.passwordless", "/settings/passwordless", { ...free, ...operator("An instance operator sets Passwordless.", "/settings") });
|
|
301
305
|
post("settings", "settings.passwordless-send", "/settings/passwordless", { callers: ["cookie"], ...operator("An instance operator sets Passwordless.", "/settings") });
|
|
306
|
+
// Release approval (docs/plans/auto-approve-green.md): an approver of the project, from a browser. Approving releases
|
|
307
|
+
// automatically takes a typed password (the handler proves it as a credential act); asking again is one click.
|
|
308
|
+
post("settings", "settings.release-approval-send", "/settings/release-approval", { callers: ["cookie"], ...approver("An approver sets how releases are approved.", "/settings/approval") });
|
|
302
309
|
post("pages", "ledger.checkpoint", "/ledger/checkpoint", operator("An instance operator makes ledger checkpoints.", "/ledger"));
|
|
303
310
|
const SETTINGS_ACT_ROLES = {
|
|
304
311
|
policy: operator("An instance operator sets the policy.", "/settings/policy"),
|
|
@@ -308,10 +315,12 @@ const SETTINGS_ACT_ROLES = {
|
|
|
308
315
|
updates: operator("An instance operator updates Toolroll.", "/settings"),
|
|
309
316
|
"updates/seen": operator("An instance operator updates Toolroll.", "/settings"),
|
|
310
317
|
"updates/cancel": operator("An instance operator updates Toolroll.", "/settings"),
|
|
318
|
+
"updates/check": operator("An instance operator updates Toolroll.", "/settings"),
|
|
311
319
|
retention: operator("An instance operator sets retention.", "/settings"),
|
|
312
320
|
storage: operator("An instance operator looks after storage.", "/settings"),
|
|
313
321
|
"storage/clean": operator("An instance operator looks after storage.", "/settings"),
|
|
314
322
|
"storage/discard": operator("An instance operator looks after storage.", "/settings"),
|
|
323
|
+
"storage/check": operator("An instance operator looks after storage.", "/settings"),
|
|
315
324
|
"backups/now": operator("An instance operator looks after backups.", "/settings"),
|
|
316
325
|
backups: operator("An instance operator looks after backups.", "/settings"),
|
|
317
326
|
data: operator("An instance operator exports data.", "/settings"),
|
|
@@ -320,7 +329,7 @@ const SETTINGS_ACT_ROLES = {
|
|
|
320
329
|
"tools/connect": approver("Sign in as an approver to connect tools.", "/settings/tools"),
|
|
321
330
|
"tools/change": approver("Sign in as an approver to manage tools.", "/settings/tools"),
|
|
322
331
|
};
|
|
323
|
-
for (const one of ["policy", "approval", "request-limits", "sessions", "sign-in", "updates", "updates/seen", "updates/cancel", "retention", "storage", "storage/clean", "storage/discard", "pull-requests", "checks", "backups/now", "backups", "data", "integrations/test", "monitoring", "tools/connect", "tools/change", "appearance"]) {
|
|
332
|
+
for (const one of ["policy", "approval", "request-limits", "sessions", "sign-in", "updates", "updates/seen", "updates/cancel", "updates/check", "retention", "storage", "storage/clean", "storage/discard", "storage/check", "pull-requests", "pull-requests/check", "checks", "backups/now", "backups", "data", "integrations/test", "monitoring", "tools/connect", "tools/change", "appearance"]) {
|
|
324
333
|
post(one === "sessions" ? "people" : "settings", `settings.${one.replaceAll("/", "-")}-send`, `/settings/${one}`, { ...(["policy", "approval", "request-limits", "sessions", "sign-in", "updates", "updates/seen", "updates/cancel", "retention", "storage", "storage/clean", "storage/discard", "pull-requests", "checks", "backups", "data", "monitoring", "tools/connect", "tools/change"].includes(one) ? { scope: "step-up" } : {}), ...SETTINGS_ACT_ROLES[one] });
|
|
325
334
|
}
|
|
326
335
|
post("settings", "settings.flows-on", "/settings/flows/on", { limited: "collection", ...approver("An approver switches starter flows on.", "/settings/flows") });
|
|
@@ -334,6 +343,9 @@ post("settings", "settings.knowledge-proposal", "/settings/knowledge/proposal",
|
|
|
334
343
|
post("settings", "settings.knowledge-decision", "/settings/knowledge/decision", { project: "form", ...approver("Sign in as an approver to change project decisions.", "/settings/knowledge") });
|
|
335
344
|
post("settings", "settings.knowledge-change", "/settings/knowledge/change", { project: "form", limited: "unscoped", ...approver("Sign in as an approver to change project knowledge.", "/settings/knowledge") });
|
|
336
345
|
post("settings", "settings.learning-change", "/settings/learning/change", { project: "form", limited: "unscoped" });
|
|
346
|
+
// A page shows the last check; these ask again (docs/plans/get-side-effects.md).
|
|
347
|
+
post("settings", "control.models-refresh", "/control/models-refresh", anyApprover("Project setup requires an approver."));
|
|
348
|
+
post("settings", "settings.connection-check", "/settings/connection/check", anyApprover("An approver checks a sign-in.", "/settings"));
|
|
337
349
|
for (const one of ["setup-preview", "setup-approve", "instructions-preview", "instructions-approve"])
|
|
338
350
|
post("settings", `control.${one}`, `/control/${one}`, { ...(one.endsWith("approve") ? { scope: "step-up" } : {}) });
|
|
339
351
|
for (const [service, name] of [["slack", "Slack"], ["teams", "Teams"], ["discord", "Discord"]]) {
|
|
@@ -348,6 +360,7 @@ post("settings", "settings.telegram-unpair", "/settings/telegram/unpair", { limi
|
|
|
348
360
|
for (const one of ["messaging", "permission-default", "quality-default", "notifications", "notifications/mute", "telegram-digest", "provider-key", "provider-key-clear", "telegram-token", "email", "email-test", "email-read-test", "google", "google/disconnect"]) {
|
|
349
361
|
post("settings", `settings.${one.replaceAll("/", "-")}-send`, `/settings/${one}`);
|
|
350
362
|
}
|
|
363
|
+
post("pages", "projects.github-list", "/projects/github", { callers: ["cookie"], ...anyApprover("your login can watch — adding projects is an approver's act") });
|
|
351
364
|
post("pages", "projects.select", "/projects/select", { project: "form-path", limited: "unscoped", viewer: true });
|
|
352
365
|
post("pages", "projects.remove", "/projects/remove", { project: "form", ...operator("An instance operator removes projects.", "/settings/project") });
|
|
353
366
|
post("pages", "projects.open", "/projects/open", { project: "form-path" });
|
|
@@ -397,6 +410,9 @@ post("pages", "projects.onboard-preview", "/projects/onboard-preview", { project
|
|
|
397
410
|
post("pages", "projects.onboard-confirm", "/projects/onboard-confirm", { scope: "step-up", project: "none" });
|
|
398
411
|
post("chat", "push.subscribe", "/push/subscribe", { scope: "step-up" });
|
|
399
412
|
post("chat", "push.remove", "/push/remove");
|
|
413
|
+
// Ask this computer's agent CLIs again (docs/plans/get-side-effects.md): Chat's first run and Settings → Lead's Check
|
|
414
|
+
// now. GET /lead/status only reads the last answer.
|
|
415
|
+
post("chat", "lead.check", "/lead/check", { callers: ["cookie"] });
|
|
400
416
|
post("chat", "onboarding.phone-dismiss", "/onboarding/phone/dismiss", approver("An approver puts this away.", "/chat"));
|
|
401
417
|
const LEAD_WORDS = { identity: "An approver names their lead.", about: "An approver edits what their lead knows about them.", "promise/cancel": "An approver manages the lead's promises.", on: "An approver turns the lead on." };
|
|
402
418
|
for (const one of ["identity", "about", "promise/cancel", "on"])
|
package/dist/server/runtime.d.ts
CHANGED
|
@@ -158,6 +158,8 @@ export interface ServerRuntime {
|
|
|
158
158
|
source: "environment" | "stored";
|
|
159
159
|
} | null;
|
|
160
160
|
chatCatalog: () => Promise<import("../converse.js").CatalogModel[] | null>;
|
|
161
|
+
/** The catalog as last fetched: what a page shows. A page never fetches. */
|
|
162
|
+
chatCatalogKnown: () => import("../converse.js").CatalogModel[] | null;
|
|
161
163
|
leadRecheckedAt: number;
|
|
162
164
|
checkLocalAgents: (fresh?: boolean) => Promise<void>;
|
|
163
165
|
localSignIn: {
|
|
@@ -167,7 +169,18 @@ export interface ServerRuntime {
|
|
|
167
169
|
} | null;
|
|
168
170
|
agentSignInCommand: () => string;
|
|
169
171
|
connectionCheck: (provider: ProviderId, fresh?: boolean) => Promise<ProviderConnection>;
|
|
170
|
-
|
|
172
|
+
/** What the last sign-in check said: what a page shows. A page never checks (docs/plans/get-side-effects.md). */
|
|
173
|
+
connectionKnown: (provider: ProviderId) => ProviderConnection | null;
|
|
174
|
+
/** The last answer of each other check a page shows; a page only reads it. */
|
|
175
|
+
lastChecks: import("./last-checks.js").LastChecks;
|
|
176
|
+
checkRelease: () => Promise<{
|
|
177
|
+
version: string;
|
|
178
|
+
} | {
|
|
179
|
+
problem: string;
|
|
180
|
+
}>;
|
|
181
|
+
modelCatalog: ((key: string | null, refresh?: boolean) => Promise<OpenRouterModels>) & {
|
|
182
|
+
known: (key: string | null) => OpenRouterModels | null;
|
|
183
|
+
};
|
|
171
184
|
modelSeams: CatalogSeams;
|
|
172
185
|
toolsViewOf: (repo: string, codex: unknown[] | null, kit?: string | null, wanted?: string | null) => ToolsView;
|
|
173
186
|
codexServers: (repo: string) => Promise<unknown[] | null>;
|
|
@@ -209,6 +222,8 @@ export interface ServerRuntime {
|
|
|
209
222
|
provider: OidcProvider;
|
|
210
223
|
at: number;
|
|
211
224
|
} | null;
|
|
225
|
+
/** The identity provider as last discovered; a page never discovers it. */
|
|
226
|
+
providerKnown: (issuer: string) => import("../oidc.js").OidcProvider | null;
|
|
212
227
|
providerFor: (issuer: string) => Promise<{
|
|
213
228
|
ok: true;
|
|
214
229
|
provider: OidcProvider;
|
package/dist/server/settings.js
CHANGED
|
@@ -39,7 +39,8 @@ import { browserCaller } from "./session.js";
|
|
|
39
39
|
import { DEFAULT_ACCENT, normalHex } from "../accent-colors.js";
|
|
40
40
|
import { resolvePhaseAgent } from "../agentconfig.js";
|
|
41
41
|
import { parseProtectedPaths } from "../approval-policy.js";
|
|
42
|
-
import { approvalRulesHtml } from "../approval-rules-ui.js";
|
|
42
|
+
import { approvalRulesHtml, releaseApprovalHtml } from "../approval-rules-ui.js";
|
|
43
|
+
import { releaseApprovalSetting, setReleaseApproval } from "../release-auto.js";
|
|
43
44
|
import { backupHtml } from "../backup-ui.js";
|
|
44
45
|
import { BACKUP_EVERY_HOURS, backupFolderOf, backupNow, checkBackupFolder, defaultBackupFolder, MAX_KEEP } from "../backup.js";
|
|
45
46
|
import { projectBatchChecks, setProjectBatchChecks } from '../batch-policy.js';
|
|
@@ -122,14 +123,14 @@ import { teamsSettingsHtml } from "../teams-settings.js";
|
|
|
122
123
|
import { telegramSettingsHtml } from "../telegram-settings.js";
|
|
123
124
|
import { hashPairingCode, loadBotToken, mintPairingCode, PAIRING_TTL_MS, redactToken, saveBotToken } from "../telegram.js";
|
|
124
125
|
import { newerThan, updatesHtml, updatesScript, updateStepsHtml } from "../toolroll-update-ui.js";
|
|
125
|
-
import { abandonRuntimeUpdate, currentRuntime, launchRuntimeUpdate, markWhatsNewSeen, prepareRuntimeUpdate,
|
|
126
|
+
import { abandonRuntimeUpdate, currentRuntime, launchRuntimeUpdate, markWhatsNewSeen, prepareRuntimeUpdate, requestRuntimeUpdateCancel, runningWorkWords, runtimeUpdateStatus, runtimeUpdateTerminal } from "../toolroll-update.js";
|
|
126
127
|
import { TOOLS_PROJECT_SCRIPT, toolsHtml, toolsProjectPicker } from "../tools-ui.js";
|
|
127
128
|
import { effectivePrimary, isMessagingChannel, savePrimary } from "../webhooks.js";
|
|
128
129
|
import { personChip, ROLE_TITLES } from "./chrome.js";
|
|
129
130
|
import { chatReturnWithSaid, goOutside } from "./http.js";
|
|
130
|
-
import {
|
|
131
|
+
import { leadSettingsHtml, notificationProjects, SETTINGS_AUTOSAVE_SCRIPT, settingsPage, settingsWorkers, telegramDeliveryWords, telegramTrouble } from "./render-settings.js";
|
|
131
132
|
export function createSettingsHandlers(runtime) {
|
|
132
|
-
const { store, providerHome, managedRepos, consoleProjects, sendScreen, chromeFor, connectionCheck, clock, modelCatalog, modelSeams, toolsViewOf, codexServers, projectViewOf, sessions, restricted, options, integrations, consoleOrigin, evidenceRoot, storagePool, visible, ssoSettings, checkLocalAgents, chatKeyFor, chatCatalog, leadWords, agentSignInCommand, settingsUpdates, phoneSetup, authenticateApprover, authenticateCredential, deletedRepos, projectOf, providerFor, admissionList, toolHome, connectVisits, mintApprovalNonce, consumeApprovalNonce, defaultProject, googleVisits } = runtime;
|
|
133
|
+
const { store, providerHome, managedRepos, consoleProjects, sendScreen, chromeFor, connectionCheck, connectionKnown, lastChecks, checkRelease, clock, modelCatalog, modelSeams, toolsViewOf, codexServers, projectViewOf, sessions, restricted, options, integrations, consoleOrigin, evidenceRoot, storagePool, visible, ssoSettings, checkLocalAgents, chatKeyFor, chatCatalog, chatCatalogKnown, leadWords, agentSignInCommand, settingsUpdates, phoneSetup, authenticateApprover, authenticateCredential, deletedRepos, projectOf, providerFor, admissionList, toolHome, connectVisits, mintApprovalNonce, consumeApprovalNonce, defaultProject, googleVisits } = runtime;
|
|
133
134
|
// v105: what agent work cost, by month, and the monthly budgets. An instance operator's page.
|
|
134
135
|
async function spendPage(ctx) {
|
|
135
136
|
const { url, who, response, now } = ctx;
|
|
@@ -225,10 +226,11 @@ ${hasGrant ? "" : html `<span class="meta">self-merging needs a merge-capable pu
|
|
|
225
226
|
if (!isProviderId(asked))
|
|
226
227
|
return refuse(response, who, 400, "Choose a known provider.", "/control");
|
|
227
228
|
const task = url.searchParams.get("task") ?? "";
|
|
228
|
-
|
|
229
|
+
// The last sign-in check: Check again posts (docs/plans/get-side-effects.md).
|
|
230
|
+
const connection = connectionKnown(asked);
|
|
229
231
|
if (view === "connection") {
|
|
230
232
|
const command = asked === "claude" ? "claude auth login" : asked === "codex" ? "codex login" : asked === "gemini" ? "gemini" : null;
|
|
231
|
-
return sendScreen(response, 200, screen(`${ASSISTANTS[asked].name} connection`, html `<h1>${ASSISTANTS[asked].name} ${connection
|
|
233
|
+
return sendScreen(response, 200, screen(`${ASSISTANTS[asked].name} connection`, html `<h1>${ASSISTANTS[asked].name} ${connection?.state === "connected" ? "is connected" : "connection"}</h1>\
|
|
232
234
|
${connectionHtml(asked, connection, task)}${command === null ? "" : html `<p>On the computer running Toolroll, sign in with:</p><pre class="recap">${command}</pre>`}\
|
|
233
235
|
<p><a href="/settings#providers">Manage API keys and authentication mode</a></p><input type="hidden" name="resume-task" value="${task}">`, { chrome: chromeFor(project, "settings") }));
|
|
234
236
|
}
|
|
@@ -241,20 +243,61 @@ ${connectionHtml(asked, connection, task)}${command === null ? "" : html `<p>On
|
|
|
241
243
|
const models = live.length === 0 ? modelChoices(asked, configured, providerHome)
|
|
242
244
|
: configured && !live.some(one => one.value === configured) ? [{ value: configured, label: `${configured} — current choice` }, ...live] : live;
|
|
243
245
|
const instructions = previewProjectInstructions(project);
|
|
244
|
-
|
|
246
|
+
// The model list as last loaded; Refresh models posts, and the background checks keep it current.
|
|
247
|
+
const catalog = asked === "openrouter" ? modelCatalog.known(readProviderKey("openrouter", providerHome)) ?? { ok: false, problem: "The model list has not loaded yet." } : null;
|
|
245
248
|
const inputs = { provider: asked, model: saved?.provider === asked ? saved.model ?? "" : models[0]?.value ?? "", command: setup?.command ?? (saved === null ? preparation?.command ?? "" : ""), seconds: String((setup?.timeoutMs ?? 300_000) / 1000) };
|
|
246
249
|
return sendScreen(response, 200, screen("Project setup", controlSetupHtml({ repo: project, csrf: who.via === "cookie" ? who.session.csrf : "", provider: asked, inputs, models, connection, catalog, task, preparation,
|
|
247
250
|
instructions: instructions.ok ? { installed: instructions.installed } : { installed: false, message: instructions.message } }), { chrome: chromeFor(project, "settings"), ...(catalog === null ? {} : { functional: { script: openRouterPickerScript() } }) }));
|
|
248
251
|
};
|
|
252
|
+
// Settings → Projects → Pull requests → Check setup: git and GitHub, for one project, by an approver who may turn it on.
|
|
253
|
+
async function pullRequestsCheck(ctx) {
|
|
254
|
+
const { who, response, posted } = ctx;
|
|
255
|
+
const repo = readForm(posted, CONSOLE_FORMS.pullRequestsCheck).get("repo") ?? "";
|
|
256
|
+
const known = [...new Set([...managedRepos(), ...store.knownRepos(), ...store.listProjects().map(one => one.path)])];
|
|
257
|
+
if (who.via !== "cookie" || !known.includes(repo) || !visible(repo))
|
|
258
|
+
return refuse(response, who, 404, "No such project.", "/projects");
|
|
259
|
+
if (who.role !== "approver" || store.isDemo())
|
|
260
|
+
return refuse(response, who, 403, "An approver can turn on pull requests.", "/projects");
|
|
261
|
+
await lastChecks.run(`publishing:${repo}`, () => checkPublishing(repo, options.publishExec === undefined ? {} : { exec: options.publishExec }));
|
|
262
|
+
return redirect(response, `/settings/pull-requests?repo=${encodeURIComponent(repo)}`);
|
|
263
|
+
}
|
|
264
|
+
// Settings → Storage → Check now: measure the checkouts again.
|
|
265
|
+
async function storageCheck(ctx) {
|
|
266
|
+
const databaseFile = store.databaseFile();
|
|
267
|
+
if (databaseFile !== null)
|
|
268
|
+
await measureStorage(databaseFile);
|
|
269
|
+
return redirect(ctx.response, "/settings/storage");
|
|
270
|
+
}
|
|
271
|
+
const measureStorage = (databaseFile) => lastChecks.run("storage", () => checkoutPlan(store, storagePool(databaseFile), clock(), { manual: true }));
|
|
272
|
+
// Settings → Updates → Check now: ask npm once, whether or not automatic checks are on.
|
|
273
|
+
async function updatesCheck(ctx) {
|
|
274
|
+
await checkRelease();
|
|
275
|
+
return redirect(ctx.response, "/settings/updates");
|
|
276
|
+
}
|
|
277
|
+
// Check again (Settings → providers, Project setup): ask this computer's CLI, or test the saved key, now.
|
|
278
|
+
async function connectionCheckNow(ctx) {
|
|
279
|
+
const { response, posted } = ctx;
|
|
280
|
+
const body = readForm(posted, CONSOLE_FORMS.connectionCheck);
|
|
281
|
+
const provider = body.get("provider") ?? "";
|
|
282
|
+
const back = safeReturn(body.get("return") ?? "/settings#providers");
|
|
283
|
+
if (!isProviderId(provider))
|
|
284
|
+
return redirect(response, back);
|
|
285
|
+
await connectionCheck(provider, true).catch(() => undefined);
|
|
286
|
+
return redirect(response, back);
|
|
287
|
+
}
|
|
288
|
+
// Refresh models (Project setup, OpenRouter): load the list again, then show the setup page with it.
|
|
289
|
+
async function modelsRefresh(ctx) {
|
|
290
|
+
const { response, posted } = ctx;
|
|
291
|
+
const body = readForm(posted, CONSOLE_FORMS.modelsRefresh);
|
|
292
|
+
await modelCatalog(readProviderKey("openrouter", providerHome), true);
|
|
293
|
+
const query = new URLSearchParams({ provider: "openrouter", ...(body.get("task") ? { task: body.get("task") } : {}) });
|
|
294
|
+
return redirect(response, `/control?${query.toString()}`);
|
|
295
|
+
}
|
|
249
296
|
async function modelsPage(ctx) {
|
|
250
297
|
const { url, who, response, project } = ctx;
|
|
251
298
|
const now = clock();
|
|
252
|
-
|
|
253
|
-
//
|
|
254
|
-
// the check sends nothing about the person or their projects.
|
|
255
|
-
if (who.role === "approver" && (watch.checkedAt === null || now.getTime() - Date.parse(watch.checkedAt) > 3_600_000)) {
|
|
256
|
-
await checkModels(store, now, modelSeams).catch(() => undefined);
|
|
257
|
-
}
|
|
299
|
+
// The page shows the last check and when it ran (docs/plans/get-side-effects.md): Check now, or the watch loop's
|
|
300
|
+
// automatic checks, fetch the lists and may queue a notice; opening the page never does.
|
|
258
301
|
const view = modelsView(who.role === "approver", who.via === "cookie" ? who.session.csrf : "", now, url.searchParams.get("said"), url.searchParams.get("problem"));
|
|
259
302
|
return sendScreen(response, 200, screen("Models", html `<p><a href="/settings">Settings</a></p><h1>Models</h1>${modelsHtml(view)}`, { chrome: chromeFor(project, "settings"), functional: { script: modelsScript() } }));
|
|
260
303
|
}
|
|
@@ -290,7 +333,8 @@ ${connectionHtml(asked, connection, task)}${command === null ? "" : html `<p>On
|
|
|
290
333
|
if (chosen) {
|
|
291
334
|
try {
|
|
292
335
|
const approver = who.role === "approver" && who.via === "cookie";
|
|
293
|
-
|
|
336
|
+
// What Codex had configured at the last background check (docs/plans/get-side-effects.md): the page starts no codex.
|
|
337
|
+
const view = toolsViewOf(chosen, approver ? lastChecks.known(`codex-tools:${chosen}`)?.value ?? null : null, kit, wanted);
|
|
294
338
|
if (approver)
|
|
295
339
|
view.others = projects.filter(one => one !== chosen).map(one => ({ repo: one, project: projectName(one), open: connectionsOf(store, one).filter(c => c.state === "open").map(c => c.id) }));
|
|
296
340
|
content = (toolsHtml(view, who.via === "cookie" ? who.session.csrf : "", approver, { said: url.searchParams.get("said"), problem: url.searchParams.get("problem") }));
|
|
@@ -323,7 +367,8 @@ ${connectionHtml(asked, connection, task)}${command === null ? "" : html `<p>On
|
|
|
323
367
|
return refuse(response, who, 403, "That project is outside your access.", "/settings/approval");
|
|
324
368
|
const selector = projects.length > 1 ? html `<form class="approval-project" method="get" action="/settings/approval"><label>Project<select name="repo">${projects.map(p => html `<option value="${p}"${p === chosen ? " selected" : ""}>${projectName(p)}</option>`)}</select></label><button>Show</button></form>` : "";
|
|
325
369
|
const approvers = chosen === "" ? 0 : store.listApprovers().filter(one => { const account = store.accountOf(one.name); return account !== null && account.revokedAt === null && account.role === "approver" && store.accountCanAccess(one.name, chosen); }).length;
|
|
326
|
-
const
|
|
370
|
+
const release = chosen === "" ? null : releaseApprovalSetting(store, chosen);
|
|
371
|
+
const content = chosen === "" ? html `<p>Add a project to set its approval rules.</p>` : html `${approvalRulesHtml({ repo: chosen, name: projectName(chosen), rules: store.approvalRules(chosen), canChange: who.via === "cookie" && store.isInstanceOperator(who.name), approvers }, { said: url.searchParams.get("said"), problem: url.searchParams.get("problem") })}${releaseApprovalHtml({ repo: chosen, auto: release.auto, setBy: release.setBy, at: release.at, canChange: who.via === "cookie" && who.role === "approver" && !store.isDemo() && store.accountCanAccess(who.name, chosen) })}`;
|
|
327
372
|
return sendScreen(response, 200, screen("Approval rules", html `<p><a href="/settings">Settings</a></p><h1>Approval rules</h1>${selector}${content}`, { chrome: chromeFor(chosen || project, "settings") }));
|
|
328
373
|
}
|
|
329
374
|
// Sprint 8: Settings → Policy, the organisation policy and its history. Anyone signed in reads it; an instance operator changes it.
|
|
@@ -376,19 +421,16 @@ ${connectionHtml(asked, connection, task)}${command === null ? "" : html `<p>On
|
|
|
376
421
|
const databaseFile = store.databaseFile();
|
|
377
422
|
if (databaseFile === null)
|
|
378
423
|
return refuse(response, who, 403, "An instance operator updates Toolroll.", "/settings");
|
|
379
|
-
// A
|
|
380
|
-
try {
|
|
381
|
-
releaseStalledUpdate(dirname(databaseFile), clock());
|
|
382
|
-
}
|
|
383
|
-
catch { /* shown as last saved */ }
|
|
424
|
+
// A stalled update is released by the background pass, never by opening this page (docs/plans/get-side-effects.md).
|
|
384
425
|
const status = runtimeUpdateStatus(dirname(databaseFile));
|
|
385
426
|
if (url.searchParams.get("fragment") === "steps")
|
|
386
427
|
return respond(response, 200, "text/html; charset=utf-8", status.journal ? updateStepsHtml(status.journal, status.running) : html `<div id="update-live" data-done="1"></div>`);
|
|
387
428
|
const active = status.journal !== null && !runtimeUpdateTerminal(status.journal.phase);
|
|
388
429
|
const current = options.updates?.current ?? PACKAGE_VERSION;
|
|
389
|
-
//
|
|
390
|
-
const checksOff = updateChecksOff(process.env, options.configDir ?? dirname(databaseFile)).off
|
|
391
|
-
const
|
|
430
|
+
// The last answer npm gave (the background checks ask while update checks are on; Check now asks once).
|
|
431
|
+
const checksOff = updateChecksOff(process.env, options.configDir ?? dirname(databaseFile)).off;
|
|
432
|
+
const known = lastChecks.known("release")?.value ?? null;
|
|
433
|
+
const view = { current, latest: active ? { version: current } : known ?? (checksOff ? { off: true } : { pending: true }), method: options.updates?.method ?? installMethod(), journal: status.journal, running: status.running, whatsNew: status.whatsNew, rollbackTo: status.lastUpdate?.to === current ? status.lastUpdate.from : null, csrf: who.session.csrf };
|
|
392
434
|
return sendScreen(response, 200, screen("Updates", html `<p><a href="/settings">Settings</a></p><h1>Updates</h1>${(updatesHtml(view, { said: url.searchParams.get("said"), problem: url.searchParams.get("problem") }))}`, { chrome: chromeFor(project, "settings"), ...(active ? { functional: { script: updatesScript(), fetches: true } } : {}) }));
|
|
393
435
|
}
|
|
394
436
|
// v105: Settings → Retention, how long each kind of data is kept. An instance operator's page.
|
|
@@ -406,9 +448,16 @@ ${connectionHtml(asked, connection, task)}${command === null ? "" : html `<p>On
|
|
|
406
448
|
const databaseFile = store.databaseFile();
|
|
407
449
|
if (databaseFile === null)
|
|
408
450
|
return refuse(response, who, 403, "An instance operator looks after storage.", "/settings");
|
|
409
|
-
|
|
451
|
+
// The last measurement (docs/plans/get-side-effects.md): the background checks measure the checkouts, and so do
|
|
452
|
+
// Check now and every clean-up; opening the page runs no du or git.
|
|
453
|
+
const measured = lastChecks.known("storage");
|
|
454
|
+
const check = postForm("/settings/storage/check", html `<button type="submit" class="secondary">Check now</button>`, { attrs: { class: "inline" } });
|
|
455
|
+
if (measured === null) {
|
|
456
|
+
return sendScreen(response, 200, screen("Storage", html `<p><a href="/settings">Settings</a></p><h1>Storage</h1><section class="storage"><div class="card storage-state"><h2>Checkouts not measured yet</h2>${check}</div></section>`, { chrome: chromeFor(project, "settings") }));
|
|
457
|
+
}
|
|
458
|
+
const plan = { ...measured.value, cleanup: store.checkoutCleanup() };
|
|
410
459
|
const sweep = { last: lastSweep(store), off: storageSweepOff(), schedule: sweepSchedule(store, now) };
|
|
411
|
-
return sendScreen(response, 200, screen("Storage", html `<p><a href="/settings">Settings</a></p><h1>Storage</h1>${(storageHtml({ plan, csrf: who.session.csrf, sweep, landed: landedSummary(store) }, { said: url.searchParams.get("said"), problem: url.searchParams.get("problem") }))}`, { chrome: chromeFor(project, "settings") }));
|
|
460
|
+
return sendScreen(response, 200, screen("Storage", html `<p><a href="/settings">Settings</a></p><h1>Storage</h1>${(storageHtml({ plan, csrf: who.session.csrf, sweep, landed: landedSummary(store), measured: html `<p class="meta">Measured ${new Date(measured.at).toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" })} ${check}</p>` }, { said: url.searchParams.get("said"), problem: url.searchParams.get("problem") }))}`, { chrome: chromeFor(project, "settings") }));
|
|
412
461
|
}
|
|
413
462
|
// Settings → Projects → Pull requests: one project's setup. Off, the checks run on open, so the page says
|
|
414
463
|
// exactly what turning on would do, or what to fix first.
|
|
@@ -420,7 +469,8 @@ ${connectionHtml(asked, connection, task)}${command === null ? "" : html `<p>On
|
|
|
420
469
|
return refuse(response, who, 404, "No such project.", "/projects");
|
|
421
470
|
const publishing = publishingOf(store, repo);
|
|
422
471
|
const canChange = who.role === "approver" && !store.isDemo();
|
|
423
|
-
|
|
472
|
+
// The last setup check for this project (Check setup posts); opening the page asks git and GitHub nothing.
|
|
473
|
+
const check = publishing.on || !canChange ? null : lastChecks.known(`publishing:${repo}`)?.value ?? "unchecked";
|
|
424
474
|
const body = (pullRequestSettingsHtml({ repo, name: projectName(repo), csrf: who.session.csrf, canChange, publishing, check,
|
|
425
475
|
said: url.searchParams.get("said"), problem: url.searchParams.get("problem") }));
|
|
426
476
|
return sendScreen(response, 200, screen("Pull requests", html `<p><a href="/projects">Projects</a></p><h1>Pull requests</h1>${body}`, { chrome: chromeFor(project, "projects") }));
|
|
@@ -563,15 +613,13 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
|
|
|
563
613
|
async function leadPage(ctx) {
|
|
564
614
|
const { url, response, now, project } = ctx;
|
|
565
615
|
const who = browserCaller(ctx.who);
|
|
566
|
-
if (runtime.localSignIn === null)
|
|
567
|
-
await Promise.race([checkLocalAgents(), new Promise(done => setTimeout(done, 6_000).unref?.())]);
|
|
568
616
|
const config = store.getChatConfig();
|
|
569
617
|
const facts = {
|
|
570
618
|
keyFacts: ["anthropic-api", "openrouter-api"].map(one => {
|
|
571
619
|
const found = chatKeyFor(one);
|
|
572
620
|
return { provider: one, state: found === null ? "none" : found.source, tail: found === null || found.source === "environment" ? null : redactToken(found.key) };
|
|
573
621
|
}),
|
|
574
|
-
openrouterModels: (
|
|
622
|
+
openrouterModels: chatCatalogKnown()?.map(one => one.id) ?? null,
|
|
575
623
|
liveModels: [...modelOptions(store, "claude", now, providerHome), ...modelOptions(store, "codex", now, providerHome)],
|
|
576
624
|
csrf: who.session.csrf,
|
|
577
625
|
returnTo: "/settings/lead",
|
|
@@ -646,7 +694,7 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
|
|
|
646
694
|
provider,
|
|
647
695
|
envName: PROVIDER_KEY_ENV[provider],
|
|
648
696
|
...keyStatus(provider, providerHome),
|
|
649
|
-
|
|
697
|
+
...(connectionKnown(provider) === null ? {} : { connection: connectionKnown(provider) }),
|
|
650
698
|
ambient: (process.env[PROVIDER_KEY_ENV[provider]] ?? "") !== "",
|
|
651
699
|
mode: readAuthMode(provider, providerHome) === "api-key" ? "api-key" : "subscription",
|
|
652
700
|
subscriptionCapable: SUBSCRIPTION_CAPABLE[provider],
|
|
@@ -1114,7 +1162,9 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
|
|
|
1114
1162
|
const databaseFile = store.databaseFile();
|
|
1115
1163
|
if (databaseFile === null)
|
|
1116
1164
|
return refuse(response, who, 403, "An instance operator looks after storage.", "/settings");
|
|
1117
|
-
|
|
1165
|
+
// What the page shows next is measured again after the change.
|
|
1166
|
+
const said = sayTo(response, "/settings/storage");
|
|
1167
|
+
const back = async (kind, words) => { await measureStorage(databaseFile).catch(() => undefined); return said(kind, words); };
|
|
1118
1168
|
const what = op === "clean" ? "clean up" : op === "discard" ? "discard a checkout" : "change checkout cleanup";
|
|
1119
1169
|
if (!authenticateApprover(who, body.get("password") ?? "").ok)
|
|
1120
1170
|
return back("problem", `Enter your Toolroll password to ${what}.`);
|
|
@@ -2468,6 +2518,11 @@ ${modeWords(terms)
|
|
|
2468
2518
|
"mode": modePage,
|
|
2469
2519
|
"control": controlPage("setup"),
|
|
2470
2520
|
"control.connection": controlPage("connection"),
|
|
2521
|
+
"settings.updates-check-send": updatesCheck,
|
|
2522
|
+
"settings.storage-check-send": storageCheck,
|
|
2523
|
+
"settings.pull-requests-check-send": pullRequestsCheck,
|
|
2524
|
+
"settings.connection-check": connectionCheckNow,
|
|
2525
|
+
"control.models-refresh": modelsRefresh,
|
|
2471
2526
|
"settings.models": modelsPage,
|
|
2472
2527
|
"settings.tools": toolsPage,
|
|
2473
2528
|
"settings.project": projectPage,
|
|
@@ -2485,6 +2540,7 @@ ${modeWords(terms)
|
|
|
2485
2540
|
"settings.sign-in": signInPage,
|
|
2486
2541
|
"settings.passwordless": passwordlessPage,
|
|
2487
2542
|
"settings.passwordless-send": passwordlessSave,
|
|
2543
|
+
"settings.release-approval-send": releaseApprovalSave,
|
|
2488
2544
|
"settings.lead": leadPage,
|
|
2489
2545
|
"settings.teams": teamsPage,
|
|
2490
2546
|
"settings.discord": discordPage,
|
|
@@ -2672,5 +2728,29 @@ ${modeWords(terms)
|
|
|
2672
2728
|
return back("problem", "An approver for this project changes its sandbox. Nothing changed.");
|
|
2673
2729
|
return back("said", JSON.stringify(changed.before) === JSON.stringify(changed.after) ? "Nothing changed." : "Saved. Tasks scoped from now on use it; tasks already approved keep what they were approved with.");
|
|
2674
2730
|
}
|
|
2731
|
+
// Release approval (docs/plans/auto-approve-green.md): an approver of the project. Approving automatically is a
|
|
2732
|
+
// credential act (a password typed now, never a session); asking again is one click and takes back automatic seals.
|
|
2733
|
+
async function releaseApprovalSave(ctx) {
|
|
2734
|
+
const { response, now, posted } = ctx;
|
|
2735
|
+
const who = browserCaller(ctx.who);
|
|
2736
|
+
const body = readForm(posted, CONSOLE_FORMS.releaseApproval);
|
|
2737
|
+
const repo = body.get("repo") ?? "";
|
|
2738
|
+
if (!consoleProjects().includes(repo) || !visible(repo))
|
|
2739
|
+
return refuse(response, who, 403, "That project is outside your access.", "/settings/approval");
|
|
2740
|
+
const back = sayTo(response, `/settings/approval?repo=${encodeURIComponent(repo)}`);
|
|
2741
|
+
if (who.role !== "approver" || store.isDemo() || !store.accountCanAccess(who.name, repo))
|
|
2742
|
+
return back("problem", "An approver of this project sets how its releases are approved.");
|
|
2743
|
+
const action = body.get("action");
|
|
2744
|
+
if (action === "ask") {
|
|
2745
|
+
const changed = setReleaseApproval(store, repo, false, who.name, now);
|
|
2746
|
+
return back("said", !changed.changed ? "Saved." : `Releases ask you again.${changed.withdrawn.length === 0 ? "" : ` ${changed.withdrawn.length} automatic approval${changed.withdrawn.length === 1 ? " was" : "s were"} taken back.`}`);
|
|
2747
|
+
}
|
|
2748
|
+
if (action !== "auto")
|
|
2749
|
+
return back("problem", "Choose Ask me or Approve automatically when checks pass.");
|
|
2750
|
+
if (!authenticateCredential(who, body.get("password") ?? "", repo).ok)
|
|
2751
|
+
return back("problem", "Enter your Toolroll password to approve releases automatically.");
|
|
2752
|
+
const changed = setReleaseApproval(store, repo, true, who.name, now);
|
|
2753
|
+
return back("said", changed.changed ? "Releases approve themselves once CI and the release check pass." : "Saved.");
|
|
2754
|
+
}
|
|
2675
2755
|
return { registrations, modelsView };
|
|
2676
2756
|
}
|
|
@@ -487,7 +487,6 @@ export function createTaskViews(runtime) {
|
|
|
487
487
|
coordinatorProposals: (() => {
|
|
488
488
|
if (ref === null || ref.repo === null || who.role !== "approver")
|
|
489
489
|
return null;
|
|
490
|
-
runtime.store.sweepCoordinatorProposals(now);
|
|
491
490
|
const rows = runtime.store.listCoordinatorProposals({ repos: [ref.repo], taskId });
|
|
492
491
|
return { rows, decisions: decisionsFor(runtime.store, rows), now };
|
|
493
492
|
})(),
|
|
@@ -497,6 +496,7 @@ export function createTaskViews(runtime) {
|
|
|
497
496
|
steeringRun: ref === null ? null : runtime.store.steeringLiveBuild(ref.id, now),
|
|
498
497
|
release: ref === null ? null : releaseViewOf(runtime.store, ref.externalId, now, authority => releaseObligationsOf(runtime.store, runtime.evidenceRoot, authority)),
|
|
499
498
|
canStopRelease: who.role === "approver" && who.via === "cookie",
|
|
499
|
+
autoRelease: ref === null ? null : autoReleaseOf(runtime.store.getScope(ref.externalId)),
|
|
500
500
|
approvalProof: (() => {
|
|
501
501
|
// The proof of the approval shown: its approver, its exact sealed digest (approvedDigest === digest when approved).
|
|
502
502
|
const scope = ref === null ? null : runtime.store.getScope(ref.externalId);
|
|
@@ -1140,3 +1140,10 @@ export function createTaskViews(runtime) {
|
|
|
1140
1140
|
}
|
|
1141
1141
|
return { guided, projectFamilyPeek, familiesInView, familyTasksInView, revisionDestination, earlierLiveVersions, familyHistory, freshAssignment, workRowOf, taskListPane, runIsLive, runIsTaskResult, revisionViewOf, taskViewData, routeViewOf, taskChatFocus, pullRequestTargetOf, taskPullRequestOf, failureOf, explainAttempt, runChecksHere, taskScreen, armTaskResume, planViewOf, planContractViewOf, revisionDocOf, revisionLedgerOf, progressOf, resultDetailOf };
|
|
1142
1142
|
}
|
|
1143
|
+
/** A release task the plane approved (or is checking) itself, for the task page (docs/plans/auto-approve-green.md). */
|
|
1144
|
+
function autoReleaseOf(scope) {
|
|
1145
|
+
if (scope === null || scope.approvedDigest !== scope.digest || (scope.candidate ?? null) === null)
|
|
1146
|
+
return null;
|
|
1147
|
+
return scope.approvalBasis === "auto-green" ? { kind: "approved", candidate: scope.candidate }
|
|
1148
|
+
: scope.approvalBasis === "auto-check" ? { kind: "checking", candidate: scope.candidate } : null;
|
|
1149
|
+
}
|
package/dist/server/tasks.js
CHANGED
|
@@ -12,7 +12,7 @@ import { handlersOf } from './handler-registry.js';
|
|
|
12
12
|
import { closedQuestionWords } from "../decision.js";
|
|
13
13
|
import { DEPRECATED_PAGE } from "../deprecations.js";
|
|
14
14
|
/** tasks handlers, moved without changing their route bodies. */
|
|
15
|
-
import { html, htmlString, joinHtml, textHtml } from "../html.js";
|
|
15
|
+
import { html, htmlString, joinHtml, postForm, textHtml } from "../html.js";
|
|
16
16
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
17
17
|
import { lstatSync, realpathSync } from "node:fs";
|
|
18
18
|
import {} from "node:http";
|
|
@@ -75,7 +75,9 @@ export function createTasksHandlers(runtime) {
|
|
|
75
75
|
function noPage(who, response) {
|
|
76
76
|
return refuse(response, who, 404, "There's no page at this address.", "/chat");
|
|
77
77
|
}
|
|
78
|
-
|
|
78
|
+
// `posted`: the session page's own POST (form token). A GET of changes or the shipping preview runs no git in the
|
|
79
|
+
// agent's checkout: changes go back to the session page, the preview asks for one click.
|
|
80
|
+
const showCode = (part, posted = false) => async (ctx) => {
|
|
79
81
|
const { url, who, response, project } = ctx;
|
|
80
82
|
if (who.via !== 'cookie' || !codingActorAllowed({ name: who.name, generation: who.session.generation }))
|
|
81
83
|
return refuse(response, who, 403, 'Coding sessions require installation-wide operator access. You can manage project tasks in Work.', '/work');
|
|
@@ -86,6 +88,11 @@ export function createTasksHandlers(runtime) {
|
|
|
86
88
|
const selected = sessionId !== null && runtime.coding ? runtime.coding.get(sessionId, actor) : null;
|
|
87
89
|
if (selected && !codingProjectAllowed(selected.repo))
|
|
88
90
|
return refuse(response, who, 403, 'That project is outside your access.', '/projects');
|
|
91
|
+
if (!posted && part === 'changes' && selected)
|
|
92
|
+
return redirect(response, `/code/${selected.id}`);
|
|
93
|
+
if (!posted && part === 'ship' && selected) {
|
|
94
|
+
return sendScreen(response, 200, screen('Review for shipping', html `<section class="coding-workspace coding-handoff"><h1>Review for shipping</h1><p>${selected.title}</p>${postForm(`/code/${selected.id}/ship/preview`, html `<button type="submit">Review the committed change</button>`)}<p><a href="/code/${selected.id}">Back to coding</a></p></section>`, { chrome: chromeFor(selected.repo, 'code') }));
|
|
95
|
+
}
|
|
89
96
|
if (part === 'ship' && selected) {
|
|
90
97
|
const preview = codingHandoffPreview(store, { sessionId: selected.id, actor: who.name });
|
|
91
98
|
return sendScreen(response, 200, screen('Review for shipping', codingShippingHtml(preview), { chrome: chromeFor(selected.repo, 'code') }));
|
|
@@ -2996,6 +3003,8 @@ export function createTasksHandlers(runtime) {
|
|
|
2996
3003
|
"code.state": showCode("state"),
|
|
2997
3004
|
"code.changes": showCode("changes"),
|
|
2998
3005
|
"code.ship": showCode("ship"),
|
|
3006
|
+
"code.changes-read": showCode("changes", true),
|
|
3007
|
+
"code.ship-preview": showCode("ship", true),
|
|
2999
3008
|
"code.other": showCode("unknown"),
|
|
3000
3009
|
"inbox": showInbox,
|
|
3001
3010
|
"work": showWork,
|