toolroll 0.9.82 → 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.
Files changed (92) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +4 -3
  3. package/dist/assignment-ui.js +5 -1
  4. package/dist/browser/workspace.css +1 -1
  5. package/dist/browser/workspace.js +22 -22
  6. package/dist/browser-crew.js +5 -1
  7. package/dist/browser-workspace.d.ts +7 -0
  8. package/dist/contracts/console-api.d.ts +2 -1
  9. package/dist/contracts/console-api.js +5 -1
  10. package/dist/control-setup.d.ts +9 -0
  11. package/dist/control-setup.js +31 -4
  12. package/dist/control-ui.d.ts +7 -1
  13. package/dist/control-ui.js +3 -2
  14. package/dist/dispatch.d.ts +15 -0
  15. package/dist/dispatch.js +26 -4
  16. package/dist/first-run.d.ts +5 -2
  17. package/dist/first-run.js +13 -2
  18. package/dist/first-tasks-child.d.ts +1 -0
  19. package/dist/first-tasks-child.js +11 -0
  20. package/dist/folder-look.d.ts +110 -0
  21. package/dist/folder-look.js +306 -0
  22. package/dist/guides.js +4 -3
  23. package/dist/learning-child.d.ts +1 -0
  24. package/dist/learning-child.js +34 -0
  25. package/dist/learning-look.d.ts +25 -0
  26. package/dist/learning-look.js +72 -0
  27. package/dist/model-catalog.d.ts +5 -0
  28. package/dist/model-catalog.js +11 -2
  29. package/dist/model-choices.d.ts +188 -0
  30. package/dist/model-choices.js +405 -0
  31. package/dist/model-names.d.ts +6 -0
  32. package/dist/model-names.js +34 -0
  33. package/dist/models-ui.d.ts +69 -5
  34. package/dist/models-ui.js +53 -10
  35. package/dist/needs-you.d.ts +8 -0
  36. package/dist/needs-you.js +8 -0
  37. package/dist/operate.d.ts +53 -0
  38. package/dist/operate.js +277 -49
  39. package/dist/project-admission.d.ts +4 -1
  40. package/dist/project-admission.js +23 -2
  41. package/dist/project-identity.d.ts +44 -0
  42. package/dist/project-identity.js +153 -0
  43. package/dist/project-knowledge.d.ts +6 -6
  44. package/dist/project-tools.d.ts +4 -1
  45. package/dist/project-tools.js +7 -2
  46. package/dist/project.d.ts +4 -1
  47. package/dist/project.js +11 -4
  48. package/dist/repos.d.ts +7 -0
  49. package/dist/repos.js +57 -0
  50. package/dist/serve.js +91 -17
  51. package/dist/server/chat.js +5 -4
  52. package/dist/server/chrome.d.ts +3 -0
  53. package/dist/server/chrome.js +5 -2
  54. package/dist/server/console.css +5 -2
  55. package/dist/server/flows.js +13 -3
  56. package/dist/server/guards.d.ts +4 -0
  57. package/dist/server/guards.js +12 -3
  58. package/dist/server/http.d.ts +12 -0
  59. package/dist/server/http.js +9 -0
  60. package/dist/server/pages.js +102 -92
  61. package/dist/server/people-tokens.js +40 -20
  62. package/dist/server/render-pages.d.ts +12 -16
  63. package/dist/server/render-pages.js +30 -69
  64. package/dist/server/render-people.d.ts +14 -2
  65. package/dist/server/render-people.js +35 -12
  66. package/dist/server/render-settings.js +1 -0
  67. package/dist/server/render-tasks.js +7 -14
  68. package/dist/server/request-context.d.ts +2 -0
  69. package/dist/server/route-table.js +8 -1
  70. package/dist/server/runtime.d.ts +4 -0
  71. package/dist/server/runtime.js +1 -0
  72. package/dist/server/settings.d.ts +58 -1
  73. package/dist/server/settings.js +211 -71
  74. package/dist/server/signup-gate.d.ts +9 -0
  75. package/dist/server/signup-gate.js +40 -0
  76. package/dist/server/tasks.js +19 -5
  77. package/dist/settings-kit-script.d.ts +21 -0
  78. package/dist/settings-kit-script.js +67 -0
  79. package/dist/settings-kit.d.ts +137 -0
  80. package/dist/settings-kit.js +186 -0
  81. package/dist/settings-proofs.d.ts +64 -0
  82. package/dist/settings-proofs.js +101 -0
  83. package/dist/setup-guide.js +4 -2
  84. package/dist/spend-report.d.ts +87 -0
  85. package/dist/spend-report.js +265 -0
  86. package/dist/spend-ui.d.ts +29 -8
  87. package/dist/spend-ui.js +74 -39
  88. package/dist/task-sizing.d.ts +5 -3
  89. package/dist/task-sizing.js +4 -4
  90. package/dist/up-processes.d.ts +13 -0
  91. package/dist/up-processes.js +6 -2
  92. package/package.json +1 -1
@@ -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
- phase: "plan" | "build" | "review" | "repair";
8
+ /** The slot's form field (model-choices.ts slotKey). */
9
+ key: string;
8
10
  label: string;
