toolroll 0.9.86 → 0.9.88
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 +86 -0
- package/dist/acp-check.d.ts +9 -0
- package/dist/acp-check.js +40 -28
- package/dist/agents-board.d.ts +12 -0
- package/dist/agents-board.js +26 -0
- package/dist/approval-rules-ui.d.ts +4 -1
- package/dist/approval-rules-ui.js +31 -15
- package/dist/assignment-ui.js +1 -1
- package/dist/backup-ui.d.ts +5 -4
- package/dist/backup-ui.js +65 -32
- package/dist/browser/terminal.css +1 -1
- package/dist/browser/terminal.js +57 -54
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +35 -70
- package/dist/browser-crew.js +1 -1
- package/dist/browser-workspace.d.ts +329 -183
- package/dist/browser-workspace.js +10 -9
- package/dist/chat-controls.d.ts +1 -1
- package/dist/chat-controls.js +1 -1
- package/dist/chat-settings-page.d.ts +29 -9
- package/dist/chat-settings-page.js +59 -28
- package/dist/check-levels-ui.d.ts +2 -1
- package/dist/check-levels-ui.js +67 -58
- package/dist/contracts/cli.d.ts +43 -0
- package/dist/contracts/cli.js +4 -0
- package/dist/contracts/console-api.d.ts +1 -1
- package/dist/contracts/console-api.js +1 -1
- package/dist/credentials-ui.d.ts +5 -3
- package/dist/credentials-ui.js +58 -31
- package/dist/design/kit.css +722 -0
- package/dist/design/kit.d.ts +208 -0
- package/dist/design/kit.js +177 -0
- package/dist/design/tokens.css +31 -1
- package/dist/discord-settings.js +8 -4
- package/dist/export-ui.d.ts +6 -4
- package/dist/export-ui.js +18 -8
- package/dist/flow-changed.d.ts +14 -0
- package/dist/flow-changed.js +53 -0
- package/dist/flow-gallery-ui.d.ts +41 -4
- package/dist/flow-gallery-ui.js +95 -63
- package/dist/flow-starters-ui.d.ts +3 -9
- package/dist/flow-starters-ui.js +12 -23
- package/dist/flow-unattended-words.d.ts +20 -0
- package/dist/flow-unattended-words.js +24 -0
- package/dist/flow-unattended.d.ts +9 -5
- package/dist/flow-unattended.js +45 -23
- package/dist/flows-cli.js +7 -3
- package/dist/flows-ui.d.ts +12 -6
- package/dist/flows-ui.js +141 -30
- package/dist/integrations-ui.d.ts +6 -4
- package/dist/integrations-ui.js +34 -40
- package/dist/integrations.d.ts +2 -2
- package/dist/integrations.js +4 -4
- package/dist/kits-ui.d.ts +8 -10
- package/dist/kits-ui.js +29 -33
- package/dist/kits.js +1 -1
- package/dist/knowledge-ui.js +76 -19
- package/dist/ledger-view.d.ts +2 -0
- package/dist/ledger-view.js +41 -38
- package/dist/models-ui.d.ts +8 -3
- package/dist/models-ui.js +82 -59
- package/dist/monitoring-ui.d.ts +4 -2
- package/dist/monitoring-ui.js +44 -13
- package/dist/needs-you.d.ts +1 -1
- package/dist/needs-you.js +8 -0
- package/dist/operate-remote-arguments.js +1 -0
- package/dist/operate-remote.js +1 -1
- package/dist/operate.d.ts +1 -1
- package/dist/operate.js +38 -4
- package/dist/passwordless.d.ts +1 -1
- package/dist/passwordless.js +1 -1
- package/dist/people-audit-ui.d.ts +2 -1
- package/dist/people-audit-ui.js +25 -23
- package/dist/policy-ui.d.ts +3 -2
- package/dist/policy-ui.js +27 -21
- package/dist/policy.d.ts +3 -0
- package/dist/policy.js +14 -1
- package/dist/project-delete-ui.d.ts +4 -2
- package/dist/project-delete-ui.js +135 -126
- package/dist/project-knowledge.d.ts +10 -10
- package/dist/publish.d.ts +11 -0
- package/dist/publish.js +16 -3
- package/dist/pull-request-ui.d.ts +2 -1
- package/dist/pull-request-ui.js +39 -28
- package/dist/recipe-ui.d.ts +4 -2
- package/dist/recipe-ui.js +14 -4
- package/dist/release-adapters.d.ts +3 -0
- package/dist/release-adapters.js +22 -2
- package/dist/release-cli.js +81 -9
- package/dist/release.d.ts +23 -0
- package/dist/release.js +46 -4
- package/dist/releases.js +1 -1
- package/dist/request-budget-ui.d.ts +4 -2
- package/dist/request-budget-ui.js +30 -19
- package/dist/retention-ui.d.ts +4 -3
- package/dist/retention-ui.js +15 -12
- package/dist/scope.d.ts +3 -0
- package/dist/scope.js +6 -0
- package/dist/serve.d.ts +0 -1
- package/dist/serve.js +51 -7
- package/dist/server/agents-computers.d.ts +9 -0
- package/dist/server/agents-computers.js +67 -0
- package/dist/server/chat.js +16 -16
- package/dist/server/chrome.d.ts +1 -1
- package/dist/server/chrome.js +19 -9
- package/dist/server/console.css +24 -12
- package/dist/server/flows.js +64 -36
- package/dist/server/home.d.ts +35 -0
- package/dist/server/home.js +104 -0
- package/dist/server/http.d.ts +3 -0
- package/dist/server/http.js +5 -0
- package/dist/server/kit-standalone.d.ts +22 -0
- package/dist/server/kit-standalone.js +70 -0
- package/dist/server/kit.d.ts +419 -0
- package/dist/server/kit.js +369 -0
- package/dist/server/pages.d.ts +2 -1
- package/dist/server/pages.js +213 -174
- package/dist/server/people-tokens.js +26 -85
- package/dist/server/render-history.d.ts +51 -0
- package/dist/server/render-history.js +228 -0
- package/dist/server/render-new-task.d.ts +29 -0
- package/dist/server/render-new-task.js +96 -0
- package/dist/server/render-next.d.ts +63 -0
- package/dist/server/render-next.js +109 -0
- package/dist/server/render-pages.d.ts +22 -190
- package/dist/server/render-pages.js +123 -575
- package/dist/server/render-people.d.ts +24 -3
- package/dist/server/render-people.js +93 -72
- package/dist/server/render-projects.d.ts +103 -0
- package/dist/server/render-projects.js +247 -0
- package/dist/server/render-results.d.ts +5 -24
- package/dist/server/render-results.js +48 -185
- package/dist/server/render-settings.d.ts +5 -74
- package/dist/server/render-settings.js +102 -493
- package/dist/server/render-tasks.d.ts +35 -5
- package/dist/server/render-tasks.js +114 -85
- package/dist/server/route-table.js +20 -5
- package/dist/server/settings.js +276 -255
- package/dist/server/task-views.d.ts +8 -0
- package/dist/server/task-views.js +85 -2
- package/dist/server/tasks.d.ts +8 -0
- package/dist/server/tasks.js +95 -40
- package/dist/server/terminals.js +1 -1
- package/dist/server/welcome.d.ts +22 -0
- package/dist/server/welcome.js +69 -0
- package/dist/settings-groups-ui.d.ts +279 -0
- package/dist/settings-groups-ui.js +615 -0
- package/dist/settings-kit.d.ts +42 -6
- package/dist/settings-kit.js +69 -7
- package/dist/setup-guide.d.ts +2 -2
- package/dist/skills-ui.js +59 -7
- package/dist/slack-settings.js +12 -5
- package/dist/spend-ui.d.ts +45 -26
- package/dist/spend-ui.js +124 -56
- package/dist/sso-ui.d.ts +5 -2
- package/dist/sso-ui.js +63 -31
- package/dist/storage-ui.d.ts +3 -2
- package/dist/storage-ui.js +56 -50
- package/dist/store.d.ts +112 -3
- package/dist/store.js +292 -20
- package/dist/surface.js +5 -1
- package/dist/teams-settings.d.ts +2 -1
- package/dist/teams-settings.js +24 -41
- package/dist/telegram-settings.d.ts +0 -4
- package/dist/telegram-settings.js +52 -25
- package/dist/toolroll-update-ui.d.ts +8 -4
- package/dist/toolroll-update-ui.js +55 -45
- package/dist/tools-ui.d.ts +5 -3
- package/dist/tools-ui.js +107 -42
- package/dist/work-index.js +11 -7
- package/dist/workspace-ui.d.ts +1 -1
- package/dist/workspace-ui.js +7 -4
- package/package.json +1 -1
package/dist/models-ui.js
CHANGED
|
@@ -1,22 +1,40 @@
|
|
|
1
|
-
/** Settings → Models
|
|
2
|
-
*
|
|
3
|
-
|
|
1
|
+
/** Settings → Models, in the kit's Settings frame (a sub-page of "Agents and models"): what is in use and the one
|
|
2
|
+
* choice (a preset, previewed before it applies), then the model for each role behind a disclosure, the CLIs on
|
|
3
|
+
* this computer and the models that just arrived. Saving is an instance operator's step-up. */
|
|
4
|
+
import { html, postForm } from "./html.js";
|
|
5
|
+
import { approvalAsk, termsOf } from "./design/kit.js";
|
|
6
|
+
import { acts, btn, chip, disclose, dock, facts as factList, field, formRow, group, input, note, notice, radios, settingsShell } from "./server/kit.js";
|
|
4
7
|
import { priceWords } from "./model-catalog.js";
|
|
5
8
|
import { whenUtc } from "./when-html.js";
|
|
6
9
|
const when = (iso) => iso === null ? "never" : whenUtc(iso);
|
|
7
|
-
|
|
10
|
+
/** The page's own layout on the kit: one-column preset cards, the role pickers in the form rows' control column. */
|
|
11
|
+
export const MODELS_CSS = `.models{display:grid;gap:24px;min-width:0;overflow-wrap:anywhere}.models .k-radios{grid-template-columns:minmax(0,1fr);padding-top:12px}` +
|
|
12
|
+
`.models .k-secthead small{font-weight:400;color:var(--so-muted);font-size:12.5px}.models-presets{display:grid;gap:12px}.models-scope{display:flex;gap:8px;width:100%;max-width:300px;margin:0}.models-scope .k-selw{flex:1;min-width:0}.k-frow-c .models-scope .k-selw{max-width:none}.models-presets .k-acts{padding-top:4px}` +
|
|
13
|
+
`.models .role{display:grid;gap:6px;width:100%;max-width:300px;min-width:0}.models .role .k-input{max-width:none}.models-editor{display:grid;gap:12px;padding-top:4px}` +
|
|
14
|
+
`.models-editor > .k-field{max-width:300px}.models .k-grp{padding:16px 0 0}.models .k-disclose[open]>summary{margin-bottom:8px}.models-diff dd .k-mono{overflow-wrap:anywhere}` +
|
|
15
|
+
`.models-sizing{display:grid;gap:8px;padding-top:4px}.models-sizing p{margin:0;max-width:66ch}` +
|
|
16
|
+
`@media(max-width:760px){.models .role,.models-scope{max-width:none}.models-editor > .k-field{max-width:none}.models .k-btn.k-btn-primary.k-docked{display:none}}`;
|
|
17
|
+
const sec = (id, title, state, body) => html `<section class="k-sec" aria-labelledby="models-${id}"><div class="k-secthead"><h3 id="models-${id}">${title}${state ? html ` <small>· ${state}</small>` : ""}</h3></div>${body}</section>`;
|
|
18
|
+
const back = (view) => `/settings/models${view.scope.value === "installation" ? "" : `?scope=${encodeURIComponent(view.scope.value)}`}`;
|
|
19
|
+
const scopeQuery = (view) => view.scope.value === "installation" ? "" : `&scope=${encodeURIComponent(view.scope.value)}`;
|
|
20
|
+
const factsOf = (view) => view.facts ?? { passwordless: false, remembered: false };
|
|
21
|
+
/** The step-up's password, only when the rules ask for it (never under Passwordless). Named `password`, as the
|
|
22
|
+
* model posts read it; the server proves it again in the write. */
|
|
23
|
+
function password(view, id) {
|
|
24
|
+
return approvalAsk("step-up", factsOf(view)) === "password"
|
|
25
|
+
? field({ id, label: "Your password", control: input({ id, name: "password", type: "password", attrs: { autocomplete: "current-password", required: true } }) })
|
|
26
|
+
: "";
|
|
27
|
+
}
|
|
8
28
|
function toolRow(one, manage) {
|
|
9
|
-
const state = one.installed === null ? "Version unknown"
|
|
10
|
-
: one.behind ? html `${one.installed} · <strong>${one.latest} available</strong>`
|
|
11
|
-
: `${one.installed} · Up to date`;
|
|
29
|
+
const state = one.installed === null ? "Version unknown" : one.behind ? `${one.installed} · ${one.latest} available` : `${one.installed} · up to date`;
|
|
12
30
|
const action = manage && one.behind && one.updateCommand !== null
|
|
13
|
-
? postForm("/settings/models/update",
|
|
14
|
-
: one.behind ? html `<span class="
|
|
15
|
-
return
|
|
31
|
+
? postForm("/settings/models/update", btn(`Update ${one.name}`, { sm: true, type: "submit" }), { hidden: { tool: one.tool } })
|
|
32
|
+
: one.behind ? html `<span class="k-note">Update it the way you installed it</span>` : html ``;
|
|
33
|
+
return formRow(one.name, one.problem === null ? state : `${state} · ${one.problem}`, action, "inline");
|
|
16
34
|
}
|
|
17
|
-
function
|
|
35
|
+
function roleRow(role, manage) {
|
|
18
36
|
if (!manage)
|
|
19
|
-
return html `<
|
|
37
|
+
return formRow(role.label, null, html `<span>${role.words}</span>`);
|
|
20
38
|
const known = role.groups.some(group => group.options.some(option => `${group.provider}|${option.value}` === role.current));
|
|
21
39
|
const saved = !known && role.current !== "" ? html `<option value="${role.current}" selected>${role.words} · current</option>` : "";
|
|
22
40
|
const groups = role.groups.map(group => html `<optgroup label="${group.label}">${group.options.map(option => {
|
|
@@ -26,24 +44,23 @@ function roleForm(role, manage) {
|
|
|
26
44
|
const unset = html `<option value="clear"${role.current === "" ? html ` selected` : ""}>${role.unset}</option>`;
|
|
27
45
|
const count = role.groups.reduce((sum, group) => sum + group.options.length, 0);
|
|
28
46
|
const id = `role-${role.key}`;
|
|
29
|
-
|
|
47
|
+
const control = html `<span class="role">${count > 40 ? html `<input class="k-input" type="search" data-model-filter hidden aria-label="Search ${role.label.toLowerCase()} models" placeholder="Search ${count} models" autocomplete="off">` : ""}\
|
|
48
|
+
<span class="k-selw wide"><select class="k-sel" id="${id}" name="${role.key}" aria-label="${role.label}">${unset}${saved}${groups}</select></span></span>`;
|
|
49
|
+
return formRow(role.label, role.current === "" ? role.words : null, control);
|
|
30
50
|
}
|
|
31
51
|
export function modelsHtml(view) {
|
|
32
52
|
const manage = view.canManage && view.csrf !== "";
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
? html `<details><summary>Automatic checks · ${view.watch.enabled ? "On" : "Off"}</summary>${postForm("/settings/models/watch", html `<p>Check every 6 hours and send a message when a new model or CLI update appears. It uses public lists and sends nothing about you or your projects.</p><button type="submit" class="secondary">${view.watch.enabled ? "Turn off" : "Turn on"}</button>`, { hidden: { enabled: view.watch.enabled ? "0" : "1" } })}</details>`
|
|
45
|
-
: "";
|
|
46
|
-
return html `<section class="models">${status}${problem}${previewCard(view, manage)}${editorSection(view, manage)}${presetsSection(view, manage)}${sizingSection(view)}${recentSection(view)}<h2>AI tools</h2>${tools}<p class="meta">Checked ${when(view.watch.checkedAt)} ${check}</p>${fresh}${view.chat === null ? "" : html `<h2>Chat</h2><p>${view.chat.words} · <a href="/settings/lead">Change</a></p>`}${watch}</section>`;
|
|
53
|
+
const outcome = view.problem !== null ? notice(view.problem) : view.said !== null ? html `<p class="k-note" role="status">${view.said}</p>` : "";
|
|
54
|
+
const preview = previewSection(view, manage);
|
|
55
|
+
const previewing = manage && view.preview !== null && view.preview.rows.length > 0;
|
|
56
|
+
const tools = view.runtimes.length === 0 ? note("No Claude, Codex or Gemini CLI found on this computer yet.") : view.runtimes.map(one => toolRow(one, manage));
|
|
57
|
+
const checks = manage ? html `${formRow("Check for new versions", `Checked ${when(view.watch.checkedAt)}.`, postForm("/settings/models/check", btn("Check now", { sm: true, type: "submit" })), "inline")}\
|
|
58
|
+
${formRow("Automatic checks", `${view.watch.enabled ? "On" : "Off"}. Every 6 hours, with a message when a new model or CLI update appears. Uses public lists and sends nothing about you or your projects.`, postForm("/settings/models/watch", btn(view.watch.enabled ? "Turn off" : "Turn on", { sm: true, type: "submit" }), { hidden: { enabled: view.watch.enabled ? "0" : "1" } }), "inline")}`
|
|
59
|
+
: note(`Checked ${when(view.watch.checkedAt)}.`);
|
|
60
|
+
const fresh = view.fresh.length === 0 ? "" : sec("new", "New models", "", view.fresh.map(model => formRow(model.name, `${priceWords(model)}${model.releasedAt === null ? "" : ` · released ${model.releasedAt.slice(0, 10)}`}`, chip("New", "", { dot: false }), "inline")));
|
|
61
|
+
const chat = view.chat === null ? "" : formRow("Chat", view.chat.words, btn("Change", { sm: true, variant: "ghost", href: "/settings/lead" }), "inline");
|
|
62
|
+
const body = html `<div class="models">${outcome}${preview}${choiceSection(view, manage, previewing)}${recentSection(view)}${sec("tools", "AI tools on this computer", "", html `${tools}${checks}`)}${fresh}${chat}</div>`;
|
|
63
|
+
return settingsShell({ group: "agents", sub: true, title: "Models", lead: "Which model plans, builds and reviews each task. Changes apply to tasks filed from now on.", body });
|
|
47
64
|
}
|
|
48
65
|
/** Filters one long picker as you type; the saved choice always stays listed. */
|
|
49
66
|
export function modelsScript() {
|
|
@@ -56,47 +73,53 @@ export function modelsScript() {
|
|
|
56
73
|
select.value=chosen;});
|
|
57
74
|
})})();`;
|
|
58
75
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const scopes = view.scope.options.length < 2 ? "" :
|
|
76
|
+
/** What is in use and the one choice (a preset, previewed before it applies); the model for each role behind a
|
|
77
|
+
* disclosure, saved with one step-up; how tasks are sized, on demand. */
|
|
78
|
+
function choiceSection(view, manage, previewing) {
|
|
79
|
+
const inUse = view.inUse === null ? "a custom set" : view.inUse;
|
|
80
|
+
const scopes = view.scope.options.length < 2 ? "" : formRow("Settings for", "A project can follow these or choose its own.", html `<form method="get" action="/settings/models" class="models-scope">\
|
|
81
|
+
<span class="k-selw wide"><select class="k-sel" id="models-scope" name="scope" aria-label="Settings for">${view.scope.options.map(one => html `<option value="${one.value}"${one.value === view.scope.value ? html ` selected` : ""}>${one.label}</option>`)}</select></span>${btn("Show", { sm: true, type: "submit" })}</form>`);
|
|
82
|
+
const unresolved = view.unresolved === null ? "" : notice(`New tasks can't be filed here yet: ${view.unresolved}`);
|
|
83
|
+
const current = view.preview?.hidden["preset"] ?? view.presets.find(one => one.inUse)?.name ?? "";
|
|
84
|
+
const presetItems = view.presets.map(one => ({ value: one.name, label: one.label, text: `${one.words}${one.recommended ? " Recommended." : ""}${one.inUse ? " In use." : ""}` }));
|
|
85
|
+
const presets = manage
|
|
86
|
+
? html `<form method="get" action="/settings/models" class="models-presets" id="models-preset">${view.scope.value === "installation" ? "" : html `<input type="hidden" name="scope" value="${view.scope.value}">`}\
|
|
87
|
+
${radios({ name: "preset", legend: "Preset", current, items: presetItems })}${acts(btn("Review changes", { variant: previewing ? "outline" : "primary", type: "submit", docked: !previewing }))}</form>`
|
|
88
|
+
: factList(view.presets.map(one => [one.label, `${one.words}${one.inUse ? " In use." : ""}`]));
|
|
89
|
+
const newer = view.newer.length === 0 ? "" : html `${group("Newer models")}${view.newer.map(one => formRow(one.name, `Newer than ${one.from}, used by ${one.roles}.`, manage ? btn("Switch to it", { sm: true, href: `/settings/models?switch=${encodeURIComponent(one.from)}&to=${encodeURIComponent(one.to)}${scopeQuery(view)}` }) : html ``, "inline"))}`;
|
|
64
90
|
const main = view.roles.filter(role => !role.tier), tiers = view.roles.filter(role => role.tier);
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
91
|
+
const rows = html `${main.map(role => roleRow(role, manage))}${tiers.length === 0 ? "" : html `${group("By task size")}${tiers.map(role => roleRow(role, manage))}`}`;
|
|
92
|
+
const editor = manage
|
|
93
|
+
? postForm("/settings/models/agent", html `${rows}${note(`Approved and waiting tasks keep the models they were filed with. ${termsOf("step-up", factsOf(view))}`)}${password(view, "models-agent-pw")}${acts(btn("Save models", { type: "submit" }))}`, { attrs: { class: "models-editor" }, hidden: { scope: view.scope.value, digest: view.scope.digest } })
|
|
94
|
+
: html `<div class="models-editor">${rows}</div>`;
|
|
95
|
+
const docked = !manage || previewing ? "" : dock(btn("Review changes", { variant: "primary", type: "submit", attrs: { form: "models-preset" } }));
|
|
96
|
+
return html `${sec("choice", "Models for new tasks", `in use: ${inUse}${view.scope.value === "installation" ? "" : ` · ${view.scope.label}`}`, html `${scopes}${unresolved}${presets}${newer}`)}\
|
|
97
|
+
${disclose("Model for each role", editor)}${sizingSection(view)}${docked}`;
|
|
72
98
|
}
|
|
73
|
-
/** A preset or a switch shows exactly what it changes, and applies only with a second,
|
|
74
|
-
function
|
|
99
|
+
/** A preset or a switch shows exactly what it changes, and applies only with a second, step-up submit. */
|
|
100
|
+
function previewSection(view, manage) {
|
|
75
101
|
const preview = view.preview;
|
|
76
102
|
if (preview === null || !manage)
|
|
77
|
-
return
|
|
78
|
-
const back = `/settings/models${view.scope.value === "installation" ? "" : `?scope=${encodeURIComponent(view.scope.value)}`}`;
|
|
103
|
+
return "";
|
|
79
104
|
if (preview.rows.length === 0)
|
|
80
|
-
return html `<
|
|
81
|
-
const rows = preview.rows.map(row => html `<
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const presets = view.presets.map(one => html `<div class="card preset"><div><strong>${one.label}</strong>${one.recommended ? html ` <span class="meta">Default</span>` : ""}<p>${one.words}</p></div>${one.inUse ? html `<span class="meta">In use</span>` : manage ? html `<a class="button-link" href="/settings/models?preset=${one.name}${scopeQuery(view)}">Review changes</a>` : ""}</div>`);
|
|
86
|
-
const newer = view.newer.length === 0 ? "" : html `<h2>Newer models</h2>${view.newer.map(one => html `<div class="card preset"><div><strong>${one.name}</strong><p>Newer than <span class="mono">${one.from}</span>, used by ${one.roles}.</p></div>${manage ? html `<a class="button-link" href="/settings/models?switch=${encodeURIComponent(one.from)}&to=${encodeURIComponent(one.to)}${scopeQuery(view)}">Switch to it</a>` : ""}</div>`)}`;
|
|
87
|
-
return html `<h2>Presets</h2>${presets}${newer}`;
|
|
105
|
+
return sec("preview", preview.title, "", html `<p class="k-note" role="status">Already in use. Nothing would change.</p>${acts(btn("Back", { sm: true, href: back(view) }))}`);
|
|
106
|
+
const rows = factList(preview.rows.map(row => [row.role, html `<span class="k-mono">${row.before}</span> → <span class="k-mono">${row.after}</span>${row.notes.length === 0 ? "" : html `<br><span class="k-note">${row.notes.join(" ")}</span>`}`]));
|
|
107
|
+
const label = `Apply ${preview.rows.length} change${preview.rows.length === 1 ? "" : "s"}`;
|
|
108
|
+
const form = postForm("/settings/models/apply", html `${note(`Tasks filed from now on use these. ${termsOf("step-up", factsOf(view))}`)}${password(view, "models-apply-pw")}${acts(btn(label, { variant: "primary", type: "submit", docked: true }), btn("Cancel", { variant: "ghost", href: back(view) }))}`, { hidden: preview.hidden, attrs: { id: "models-apply", class: "models-editor" } });
|
|
109
|
+
return html `<div class="models-diff">${sec("preview", preview.title, "", html `${rows}${form}`)}</div>${dock(btn(label, { variant: "primary", type: "submit", attrs: { form: "models-apply" } }))}`;
|
|
88
110
|
}
|
|
89
|
-
/** How a task's size is decided, in plain words, and what routing does at filing. */
|
|
111
|
+
/** How a task's size is decided, in plain words, and what routing does at filing; on demand. */
|
|
90
112
|
function sizingSection(view) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
113
|
+
const waiting = view.waiting.length === 0 ? "" : html `<p>${view.waiting.length} plan${view.waiting.length === 1 ? "" : "s"} waiting for approval keep the models they were filed with: ${view.waiting.map((one, index) => html `${index === 0 ? "" : ", "}<a class="k-link" href="/t/${encodeURIComponent(one.taskId)}">${one.title}</a>`)}.</p>`;
|
|
114
|
+
return disclose("How tasks are sized", html `<div class="models-sizing"><p>Each task is sized once, when it is filed. ${view.sizing.classifier} If it doesn't answer within 5 seconds, the size comes from the description.</p>${factList([
|
|
115
|
+
["Small", `${view.sizing.small}. Built by the builder for small tasks, with no plan.`],
|
|
116
|
+
["Routine", `${view.sizing.routine}. Planned and built by the planner and builder for routine tasks.`],
|
|
117
|
+
["Large or risky", `Large: ${view.sizing.large}. Risky: ${view.sizing.risky} Planned and built by the large or risky models.`],
|
|
118
|
+
])}<p>${view.routing}</p><p>Changing a model never re-sizes or re-routes a task that was already filed. One exception: an older build review saved without its own route uses the reviewer set here now.</p>${waiting}</div>`);
|
|
96
119
|
}
|
|
97
120
|
/** The model each recent run was set to, next to the model its provider reported. */
|
|
98
121
|
function recentSection(view) {
|
|
99
122
|
if (view.recent.length === 0)
|
|
100
|
-
return
|
|
101
|
-
return
|
|
123
|
+
return "";
|
|
124
|
+
return sec("recent", "Recent work", "", view.recent.map(one => formRow(one.role, `Chosen ${one.chosen} → ran on ${one.ranOn}`, html `<a class="k-link k-mono" href="/t/${encodeURIComponent(one.taskId)}">${one.taskId}</a>`, "inline")));
|
|
102
125
|
}
|
package/dist/monitoring-ui.d.ts
CHANGED
|
@@ -2,18 +2,20 @@
|
|
|
2
2
|
* Settings → Monitoring (v104): an instance operator points Toolroll at
|
|
3
3
|
* the company's tools. One form, the status of each destination beside it,
|
|
4
4
|
* and how Prometheus reads `/metrics`. The signing secret is shown once.
|
|
5
|
+
* Built from the revamp kit; the Settings frame adds the title.
|
|
5
6
|
*/
|
|
6
7
|
import { type Html } from "./html.js";
|
|
7
8
|
import type { MonitoringStatus } from "./store.js";
|
|
8
9
|
import type { MonitoringSettings } from "./monitoring-settings.js";
|
|
9
|
-
export
|
|
10
|
+
/** The page is kit pieces now; nothing of the old look is left. Kept as an export for the shared stylesheet. */
|
|
11
|
+
export declare const MONITORING_CSS = "";
|
|
10
12
|
export type MonitoringView = {
|
|
11
13
|
settings: MonitoringSettings;
|
|
12
14
|
status: MonitoringStatus[];
|
|
13
15
|
head: number;
|
|
14
16
|
origin: string | null;
|
|
15
17
|
};
|
|
16
|
-
export declare function monitoringHtml(view: MonitoringView,
|
|
18
|
+
export declare function monitoringHtml(view: MonitoringView, said: {
|
|
17
19
|
said?: string | null;
|
|
18
20
|
problem?: string | null;
|
|
19
21
|
}): Html;
|
package/dist/monitoring-ui.js
CHANGED
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
* Settings → Monitoring (v104): an instance operator points Toolroll at
|
|
3
3
|
* the company's tools. One form, the status of each destination beside it,
|
|
4
4
|
* and how Prometheus reads `/metrics`. The signing secret is shown once.
|
|
5
|
+
* Built from the revamp kit; the Settings frame adds the title.
|
|
5
6
|
*/
|
|
6
|
-
import { html,
|
|
7
|
+
import { html, joinHtml } from "./html.js";
|
|
7
8
|
import { targetOf } from "./monitoring.js";
|
|
8
9
|
import { whenUtc } from "./when-html.js";
|
|
10
|
+
import { codeLine, disclose, doc, docHeader, formRow, icon, infoLine, input, note, notice, settingsSection, switchInput } from "./server/kit.js";
|
|
11
|
+
import { kitSectionForm, SECRET_OPT_OUTS } from "./settings-kit.js";
|
|
9
12
|
const when = (at) => whenUtc(at);
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
`.monitoring input[type=text],.monitoring input[type=url],.monitoring input[type=password]{width:100%;box-sizing:border-box}.monitoring .choice{display:flex;gap:8px;align-items:center}` +
|
|
13
|
-
`.monitoring .status{margin:4px 0 0;font-size:.8125rem}.monitoring .status.problem{color:var(--danger)}.monitoring .step-up{margin-top:18px}.monitoring button[type=submit]{margin-top:14px}` +
|
|
14
|
-
`.monitoring pre{white-space:pre-wrap;overflow-wrap:anywhere;font-size:.75rem}.monitoring details{margin-top:6px}`;
|
|
13
|
+
/** The page is kit pieces now; nothing of the old look is left. Kept as an export for the shared stylesheet. */
|
|
14
|
+
export const MONITORING_CSS = "";
|
|
15
15
|
/** How a destination is doing, in one line. */
|
|
16
16
|
function statusLine(view, sink, address) {
|
|
17
17
|
// An empty field already says it's off.
|
|
@@ -20,20 +20,51 @@ function statusLine(view, sink, address) {
|
|
|
20
20
|
// A status for another address (it just changed) says nothing about this one yet.
|
|
21
21
|
const one = view.status.find(row => row.sink === sink && row.target === targetOf(address));
|
|
22
22
|
if (one === undefined || (one.lastOkAt === null && one.lastError === null))
|
|
23
|
-
return html `<p class="
|
|
23
|
+
return html `<p class="k-note" data-monitoring="${sink}" data-state="starting">Starting: the first delivery goes out within a few seconds.</p>`;
|
|
24
24
|
if (one.failures > 0 && one.lastError !== null) {
|
|
25
|
-
return html `<p class="
|
|
25
|
+
return html `<p class="k-notice" role="alert" data-monitoring="${sink}" data-state="failing">${icon("alert", true)}<span>Failing: ${one.lastError}${one.nextTryAt === null ? "" : html ` · trying again ${when(one.nextTryAt)}`}</span></p>`;
|
|
26
26
|
}
|
|
27
27
|
const behind = Math.max(0, view.head - one.through);
|
|
28
|
-
return html `<p class="
|
|
28
|
+
return html `<p class="k-note" data-monitoring="${sink}" data-state="ok">${sink === "traces" ? "Sending" : `Sent through entry #${one.through}`}${one.lastOkAt === null ? "" : html ` · last ${when(one.lastOkAt)}`}${behind > 0 && sink !== "traces" ? ` · ${behind} to go` : ""}</p>`;
|
|
29
29
|
}
|
|
30
|
-
export function monitoringHtml(view,
|
|
30
|
+
export function monitoringHtml(view, said) {
|
|
31
31
|
const s = view.settings;
|
|
32
|
-
const note = notice.problem ? html `<p class="problem" role="alert">${notice.problem}</p>` : notice.said ? html `<p role="status">${notice.said}</p>` : "";
|
|
33
32
|
const metrics = view.origin === null ? "/metrics" : `${view.origin}/metrics`;
|
|
34
|
-
|
|
33
|
+
const audit = settingsSection({ id: "audit-stream", title: "Audit stream", lead: "Every ledger entry with its seal, as it happens, to your log system.",
|
|
34
|
+
body: joinHtml([
|
|
35
|
+
formRow("Webhook", "Blank: off.", input({ id: "monitoring-webhook", name: "webhook", type: "url", value: s.webhook?.url ?? "",
|
|
36
|
+
attrs: { placeholder: "https://logs.example.com/standing-orders", spellcheck: "false", "aria-label": "Webhook" } })),
|
|
37
|
+
statusLine(view, "webhook", s.webhook?.url ?? null),
|
|
38
|
+
s.webhook === null ? "" : formRow("Signing secret", "Make a new one. It is shown once, after Save.", switchInput({ id: "monitoring-rotate", name: "rotate", label: "Make a new signing secret", on: false, attrs: { value: "1" } }), "inline"),
|
|
39
|
+
formRow("Folder", "JSON Lines, a file per day. Blank: off.", input({ id: "monitoring-folder", name: "folder", value: s.folder?.path ?? "",
|
|
40
|
+
attrs: { placeholder: "/var/log/standing-orders", spellcheck: "false", "aria-label": "Folder" } })),
|
|
41
|
+
statusLine(view, "folder", s.folder?.path ?? null),
|
|
42
|
+
]) });
|
|
43
|
+
const traces = settingsSection({ id: "traces", title: "Traces", lead: "Each run as an OpenTelemetry span: timings, model, tokens and cost. Never a prompt or code.",
|
|
44
|
+
body: joinHtml([
|
|
45
|
+
formRow("Collector", "OTLP over HTTP. Blank: off.", input({ id: "monitoring-traces", name: "traces", type: "url", value: s.traces?.endpoint ?? "",
|
|
46
|
+
attrs: { placeholder: "https://otel.example.com:4318", spellcheck: "false", "aria-label": "Collector" } })),
|
|
47
|
+
statusLine(view, "traces", s.traces?.endpoint ?? null),
|
|
48
|
+
formRow("Header name", "Optional.", input({ id: "monitoring-header-name", name: "header-name", value: s.traces?.header?.name ?? "",
|
|
49
|
+
attrs: { placeholder: "x-honeycomb-team", spellcheck: "false", "aria-label": "Header name" } })),
|
|
50
|
+
formRow("Header value", s.traces?.header ? "Saved. Leave blank to keep it." : null, input({ id: "monitoring-header-value", name: "header-value", type: "password",
|
|
51
|
+
attrs: { autocomplete: "off", spellcheck: "false", autocapitalize: "none", "data-so-secret": true, ...SECRET_OPT_OUTS, placeholder: s.traces?.header ? "Saved; leave blank to keep" : "API key", "aria-label": "Header value" } })),
|
|
52
|
+
]) });
|
|
53
|
+
const metricsSection = settingsSection({ id: "metrics", title: "Metrics",
|
|
54
|
+
lead: html `Prometheus reads <code class="k-mono">${metrics}</code> with an instance operator's API token (<a class="k-link" href="/settings/sessions">Sessions and tokens</a>).`,
|
|
55
|
+
body: disclose("Example", codeLine(`curl -H "Authorization: Bearer so_…" ${metrics}`)) });
|
|
56
|
+
return joinHtml([
|
|
57
|
+
said.problem ? notice(said.problem) : said.said ? infoLine(said.said) : "",
|
|
58
|
+
// A new signing secret answers with a page of its own (shown once): post as a navigation.
|
|
59
|
+
kitSectionForm({ proof: "monitoring", action: "/settings/monitoring", id: "monitoring-form", gate: true, native: true, body: joinHtml([audit, traces]) }),
|
|
60
|
+
metricsSection,
|
|
61
|
+
]);
|
|
35
62
|
}
|
|
36
63
|
/** The webhook's signing secret, once: a focused page with no script. */
|
|
37
64
|
export function signingSecretHtml(url, secret) {
|
|
38
|
-
return
|
|
65
|
+
return doc(docHeader({ title: "Signing secret", back: ["/settings/monitoring", "Monitoring"] }), settingsSection({ id: "signing-secret", title: "Shown once", lead: html `Give this to the receiver at <span class="k-mono">${url}</span>. It isn't shown again.`,
|
|
66
|
+
body: joinHtml([
|
|
67
|
+
html `<p class="k-secret-val">${secret}</p>`,
|
|
68
|
+
note(html `Each request carries <code class="k-mono">x-standing-orders-signature: t=<time>,v1=<HMAC-SHA256 of "time.body"></code>. Check it, and that the time is recent.`),
|
|
69
|
+
]) }));
|
|
39
70
|
}
|
package/dist/needs-you.d.ts
CHANGED
|
@@ -45,7 +45,7 @@ export declare const ASKS: readonly Ask[];
|
|
|
45
45
|
export declare const ASK_LABEL: Readonly<Record<Ask, string>>;
|
|
46
46
|
/** A waiting row's chip: the specific thing asked, never its group's own word (no "Decide" chip under Decide).
|
|
47
47
|
* Null: the group heading and the row's sentence already say it, so the row wears no chip. */
|
|
48
|
-
export type AskChip = "Plan" | "Result" | "Mismatch" | "Plan changed" | "Failed" | "Builder offline";
|
|
48
|
+
export type AskChip = "Plan" | "Result" | "Mismatch" | "Plan changed" | "Failed" | "Builder offline" | "Question" | "On hold" | "Stopped" | "Sign in";
|
|
49
49
|
/** `mismatch`: the saved report doesn't match its changes; `planChanged`: the plan changed after it was built. */
|
|
50
50
|
export declare function askChipOf(read: {
|
|
51
51
|
headline: string;
|
package/dist/needs-you.js
CHANGED
|
@@ -66,6 +66,14 @@ export function askChipOf(read) {
|
|
|
66
66
|
return "Plan";
|
|
67
67
|
if (read.need === "start-builder" || read.need === "connect-builder")
|
|
68
68
|
return "Builder offline";
|
|
69
|
+
if (read.need === "answer" || read.need === "questions")
|
|
70
|
+
return "Question";
|
|
71
|
+
if (read.need === "hold")
|
|
72
|
+
return "On hold";
|
|
73
|
+
if (read.need === "vanished" || read.need === "confirm-stopped" || read.need === "check-stopped")
|
|
74
|
+
return "Stopped";
|
|
75
|
+
if (read.need === "sign-in")
|
|
76
|
+
return "Sign in";
|
|
69
77
|
return null;
|
|
70
78
|
}
|
|
71
79
|
export function resultHoldUpSentence(facts) {
|
|
@@ -19,6 +19,7 @@ export const REMOTE_ARGUMENTS = new Map([
|
|
|
19
19
|
['task add', args('title*', 'key report checks', { id: 'refused', replaces: 'task' })],
|
|
20
20
|
['task ask', args('id:task', 'person why')],
|
|
21
21
|
['task checks', args('task:task', 'level')],
|
|
22
|
+
['task ship', args('task:task')],
|
|
22
23
|
['task add-tests', args('task:task')],
|
|
23
24
|
['task list', args('', 'state view limit cursor')],
|
|
24
25
|
['task show', args('id:task')],
|
package/dist/operate-remote.js
CHANGED
|
@@ -37,7 +37,7 @@ const references = { kind: "references" }, listing = { kind: "listing" };
|
|
|
37
37
|
export const REMOTE_SCOPES = new Map([
|
|
38
38
|
["status", listing], ["integrations", installation], ["ready", listing], ["grants", installation], ["sync", installation],
|
|
39
39
|
["gaps", repo], ["task add", repo],
|
|
40
|
-
...["ask", "checks", "add-tests", "show", "wait", "complete", "revise", "state", "block", "unblock", "next", "steer", "pause", "continue", "assign", "scope", "plan", "hold",
|
|
40
|
+
...["ask", "checks", "ship", "add-tests", "show", "wait", "complete", "revise", "state", "block", "unblock", "next", "steer", "pause", "continue", "assign", "scope", "plan", "hold",
|
|
41
41
|
"unhold", "require", "requeue", "review", "repair", "route", "reopen", "stop", "resume"].map(action => [`task ${action}`, references]),
|
|
42
42
|
["check-progress", references],
|
|
43
43
|
["task list", listing],
|
package/dist/operate.d.ts
CHANGED
|
@@ -115,7 +115,7 @@ type Args = {
|
|
|
115
115
|
* exports are behavior, not commentary — and the declared command guide
|
|
116
116
|
* (surface.ts) is tested for exact equality against them.
|
|
117
117
|
*/
|
|
118
|
-
export declare const TASK_ACTIONS: readonly ["add", "list", "show", "wait", "state", "block", "unblock", "next", "steer", "pause", "continue", "assign", "reopen", "scope", "approve", "hold", "unhold", "require", "requeue", "regate", "plan", "review", "accept", "repair", "route", "stop", "resume", "complete", "revise", "merge", "ask", "checks", "add-tests", "archive", "restore", "tidy"];
|
|
118
|
+
export declare const TASK_ACTIONS: readonly ["add", "list", "show", "wait", "state", "block", "unblock", "next", "steer", "pause", "continue", "assign", "reopen", "scope", "approve", "hold", "unhold", "require", "requeue", "regate", "plan", "review", "accept", "repair", "route", "stop", "resume", "complete", "revise", "merge", "ask", "checks", "add-tests", "archive", "restore", "tidy", "ship"];
|
|
119
119
|
export declare const PUBLISH_ACTIONS: readonly ["setup", "grant", "revoke", "status", "unblock", "rearm", "merge", "refire"];
|
|
120
120
|
export declare const CONFIG_ACTIONS: readonly ["show", "set", "clear"];
|
|
121
121
|
export declare const APPROVER_ACTIONS: readonly ["list", "add"];
|
package/dist/operate.js
CHANGED
|
@@ -107,7 +107,8 @@ import { taskReviewBrief, renderReviewBrief } from "./task-review-brief.js";
|
|
|
107
107
|
import { homedir, hostname } from "node:os";
|
|
108
108
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
109
109
|
import { releaseStalledUpdate, waitingUpdate } from "./toolroll-update.js";
|
|
110
|
-
import {
|
|
110
|
+
import { dur } from "./design/kit.js";
|
|
111
|
+
import { openStore, openStoreNoMigrate, archiveTaskFamily, restoreTaskFamily, shipChecksForTask, elapsedSeconds, CONCURRENT_WRITER_WAIT_MS, isDatabaseBusy, databasePath, BUILT_IN, parseCapabilityKey, verifiedAuthor, isDigestTime, predecessorWatches, RESULT_SCREENSHOTS, lifecycleOf, reportLifecycle, beatLifecycle, } from "./store.js";
|
|
111
112
|
import { createHash, randomBytes, randomInt, randomUUID } from "node:crypto";
|
|
112
113
|
import { claimInstance, clearInstance, openInstanceSocket } from "./console-instance.js";
|
|
113
114
|
import { authorizePlanUnderMode } from "./plan-auto.js";
|
|
@@ -344,6 +345,7 @@ export const OPERATE_HELP = `toolroll — operating the queue
|
|
|
344
345
|
[--anyway] merge without waiting for the full check
|
|
345
346
|
toolroll task checks <id> [--level quick|full]
|
|
346
347
|
run checks on the result's exact commit now
|
|
348
|
+
toolroll task ship <id> its pull request's checks, each with its state and how long it took
|
|
347
349
|
toolroll task add-tests <id> file a small task to write tests for that change
|
|
348
350
|
toolroll task revise <id> --feedback "requested change"
|
|
349
351
|
toolroll assignment show <task> root, current work and exact handoff
|
|
@@ -669,7 +671,7 @@ export const TASK_ACTIONS = [
|
|
|
669
671
|
"add", "list", "show", "wait", "state", "block", "unblock", "next", "steer", "pause", "continue", "assign",
|
|
670
672
|
"reopen", "scope", "approve", "hold", "unhold", "require", "requeue", "regate", "plan",
|
|
671
673
|
"review", "accept", "repair", "route", "stop", "resume", "complete", "revise", "merge", "ask",
|
|
672
|
-
"checks", "add-tests", "archive", "restore", "tidy",
|
|
674
|
+
"checks", "add-tests", "archive", "restore", "tidy", "ship",
|
|
673
675
|
];
|
|
674
676
|
export const PUBLISH_ACTIONS = ["setup", "grant", "revoke", "status", "unblock", "rearm", "merge", "refire"];
|
|
675
677
|
export const CONFIG_ACTIONS = ["show", "set", "clear"];
|
|
@@ -8586,6 +8588,8 @@ function taskAction(action, rest, flags, context) {
|
|
|
8586
8588
|
return addTask(rest, flags, context);
|
|
8587
8589
|
case "checks":
|
|
8588
8590
|
return runTaskChecks(rest, flags, context);
|
|
8591
|
+
case "ship":
|
|
8592
|
+
return taskShipCommand(rest, context);
|
|
8589
8593
|
case "add-tests":
|
|
8590
8594
|
return addTestsTask(rest, flags, context);
|
|
8591
8595
|
case "list":
|
|
@@ -9017,9 +9021,9 @@ function listTasks(flags, context) {
|
|
|
9017
9021
|
const limit = rawLimit === undefined ? 40 : Number(rawLimit);
|
|
9018
9022
|
if (wanted !== undefined && !STATES.includes(wanted))
|
|
9019
9023
|
return fail(write, json, 'task list', 'usage', `--state takes one of ${STATES.join(', ')}`, EXIT.usage);
|
|
9020
|
-
if (rawView !== undefined && !['all', 'needs-you', 'running', 'completed', 'archived'].includes(rawView) ||
|
|
9024
|
+
if (rawView !== undefined && !['all', 'needs-you', 'running', 'queued', 'completed', 'archived'].includes(rawView) ||
|
|
9021
9025
|
!Number.isSafeInteger(limit) || limit < 1 || limit > 100) {
|
|
9022
|
-
return fail(write, json, 'task list', 'usage', '--view takes all, needs-you, running, completed or archived; --limit takes 1–100', EXIT.usage);
|
|
9026
|
+
return fail(write, json, 'task list', 'usage', '--view takes all, needs-you, running, queued, completed or archived; --limit takes 1–100', EXIT.usage);
|
|
9023
9027
|
}
|
|
9024
9028
|
// A remote person limited to some projects (remoteLensOf) pages and counts only theirs.
|
|
9025
9029
|
const lens = context.principal?.lens ?? null;
|
|
@@ -12963,3 +12967,33 @@ function firstVisibleRepo(store, name, ids) {
|
|
|
12963
12967
|
}
|
|
12964
12968
|
return store.accountCanAccess(name, null) ? null : store.knownRepos().find(repo => store.accountCanAccess(name, repo)) ?? null;
|
|
12965
12969
|
}
|
|
12970
|
+
/** The check states as `task ship` says them. */
|
|
12971
|
+
const SHIP_CHECK_WORDS = { queued: "queued", running: "running", passed: "passed", failed: "failed", skipped: "skipped", cancelled: "cancelled", neutral: "neutral" };
|
|
12972
|
+
/**
|
|
12973
|
+
* `toolroll task ship <task>` (read-only) — the task's pull request checks as the plane last observed them for that
|
|
12974
|
+
* head (store.ts ci_check, v146): each check's state and how long it took, the Ship view's own facts.
|
|
12975
|
+
*/
|
|
12976
|
+
function taskShipCommand(positional, context) {
|
|
12977
|
+
const { store, write, json } = context;
|
|
12978
|
+
const command = "task ship";
|
|
12979
|
+
const id = positional[0];
|
|
12980
|
+
if (id === undefined || positional.length > 1)
|
|
12981
|
+
return fail(write, json, command, "usage", "`toolroll task ship <task>`", EXIT.usage);
|
|
12982
|
+
const ref = store.lookupRef(id);
|
|
12983
|
+
const lens = context.principal?.lens ?? null;
|
|
12984
|
+
// A task outside the caller's projects is no task here, the same answer as one that doesn't exist.
|
|
12985
|
+
if (ref === null || store.getTask(id) === null || (lens !== null && (ref.repo === null || !lens.includes(ref.repo))))
|
|
12986
|
+
return fail(write, json, command, "unknown-task", `no task \`${id}\``, EXIT.refused);
|
|
12987
|
+
const ship = shipChecksForTask(store, id);
|
|
12988
|
+
if (ship === null)
|
|
12989
|
+
return fail(write, json, command, "no-pull-request", `${id} has no pull request with observed checks yet`, EXIT.refused);
|
|
12990
|
+
const now = (context.clock ?? (() => context.now))();
|
|
12991
|
+
const checks = ship.checks.map(one => ({ ...one, seconds: elapsedSeconds(one.startedAt, one.completedAt, now) }));
|
|
12992
|
+
const passed = checks.filter(one => one.state === "passed").length;
|
|
12993
|
+
// The overall answer is the observation's own (the one merges read), never a sum of the rows.
|
|
12994
|
+
const overall = ship.observed === "failing" ? "failing" : ship.observed === "passing" ? "passing" : ship.observed === "running" ? "running" : "not read";
|
|
12995
|
+
return succeed(write, json, command, { task: id, pullRequest: ship.prNumber, githubRepo: ship.githubRepo, head: ship.headSha, observedAt: ship.observedAt, status: ship.observed, checks }, () => [
|
|
12996
|
+
`Pull request #${ship.prNumber} (${ship.githubRepo}) at ${ship.headSha.slice(0, 7)}: checks ${overall} · ${passed} of ${checks.length} passed`,
|
|
12997
|
+
...checks.map(one => ` ${(one.workflow === null ? one.name : `${one.workflow} / ${one.name}`).padEnd(32)} ${(SHIP_CHECK_WORDS[one.state] ?? one.state).padEnd(9)} ${one.seconds === null ? "–" : dur(one.seconds)}`),
|
|
12998
|
+
]);
|
|
12999
|
+
}
|
package/dist/passwordless.d.ts
CHANGED
|
@@ -20,4 +20,4 @@ export declare const passwordlessOn: (store: Store) => boolean;
|
|
|
20
20
|
export declare function setPasswordless(store: Store, on: boolean, by: string, now: Date): {
|
|
21
21
|
changed: boolean;
|
|
22
22
|
};
|
|
23
|
-
export declare const PASSWORDLESS_TRADE = "Anyone holding a signed-in browser \u2014 an unlocked laptop, a phone, a stolen session \u2014 can then do everything you can without a second check. Signing in, session limits (12 hours idle, 7 days), signing out and ending sessions remain your protection. Creating or widening a way in (passwords, sign-in methods, API tokens, connected apps, people, workers, chat pairing, webhook secrets) still asks for your password.";
|
|
23
|
+
export declare const PASSWORDLESS_TRADE = "Anyone holding a signed-in browser \u2014 an unlocked laptop, a phone, a stolen session \u2014 can then do everything you can without a second check. Signing in, session limits (12 hours idle, 7 days), signing out and ending sessions remain your protection. Creating or widening a way in (passwords, sign-in methods, API tokens, connected apps, people, workers, chat pairing, webhook secrets), or widening what a project's builds and checks reach (its network, where builds write, its home folder and its check access), still asks for your password.";
|
package/dist/passwordless.js
CHANGED
|
@@ -23,4 +23,4 @@ export function setPasswordless(store, on, by, now) {
|
|
|
23
23
|
store.bumpWake();
|
|
24
24
|
return { changed: true };
|
|
25
25
|
}
|
|
26
|
-
export const PASSWORDLESS_TRADE = "Anyone holding a signed-in browser — an unlocked laptop, a phone, a stolen session — can then do everything you can without a second check. Signing in, session limits (12 hours idle, 7 days), signing out and ending sessions remain your protection. Creating or widening a way in (passwords, sign-in methods, API tokens, connected apps, people, workers, chat pairing, webhook secrets) still asks for your password.";
|
|
26
|
+
export const PASSWORDLESS_TRADE = "Anyone holding a signed-in browser — an unlocked laptop, a phone, a stolen session — can then do everything you can without a second check. Signing in, session limits (12 hours idle, 7 days), signing out and ending sessions remain your protection. Creating or widening a way in (passwords, sign-in methods, API tokens, connected apps, people, workers, chat pairing, webhook secrets), or widening what a project's builds and checks reach (its network, where builds write, its home folder and its check access), still asks for your password.";
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { type AuditAction } from "./remote-audit.js";
|
|
2
2
|
import type { ApiTokenRow } from "./store.js";
|
|
3
3
|
import { type Html } from "./html.js";
|
|
4
|
-
|
|
4
|
+
/** The old look; the page is built from the kit now. Kept for chrome.ts, which still concatenates it. */
|
|
5
|
+
export declare const PEOPLE_AUDIT_CSS = "";
|
|
5
6
|
export type PersonAuditView = {
|
|
6
7
|
name: string;
|
|
7
8
|
tokens: readonly ApiTokenRow[];
|
package/dist/people-audit-ui.js
CHANGED
|
@@ -3,36 +3,38 @@
|
|
|
3
3
|
* (remote-audit.ts). A token's name filters the actions to it (`token-name`: a `token` query is always refused). Read-only: making, revoking and rotating tokens stay
|
|
4
4
|
* in Settings → Sessions & tokens. Never a token's secret or a ledger line's raw detail.
|
|
5
5
|
*/
|
|
6
|
+
import { settingsSubPage } from "./settings-groups-ui.js";
|
|
6
7
|
import { basename } from "node:path";
|
|
7
8
|
import { tokenStatus } from "./remote-audit.js";
|
|
8
|
-
import { html } from "./html.js";
|
|
9
|
+
import { html, joinHtml } from "./html.js";
|
|
10
|
+
import { acts, btn, chip, note, settingsRecord, settingsSection } from "./server/kit.js";
|
|
9
11
|
const stamp = (value) => `${value.slice(0, 16).replace("T", " ")} UTC`;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
`.person-audit .row-item:last-child{border-bottom:0}.person-audit .row-item p{margin:0}.person-audit .row-item .meta{font-size:.85rem}` +
|
|
13
|
-
`.person-audit a{display:inline-flex;align-items:center;min-height:44px}.person-audit .row-item a{min-height:32px}.person-audit a:focus-visible{outline:2px solid var(--so-accent);outline-offset:2px;border-radius:4px}` +
|
|
14
|
-
`.person-audit .outcome{display:inline-block;font-size:.8rem;font-weight:600;padding:1px 8px;border-radius:999px;margin-right:8px}` +
|
|
15
|
-
`.person-audit .outcome-ok{background:var(--so-success-soft);color:var(--so-success)}.person-audit .outcome-refused{background:var(--so-warning-soft);color:var(--so-ink)}` +
|
|
16
|
-
`.person-audit .outcome-error{background:var(--so-danger-soft);color:var(--so-danger)}` +
|
|
17
|
-
`@media(max-width:600px){.person-audit .row-item a{min-height:44px}}`;
|
|
12
|
+
/** The old look; the page is built from the kit now. Kept for chrome.ts, which still concatenates it. */
|
|
13
|
+
export const PEOPLE_AUDIT_CSS = "";
|
|
18
14
|
export const personHref = (name, more = {}) => `/people?${new URLSearchParams({ person: name, ...more }).toString()}`;
|
|
19
15
|
export function personAuditHtml(view) {
|
|
20
|
-
const tokens =
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
16
|
+
const tokens = settingsSection({ id: "person-tokens", title: "API tokens", lead: "Make, revoke and rotate tokens in Sessions and tokens.",
|
|
17
|
+
body: view.tokens.length === 0 ? note("No API tokens.") : joinHtml(view.tokens.map(one => {
|
|
18
|
+
const status = tokenStatus(one, view.now);
|
|
19
|
+
return settingsRecord({
|
|
20
|
+
title: html `<a class="k-link" href="${personHref(view.name, { "token-name": one.name })}">${one.name}</a>`,
|
|
21
|
+
sub: `${status === "active" ? "Active" : status === "expired" ? "Expired" : "Revoked"} · ${one.access === "act" ? "Can act" : "Read only"} · last used ${one.lastUsedAt === null ? "never" : stamp(one.lastUsedAt)}`,
|
|
22
|
+
attrs: { "data-token-name": one.name },
|
|
23
|
+
});
|
|
24
|
+
})) });
|
|
25
|
+
const filtered = view.token === null ? undefined : btn("All tokens", { sm: true, variant: "ghost", href: personHref(view.name) });
|
|
26
|
+
const older = view.nextCursor === null ? "" : acts(btn("Older actions", { sm: true, href: personHref(view.name, { ...(view.token === null ? {} : { "token-name": view.token }), before: view.nextCursor }) }));
|
|
27
|
+
const activity = settingsSection({ id: "person-activity", title: "Remote activity", ...(view.token === null ? {} : { state: ` · with ${view.token}` }), ...(filtered ? { right: filtered } : {}),
|
|
28
|
+
lead: "What this person did through the toolroll command and agents (MCP), newest first.",
|
|
29
|
+
body: joinHtml([view.actions.length === 0 ? note(`No remote actions${view.token === null ? "" : " with this token"}.`) : joinHtml(view.actions.map(actionHtml)), older]) });
|
|
30
|
+
return settingsSubPage("access", view.name, joinHtml([note(html `<a class="k-link" href="/people">All people</a>`), tokens, activity]));
|
|
31
31
|
}
|
|
32
32
|
function actionHtml(action) {
|
|
33
|
-
const
|
|
33
|
+
const outcome = action.outcome === "ok" ? chip("OK", "done") : action.outcome === "refused" ? chip("Refused", "") : chip("Error", "failed");
|
|
34
34
|
const detail = [stamp(action.at), action.token ?? "unknown token", action.source === "mcp" ? "Agent (MCP)" : "CLI",
|
|
35
35
|
...(action.repo === null ? [] : [basename(action.repo)]), ...(action.reason === null ? [] : [action.reason])];
|
|
36
|
-
return
|
|
37
|
-
${action.taskId === null ? "" : html ` · <a href="/t/${encodeURIComponent(action.taskId)}">${action.taskId}</a>`}
|
|
36
|
+
return settingsRecord({
|
|
37
|
+
title: html `${outcome} ${action.name}${action.taskId === null ? "" : html ` · <a class="k-link" href="/t/${encodeURIComponent(action.taskId)}">${action.taskId}</a>`}`,
|
|
38
|
+
sub: detail.join(" · "), attrs: { "data-action": action.id },
|
|
39
|
+
});
|
|
38
40
|
}
|