toolroll 0.9.83 → 0.9.84
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 +27 -0
- package/README.md +4 -3
- package/dist/assignment-ui.js +5 -1
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +22 -22
- package/dist/browser-crew.js +5 -1
- package/dist/browser-workspace.d.ts +7 -0
- package/dist/contracts/console-api.d.ts +2 -1
- package/dist/contracts/console-api.js +5 -1
- package/dist/control-setup.d.ts +9 -0
- package/dist/control-setup.js +31 -4
- package/dist/control-ui.d.ts +7 -1
- package/dist/control-ui.js +3 -2
- package/dist/desktop-update.d.ts +1 -1
- package/dist/dispatch.d.ts +15 -0
- package/dist/dispatch.js +26 -4
- package/dist/first-run.d.ts +5 -2
- package/dist/first-run.js +13 -2
- package/dist/folder-look.d.ts +3 -0
- package/dist/folder-look.js +8 -4
- package/dist/guides.js +4 -3
- package/dist/model-catalog.d.ts +5 -0
- package/dist/model-catalog.js +11 -2
- package/dist/model-choices.d.ts +188 -0
- package/dist/model-choices.js +405 -0
- package/dist/model-names.d.ts +6 -0
- package/dist/model-names.js +34 -0
- package/dist/models-ui.d.ts +69 -5
- package/dist/models-ui.js +53 -10
- package/dist/needs-you.d.ts +8 -0
- package/dist/needs-you.js +8 -0
- package/dist/operate.d.ts +21 -0
- package/dist/operate.js +96 -26
- package/dist/project-admission.d.ts +4 -1
- package/dist/project-admission.js +23 -2
- package/dist/project-identity.d.ts +9 -2
- package/dist/project-identity.js +80 -33
- package/dist/project-knowledge.d.ts +6 -6
- package/dist/serve.js +60 -13
- package/dist/server/chat.js +5 -4
- package/dist/server/chrome.d.ts +3 -0
- package/dist/server/chrome.js +5 -2
- package/dist/server/console.css +5 -2
- package/dist/server/flows.js +4 -0
- package/dist/server/http.d.ts +1 -0
- package/dist/server/http.js +6 -0
- package/dist/server/pages.js +42 -6
- package/dist/server/people-tokens.js +40 -20
- package/dist/server/render-pages.d.ts +10 -0
- package/dist/server/render-pages.js +29 -14
- package/dist/server/render-people.d.ts +14 -2
- package/dist/server/render-people.js +35 -12
- package/dist/server/render-settings.js +1 -0
- package/dist/server/render-tasks.js +7 -14
- package/dist/server/request-context.d.ts +2 -0
- package/dist/server/route-table.js +8 -1
- package/dist/server/settings.d.ts +58 -1
- package/dist/server/settings.js +198 -69
- package/dist/server/signup-gate.d.ts +9 -0
- package/dist/server/signup-gate.js +40 -0
- package/dist/server/tasks.js +12 -2
- package/dist/settings-kit-script.d.ts +21 -0
- package/dist/settings-kit-script.js +67 -0
- package/dist/settings-kit.d.ts +137 -0
- package/dist/settings-kit.js +186 -0
- package/dist/settings-proofs.d.ts +64 -0
- package/dist/settings-proofs.js +101 -0
- package/dist/setup-guide.js +4 -2
- package/dist/spend-report.d.ts +87 -0
- package/dist/spend-report.js +265 -0
- package/dist/spend-ui.d.ts +29 -8
- package/dist/spend-ui.js +74 -39
- package/dist/task-sizing.d.ts +5 -3
- package/dist/task-sizing.js +4 -4
- package/package.json +1 -1
package/dist/models-ui.d.ts
CHANGED
|
@@ -2,20 +2,31 @@
|
|
|
2
2
|
* role picked from live lists, and the models that just arrived. */
|
|
3
3
|
import { type Html } from "./html.js";
|
|
4
4
|
import type { ModelOption, RuntimeState, SeenModel, WatchState } from "./model-catalog.js";
|
|
5
|
-
export declare const MODELS_CSS = ".models{max-width:780px;min-width:0;overflow-wrap:anywhere}.models .card{padding:16px 20px;margin:12px 0;min-width:0}.models h2{margin:24px 0 8px}.models form{margin:0}.models label{display:grid;gap:6px}.models select,.models input[type=search]{box-sizing:border-box;width:100%;max-width:100%;min-width:0}.models button,.models summary,.models .button-link{min-height:44px}.models button{white-space:nowrap}.models summary{padding:12px 0;cursor:pointer}.models .tool{display:flex;gap:12px;align-items:center;justify-content:space-between;flex-wrap:wrap}.models .role{display:grid;grid-template-columns:minmax(0,1fr);gap:8px}.models .role-row{display:flex;gap:12px;align-items:end;flex-wrap:wrap}.models .role-row select{flex:1 1 240px;width:auto;min-width:0}.models .role-name{font-weight:600}.models .new{font-weight:600}.models ul{padding-left:20px}.models .meta{font-size:.86rem}@media(max-width:600px){.models .card{padding:14px}}";
|
|
5
|
+
export declare const MODELS_CSS = ".models{max-width:780px;min-width:0;overflow-wrap:anywhere}.models .card{padding:16px 20px;margin:12px 0;min-width:0}.models h2{margin:24px 0 8px}.models form{margin:0}.models label{display:grid;gap:6px}.models select,.models input[type=search]{box-sizing:border-box;width:100%;max-width:100%;min-width:0}.models button,.models summary,.models .button-link{min-height:44px}.models button{white-space:nowrap}.models summary{padding:12px 0;cursor:pointer}.models .tool{display:flex;gap:12px;align-items:center;justify-content:space-between;flex-wrap:wrap}.models .role{display:grid;grid-template-columns:minmax(0,1fr);gap:8px}.models .role-row{display:flex;gap:12px;align-items:end;flex-wrap:wrap}.models .role-row select{flex:1 1 240px;width:auto;min-width:0}.models .role-name{font-weight:600}.models .new{font-weight:600}.models ul{padding-left:20px}.models .meta{font-size:.86rem}.models .role-read{display:grid;gap:2px;padding:8px 0}.models .editor{display:grid;gap:14px}.models .editor .role select{width:100%}.models .tiers{border-top:1px solid var(--so-line)}.models .tiers .role{margin:0 0 14px}.models .mono{font-family:var(--font-mono,ui-monospace,monospace);font-size:.84rem}.models .preset{display:flex;gap:12px;align-items:center;justify-content:space-between;flex-wrap:wrap}.models .preset p{margin:4px 0 0}.models .preset>div{flex:1 1 280px;min-width:0}.models .button-link{display:inline-flex;align-items:center;padding:0 14px;border:1px solid var(--so-input-line);border-radius:8px;text-decoration:none;color:var(--so-ink);font-weight:600}.models table{width:100%;border-collapse:collapse}.models th,.models td{text-align:left;vertical-align:top;padding:8px 6px;border-bottom:1px solid var(--so-line)}.models .diff th{width:38%;font-weight:600}.models .diff ul{margin:4px 0 0}.models .preview{border-color:var(--so-input-line)}.models .preview form{display:grid;gap:10px;margin-top:12px}.models .preview .cancel{justify-self:start;display:inline-flex;align-items:center;min-height:44px}.models .sizing dt{font-weight:600;margin-top:8px}.models .sizing dd{margin:2px 0 0}.models .editor label:has(input[type=password]){margin-top:4px}@media(max-width:600px){.models .card{padding:14px}.models .diff th,.models .diff td{display:block;width:auto;border:0;padding:4px 0}.models .diff tr{display:block;border-bottom:1px solid var(--so-line);padding:6px 0}.models .recent td{display:block;border:0;padding:2px 0}.models .recent tr{display:block;border-bottom:1px solid var(--so-line);padding:6px 0}.models .recent thead{display:none}}";
|
|
6
|
+
/** One row of the editor: a role at the scope being edited. */
|
|
6
7
|
export type RoleView = {
|
|
7
|
-
|
|
8
|
+
/** The slot's form field (model-choices.ts slotKey). */
|
|
9
|
+
key: string;
|
|
8
10
|
label: string;
|
|
9
|
-
/** "provider|model", or "
|
|
11
|
+
/** "provider|model", or "" when this scope has no row of its own. */
|
|
10
12
|
current: string;
|
|
13
|
+
/** What runs now, in words, including what is inherited. */
|
|
11
14
|
words: string;
|
|
15
|
+
/** What "no row here" means for this role: "Same as the builder", "Same as the installation (…)", "Not set". */
|
|
16
|
+
unset: string;
|
|
12
17
|
groups: {
|
|
13
18
|
label: string;
|
|
14
19
|
provider: string;
|
|
15
20
|
options: ModelOption[];
|
|
16
21
|
}[];
|
|
17
|
-
/**
|
|
18
|
-
|
|
22
|
+
/** Behind the tiers disclosure. */
|
|
23
|
+
tier: boolean;
|
|
24
|
+
};
|
|
25
|
+
export type DiffView = {
|
|
26
|
+
role: string;
|
|
27
|
+
before: string;
|
|
28
|
+
after: string;
|
|
29
|
+
notes: string[];
|
|
19
30
|
};
|
|
20
31
|
export type ModelsView = {
|
|
21
32
|
runtimes: RuntimeState[];
|
|
@@ -29,6 +40,59 @@ export type ModelsView = {
|
|
|
29
40
|
canManage: boolean;
|
|
30
41
|
said: string | null;
|
|
31
42
|
problem: string | null;
|
|
43
|
+
/** The scope being edited, the scopes on offer, and the digest of what this page shows. */
|
|
44
|
+
scope: {
|
|
45
|
+
value: string;
|
|
46
|
+
label: string;
|
|
47
|
+
options: {
|
|
48
|
+
value: string;
|
|
49
|
+
label: string;
|
|
50
|
+
}[];
|
|
51
|
+
digest: string;
|
|
52
|
+
};
|
|
53
|
+
/** "Sized by job", "Opus for everything", or null for a custom set. */
|
|
54
|
+
inUse: string | null;
|
|
55
|
+
presets: {
|
|
56
|
+
name: string;
|
|
57
|
+
label: string;
|
|
58
|
+
words: string;
|
|
59
|
+
inUse: boolean;
|
|
60
|
+
recommended: boolean;
|
|
61
|
+
}[];
|
|
62
|
+
/** Newer models the watch has seen for exact ids in use here. */
|
|
63
|
+
newer: {
|
|
64
|
+
from: string;
|
|
65
|
+
to: string;
|
|
66
|
+
name: string;
|
|
67
|
+
roles: string;
|
|
68
|
+
}[];
|
|
69
|
+
/** A preset or switch's exact diff, shown before it applies. */
|
|
70
|
+
preview: {
|
|
71
|
+
title: string;
|
|
72
|
+
rows: DiffView[];
|
|
73
|
+
hidden: Record<string, string>;
|
|
74
|
+
} | null;
|
|
75
|
+
/** What still keeps tasks from being filed at this scope, or null. */
|
|
76
|
+
unresolved: string | null;
|
|
77
|
+
sizing: {
|
|
78
|
+
small: string;
|
|
79
|
+
routine: string;
|
|
80
|
+
large: string;
|
|
81
|
+
risky: string;
|
|
82
|
+
classifier: string;
|
|
83
|
+
};
|
|
84
|
+
routing: string;
|
|
85
|
+
recent: {
|
|
86
|
+
taskId: string;
|
|
87
|
+
runId: number;
|
|
88
|
+
role: string;
|
|
89
|
+
chosen: string;
|
|
90
|
+
ranOn: string;
|
|
91
|
+
}[];
|
|
92
|
+
waiting: {
|
|
93
|
+
taskId: string;
|
|
94
|
+
title: string;
|
|
95
|
+
}[];
|
|
32
96
|
};
|
|
33
97
|
export declare function modelsHtml(view: ModelsView): Html;
|
|
34
98
|
/** Filters one long picker as you type; the saved choice always stays listed. */
|
package/dist/models-ui.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/** Settings → Models: the CLIs on this computer, the default agent for each
|
|
2
2
|
* role picked from live lists, and the models that just arrived. */
|
|
3
|
-
import { html, postForm } from "./html.js";
|
|
3
|
+
import { html, postForm, reauth } from "./html.js";
|
|
4
4
|
import { priceWords } from "./model-catalog.js";
|
|
5
5
|
import { whenUtc } from "./when-html.js";
|
|
6
6
|
const when = (iso) => iso === null ? "never" : whenUtc(iso);
|
|
7
|
-
export const MODELS_CSS = `.models{max-width:780px;min-width:0;overflow-wrap:anywhere}.models .card{padding:16px 20px;margin:12px 0;min-width:0}.models h2{margin:24px 0 8px}.models form{margin:0}.models label{display:grid;gap:6px}.models select,.models input[type=search]{box-sizing:border-box;width:100%;max-width:100%;min-width:0}.models button,.models summary,.models .button-link{min-height:44px}.models button{white-space:nowrap}.models summary{padding:12px 0;cursor:pointer}.models .tool{display:flex;gap:12px;align-items:center;justify-content:space-between;flex-wrap:wrap}.models .role{display:grid;grid-template-columns:minmax(0,1fr);gap:8px}.models .role-row{display:flex;gap:12px;align-items:end;flex-wrap:wrap}.models .role-row select{flex:1 1 240px;width:auto;min-width:0}.models .role-name{font-weight:600}.models .new{font-weight:600}.models ul{padding-left:20px}.models .meta{font-size:.86rem}@media(max-width:600px){.models .card{padding:14px}}`;
|
|
7
|
+
export const MODELS_CSS = `.models{max-width:780px;min-width:0;overflow-wrap:anywhere}.models .card{padding:16px 20px;margin:12px 0;min-width:0}.models h2{margin:24px 0 8px}.models form{margin:0}.models label{display:grid;gap:6px}.models select,.models input[type=search]{box-sizing:border-box;width:100%;max-width:100%;min-width:0}.models button,.models summary,.models .button-link{min-height:44px}.models button{white-space:nowrap}.models summary{padding:12px 0;cursor:pointer}.models .tool{display:flex;gap:12px;align-items:center;justify-content:space-between;flex-wrap:wrap}.models .role{display:grid;grid-template-columns:minmax(0,1fr);gap:8px}.models .role-row{display:flex;gap:12px;align-items:end;flex-wrap:wrap}.models .role-row select{flex:1 1 240px;width:auto;min-width:0}.models .role-name{font-weight:600}.models .new{font-weight:600}.models ul{padding-left:20px}.models .meta{font-size:.86rem}.models .role-read{display:grid;gap:2px;padding:8px 0}.models .editor{display:grid;gap:14px}.models .editor .role select{width:100%}.models .tiers{border-top:1px solid var(--so-line)}.models .tiers .role{margin:0 0 14px}.models .mono{font-family:var(--font-mono,ui-monospace,monospace);font-size:.84rem}.models .preset{display:flex;gap:12px;align-items:center;justify-content:space-between;flex-wrap:wrap}.models .preset p{margin:4px 0 0}.models .preset>div{flex:1 1 280px;min-width:0}.models .button-link{display:inline-flex;align-items:center;padding:0 14px;border:1px solid var(--so-input-line);border-radius:8px;text-decoration:none;color:var(--so-ink);font-weight:600}.models table{width:100%;border-collapse:collapse}.models th,.models td{text-align:left;vertical-align:top;padding:8px 6px;border-bottom:1px solid var(--so-line)}.models .diff th{width:38%;font-weight:600}.models .diff ul{margin:4px 0 0}.models .preview{border-color:var(--so-input-line)}.models .preview form{display:grid;gap:10px;margin-top:12px}.models .preview .cancel{justify-self:start;display:inline-flex;align-items:center;min-height:44px}.models .sizing dt{font-weight:600;margin-top:8px}.models .sizing dd{margin:2px 0 0}.models .editor label:has(input[type=password]){margin-top:4px}@media(max-width:600px){.models .card{padding:14px}.models .diff th,.models .diff td{display:block;width:auto;border:0;padding:4px 0}.models .diff tr{display:block;border-bottom:1px solid var(--so-line);padding:6px 0}.models .recent td{display:block;border:0;padding:2px 0}.models .recent tr{display:block;border-bottom:1px solid var(--so-line);padding:6px 0}.models .recent thead{display:none}}`;
|
|
8
8
|
function toolRow(one, manage) {
|
|
9
9
|
const state = one.installed === null ? "Version unknown"
|
|
10
10
|
: one.behind ? html `${one.installed} · <strong>${one.latest} available</strong>`
|
|
@@ -16,18 +16,17 @@ function toolRow(one, manage) {
|
|
|
16
16
|
}
|
|
17
17
|
function roleForm(role, manage) {
|
|
18
18
|
if (!manage)
|
|
19
|
-
return html `<div class="
|
|
19
|
+
return html `<div class="role-read"><span class="role-name">${role.label}</span><span>${role.words}</span></div>`;
|
|
20
20
|
const known = role.groups.some(group => group.options.some(option => `${group.provider}|${option.value}` === role.current));
|
|
21
|
-
const saved = !known && role.current !== "
|
|
21
|
+
const saved = !known && role.current !== "" ? html `<option value="${role.current}" selected>${role.words} · current</option>` : "";
|
|
22
22
|
const groups = role.groups.map(group => html `<optgroup label="${group.label}">${group.options.map(option => {
|
|
23
23
|
const value = `${group.provider}|${option.value}`;
|
|
24
24
|
return html `<option value="${value}"${value === role.current ? html ` selected` : ""}>${option.label}</option>`;
|
|
25
25
|
})}</optgroup>`);
|
|
26
|
-
const
|
|
27
|
-
const long = role.groups.reduce((sum, group) => sum + group.options.length, 0) > 40;
|
|
26
|
+
const unset = html `<option value="clear"${role.current === "" ? html ` selected` : ""}>${role.unset}</option>`;
|
|
28
27
|
const count = role.groups.reduce((sum, group) => sum + group.options.length, 0);
|
|
29
|
-
const id = `role-${role.
|
|
30
|
-
return
|
|
28
|
+
const id = `role-${role.key}`;
|
|
29
|
+
return html `<div class="role"><label class="role-name" for="${id}">${role.label}</label>${count > 40 ? html `<input type="search" data-model-filter hidden aria-label="Search ${role.label.toLowerCase()} models" placeholder="Search ${count} models" autocomplete="off">` : ""}<select id="${id}" name="${role.key}">${unset}${saved}${groups}</select>${role.current === "" ? html `<span class="meta">${role.words}</span>` : ""}</div>`;
|
|
31
30
|
}
|
|
32
31
|
export function modelsHtml(view) {
|
|
33
32
|
const manage = view.canManage && view.csrf !== "";
|
|
@@ -44,11 +43,11 @@ export function modelsHtml(view) {
|
|
|
44
43
|
const watch = manage
|
|
45
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>`
|
|
46
45
|
: "";
|
|
47
|
-
return html `<section class="models">${status}${problem}<h2>AI tools</h2>${tools}<p class="meta">Checked ${when(view.watch.checkedAt)} ${check}</p>${fresh}
|
|
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>`;
|
|
48
47
|
}
|
|
49
48
|
/** Filters one long picker as you type; the saved choice always stays listed. */
|
|
50
49
|
export function modelsScript() {
|
|
51
|
-
return `(function(){document.querySelectorAll('.models
|
|
50
|
+
return `(function(){document.querySelectorAll('.models .role').forEach(function(form){
|
|
52
51
|
var search=form.querySelector('[data-model-filter]'),select=form.querySelector('select');
|
|
53
52
|
if(!search||!select)return;search.hidden=false;
|
|
54
53
|
var groups=Array.from(select.querySelectorAll('optgroup')).map(function(g){return{g:g,options:Array.from(g.children)}});
|
|
@@ -57,3 +56,47 @@ export function modelsScript() {
|
|
|
57
56
|
select.value=chosen;});
|
|
58
57
|
})})();`;
|
|
59
58
|
}
|
|
59
|
+
const scopeQuery = (view) => view.scope.value === "installation" ? "" : `&scope=${encodeURIComponent(view.scope.value)}`;
|
|
60
|
+
const password = () => reauth(html `<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password"></label>`);
|
|
61
|
+
/** The roles at the scope being edited: one form, one save, one password. Sizes and tiers sit behind a disclosure. */
|
|
62
|
+
function editorSection(view, manage) {
|
|
63
|
+
const scopes = view.scope.options.length < 2 ? "" : html `<form method="get" action="/settings/models" class="models-scope"><label for="models-scope">Settings for</label><div class="role-row"><select id="models-scope" name="scope">${view.scope.options.map(one => html `<option value="${one.value}"${one.value === view.scope.value ? html ` selected` : ""}>${one.label}</option>`)}</select><button type="submit" class="secondary">Show</button></div></form>`;
|
|
64
|
+
const main = view.roles.filter(role => !role.tier), tiers = view.roles.filter(role => role.tier);
|
|
65
|
+
const inUse = html `<p class="meta">In use: ${view.inUse === null ? "a custom set" : view.inUse}${view.scope.value === "installation" ? "" : html ` · ${view.scope.label}`}</p>`;
|
|
66
|
+
const unresolved = view.unresolved === null ? "" : html `<p class="problem" role="alert">New tasks can't be filed here yet: ${view.unresolved}</p>`;
|
|
67
|
+
const rows = html `${main.map(role => roleForm(role, manage))}<details class="tiers"${manage ? "" : html ` open`}><summary>By task size</summary>${tiers.map(role => roleForm(role, manage))}</details>`;
|
|
68
|
+
const body = manage
|
|
69
|
+
? postForm("/settings/models/agent", html `${rows}${password()}<button type="submit">Save</button>`, { attrs: { class: "card editor" }, hidden: { scope: view.scope.value, digest: view.scope.digest } })
|
|
70
|
+
: html `<div class="card editor">${rows}</div>`;
|
|
71
|
+
return html `<h2>Models for new tasks</h2>${scopes}${inUse}${unresolved}${body}<p class="meta">Changes apply to tasks filed from now on. Approved and waiting tasks keep the models they were filed with.</p>`;
|
|
72
|
+
}
|
|
73
|
+
/** A preset or a switch shows exactly what it changes, and applies only with a second, password-checked submit. */
|
|
74
|
+
function previewCard(view, manage) {
|
|
75
|
+
const preview = view.preview;
|
|
76
|
+
if (preview === null || !manage)
|
|
77
|
+
return html ``;
|
|
78
|
+
const back = `/settings/models${view.scope.value === "installation" ? "" : `?scope=${encodeURIComponent(view.scope.value)}`}`;
|
|
79
|
+
if (preview.rows.length === 0)
|
|
80
|
+
return html `<div class="card preview" role="status"><h2>${preview.title}</h2><p>Already in use. Nothing would change.</p><a class="button-link" href="${back}">Back</a></div>`;
|
|
81
|
+
const rows = preview.rows.map(row => html `<tr><th scope="row">${row.role}</th><td><span class="mono">${row.before}</span> → <span class="mono">${row.after}</span>${row.notes.length === 0 ? "" : html `<ul class="meta">${row.notes.map(note => html `<li>${note}</li>`)}</ul>`}</td></tr>`);
|
|
82
|
+
return html `<div class="card preview"><h2>${preview.title}</h2><p>Review what changes for tasks filed from now on.</p><table class="diff"><tbody>${rows}</tbody></table>${postForm("/settings/models/apply", html `${password()}<button type="submit" class="primary">Apply ${preview.rows.length} change${preview.rows.length === 1 ? "" : "s"}</button><a class="cancel" href="${back}">Cancel</a>`, { hidden: preview.hidden })}</div>`;
|
|
83
|
+
}
|
|
84
|
+
function presetsSection(view, manage) {
|
|
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}`;
|
|
88
|
+
}
|
|
89
|
+
/** How a task's size is decided, in plain words, and what routing does at filing. */
|
|
90
|
+
function sizingSection(view) {
|
|
91
|
+
return html `<h2>How tasks are sized</h2><div class="card 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><dl>
|
|
92
|
+
<dt>Small</dt><dd>${view.sizing.small}. Built by the builder for small tasks, with no plan.</dd>
|
|
93
|
+
<dt>Routine</dt><dd>${view.sizing.routine}. Planned and built by the planner and builder for routine tasks.</dd>
|
|
94
|
+
<dt>Large or risky</dt><dd>Large: ${view.sizing.large}. Risky: ${view.sizing.risky} Planned and built by the large or risky models.</dd></dl>
|
|
95
|
+
<details><summary>More about routing</summary><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></details></div>${view.waiting.length === 0 ? "" : html `<details class="waiting"><summary>${view.waiting.length} plan${view.waiting.length === 1 ? "" : "s"} waiting for approval</summary><p class="meta">They keep the models they were filed with.</p><ul>${view.waiting.map(one => html `<li><a href="/t/${encodeURIComponent(one.taskId)}">${one.title}</a></li>`)}</ul></details>`}`;
|
|
96
|
+
}
|
|
97
|
+
/** The model each recent run was set to, next to the model its provider reported. */
|
|
98
|
+
function recentSection(view) {
|
|
99
|
+
if (view.recent.length === 0)
|
|
100
|
+
return html ``;
|
|
101
|
+
return html `<h2>Recent work</h2><table class="recent"><thead><tr><th scope="col">Task</th><th scope="col">Chosen → ran on</th></tr></thead><tbody>${view.recent.map(one => html `<tr><td><a href="/t/${encodeURIComponent(one.taskId)}">${one.taskId}</a> <span class="meta">${one.role}</span></td><td><span class="mono">${one.chosen}</span> → <span class="mono">${one.ranOn}</span></td></tr>`)}</tbody></table>`;
|
|
102
|
+
}
|
package/dist/needs-you.d.ts
CHANGED
|
@@ -15,6 +15,14 @@ export type NeedAction = {
|
|
|
15
15
|
code: NeedActionCode;
|
|
16
16
|
label: string;
|
|
17
17
|
};
|
|
18
|
+
/** Where "Connect a builder" and "Start the builder" lead (code `start-worker`): the Start a builder page, which shows
|
|
19
|
+
* the one command to run and the open project's builder state. Every signed-in person can open it, a project-limited
|
|
20
|
+
* one included (System is instance-only). Never the task's own page, which only repeats that nothing is connected. */
|
|
21
|
+
export declare const START_BUILDER_HREF = "/start-builder";
|
|
22
|
+
/** Start a builder for one task: the page reports that task's own project, whatever project is open. */
|
|
23
|
+
export declare const startBuilderHref: (taskId: string | null) => string;
|
|
24
|
+
/** What starts a builder: run on the machine where the project lives, in the project's folder. */
|
|
25
|
+
export declare const START_BUILDER_COMMAND = "toolroll up";
|
|
18
26
|
/** `build`: the run a sentence names; `provider`: the agent that signed out. */
|
|
19
27
|
export type NeedContext = {
|
|
20
28
|
build?: number | null;
|
package/dist/needs-you.js
CHANGED
|
@@ -5,6 +5,14 @@
|
|
|
5
5
|
*
|
|
6
6
|
* The words never name the machine's internals; `plainSentence` keeps a
|
|
7
7
|
* recorded reason only when it is free of them. */
|
|
8
|
+
/** Where "Connect a builder" and "Start the builder" lead (code `start-worker`): the Start a builder page, which shows
|
|
9
|
+
* the one command to run and the open project's builder state. Every signed-in person can open it, a project-limited
|
|
10
|
+
* one included (System is instance-only). Never the task's own page, which only repeats that nothing is connected. */
|
|
11
|
+
export const START_BUILDER_HREF = "/start-builder";
|
|
12
|
+
/** Start a builder for one task: the page reports that task's own project, whatever project is open. */
|
|
13
|
+
export const startBuilderHref = (taskId) => taskId === null ? START_BUILDER_HREF : `${START_BUILDER_HREF}?task=${encodeURIComponent(taskId)}`;
|
|
14
|
+
/** What starts a builder: run on the machine where the project lives, in the project's folder. */
|
|
15
|
+
export const START_BUILDER_COMMAND = "toolroll up";
|
|
8
16
|
const always = () => true;
|
|
9
17
|
const build = (context) => context.build == null ? "the last build" : `build #${context.build}`;
|
|
10
18
|
const Build = (context) => context.build == null ? "The last build" : `Build #${context.build}`;
|
package/dist/operate.d.ts
CHANGED
|
@@ -9,10 +9,12 @@ import { type Store } from "./store.js";
|
|
|
9
9
|
import { type DispatchAdapter } from "./sync.js";
|
|
10
10
|
import { type RolePort, type UpRoleStart } from "./up-processes.js";
|
|
11
11
|
import { type TelegramTransport } from "./telegram.js";
|
|
12
|
+
import { type ProjectIdentities } from "./project-identity.js";
|
|
12
13
|
import { type FolderLookOptions } from "./folder-look.js";
|
|
13
14
|
import { type LeadCliSeams } from "./lead-cli.js";
|
|
14
15
|
import { type Principal, type RemoteRun, type RemoteSource } from "./operate-remote.js";
|
|
15
16
|
import { type PublishExec } from "./publish.js";
|
|
17
|
+
import { type ApplyResult, type ExtraWrite, type ModelChange, type Slot } from "./model-choices.js";
|
|
16
18
|
import { type Runner as CommandRunner } from "./builder.js";
|
|
17
19
|
import { type ReleaseIo } from "./releases.js";
|
|
18
20
|
export type Write = (line: string) => void;
|
|
@@ -94,6 +96,8 @@ export type OperateOptions = {
|
|
|
94
96
|
filingSizer?: Sizer;
|
|
95
97
|
/** Set only by up-role-child.ts: this process is one role of a `toolroll up` (the console or the worker). */
|
|
96
98
|
upRole?: UpRoleHost;
|
|
99
|
+
/** Injected by tests: how the console looks at project folders off its thread (folder-look.ts). */
|
|
100
|
+
folderLook?: FolderLookOptions;
|
|
97
101
|
};
|
|
98
102
|
export type OnboardSeams = Partial<Pick<OnboardIo, "home" | "env" | "cwd" | "interactive" | "confirm" | "findRepo" | "checkConnection" | "pullRequests">>;
|
|
99
103
|
export declare const OPERATE_HELP: string;
|
|
@@ -153,6 +157,8 @@ export declare function runOperateAs(argv: string[], opts: {
|
|
|
153
157
|
options?: OperateOptions;
|
|
154
158
|
}): Promise<number>;
|
|
155
159
|
type Context = {
|
|
160
|
+
/** Injected by tests: how the console looks at project folders off its thread. */
|
|
161
|
+
folderLook?: FolderLookOptions;
|
|
156
162
|
/** A remote run's person (runOperateAs): every credential check resolves to them. Absent for every local command. */
|
|
157
163
|
principal?: RemoteRun;
|
|
158
164
|
/** The lead token this command runs under: it signs in as its owner. */
|
|
@@ -303,3 +309,18 @@ export declare function upProveSaved(gitRun: (file: string, args: readonly strin
|
|
|
303
309
|
/** What `up` hands its next worker after the worker reports which saved projects it still holds start authority for:
|
|
304
310
|
* only what both still hold. A worker's report can give up an authority, never add or give back one. */
|
|
305
311
|
export declare function upNarrowDeferred(held: readonly string[], reported: readonly string[]): string[];
|
|
312
|
+
/** Resolve the console's project identities after it is listening, never before: the deploy's health check and every
|
|
313
|
+
* page answer at once, and each project shows up as its folder answers (project-identity.ts `refresh`, its looks a
|
|
314
|
+
* few at a time under their deadline). Nothing waits on it; a pass that fails leaves those paths unresolved, which
|
|
315
|
+
* fails closed, for the background pass to try again. */
|
|
316
|
+
export declare function consoleIdentitiesInBackground(identities: ProjectIdentities, paths: readonly string[]): void;
|
|
317
|
+
/** `config set` and `config clear` on the command line, through Settings → Models' one write path (model-choices.ts,
|
|
318
|
+
* docs/plans/spend-dashboard.md slice 3), under the same target and resulting-configuration checks as the editor: the
|
|
319
|
+
* person proved themselves with their own password or login at `proved.generation`; a machine credential (a remote
|
|
320
|
+
* token, the lead's token) never changes which models run. The write re-proves them inside its transaction: still an
|
|
321
|
+
* unrestricted instance operator, at that very generation, by a proof that still stands. */
|
|
322
|
+
export declare function writeModelRow(store: Store, proved: {
|
|
323
|
+
actor: string;
|
|
324
|
+
generation: number;
|
|
325
|
+
method: string;
|
|
326
|
+
}, scope: string, knownProject: (repo: string) => boolean, slot: Slot | null, to: ModelChange["to"], now: Date, extra?: readonly ExtraWrite[]): ApplyResult;
|
package/dist/operate.js
CHANGED
|
@@ -222,6 +222,7 @@ import { fileTaskProposal, validateTaskText } from "./proposal.js";
|
|
|
222
222
|
import { TEMPLATES, templateByName } from "./templates.js";
|
|
223
223
|
import { isDirectChatProvider, isSubscriptionChatProvider, priceOf, PRICED_MODELS } from "./converse.js";
|
|
224
224
|
import { resolvePhaseAgent, resolveScopeProfile, resolveRouteCandidates, routeOfTask, INSTALLATION_SCOPE } from "./agentconfig.js";
|
|
225
|
+
import { applyModelChoices, stateDigest } from "./model-choices.js";
|
|
225
226
|
import { isTaskSize, legOf, projectRoute, routeDigestOf, routeWords, TASK_SIZES, PHASES as ROUTE_PHASES } from "./phase-routing.js";
|
|
226
227
|
import { observeProviderReadiness, reportProviderReadinessAuthed } from "./runner.js";
|
|
227
228
|
import { parseDemoUrl, projectDemoUrl, saveProjectDemo } from "./project-demo.js";
|
|
@@ -967,6 +968,7 @@ async function dispatchOn(command, positional, flags, repoList, base, options, e
|
|
|
967
968
|
...(options.onboardSeams === undefined ? {} : { onboardSeams: options.onboardSeams }),
|
|
968
969
|
...(options.upSeams === undefined ? {} : { upSeams: options.upSeams }),
|
|
969
970
|
...(options.upRole === undefined ? {} : { upRole: options.upRole }),
|
|
971
|
+
...(options.folderLook === undefined ? {} : { folderLook: options.folderLook }),
|
|
970
972
|
...(upNeedsOneProcess(options) ? { upOneProcess: true } : {}),
|
|
971
973
|
...(activeRemote() === null ? {} : { principal: activeRemote() }),
|
|
972
974
|
});
|
|
@@ -3941,11 +3943,11 @@ async function startConsole(options) {
|
|
|
3941
3943
|
const { context } = options;
|
|
3942
3944
|
// A git incident (teardown-git.ts) raises an attention notification in this console's outbox.
|
|
3943
3945
|
noticeGitIncidents(context.store, context.clock);
|
|
3944
|
-
// Every project folder this console starts with is resolved off its thread, under a deadline
|
|
3945
|
-
//
|
|
3946
|
-
//
|
|
3947
|
-
|
|
3948
|
-
|
|
3946
|
+
// Every project folder this console starts with is resolved off its thread, under a deadline, after it is listening
|
|
3947
|
+
// (consoleIdentitiesInBackground): the console answers at once, and a project shows up once its folder answers. Until
|
|
3948
|
+
// then it is unresolved and kept restrictive; one that doesn't answer (a drive away, an iCloud folder macOS is
|
|
3949
|
+
// holding) is looked at again by the background pass (project-identity.ts).
|
|
3950
|
+
const identities = projectIdentities(context.folderLook);
|
|
3949
3951
|
const server = createDecisionServer({
|
|
3950
3952
|
identities,
|
|
3951
3953
|
store: context.store,
|
|
@@ -3982,6 +3984,7 @@ async function startConsole(options) {
|
|
|
3982
3984
|
}).catch(error => {
|
|
3983
3985
|
throw new Error(`could not listen on ${options.host}:${options.port} — ${describe(error)}`);
|
|
3984
3986
|
});
|
|
3987
|
+
consoleIdentitiesInBackground(identities, [...options.repos ?? [], ...options.projectRoots ?? [], ...context.store.listProjects().map(one => one.path), ...context.store.knownRepos()]);
|
|
3985
3988
|
// v104: monitoring runs beside the console (every setup has one): one loop sends the audit stream and traces where
|
|
3986
3989
|
// Settings → Monitoring says, and stops when the console does. A lease keeps a second process from sending too.
|
|
3987
3990
|
const stopMonitoring = startMonitoring({
|
|
@@ -5042,6 +5045,9 @@ async function configCommand(positional, flags, context) {
|
|
|
5042
5045
|
if (phase === undefined || !["plan", "build", "repair", "review"].includes(phase))
|
|
5043
5046
|
return fail(write, json, `config ${action}`, "usage", "which phase? plan, build, repair, review — or chat", EXIT.usage);
|
|
5044
5047
|
const acting = await askCredentials(flags, context);
|
|
5048
|
+
// The credential generation the proof below is checked against, read BEFORE it: a rotation after this read makes
|
|
5049
|
+
// the write refuse (model-choices.ts re-checks it inside the transaction), never borrow the new generation.
|
|
5050
|
+
const provedGeneration = acting === null ? -1 : store.accountOf(acting.name)?.generation ?? -1;
|
|
5045
5051
|
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
5046
5052
|
if (acting === null || !verified?.ok)
|
|
5047
5053
|
return refuseApprover(context, `config ${action}`, verified, "changing spend routing takes `--as <you> --token <t>`", phase);
|
|
@@ -5057,6 +5063,18 @@ async function configCommand(positional, flags, context) {
|
|
|
5057
5063
|
// Clearing one left from before stays possible.
|
|
5058
5064
|
if (tierGiven === "light" && phase !== "build" && !(action === "clear" && phase === "plan" && !flag(flags, "also")))
|
|
5059
5065
|
return fail(write, json, `config ${action}`, "usage", "--tier light is for build only — a small change makes no plan, and repairs resume the builder's session", EXIT.usage);
|
|
5066
|
+
// Every row below — plain, tier, `--also`, and a leftover light planner's removal — is written through the one write
|
|
5067
|
+
// path (model-choices.ts): one transaction, the actor re-proved as an instance operator at the credential generation
|
|
5068
|
+
// proved above by a proof that still stands, the target and the resulting configuration checked, every row ledgered.
|
|
5069
|
+
// A project named with --repo must be one Toolroll knows (the editor's own rule).
|
|
5070
|
+
const projectKnown = repoGiven === undefined || (await knownProject(context, repoGiven, { listed: true, canonical: true })) !== undefined;
|
|
5071
|
+
const proof = { actor: acting.name, generation: provedGeneration, method: verified.method };
|
|
5072
|
+
const written = (slot, to, extra = []) => writeModelRow(store, proof, scope, () => projectKnown, slot, to, clock(), extra);
|
|
5073
|
+
// A configuration still missing a role's model is saved, and said (JSON: `incomplete`).
|
|
5074
|
+
const said = (command, result, payload, lines) => {
|
|
5075
|
+
const incomplete = result.ok ? result.unresolved : null;
|
|
5076
|
+
return succeed(write, json, command, { ...payload, ...(incomplete === null ? {} : { incomplete }) }, () => [...lines(), ...(incomplete === null ? [] : [` ! new tasks can't be filed here yet: ${incomplete}`])]);
|
|
5077
|
+
};
|
|
5060
5078
|
// Another provider's candidate on the same tier (`--also`): one per provider, and at filing the one whose plan
|
|
5061
5079
|
// has more room runs.
|
|
5062
5080
|
if (flag(flags, "also")) {
|
|
@@ -5067,8 +5085,11 @@ async function configCommand(positional, flags, context) {
|
|
|
5067
5085
|
if (alsoProvider !== undefined && !isProviderId(alsoProvider))
|
|
5068
5086
|
return fail(write, json, `config ${action}`, "usage", `--provider is one of ${PROVIDER_IDS.join(", ")}`, EXIT.usage);
|
|
5069
5087
|
if (action === "clear") {
|
|
5070
|
-
const
|
|
5071
|
-
|
|
5088
|
+
const alsoClear = written(null, null, [{ kind: "also-clear", phase, tier, provider: alsoProvider ?? null }]);
|
|
5089
|
+
if (!alsoClear.ok)
|
|
5090
|
+
return fail(write, json, "config clear", alsoClear.reason, alsoClear.problem, EXIT.refused);
|
|
5091
|
+
const removed = alsoClear.extra;
|
|
5092
|
+
return said("config clear", alsoClear, { scope, phase, tier, also: true, provider: alsoProvider ?? null, cleared: removed }, () => [
|
|
5072
5093
|
removed > 0 ? `Removed ${removed} other-provider ${tier} ${phase} agent${removed === 1 ? "" : "s"} at ${scope}; sealed routes are untouched.` : `No other-provider ${tier} ${phase} agent${alsoProvider === undefined ? "" : ` on ${alsoProvider}`} was configured at ${scope}.`,
|
|
5073
5094
|
]);
|
|
5074
5095
|
}
|
|
@@ -5078,10 +5099,12 @@ async function configCommand(positional, flags, context) {
|
|
|
5078
5099
|
const alsoValid = validateSpec({ provider: alsoProvider, model: alsoModel });
|
|
5079
5100
|
if (!alsoValid.ok)
|
|
5080
5101
|
return fail(write, json, "config set", "invalid", alsoValid.problem, EXIT.usage);
|
|
5081
|
-
|
|
5102
|
+
const alsoSet = written(null, null, [{ kind: "also-set", phase, tier, provider: alsoProvider, model: alsoModel }]);
|
|
5103
|
+
if (!alsoSet.ok)
|
|
5104
|
+
return fail(write, json, "config set", alsoSet.reason, alsoSet.problem, EXIT.refused);
|
|
5082
5105
|
const resolvedNow = resolveRouteCandidates(store, scope === INSTALLATION_SCOPE ? null : scope);
|
|
5083
5106
|
const own = !resolvedNow.ok ? null : tier === "routine" ? resolvedNow.candidates[phase].routine : tier === "light" ? resolvedNow.candidates[phase].light ?? null : resolvedNow.candidates[phase].strong;
|
|
5084
|
-
return
|
|
5107
|
+
return said("config set", alsoSet, { scope, phase, tier, also: true, provider: alsoProvider, model: alsoModel }, () => [
|
|
5085
5108
|
`${tier} ${phase} at ${scope === INSTALLATION_SCOPE ? "the installation" : scope} can also run ${alsoProvider} · ${alsoModel}, set by ${acting.name}.`,
|
|
5086
5109
|
own !== null && own.provider === alsoProvider && !own.source.includes("also")
|
|
5087
5110
|
? ` ! the ${tier} ${phase} agent already runs ${alsoProvider} (${own.model}) — this one is not used while it does.`
|
|
@@ -5089,20 +5112,39 @@ async function configCommand(positional, flags, context) {
|
|
|
5089
5112
|
]);
|
|
5090
5113
|
}
|
|
5091
5114
|
if (action === "clear") {
|
|
5115
|
+
if (tierGiven === "light" && phase === "build") {
|
|
5116
|
+
const lightWrite = written({ phase, tier: "light" }, null);
|
|
5117
|
+
if (!lightWrite.ok)
|
|
5118
|
+
return fail(write, json, "config clear", lightWrite.reason, lightWrite.problem, EXIT.refused);
|
|
5119
|
+
const clearedLight = lightWrite.diff.length > 0;
|
|
5120
|
+
return said("config clear", lightWrite, { scope, phase, tier: "light", cleared: clearedLight }, () => [
|
|
5121
|
+
clearedLight ? `Cleared the light ${phase} agent at ${scope} — small changes keep the default and say so; sealed routes are untouched.` : `No light ${phase} agent was configured at ${scope}.`,
|
|
5122
|
+
]);
|
|
5123
|
+
}
|
|
5092
5124
|
if (tierGiven === "light") {
|
|
5093
|
-
|
|
5094
|
-
|
|
5125
|
+
// A light planner from before light became build-only: removed here, never written.
|
|
5126
|
+
const legacyClear = written(null, null, [{ kind: "legacy-light-clear", phase: phase }]);
|
|
5127
|
+
if (!legacyClear.ok)
|
|
5128
|
+
return fail(write, json, "config clear", legacyClear.reason, legacyClear.problem, EXIT.refused);
|
|
5129
|
+
const clearedLight = legacyClear.extra > 0;
|
|
5130
|
+
return said("config clear", legacyClear, { scope, phase, tier: "light", cleared: clearedLight }, () => [
|
|
5095
5131
|
clearedLight ? `Cleared the light ${phase} agent at ${scope} — small changes keep the default and say so; sealed routes are untouched.` : `No light ${phase} agent was configured at ${scope}.`,
|
|
5096
5132
|
]);
|
|
5097
5133
|
}
|
|
5098
5134
|
if (tierGiven === "strong") {
|
|
5099
|
-
const
|
|
5100
|
-
|
|
5135
|
+
const strongWrite = written({ phase: phase, tier: "strong" }, null);
|
|
5136
|
+
if (!strongWrite.ok)
|
|
5137
|
+
return fail(write, json, "config clear", strongWrite.reason, strongWrite.problem, EXIT.refused);
|
|
5138
|
+
const clearedStrong = strongWrite.diff.length > 0;
|
|
5139
|
+
return said("config clear", strongWrite, { scope, phase, tier: "strong", cleared: clearedStrong }, () => [
|
|
5101
5140
|
clearedStrong ? `Cleared the strong ${phase} agent at ${scope} — demanding routes keep the default and say so; sealed routes are untouched.` : `No strong ${phase} agent was configured at ${scope}.`,
|
|
5102
5141
|
]);
|
|
5103
5142
|
}
|
|
5104
|
-
const
|
|
5105
|
-
|
|
5143
|
+
const routineWrite = written({ phase: phase, tier: "routine" }, null);
|
|
5144
|
+
if (!routineWrite.ok)
|
|
5145
|
+
return fail(write, json, "config clear", routineWrite.reason, routineWrite.problem, EXIT.refused);
|
|
5146
|
+
const cleared = routineWrite.diff.length > 0;
|
|
5147
|
+
return said("config clear", routineWrite, { scope, phase, cleared }, () => [
|
|
5106
5148
|
cleared ? `Cleared ${phase} at ${scope} — it resolves one layer down now.` : `Nothing was configured for ${phase} at ${scope}.`,
|
|
5107
5149
|
]);
|
|
5108
5150
|
}
|
|
@@ -5116,8 +5158,10 @@ async function configCommand(positional, flags, context) {
|
|
|
5116
5158
|
if (tierGiven === "light") {
|
|
5117
5159
|
if (modelGiven === null)
|
|
5118
5160
|
return fail(write, json, "config set", "usage", `a light ${phase} agent names an exact --model — approvals bind exact routing`, EXIT.usage);
|
|
5119
|
-
|
|
5120
|
-
|
|
5161
|
+
const lightSet = written({ phase: phase, tier: "light" }, { provider: providerGiven, model: modelGiven });
|
|
5162
|
+
if (!lightSet.ok)
|
|
5163
|
+
return fail(write, json, "config set", lightSet.reason, lightSet.problem, EXIT.refused);
|
|
5164
|
+
return said("config set", lightSet, { scope, phase, tier: "light", provider: providerGiven, model: modelGiven }, () => [
|
|
5121
5165
|
`light ${phase} at ${scope === INSTALLATION_SCOPE ? "the installation" : scope} is ${providerGiven} · ${modelGiven}, set by ${acting.name}.`,
|
|
5122
5166
|
" small changes filed from now on build on it with no plan; sealed routes are untouched.",
|
|
5123
5167
|
]);
|
|
@@ -5127,14 +5171,19 @@ async function configCommand(positional, flags, context) {
|
|
|
5127
5171
|
return fail(write, json, "config set", "invalid", "gemini has no isolation posture for the review phase yet — name claude or codex as the strong reviewer", EXIT.usage);
|
|
5128
5172
|
if (modelGiven === null)
|
|
5129
5173
|
return fail(write, json, "config set", "usage", `a strong ${phase} agent names an exact --model — approvals bind exact routing`, EXIT.usage);
|
|
5130
|
-
|
|
5131
|
-
|
|
5174
|
+
const strongSet = written({ phase: phase, tier: "strong" }, { provider: providerGiven, model: modelGiven });
|
|
5175
|
+
if (!strongSet.ok)
|
|
5176
|
+
return fail(write, json, "config set", strongSet.reason, strongSet.problem, EXIT.refused);
|
|
5177
|
+
return said("config set", strongSet, { scope, phase, tier: "strong", provider: providerGiven, model: modelGiven }, () => [
|
|
5132
5178
|
`strong ${phase} at ${scope === INSTALLATION_SCOPE ? "the installation" : scope} is ${providerGiven}${modelGiven === null ? "" : ` · ${modelGiven}`}, set by ${acting.name}.`,
|
|
5133
5179
|
" large or risky, strict, screenshot-proof, and automerge routes filed from now on reach for it; sealed routes are untouched.",
|
|
5134
5180
|
...(phase === "repair" ? [" a strong repair agent applies only when its provider is the build's — cross-provider repair does not exist."] : []),
|
|
5135
5181
|
]);
|
|
5136
5182
|
}
|
|
5137
|
-
|
|
5183
|
+
// A routine row may still name no model on the command line (said below); the editor never writes one.
|
|
5184
|
+
const routineSet = written({ phase: phase, tier: "routine" }, { provider: providerGiven, model: modelGiven });
|
|
5185
|
+
if (!routineSet.ok)
|
|
5186
|
+
return fail(write, json, "config set", routineSet.reason, routineSet.problem, EXIT.refused);
|
|
5138
5187
|
const warnings = [];
|
|
5139
5188
|
if (modelGiven === null) {
|
|
5140
5189
|
warnings.push(`no --model named: approvals bind exact routing, so scopes cannot file until ${phase} has an exact model — \`config set ${phase} --provider ${providerGiven} --model <model>\``);
|
|
@@ -5145,7 +5194,7 @@ async function configCommand(positional, flags, context) {
|
|
|
5145
5194
|
if (providerGiven !== "claude") {
|
|
5146
5195
|
warnings.push(`${providerGiven} does not report dollar cost: its runs land as UNMEASURED, and any schedule with a weekly limit fails closed on them by design.`);
|
|
5147
5196
|
}
|
|
5148
|
-
return
|
|
5197
|
+
return said("config set", routineSet, { scope, phase, provider: providerGiven, model: modelGiven, warnings }, () => [
|
|
5149
5198
|
`${phase} at ${scope === INSTALLATION_SCOPE ? "the installation" : scope} now runs ${providerGiven}${modelGiven === null ? "" : ` · ${modelGiven}`}, set by ${acting.name}.`,
|
|
5150
5199
|
...warnings.map(one => ` ! ${one}`),
|
|
5151
5200
|
]);
|
|
@@ -7493,11 +7542,14 @@ async function upCommand(flags, context) {
|
|
|
7493
7542
|
pendingLinks.set(id, value => { clearTimeout(timer); resolveLink(value); });
|
|
7494
7543
|
consoleRole?.send({ type: "sign-in-link", id, account });
|
|
7495
7544
|
});
|
|
7496
|
-
|
|
7497
|
-
|
|
7545
|
+
// Every start says where the saved password is and the exact command that shows it, browser or not (Settings
|
|
7546
|
+
// revamp S4, D2). Only the path is printed, never what the file holds: absolute (a relative --db names this
|
|
7547
|
+
// directory), single-quoted (no ~ or $ expands), after `--` (a leading dash is never an option).
|
|
7548
|
+
const saved = approverPlan2.passwordFile === null ? null : resolve(approverPlan2.passwordFile);
|
|
7549
|
+
const showCommand = saved === null ? null : `cat -- '${saved.replaceAll("'", "'\\''")}'`;
|
|
7498
7550
|
const signInWords = account === null
|
|
7499
7551
|
? `sign in as one of ${approverPlan2.approvers.join(", ")}`
|
|
7500
|
-
:
|
|
7552
|
+
: showCommand === null ? `sign in as ${account} with your password` : `sign in as ${account}`;
|
|
7501
7553
|
write("");
|
|
7502
7554
|
write(`Toolroll is on ${url}`);
|
|
7503
7555
|
if (link !== null) {
|
|
@@ -7507,8 +7559,11 @@ async function upCommand(flags, context) {
|
|
|
7507
7559
|
}
|
|
7508
7560
|
else {
|
|
7509
7561
|
write(`${signInWords.charAt(0).toUpperCase()}${signInWords.slice(1)}.`);
|
|
7510
|
-
write("Ctrl-C stops it.");
|
|
7511
7562
|
}
|
|
7563
|
+
if (showCommand !== null)
|
|
7564
|
+
write(`Your password: run \`${showCommand}\``);
|
|
7565
|
+
if (link === null)
|
|
7566
|
+
write("Ctrl-C stops it.");
|
|
7512
7567
|
}
|
|
7513
7568
|
}
|
|
7514
7569
|
// A bounded run (--for) counts from readiness, so a slow start never eats the window it asked for.
|
|
@@ -11744,7 +11799,7 @@ function upNeedsOneProcess(options) {
|
|
|
11744
11799
|
options.flowEveryMs !== undefined || options.platform !== undefined || options.fenceAvailable !== undefined || options.flowFetch !== undefined ||
|
|
11745
11800
|
options.integrationIo !== undefined || options.shouldStop !== undefined || options.leadSeams !== undefined || options.waitSleep !== undefined ||
|
|
11746
11801
|
options.releaseIo !== undefined || options.installBin !== undefined || options.tempRoots !== undefined || options.filingSizer !== undefined ||
|
|
11747
|
-
options.now !== undefined;
|
|
11802
|
+
options.now !== undefined || options.folderLook !== undefined;
|
|
11748
11803
|
}
|
|
11749
11804
|
/** The entry a role child runs: the built sibling when installed, the TypeScript source through tsx in a checkout. */
|
|
11750
11805
|
/**
|
|
@@ -12688,3 +12743,18 @@ export function upNarrowDeferred(held, reported) {
|
|
|
12688
12743
|
const kept = new Set(reported);
|
|
12689
12744
|
return held.filter(one => kept.has(one));
|
|
12690
12745
|
}
|
|
12746
|
+
/** Resolve the console's project identities after it is listening, never before: the deploy's health check and every
|
|
12747
|
+
* page answer at once, and each project shows up as its folder answers (project-identity.ts `refresh`, its looks a
|
|
12748
|
+
* few at a time under their deadline). Nothing waits on it; a pass that fails leaves those paths unresolved, which
|
|
12749
|
+
* fails closed, for the background pass to try again. */
|
|
12750
|
+
export function consoleIdentitiesInBackground(identities, paths) {
|
|
12751
|
+
void identities.refresh(paths).catch(() => false);
|
|
12752
|
+
}
|
|
12753
|
+
/** `config set` and `config clear` on the command line, through Settings → Models' one write path (model-choices.ts,
|
|
12754
|
+
* docs/plans/spend-dashboard.md slice 3), under the same target and resulting-configuration checks as the editor: the
|
|
12755
|
+
* person proved themselves with their own password or login at `proved.generation`; a machine credential (a remote
|
|
12756
|
+
* token, the lead's token) never changes which models run. The write re-proves them inside its transaction: still an
|
|
12757
|
+
* unrestricted instance operator, at that very generation, by a proof that still stands. */
|
|
12758
|
+
export function writeModelRow(store, proved, scope, knownProject, slot, to, now, extra = []) {
|
|
12759
|
+
return applyModelChoices(store, { scope, change: slot === null ? [] : [{ slot, to }], expected: null, strict: false, knownProject, extra }, { ...proved, live: () => true }, now);
|
|
12760
|
+
}
|
|
@@ -21,7 +21,7 @@ export type AdmissionActor = {
|
|
|
21
21
|
};
|
|
22
22
|
export type AdmissionFailure = {
|
|
23
23
|
ok: false;
|
|
24
|
-
reason: "unavailable" | "not-git" | "locked" | "registry" | "abandoned" | "failed";
|
|
24
|
+
reason: "unavailable" | "not-git" | "locked" | "registry" | "abandoned" | "failed" | "moved";
|
|
25
25
|
message: string;
|
|
26
26
|
};
|
|
27
27
|
/** `remote`: the origin URL this add recorded as the project's trusted remote (null: none), and why one wasn't. */
|
|
@@ -60,6 +60,9 @@ export declare function admitProject(store: Store, input: {
|
|
|
60
60
|
captureOrigin?: boolean;
|
|
61
61
|
/** Explicit value from the owner/instance-operator CLI boundary. */
|
|
62
62
|
remoteUrl?: string;
|
|
63
|
+
/** The caller's authority, asked again about the proved repo inside the locked registry update and inside the
|
|
64
|
+
* database transaction (the console: nothing it proved before an await moved, and the repo is still visible). */
|
|
65
|
+
stillAllowed?: (repo: string) => boolean;
|
|
63
66
|
}): Promise<Admitted | AdmissionFailure>;
|
|
64
67
|
/**
|
|
65
68
|
* Remove one project from the lists and the builder. Its tasks, results and
|
|
@@ -61,13 +61,29 @@ export async function admitProject(store, input) {
|
|
|
61
61
|
? await originOf(repo, input.exec ?? run) : null;
|
|
62
62
|
let remote = { url: store.projectRemote(repo), why: null };
|
|
63
63
|
let registryHad = true;
|
|
64
|
+
const allowed = () => input.stillAllowed === undefined || input.stillAllowed(repo);
|
|
65
|
+
const moved = { ok: false, reason: "moved", message: `${projectName(repo)} wasn't added: a project folder changed while it was being checked. Try again.` };
|
|
64
66
|
if (input.registryFile !== null) {
|
|
65
|
-
|
|
67
|
+
let refused = false;
|
|
68
|
+
let had = false;
|
|
69
|
+
const enrolled = await updateRepos(input.registryFile, repos => {
|
|
70
|
+
had = repos.includes(repo);
|
|
71
|
+
if (!allowed()) {
|
|
72
|
+
refused = true;
|
|
73
|
+
return repos;
|
|
74
|
+
}
|
|
75
|
+
return addRepos(repos, [repo]);
|
|
76
|
+
});
|
|
66
77
|
if (!enrolled.ok)
|
|
67
78
|
return { ok: false, reason: enrolled.reason, message: `${projectName(repo)} wasn't added: ${enrolled.message}` };
|
|
68
|
-
|
|
79
|
+
if (refused)
|
|
80
|
+
return moved;
|
|
81
|
+
registryHad = had;
|
|
69
82
|
}
|
|
70
83
|
const added = store.transact(() => {
|
|
84
|
+
// Re-asked with the write; a refusal writes nothing here and takes back the registry line this call added below.
|
|
85
|
+
if (!allowed())
|
|
86
|
+
return null;
|
|
71
87
|
const rowHad = store.listProjects().some(one => one.path === repo);
|
|
72
88
|
const previous = store.projectRemote(repo);
|
|
73
89
|
const first = !known && !store.projectAdmissionKnown(repo);
|
|
@@ -81,6 +97,11 @@ export async function admitProject(store, input) {
|
|
|
81
97
|
store.recordAction({ at: input.now.toISOString(), actor: input.actor.label, repo, taskId: null, runId: null, action: PROJECT_ADDED, outcome: "added", source: "access", detail: `from ${input.actor.origin}` });
|
|
82
98
|
return true;
|
|
83
99
|
});
|
|
100
|
+
if (added === null) {
|
|
101
|
+
if (input.registryFile !== null && !registryHad)
|
|
102
|
+
await updateRepos(input.registryFile, repos => removeRepos(repos, [repo]));
|
|
103
|
+
return moved;
|
|
104
|
+
}
|
|
84
105
|
return { ok: true, repo, name: projectName(repo), added, remote };
|
|
85
106
|
}
|
|
86
107
|
/**
|