9
- /** "provider|model", or "inherit" for a repair that follows the builder. */
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
- /** Repair only: offer "Same as the builder". */
18
- inherit?: boolean;
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="card role"><strong>${role.label}</strong><span>${role.words}</span></div>`;
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 !== "inherit" && role.current !== "" ? html `<option value="${role.current}" selected>${role.words} · current</option>` : "";
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 inherit = role.inherit ? html `<option value="inherit"${role.current === "inherit" ? html ` selected` : ""}>Same as the builder</option>` : "";
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.phase}`;
30
- return postForm("/settings/models/agent", html `<label class="role-name" for="${id}">${role.label}</label>${long ? html `<input type="search" data-model-filter hidden aria-label="Search ${role.label.toLowerCase()} models" placeholder="Search ${count} models" autocomplete="off">` : ""}<div class="role-row"><select id="${id}" name="agent">${inherit}${saved}${groups}</select><button type="submit" class="secondary">Save</button></div>`, { attrs: { class: "card role" }, hidden: { phase: role.phase } });
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}<h2>Default agents</h2><p class="meta">New tasks use these. Approved tasks keep the agents they were approved with.</p>${view.roles.map(role => roleForm(role, manage))}${view.chat === null ? "" : html `<h2>Chat</h2><p>${view.chat.words} · <a href="/settings/lead">Change</a></p>`}${watch}</section>`;
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 form.role').forEach(function(form){
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
+ }
@@ -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,9 +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";
13
+ import { type FolderLookOptions } from "./folder-look.js";
12
14
  import { type LeadCliSeams } from "./lead-cli.js";
13
15
  import { type Principal, type RemoteRun, type RemoteSource } from "./operate-remote.js";
14
16
  import { type PublishExec } from "./publish.js";
17
+ import { type ApplyResult, type ExtraWrite, type ModelChange, type Slot } from "./model-choices.js";
15
18
  import { type Runner as CommandRunner } from "./builder.js";
16
19
  import { type ReleaseIo } from "./releases.js";
17
20
  export type Write = (line: string) => void;
@@ -93,6 +96,8 @@ export type OperateOptions = {
93
96
  filingSizer?: Sizer;
94
97
  /** Set only by up-role-child.ts: this process is one role of a `toolroll up` (the console or the worker). */
95
98
  upRole?: UpRoleHost;
99
+ /** Injected by tests: how the console looks at project folders off its thread (folder-look.ts). */
100
+ folderLook?: FolderLookOptions;
96
101
  };
97
102
  export type OnboardSeams = Partial<Pick<OnboardIo, "home" | "env" | "cwd" | "interactive" | "confirm" | "findRepo" | "checkConnection" | "pullRequests">>;
98
103
  export declare const OPERATE_HELP: string;
@@ -152,6 +157,8 @@ export declare function runOperateAs(argv: string[], opts: {
152
157
  options?: OperateOptions;
153
158
  }): Promise<number>;
154
159
  type Context = {
160
+ /** Injected by tests: how the console looks at project folders off its thread. */
161
+ folderLook?: FolderLookOptions;
155
162
  /** A remote run's person (runOperateAs): every credential check resolves to them. Absent for every local command. */
156
163
  principal?: RemoteRun;
157
164
  /** The lead token this command runs under: it signs in as its owner. */
@@ -271,3 +278,49 @@ export declare function refreshGithubNightlies(store: Store, now: () => Date, re
271
278
  * - `recover [--abandon <campaign>]`: settle campaigns whose runner stopped; with --abandon, a person ends a stuck one.
272
279
  */
273
280
  export declare function evalsCommand(positional: readonly string[], flags: Map<string, string | true>, context: Context): Promise<number>;
281
+ /**
282
+ * The saved projects an `up` start couldn't reach (upCommand): each is proved again every `everyMs` while the worker
283
+ * runs, by `prove` (upProveSaved), so a folder that answers again connects without a restart. Only what the start itself
284
+ * read from the saved list is here, and the set only shrinks: a line connected, removed from the saved list
285
+ * (`keepOnly`) or come back as some other checkout (`revoke`) is gone for good, so a line saved again later needs its
286
+ * recorded admission like any other addition. `retry` answers the proof when one is due and answered, else null.
287
+ */
288
+ export declare function upDeferredRetry<T>(inputs: readonly string[], prove: (input: string) => Promise<T | null>, everyMs?: number, now?: () => number): {
289
+ has: (input: string) => boolean;
290
+ list: () => string[];
291
+ retry: (input: string) => Promise<T | null>;
292
+ revoke: (input: string) => void;
293
+ keepOnly: (saved: readonly string[]) => boolean;
294
+ };
295
+ /**
296
+ * Whether a saved project's folder is a Git checkout now, read without blocking this thread: `git rev-parse
297
+ * --show-toplevel` (bounded, its own process), then the real paths of the saved line and of the checkout, resolved off
298
+ * the thread under a deadline (folder-look.ts). Null when it isn't one, or didn't answer.
299
+ */
300
+ export declare function upProveSaved(gitRun: (file: string, args: readonly string[], opts?: {
301
+ cwd?: string;
302
+ }) => Promise<{
303
+ code: number | null;
304
+ stdout: string;
305
+ }>, input: string, look?: FolderLookOptions): Promise<{
306
+ line: string;
307
+ root: string;
308
+ } | null>;
309
+ /** What `up` hands its next worker after the worker reports which saved projects it still holds start authority for:
310
+ * only what both still hold. A worker's report can give up an authority, never add or give back one. */
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;