toolroll 0.9.83 → 0.9.85
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 +69 -0
- package/README.md +94 -1192
- package/dist/agent-fence.d.ts +3 -3
- package/dist/agent-fence.js +3 -3
- package/dist/agent-onboard.d.ts +2 -0
- package/dist/agent-onboard.js +3 -0
- package/dist/assignment-ui.js +5 -1
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +31 -31
- package/dist/browser-crew.js +5 -1
- package/dist/browser-workspace.d.ts +12 -0
- package/dist/chat-channel.d.ts +10 -1
- package/dist/chat-channel.js +35 -5
- package/dist/chat-decide.d.ts +16 -4
- package/dist/chat-decide.js +239 -31
- package/dist/chat-messages.d.ts +5 -0
- package/dist/chat-messages.js +1 -1
- package/dist/chat-quiet.js +3 -2
- package/dist/chat-review.d.ts +4 -0
- package/dist/chat-review.js +13 -4
- package/dist/chat-terms.d.ts +9 -0
- package/dist/chat-terms.js +46 -0
- package/dist/contracts/console-api.d.ts +4 -2
- package/dist/contracts/console-api.js +7 -2
- package/dist/contracts/lead-tools.d.ts +30 -0
- package/dist/contracts/lead-tools.js +3 -0
- package/dist/contracts/youiyoux-request.d.ts +248 -0
- package/dist/contracts/youiyoux-request.js +152 -0
- 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/flow-cadence.d.ts +8 -2
- package/dist/flow-cadence.js +16 -3
- package/dist/flow-card-task.d.ts +9 -0
- package/dist/flow-card-task.js +15 -6
- package/dist/folder-look.d.ts +3 -0
- package/dist/folder-look.js +8 -4
- package/dist/guides.js +4 -3
- package/dist/integrations-ui.js +9 -4
- package/dist/integrations.d.ts +9 -1
- package/dist/integrations.js +43 -2
- package/dist/landed-cleanup.d.ts +4 -0
- package/dist/landed-cleanup.js +3 -2
- package/dist/lead-capabilities.js +2 -1
- package/dist/lead-cli.js +3 -1
- package/dist/lead-tools.d.ts +2 -0
- package/dist/lead-tools.js +30 -4
- package/dist/lead.d.ts +2 -0
- package/dist/lead.js +3 -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 +22 -2
- package/dist/operate.js +107 -41
- package/dist/project-activity.d.ts +20 -0
- package/dist/project-activity.js +23 -0
- package/dist/project-admission.d.ts +4 -1
- package/dist/project-admission.js +23 -2
- package/dist/project-cli.d.ts +1 -0
- package/dist/project-cli.js +36 -19
- package/dist/project-identity.d.ts +9 -2
- package/dist/project-identity.js +80 -33
- package/dist/project-knowledge.d.ts +9 -9
- package/dist/project-outcomes.d.ts +119 -0
- package/dist/project-outcomes.js +79 -0
- package/dist/result-actions.d.ts +4 -0
- package/dist/result-actions.js +41 -9
- package/dist/result-review.d.ts +9 -0
- package/dist/result-review.js +45 -5
- 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 +17 -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 +53 -7
- package/dist/server/people-tokens.js +40 -20
- package/dist/server/render-chat.js +3 -2
- package/dist/server/render-pages.d.ts +12 -2
- package/dist/server/render-pages.js +36 -16
- package/dist/server/render-people.d.ts +14 -2
- package/dist/server/render-people.js +35 -12
- package/dist/server/render-results.d.ts +2 -0
- package/dist/server/render-results.js +20 -7
- package/dist/server/render-settings.js +1 -0
- package/dist/server/render-tasks.d.ts +1 -0
- package/dist/server/render-tasks.js +22 -19
- package/dist/server/request-context.d.ts +2 -0
- package/dist/server/route-table.js +9 -1
- package/dist/server/settings.d.ts +58 -1
- package/dist/server/settings.js +216 -70
- package/dist/server/signup-gate.d.ts +9 -0
- package/dist/server/signup-gate.js +40 -0
- package/dist/server/task-views.js +6 -0
- package/dist/server/tasks.js +54 -8
- 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/store.d.ts +46 -3
- package/dist/store.js +165 -6
- package/dist/task-sizing.d.ts +5 -3
- package/dist/task-sizing.js +4 -4
- package/dist/task-source.d.ts +13 -1
- package/dist/task-source.js +17 -1
- package/dist/telegram-lead.js +7 -5
- package/dist/telegram.js +1 -1
- package/dist/toolroll-update.js +2 -2
- package/dist/webhooks.d.ts +6 -0
- package/dist/webhooks.js +25 -0
- package/dist/work-index.d.ts +4 -0
- package/dist/work-index.js +32 -0
- package/dist/youiyoux-intake.d.ts +234 -0
- package/dist/youiyoux-intake.js +908 -0
- package/package.json +11 -8
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,8 +96,10 @@ 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
|
-
export type OnboardSeams = Partial<Pick<OnboardIo, "home" | "env" | "cwd" | "interactive" | "confirm" | "findRepo" | "checkConnection" | "pullRequests">>;
|
|
102
|
+
export type OnboardSeams = Partial<Pick<OnboardIo, "home" | "env" | "cwd" | "interactive" | "confirm" | "findRepo" | "checkConnection" | "pullRequests" | "youiyoux">>;
|
|
99
103
|
export declare const OPERATE_HELP: string;
|
|
100
104
|
/** Parsed flags, with the positionals left over. */
|
|
101
105
|
type Args = {
|
|
@@ -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. */
|
|
@@ -231,7 +237,6 @@ type Context = {
|
|
|
231
237
|
export declare function writeLoginFileDurably(path: string, name: string, password: string): void;
|
|
232
238
|
/** The platform browser opener — detached, silent, never load-bearing. */
|
|
233
239
|
export declare function openBrowser(url: string): void;
|
|
234
|
-
export declare function readUpConsoleUrl(dir: string): string | null;
|
|
235
240
|
/**
|
|
236
241
|
* The people allowed to agree to a scope.
|
|
237
242
|
*
|
|
@@ -303,3 +308,18 @@ export declare function upProveSaved(gitRun: (file: string, args: readonly strin
|
|
|
303
308
|
/** What `up` hands its next worker after the worker reports which saved projects it still holds start authority for:
|
|
304
309
|
* only what both still hold. A worker's report can give up an authority, never add or give back one. */
|
|
305
310
|
export declare function upNarrowDeferred(held: readonly string[], reported: readonly string[]): string[];
|
|
311
|
+
/** Resolve the console's project identities after it is listening, never before: the deploy's health check and every
|
|
312
|
+
* page answer at once, and each project shows up as its folder answers (project-identity.ts `refresh`, its looks a
|
|
313
|
+
* few at a time under their deadline). Nothing waits on it; a pass that fails leaves those paths unresolved, which
|
|
314
|
+
* fails closed, for the background pass to try again. */
|
|
315
|
+
export declare function consoleIdentitiesInBackground(identities: ProjectIdentities, paths: readonly string[]): void;
|
|
316
|
+
/** `config set` and `config clear` on the command line, through Settings → Models' one write path (model-choices.ts,
|
|
317
|
+
* docs/plans/spend-dashboard.md slice 3), under the same target and resulting-configuration checks as the editor: the
|
|
318
|
+
* person proved themselves with their own password or login at `proved.generation`; a machine credential (a remote
|
|
319
|
+
* token, the lead's token) never changes which models run. The write re-proves them inside its transaction: still an
|
|
320
|
+
* unrestricted instance operator, at that very generation, by a proof that still stands. */
|
|
321
|
+
export declare function writeModelRow(store: Store, proved: {
|
|
322
|
+
actor: string;
|
|
323
|
+
generation: number;
|
|
324
|
+
method: string;
|
|
325
|
+
}, scope: string, knownProject: (repo: string) => boolean, slot: Slot | null, to: ModelChange["to"], now: Date, extra?: readonly ExtraWrite[]): ApplyResult;
|
package/dist/operate.js
CHANGED
|
@@ -54,6 +54,7 @@ import { packageVersion, startMonitoring, targetOf } from "./monitoring.js";
|
|
|
54
54
|
import { origin, readMonitoring } from "./monitoring-settings.js";
|
|
55
55
|
import { logEvent } from "./log.js";
|
|
56
56
|
import { FLOW_EVERY_MS, flowHousekeeping, moveCards, moveCardsAfter } from "./flow-cadence.js";
|
|
57
|
+
import { NO_YOUIYOUX_PASS, youiyouxOnboardLine } from "./youiyoux-intake.js";
|
|
57
58
|
import { readHooksBase, scheduleFromWords } from "./flow-triggers.js";
|
|
58
59
|
import { sendSubagentSummaries } from "./subagent-admin.js";
|
|
59
60
|
import { runRequestedUndos, sendSubagentWeeklies } from "./subagent-week.js";
|
|
@@ -222,11 +223,12 @@ import { fileTaskProposal, validateTaskText } from "./proposal.js";
|
|
|
222
223
|
import { TEMPLATES, templateByName } from "./templates.js";
|
|
223
224
|
import { isDirectChatProvider, isSubscriptionChatProvider, priceOf, PRICED_MODELS } from "./converse.js";
|
|
224
225
|
import { resolvePhaseAgent, resolveScopeProfile, resolveRouteCandidates, routeOfTask, INSTALLATION_SCOPE } from "./agentconfig.js";
|
|
226
|
+
import { applyModelChoices, stateDigest } from "./model-choices.js";
|
|
225
227
|
import { isTaskSize, legOf, projectRoute, routeDigestOf, routeWords, TASK_SIZES, PHASES as ROUTE_PHASES } from "./phase-routing.js";
|
|
226
228
|
import { observeProviderReadiness, reportProviderReadinessAuthed } from "./runner.js";
|
|
227
229
|
import { parseDemoUrl, projectDemoUrl, saveProjectDemo } from "./project-demo.js";
|
|
228
230
|
import { effectiveConcurrency, maySlotTake, parseProjectConcurrency, PROJECT_CONCURRENCY_DEFAULT, ProjectPasses, projectConcurrency, saveProjectConcurrency, savedProjectConcurrency } from "./project-concurrency.js";
|
|
229
|
-
import { activeWebhookTargets, webhookPass, LEGACY_WEBHOOK_WARNING, loadWebhookTargets, effectivePrimary, isMessagingChannel, loadConsoleUrl, phoneOrigin, saveConsoleUrl, savePrimary } from "./webhooks.js";
|
|
231
|
+
import { activeWebhookTargets, webhookPass, LEGACY_WEBHOOK_WARNING, loadWebhookTargets, effectivePrimary, isMessagingChannel, loadConsoleUrl, phoneOrigin, readUpConsoleUrl, saveConsoleUrl, savePrimary, UP_CONSOLE_FILE } from "./webhooks.js";
|
|
230
232
|
import { auditOf, inspectionOf, isProviderId, MONEY_CAPABILITIES, PROVIDER_IDS, validModelId, validateSpec, ALL_CREDENTIAL_ENV } from "./provider.js";
|
|
231
233
|
import { attestProvider, attestationOf, versionInRange } from "./attest.js";
|
|
232
234
|
import { build, proveApprovedProfile, } from "./builder.js";
|
|
@@ -277,6 +279,7 @@ export const OPERATE_HELP = `toolroll — operating the queue
|
|
|
277
279
|
wait for this attempt to finish or need a person
|
|
278
280
|
toolroll project use <path> remember a saved project (optional --token-file)
|
|
279
281
|
toolroll project show show the current project and credential reference
|
|
282
|
+
toolroll project activity --json read the current project supervision report
|
|
280
283
|
toolroll project rules --repo <p> a project's approval rules; an instance operator changes them with
|
|
281
284
|
--not-requester on|off, --protect none|project, --protect-paths "a/**,b/**"
|
|
282
285
|
toolroll project network --repo <p> [--allow h,…] [--remove h,…] [--open|--allowlist] [--local any|none]
|
|
@@ -967,6 +970,7 @@ async function dispatchOn(command, positional, flags, repoList, base, options, e
|
|
|
967
970
|
...(options.onboardSeams === undefined ? {} : { onboardSeams: options.onboardSeams }),
|
|
968
971
|
...(options.upSeams === undefined ? {} : { upSeams: options.upSeams }),
|
|
969
972
|
...(options.upRole === undefined ? {} : { upRole: options.upRole }),
|
|
973
|
+
...(options.folderLook === undefined ? {} : { folderLook: options.folderLook }),
|
|
970
974
|
...(upNeedsOneProcess(options) ? { upOneProcess: true } : {}),
|
|
971
975
|
...(activeRemote() === null ? {} : { principal: activeRemote() }),
|
|
972
976
|
});
|
|
@@ -2257,6 +2261,7 @@ function flowIoOf(context, pool, git, base) {
|
|
|
2257
2261
|
dir: context.flowStepIo?.dir ?? dir, scratch: context.flowStepIo?.scratch ?? join(pool, "flow-checks"), base,
|
|
2258
2262
|
...(context.evidenceRoot === undefined ? {} : { evidenceRoot: context.evidenceRoot }) },
|
|
2259
2263
|
...(context.evidenceRoot === undefined ? {} : { evidenceRoot: context.evidenceRoot }),
|
|
2264
|
+
youiyouxGit: gitOf(context.gitRunner),
|
|
2260
2265
|
};
|
|
2261
2266
|
}
|
|
2262
2267
|
/**
|
|
@@ -3382,8 +3387,10 @@ async function tickCommand(flags, context) {
|
|
|
3382
3387
|
syncAssignmentHandoffs(store, clock(), auth.runner.repos, context.evidenceRoot);
|
|
3383
3388
|
settleFinished();
|
|
3384
3389
|
const replied = replyPass.taken > 0 || replyPass.problem !== null;
|
|
3385
|
-
const
|
|
3386
|
-
|
|
3390
|
+
const requestPass = housekeeping?.youiyoux ?? NO_YOUIYOUX_PASS;
|
|
3391
|
+
const requested = requestPass.filed + requestPass.copied + requestPass.decided + requestPass.problems.length > 0;
|
|
3392
|
+
const flows = flowPass.moved + flowPass.filed.length + flowPass.problems.length + triggerPass.added + triggerPass.problems.length + stepPass.ran + stepPass.problems.length === 0 && !replied && !requested ? {} : { flows: { ...flowPass,
|
|
3393
|
+
...(triggerPass.added + triggerPass.problems.length === 0 ? {} : { triggers: triggerPass }), ...(requested ? { youiyoux: requestPass } : {}), ...(stepPass.ran + stepPass.problems.length === 0 ? {} : { steps: stepPass }), ...(replied ? { replies: replyPass } : {}) } };
|
|
3387
3394
|
const summary = () => {
|
|
3388
3395
|
const lines = [`Considered ${considered}, built ${built}, parked ${parked}, broke ${broke}.`];
|
|
3389
3396
|
for (const entry of dispatched) {
|
|
@@ -3941,11 +3948,11 @@ async function startConsole(options) {
|
|
|
3941
3948
|
const { context } = options;
|
|
3942
3949
|
// A git incident (teardown-git.ts) raises an attention notification in this console's outbox.
|
|
3943
3950
|
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
|
-
|
|
3951
|
+
// Every project folder this console starts with is resolved off its thread, under a deadline, after it is listening
|
|
3952
|
+
// (consoleIdentitiesInBackground): the console answers at once, and a project shows up once its folder answers. Until
|
|
3953
|
+
// then it is unresolved and kept restrictive; one that doesn't answer (a drive away, an iCloud folder macOS is
|
|
3954
|
+
// holding) is looked at again by the background pass (project-identity.ts).
|
|
3955
|
+
const identities = projectIdentities(context.folderLook);
|
|
3949
3956
|
const server = createDecisionServer({
|
|
3950
3957
|
identities,
|
|
3951
3958
|
store: context.store,
|
|
@@ -3982,6 +3989,7 @@ async function startConsole(options) {
|
|
|
3982
3989
|
}).catch(error => {
|
|
3983
3990
|
throw new Error(`could not listen on ${options.host}:${options.port} — ${describe(error)}`);
|
|
3984
3991
|
});
|
|
3992
|
+
consoleIdentitiesInBackground(identities, [...options.repos ?? [], ...options.projectRoots ?? [], ...context.store.listProjects().map(one => one.path), ...context.store.knownRepos()]);
|
|
3985
3993
|
// v104: monitoring runs beside the console (every setup has one): one loop sends the audit stream and traces where
|
|
3986
3994
|
// Settings → Monitoring says, and stops when the console does. A lease keeps a second process from sending too.
|
|
3987
3995
|
const stopMonitoring = startMonitoring({
|
|
@@ -5042,6 +5050,9 @@ async function configCommand(positional, flags, context) {
|
|
|
5042
5050
|
if (phase === undefined || !["plan", "build", "repair", "review"].includes(phase))
|
|
5043
5051
|
return fail(write, json, `config ${action}`, "usage", "which phase? plan, build, repair, review — or chat", EXIT.usage);
|
|
5044
5052
|
const acting = await askCredentials(flags, context);
|
|
5053
|
+
// The credential generation the proof below is checked against, read BEFORE it: a rotation after this read makes
|
|
5054
|
+
// the write refuse (model-choices.ts re-checks it inside the transaction), never borrow the new generation.
|
|
5055
|
+
const provedGeneration = acting === null ? -1 : store.accountOf(acting.name)?.generation ?? -1;
|
|
5045
5056
|
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
5046
5057
|
if (acting === null || !verified?.ok)
|
|
5047
5058
|
return refuseApprover(context, `config ${action}`, verified, "changing spend routing takes `--as <you> --token <t>`", phase);
|
|
@@ -5057,6 +5068,18 @@ async function configCommand(positional, flags, context) {
|
|
|
5057
5068
|
// Clearing one left from before stays possible.
|
|
5058
5069
|
if (tierGiven === "light" && phase !== "build" && !(action === "clear" && phase === "plan" && !flag(flags, "also")))
|
|
5059
5070
|
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);
|
|
5071
|
+
// Every row below — plain, tier, `--also`, and a leftover light planner's removal — is written through the one write
|
|
5072
|
+
// path (model-choices.ts): one transaction, the actor re-proved as an instance operator at the credential generation
|
|
5073
|
+
// proved above by a proof that still stands, the target and the resulting configuration checked, every row ledgered.
|
|
5074
|
+
// A project named with --repo must be one Toolroll knows (the editor's own rule).
|
|
5075
|
+
const projectKnown = repoGiven === undefined || (await knownProject(context, repoGiven, { listed: true, canonical: true })) !== undefined;
|
|
5076
|
+
const proof = { actor: acting.name, generation: provedGeneration, method: verified.method };
|
|
5077
|
+
const written = (slot, to, extra = []) => writeModelRow(store, proof, scope, () => projectKnown, slot, to, clock(), extra);
|
|
5078
|
+
// A configuration still missing a role's model is saved, and said (JSON: `incomplete`).
|
|
5079
|
+
const said = (command, result, payload, lines) => {
|
|
5080
|
+
const incomplete = result.ok ? result.unresolved : null;
|
|
5081
|
+
return succeed(write, json, command, { ...payload, ...(incomplete === null ? {} : { incomplete }) }, () => [...lines(), ...(incomplete === null ? [] : [` ! new tasks can't be filed here yet: ${incomplete}`])]);
|
|
5082
|
+
};
|
|
5060
5083
|
// Another provider's candidate on the same tier (`--also`): one per provider, and at filing the one whose plan
|
|
5061
5084
|
// has more room runs.
|
|
5062
5085
|
if (flag(flags, "also")) {
|
|
@@ -5067,8 +5090,11 @@ async function configCommand(positional, flags, context) {
|
|
|
5067
5090
|
if (alsoProvider !== undefined && !isProviderId(alsoProvider))
|
|
5068
5091
|
return fail(write, json, `config ${action}`, "usage", `--provider is one of ${PROVIDER_IDS.join(", ")}`, EXIT.usage);
|
|
5069
5092
|
if (action === "clear") {
|
|
5070
|
-
const
|
|
5071
|
-
|
|
5093
|
+
const alsoClear = written(null, null, [{ kind: "also-clear", phase, tier, provider: alsoProvider ?? null }]);
|
|
5094
|
+
if (!alsoClear.ok)
|
|
5095
|
+
return fail(write, json, "config clear", alsoClear.reason, alsoClear.problem, EXIT.refused);
|
|
5096
|
+
const removed = alsoClear.extra;
|
|
5097
|
+
return said("config clear", alsoClear, { scope, phase, tier, also: true, provider: alsoProvider ?? null, cleared: removed }, () => [
|
|
5072
5098
|
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
5099
|
]);
|
|
5074
5100
|
}
|
|
@@ -5078,10 +5104,12 @@ async function configCommand(positional, flags, context) {
|
|
|
5078
5104
|
const alsoValid = validateSpec({ provider: alsoProvider, model: alsoModel });
|
|
5079
5105
|
if (!alsoValid.ok)
|
|
5080
5106
|
return fail(write, json, "config set", "invalid", alsoValid.problem, EXIT.usage);
|
|
5081
|
-
|
|
5107
|
+
const alsoSet = written(null, null, [{ kind: "also-set", phase, tier, provider: alsoProvider, model: alsoModel }]);
|
|
5108
|
+
if (!alsoSet.ok)
|
|
5109
|
+
return fail(write, json, "config set", alsoSet.reason, alsoSet.problem, EXIT.refused);
|
|
5082
5110
|
const resolvedNow = resolveRouteCandidates(store, scope === INSTALLATION_SCOPE ? null : scope);
|
|
5083
5111
|
const own = !resolvedNow.ok ? null : tier === "routine" ? resolvedNow.candidates[phase].routine : tier === "light" ? resolvedNow.candidates[phase].light ?? null : resolvedNow.candidates[phase].strong;
|
|
5084
|
-
return
|
|
5112
|
+
return said("config set", alsoSet, { scope, phase, tier, also: true, provider: alsoProvider, model: alsoModel }, () => [
|
|
5085
5113
|
`${tier} ${phase} at ${scope === INSTALLATION_SCOPE ? "the installation" : scope} can also run ${alsoProvider} · ${alsoModel}, set by ${acting.name}.`,
|
|
5086
5114
|
own !== null && own.provider === alsoProvider && !own.source.includes("also")
|
|
5087
5115
|
? ` ! the ${tier} ${phase} agent already runs ${alsoProvider} (${own.model}) — this one is not used while it does.`
|
|
@@ -5089,20 +5117,39 @@ async function configCommand(positional, flags, context) {
|
|
|
5089
5117
|
]);
|
|
5090
5118
|
}
|
|
5091
5119
|
if (action === "clear") {
|
|
5120
|
+
if (tierGiven === "light" && phase === "build") {
|
|
5121
|
+
const lightWrite = written({ phase, tier: "light" }, null);
|
|
5122
|
+
if (!lightWrite.ok)
|
|
5123
|
+
return fail(write, json, "config clear", lightWrite.reason, lightWrite.problem, EXIT.refused);
|
|
5124
|
+
const clearedLight = lightWrite.diff.length > 0;
|
|
5125
|
+
return said("config clear", lightWrite, { scope, phase, tier: "light", cleared: clearedLight }, () => [
|
|
5126
|
+
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}.`,
|
|
5127
|
+
]);
|
|
5128
|
+
}
|
|
5092
5129
|
if (tierGiven === "light") {
|
|
5093
|
-
|
|
5094
|
-
|
|
5130
|
+
// A light planner from before light became build-only: removed here, never written.
|
|
5131
|
+
const legacyClear = written(null, null, [{ kind: "legacy-light-clear", phase: phase }]);
|
|
5132
|
+
if (!legacyClear.ok)
|
|
5133
|
+
return fail(write, json, "config clear", legacyClear.reason, legacyClear.problem, EXIT.refused);
|
|
5134
|
+
const clearedLight = legacyClear.extra > 0;
|
|
5135
|
+
return said("config clear", legacyClear, { scope, phase, tier: "light", cleared: clearedLight }, () => [
|
|
5095
5136
|
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
5137
|
]);
|
|
5097
5138
|
}
|
|
5098
5139
|
if (tierGiven === "strong") {
|
|
5099
|
-
const
|
|
5100
|
-
|
|
5140
|
+
const strongWrite = written({ phase: phase, tier: "strong" }, null);
|
|
5141
|
+
if (!strongWrite.ok)
|
|
5142
|
+
return fail(write, json, "config clear", strongWrite.reason, strongWrite.problem, EXIT.refused);
|
|
5143
|
+
const clearedStrong = strongWrite.diff.length > 0;
|
|
5144
|
+
return said("config clear", strongWrite, { scope, phase, tier: "strong", cleared: clearedStrong }, () => [
|
|
5101
5145
|
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
5146
|
]);
|
|
5103
5147
|
}
|
|
5104
|
-
const
|
|
5105
|
-
|
|
5148
|
+
const routineWrite = written({ phase: phase, tier: "routine" }, null);
|
|
5149
|
+
if (!routineWrite.ok)
|
|
5150
|
+
return fail(write, json, "config clear", routineWrite.reason, routineWrite.problem, EXIT.refused);
|
|
5151
|
+
const cleared = routineWrite.diff.length > 0;
|
|
5152
|
+
return said("config clear", routineWrite, { scope, phase, cleared }, () => [
|
|
5106
5153
|
cleared ? `Cleared ${phase} at ${scope} — it resolves one layer down now.` : `Nothing was configured for ${phase} at ${scope}.`,
|
|
5107
5154
|
]);
|
|
5108
5155
|
}
|
|
@@ -5116,8 +5163,10 @@ async function configCommand(positional, flags, context) {
|
|
|
5116
5163
|
if (tierGiven === "light") {
|
|
5117
5164
|
if (modelGiven === null)
|
|
5118
5165
|
return fail(write, json, "config set", "usage", `a light ${phase} agent names an exact --model — approvals bind exact routing`, EXIT.usage);
|
|
5119
|
-
|
|
5120
|
-
|
|
5166
|
+
const lightSet = written({ phase: phase, tier: "light" }, { provider: providerGiven, model: modelGiven });
|
|
5167
|
+
if (!lightSet.ok)
|
|
5168
|
+
return fail(write, json, "config set", lightSet.reason, lightSet.problem, EXIT.refused);
|
|
5169
|
+
return said("config set", lightSet, { scope, phase, tier: "light", provider: providerGiven, model: modelGiven }, () => [
|
|
5121
5170
|
`light ${phase} at ${scope === INSTALLATION_SCOPE ? "the installation" : scope} is ${providerGiven} · ${modelGiven}, set by ${acting.name}.`,
|
|
5122
5171
|
" small changes filed from now on build on it with no plan; sealed routes are untouched.",
|
|
5123
5172
|
]);
|
|
@@ -5127,14 +5176,19 @@ async function configCommand(positional, flags, context) {
|
|
|
5127
5176
|
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
5177
|
if (modelGiven === null)
|
|
5129
5178
|
return fail(write, json, "config set", "usage", `a strong ${phase} agent names an exact --model — approvals bind exact routing`, EXIT.usage);
|
|
5130
|
-
|
|
5131
|
-
|
|
5179
|
+
const strongSet = written({ phase: phase, tier: "strong" }, { provider: providerGiven, model: modelGiven });
|
|
5180
|
+
if (!strongSet.ok)
|
|
5181
|
+
return fail(write, json, "config set", strongSet.reason, strongSet.problem, EXIT.refused);
|
|
5182
|
+
return said("config set", strongSet, { scope, phase, tier: "strong", provider: providerGiven, model: modelGiven }, () => [
|
|
5132
5183
|
`strong ${phase} at ${scope === INSTALLATION_SCOPE ? "the installation" : scope} is ${providerGiven}${modelGiven === null ? "" : ` · ${modelGiven}`}, set by ${acting.name}.`,
|
|
5133
5184
|
" large or risky, strict, screenshot-proof, and automerge routes filed from now on reach for it; sealed routes are untouched.",
|
|
5134
5185
|
...(phase === "repair" ? [" a strong repair agent applies only when its provider is the build's — cross-provider repair does not exist."] : []),
|
|
5135
5186
|
]);
|
|
5136
5187
|
}
|
|
5137
|
-
|
|
5188
|
+
// A routine row may still name no model on the command line (said below); the editor never writes one.
|
|
5189
|
+
const routineSet = written({ phase: phase, tier: "routine" }, { provider: providerGiven, model: modelGiven });
|
|
5190
|
+
if (!routineSet.ok)
|
|
5191
|
+
return fail(write, json, "config set", routineSet.reason, routineSet.problem, EXIT.refused);
|
|
5138
5192
|
const warnings = [];
|
|
5139
5193
|
if (modelGiven === null) {
|
|
5140
5194
|
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 +5199,7 @@ async function configCommand(positional, flags, context) {
|
|
|
5145
5199
|
if (providerGiven !== "claude") {
|
|
5146
5200
|
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
5201
|
}
|
|
5148
|
-
return
|
|
5202
|
+
return said("config set", routineSet, { scope, phase, provider: providerGiven, model: modelGiven, warnings }, () => [
|
|
5149
5203
|
`${phase} at ${scope === INSTALLATION_SCOPE ? "the installation" : scope} now runs ${providerGiven}${modelGiven === null ? "" : ` · ${modelGiven}`}, set by ${acting.name}.`,
|
|
5150
5204
|
...warnings.map(one => ` ! ${one}`),
|
|
5151
5205
|
]);
|
|
@@ -6347,7 +6401,7 @@ async function runWatchLoop(args) {
|
|
|
6347
6401
|
base = head.stdout.trim();
|
|
6348
6402
|
}
|
|
6349
6403
|
const pass = await flowHousekeeping(store, repo, context.clock, flowIoOf(context, flowPool, flowGit, base), admissionStopped);
|
|
6350
|
-
flowsDid(pass.flows.moved + pass.triggers.added + pass.steps.ran + pass.replies.taken, pass.flows.filed.length, `flow pass — ${pass.triggers.added} card(s) added, ${pass.replies.taken} reply(ies) taken, ${pass.steps.ran} step(s) run, ${pass.flows.moved} moved`);
|
|
6404
|
+
flowsDid(pass.flows.moved + pass.triggers.added + pass.steps.ran + pass.replies.taken, pass.flows.filed.length + pass.youiyoux.filed, `flow pass — ${pass.triggers.added} card(s) added, ${pass.youiyoux.filed} youiyoux request(s) filed, ${pass.replies.taken} reply(ies) taken, ${pass.steps.ran} step(s) run, ${pass.flows.moved} moved`);
|
|
6351
6405
|
},
|
|
6352
6406
|
});
|
|
6353
6407
|
const modelWatch = startMaintenance({
|
|
@@ -6887,8 +6941,6 @@ export function openBrowser(url) {
|
|
|
6887
6941
|
// A browser that will not open is a URL the greeting already printed.
|
|
6888
6942
|
}
|
|
6889
6943
|
}
|
|
6890
|
-
/** Where `up` last served its console, kept beside the database so `onboard` names the same address. */
|
|
6891
|
-
const UP_CONSOLE_FILE = "up-console.json";
|
|
6892
6944
|
function recordUpConsoleUrl(dir, url, now) {
|
|
6893
6945
|
try {
|
|
6894
6946
|
const file = join(dir, UP_CONSOLE_FILE), temporary = `${file}.${process.pid}.tmp`;
|
|
@@ -6899,15 +6951,6 @@ function recordUpConsoleUrl(dir, url, now) {
|
|
|
6899
6951
|
// A display fact: a failed write only leaves onboard on the default port.
|
|
6900
6952
|
}
|
|
6901
6953
|
}
|
|
6902
|
-
export function readUpConsoleUrl(dir) {
|
|
6903
|
-
try {
|
|
6904
|
-
const parsed = JSON.parse(readFileSync(join(dir, UP_CONSOLE_FILE), "utf8"));
|
|
6905
|
-
return typeof parsed.url === "string" && /^https?:\/\/[^\s]+$/.test(parsed.url) ? parsed.url.replace(/\/+$/, "") : null;
|
|
6906
|
-
}
|
|
6907
|
-
catch {
|
|
6908
|
-
return null;
|
|
6909
|
-
}
|
|
6910
|
-
}
|
|
6911
6954
|
/** How onboard says to sign in: the saved login file only while it still works (as `up` advertises it), else the
|
|
6912
6955
|
* account and its password; null only when there is no account yet. Never the password itself. */
|
|
6913
6956
|
function onboardLogin(store, loginFile) {
|
|
@@ -6992,6 +7035,8 @@ async function onboardCommand(flags, context) {
|
|
|
6992
7035
|
return `off: ${checked.message}`;
|
|
6993
7036
|
return `off: turn on with toolroll publish setup --repo ${repo} --yes (asks your password), or in Settings → Projects`;
|
|
6994
7037
|
}),
|
|
7038
|
+
// A project that uses youiyoux has its change requests filed as tasks with no step of its own (youiyoux-intake.ts).
|
|
7039
|
+
youiyoux: seams.youiyoux ?? (async (repo) => youiyouxOnboardLine(store, repo)),
|
|
6995
7040
|
starterFlag: text(flags, "starter"),
|
|
6996
7041
|
// Starter flows belong to a person: the flow's decisions ask them. With no account yet, none can be switched on.
|
|
6997
7042
|
starters: {
|
|
@@ -7493,11 +7538,14 @@ async function upCommand(flags, context) {
|
|
|
7493
7538
|
pendingLinks.set(id, value => { clearTimeout(timer); resolveLink(value); });
|
|
7494
7539
|
consoleRole?.send({ type: "sign-in-link", id, account });
|
|
7495
7540
|
});
|
|
7496
|
-
|
|
7497
|
-
|
|
7541
|
+
// Every start says where the saved password is and the exact command that shows it, browser or not (Settings
|
|
7542
|
+
// revamp S4, D2). Only the path is printed, never what the file holds: absolute (a relative --db names this
|
|
7543
|
+
// directory), single-quoted (no ~ or $ expands), after `--` (a leading dash is never an option).
|
|
7544
|
+
const saved = approverPlan2.passwordFile === null ? null : resolve(approverPlan2.passwordFile);
|
|
7545
|
+
const showCommand = saved === null ? null : `cat -- '${saved.replaceAll("'", "'\\''")}'`;
|
|
7498
7546
|
const signInWords = account === null
|
|
7499
7547
|
? `sign in as one of ${approverPlan2.approvers.join(", ")}`
|
|
7500
|
-
:
|
|
7548
|
+
: showCommand === null ? `sign in as ${account} with your password` : `sign in as ${account}`;
|
|
7501
7549
|
write("");
|
|
7502
7550
|
write(`Toolroll is on ${url}`);
|
|
7503
7551
|
if (link !== null) {
|
|
@@ -7507,8 +7555,11 @@ async function upCommand(flags, context) {
|
|
|
7507
7555
|
}
|
|
7508
7556
|
else {
|
|
7509
7557
|
write(`${signInWords.charAt(0).toUpperCase()}${signInWords.slice(1)}.`);
|
|
7510
|
-
write("Ctrl-C stops it.");
|
|
7511
7558
|
}
|
|
7559
|
+
if (showCommand !== null)
|
|
7560
|
+
write(`Your password: run \`${showCommand}\``);
|
|
7561
|
+
if (link === null)
|
|
7562
|
+
write("Ctrl-C stops it.");
|
|
7512
7563
|
}
|
|
7513
7564
|
}
|
|
7514
7565
|
// A bounded run (--for) counts from readiness, so a slow start never eats the window it asked for.
|
|
@@ -11744,7 +11795,7 @@ function upNeedsOneProcess(options) {
|
|
|
11744
11795
|
options.flowEveryMs !== undefined || options.platform !== undefined || options.fenceAvailable !== undefined || options.flowFetch !== undefined ||
|
|
11745
11796
|
options.integrationIo !== undefined || options.shouldStop !== undefined || options.leadSeams !== undefined || options.waitSleep !== undefined ||
|
|
11746
11797
|
options.releaseIo !== undefined || options.installBin !== undefined || options.tempRoots !== undefined || options.filingSizer !== undefined ||
|
|
11747
|
-
options.now !== undefined;
|
|
11798
|
+
options.now !== undefined || options.folderLook !== undefined;
|
|
11748
11799
|
}
|
|
11749
11800
|
/** The entry a role child runs: the built sibling when installed, the TypeScript source through tsx in a checkout. */
|
|
11750
11801
|
/**
|
|
@@ -12688,3 +12739,18 @@ export function upNarrowDeferred(held, reported) {
|
|
|
12688
12739
|
const kept = new Set(reported);
|
|
12689
12740
|
return held.filter(one => kept.has(one));
|
|
12690
12741
|
}
|
|
12742
|
+
/** Resolve the console's project identities after it is listening, never before: the deploy's health check and every
|
|
12743
|
+
* page answer at once, and each project shows up as its folder answers (project-identity.ts `refresh`, its looks a
|
|
12744
|
+
* few at a time under their deadline). Nothing waits on it; a pass that fails leaves those paths unresolved, which
|
|
12745
|
+
* fails closed, for the background pass to try again. */
|
|
12746
|
+
export function consoleIdentitiesInBackground(identities, paths) {
|
|
12747
|
+
void identities.refresh(paths).catch(() => false);
|
|
12748
|
+
}
|
|
12749
|
+
/** `config set` and `config clear` on the command line, through Settings → Models' one write path (model-choices.ts,
|
|
12750
|
+
* docs/plans/spend-dashboard.md slice 3), under the same target and resulting-configuration checks as the editor: the
|
|
12751
|
+
* person proved themselves with their own password or login at `proved.generation`; a machine credential (a remote
|
|
12752
|
+
* token, the lead's token) never changes which models run. The write re-proves them inside its transaction: still an
|
|
12753
|
+
* unrestricted instance operator, at that very generation, by a proof that still stands. */
|
|
12754
|
+
export function writeModelRow(store, proved, scope, knownProject, slot, to, now, extra = []) {
|
|
12755
|
+
return applyModelChoices(store, { scope, change: slot === null ? [] : [{ slot, to }], expected: null, strict: false, knownProject, extra }, { ...proved, live: () => true }, now);
|
|
12756
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Shared presentation for browser and no-script Activity; facts come only
|
|
2
|
+
* from projectOutcomes. No alternate counting or status interpretation. */
|
|
3
|
+
import type { ActivityFamily, ProjectOutcomes } from './project-outcomes.js';
|
|
4
|
+
export declare const activityAge: (ms: number | null) => string;
|
|
5
|
+
export declare const activityMoney: (usd: number | null) => string;
|
|
6
|
+
export declare const ACTIVITY_ACTORS: {
|
|
7
|
+
human: string;
|
|
8
|
+
lead: string;
|
|
9
|
+
landed: string;
|
|
10
|
+
'machine-or-unknown': string;
|
|
11
|
+
};
|
|
12
|
+
export type ActivityMetric = {
|
|
13
|
+
label: string;
|
|
14
|
+
value: string;
|
|
15
|
+
detail: string;
|
|
16
|
+
rows: ActivityFamily[];
|
|
17
|
+
};
|
|
18
|
+
export declare const activityScope: (report: ProjectOutcomes) => string;
|
|
19
|
+
export declare const activityLimit: (report: ProjectOutcomes) => string;
|
|
20
|
+
export declare function activityMetrics(report: ProjectOutcomes): ActivityMetric[];
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export const activityAge = (ms) => ms === null ? 'Unknown' : ms < 3600000 ? `${Math.floor(ms / 60000)}m` : ms < 86400000 ? `${(ms / 3600000).toFixed(1)}h` : `${(ms / 86400000).toFixed(1)}d`;
|
|
2
|
+
export const activityMoney = (usd) => usd === null ? 'Unknown' : `$${usd.toFixed(2)}`;
|
|
3
|
+
export const ACTIVITY_ACTORS = { human: 'Human', lead: 'Lead', landed: 'Automatically landed', 'machine-or-unknown': 'Machine or unknown' };
|
|
4
|
+
export const activityScope = (report) => `Current Ready backlog; completions in the last ${report.days} days. Completion does not mean checks passed or work deployed.`;
|
|
5
|
+
export const activityLimit = (report) => `Partial report: only the ${report.limit} most recently updated eligible families are included. Counts and coverage below describe this sample.`;
|
|
6
|
+
export function activityMetrics(report) {
|
|
7
|
+
const ready = report.families.filter(one => one.state === 'ready');
|
|
8
|
+
const complete = report.families.filter(one => one.state === 'completed');
|
|
9
|
+
const metrics = [
|
|
10
|
+
{ label: 'Ready', value: report.ready.count === 0 ? '0' : `${report.ready.count} · Oldest ${activityAge(report.ready.oldestFinishedAgeMs)}`,
|
|
11
|
+
detail: `Age is measured from the latest result finishing, a proxy for Ready entry. ${report.ready.dated}/${report.ready.count} have usable timestamps.`, rows: ready },
|
|
12
|
+
{ label: 'Finished to decision', value: report.completed.count === 0 ? 'No completed families' : `${activityAge(report.finishedToDecision.meanMs)} average`,
|
|
13
|
+
detail: `${report.finishedToDecision.samples}/${report.completed.count} completed families have usable timestamps. Elapsed time to the saved completion decision, not human work time.`, rows: complete },
|
|
14
|
+
{ label: 'Explicit revisions', value: report.revisions.perCompletedFamily === null ? 'No completed families' : `${report.revisions.perCompletedFamily.toFixed(1)} per completed family`,
|
|
15
|
+
detail: `${report.revisions.count} revisions across ${report.revisions.samples} completed families. Retries are attempts, not revisions.`, rows: complete },
|
|
16
|
+
{ label: 'Recorded spend', value: report.completed.count === 0 ? 'No completed families' : `${activityMoney(report.cost.recordedPerCompletedFamilyUsd)} per completed family · ${report.cost.knownRuns}/${report.cost.runs} costs known`,
|
|
17
|
+
detail: `${activityMoney(report.cost.recordedUsd)} in API charges across ${report.cost.families} completed families; ${report.cost.fullyCostedFamilies}/${report.cost.families} fully costed.${report.cost.subscriptionRuns > 0 ? ` ${report.cost.subscriptionRuns} subscription runs are plan usage, not charges${report.cost.subscriptionReportedUsd === null ? '' : ` (${activityMoney(report.cost.subscriptionReportedUsd)} reported)`}.` : ''} Missing or unreported costs are unknown. All family attempts count once, including planning and retries.`, rows: complete },
|
|
18
|
+
{ label: 'Completed families', value: String(report.completed.count),
|
|
19
|
+
detail: `${Object.entries(report.completed.actors).map(([key, count]) => `${ACTIVITY_ACTORS[key]}: ${count}`).join(' · ')}. Scouts: ${report.completed.scouts}. Failed checks: ${report.completed.failedChecks}.`, rows: complete },
|
|
20
|
+
...(report.incompleteHistory > 0 ? [{ label: 'Incomplete history', value: String(report.incompleteHistory), detail: 'Excluded from completion averages; their family relationships cannot be established.', rows: report.families.filter(one => one.state === 'history-unavailable') }] : []),
|
|
21
|
+
];
|
|
22
|
+
return report.completed.count === 0 ? metrics.filter(one => one.label === 'Ready' || one.label === 'Completed families' || one.label === 'Incomplete history') : metrics;
|
|
23
|
+
}
|