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.
Files changed (137) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/README.md +94 -1192
  3. package/dist/agent-fence.d.ts +3 -3
  4. package/dist/agent-fence.js +3 -3
  5. package/dist/agent-onboard.d.ts +2 -0
  6. package/dist/agent-onboard.js +3 -0
  7. package/dist/assignment-ui.js +5 -1
  8. package/dist/browser/workspace.css +2 -2
  9. package/dist/browser/workspace.js +31 -31
  10. package/dist/browser-crew.js +5 -1
  11. package/dist/browser-workspace.d.ts +12 -0
  12. package/dist/chat-channel.d.ts +10 -1
  13. package/dist/chat-channel.js +35 -5
  14. package/dist/chat-decide.d.ts +16 -4
  15. package/dist/chat-decide.js +239 -31
  16. package/dist/chat-messages.d.ts +5 -0
  17. package/dist/chat-messages.js +1 -1
  18. package/dist/chat-quiet.js +3 -2
  19. package/dist/chat-review.d.ts +4 -0
  20. package/dist/chat-review.js +13 -4
  21. package/dist/chat-terms.d.ts +9 -0
  22. package/dist/chat-terms.js +46 -0
  23. package/dist/contracts/console-api.d.ts +4 -2
  24. package/dist/contracts/console-api.js +7 -2
  25. package/dist/contracts/lead-tools.d.ts +30 -0
  26. package/dist/contracts/lead-tools.js +3 -0
  27. package/dist/contracts/youiyoux-request.d.ts +248 -0
  28. package/dist/contracts/youiyoux-request.js +152 -0
  29. package/dist/control-setup.d.ts +9 -0
  30. package/dist/control-setup.js +31 -4
  31. package/dist/control-ui.d.ts +7 -1
  32. package/dist/control-ui.js +3 -2
  33. package/dist/desktop-update.d.ts +1 -1
  34. package/dist/dispatch.d.ts +15 -0
  35. package/dist/dispatch.js +26 -4
  36. package/dist/first-run.d.ts +5 -2
  37. package/dist/first-run.js +13 -2
  38. package/dist/flow-cadence.d.ts +8 -2
  39. package/dist/flow-cadence.js +16 -3
  40. package/dist/flow-card-task.d.ts +9 -0
  41. package/dist/flow-card-task.js +15 -6
  42. package/dist/folder-look.d.ts +3 -0
  43. package/dist/folder-look.js +8 -4
  44. package/dist/guides.js +4 -3
  45. package/dist/integrations-ui.js +9 -4
  46. package/dist/integrations.d.ts +9 -1
  47. package/dist/integrations.js +43 -2
  48. package/dist/landed-cleanup.d.ts +4 -0
  49. package/dist/landed-cleanup.js +3 -2
  50. package/dist/lead-capabilities.js +2 -1
  51. package/dist/lead-cli.js +3 -1
  52. package/dist/lead-tools.d.ts +2 -0
  53. package/dist/lead-tools.js +30 -4
  54. package/dist/lead.d.ts +2 -0
  55. package/dist/lead.js +3 -3
  56. package/dist/model-catalog.d.ts +5 -0
  57. package/dist/model-catalog.js +11 -2
  58. package/dist/model-choices.d.ts +188 -0
  59. package/dist/model-choices.js +405 -0
  60. package/dist/model-names.d.ts +6 -0
  61. package/dist/model-names.js +34 -0
  62. package/dist/models-ui.d.ts +69 -5
  63. package/dist/models-ui.js +53 -10
  64. package/dist/needs-you.d.ts +8 -0
  65. package/dist/needs-you.js +8 -0
  66. package/dist/operate.d.ts +22 -2
  67. package/dist/operate.js +107 -41
  68. package/dist/project-activity.d.ts +20 -0
  69. package/dist/project-activity.js +23 -0
  70. package/dist/project-admission.d.ts +4 -1
  71. package/dist/project-admission.js +23 -2
  72. package/dist/project-cli.d.ts +1 -0
  73. package/dist/project-cli.js +36 -19
  74. package/dist/project-identity.d.ts +9 -2
  75. package/dist/project-identity.js +80 -33
  76. package/dist/project-knowledge.d.ts +9 -9
  77. package/dist/project-outcomes.d.ts +119 -0
  78. package/dist/project-outcomes.js +79 -0
  79. package/dist/result-actions.d.ts +4 -0
  80. package/dist/result-actions.js +41 -9
  81. package/dist/result-review.d.ts +9 -0
  82. package/dist/result-review.js +45 -5
  83. package/dist/serve.js +60 -13
  84. package/dist/server/chat.js +5 -4
  85. package/dist/server/chrome.d.ts +3 -0
  86. package/dist/server/chrome.js +5 -2
  87. package/dist/server/console.css +17 -2
  88. package/dist/server/flows.js +4 -0
  89. package/dist/server/http.d.ts +1 -0
  90. package/dist/server/http.js +6 -0
  91. package/dist/server/pages.js +53 -7
  92. package/dist/server/people-tokens.js +40 -20
  93. package/dist/server/render-chat.js +3 -2
  94. package/dist/server/render-pages.d.ts +12 -2
  95. package/dist/server/render-pages.js +36 -16
  96. package/dist/server/render-people.d.ts +14 -2
  97. package/dist/server/render-people.js +35 -12
  98. package/dist/server/render-results.d.ts +2 -0
  99. package/dist/server/render-results.js +20 -7
  100. package/dist/server/render-settings.js +1 -0
  101. package/dist/server/render-tasks.d.ts +1 -0
  102. package/dist/server/render-tasks.js +22 -19
  103. package/dist/server/request-context.d.ts +2 -0
  104. package/dist/server/route-table.js +9 -1
  105. package/dist/server/settings.d.ts +58 -1
  106. package/dist/server/settings.js +216 -70
  107. package/dist/server/signup-gate.d.ts +9 -0
  108. package/dist/server/signup-gate.js +40 -0
  109. package/dist/server/task-views.js +6 -0
  110. package/dist/server/tasks.js +54 -8
  111. package/dist/settings-kit-script.d.ts +21 -0
  112. package/dist/settings-kit-script.js +67 -0
  113. package/dist/settings-kit.d.ts +137 -0
  114. package/dist/settings-kit.js +186 -0
  115. package/dist/settings-proofs.d.ts +64 -0
  116. package/dist/settings-proofs.js +101 -0
  117. package/dist/setup-guide.js +4 -2
  118. package/dist/spend-report.d.ts +87 -0
  119. package/dist/spend-report.js +265 -0
  120. package/dist/spend-ui.d.ts +29 -8
  121. package/dist/spend-ui.js +74 -39
  122. package/dist/store.d.ts +46 -3
  123. package/dist/store.js +165 -6
  124. package/dist/task-sizing.d.ts +5 -3
  125. package/dist/task-sizing.js +4 -4
  126. package/dist/task-source.d.ts +13 -1
  127. package/dist/task-source.js +17 -1
  128. package/dist/telegram-lead.js +7 -5
  129. package/dist/telegram.js +1 -1
  130. package/dist/toolroll-update.js +2 -2
  131. package/dist/webhooks.d.ts +6 -0
  132. package/dist/webhooks.js +25 -0
  133. package/dist/work-index.d.ts +4 -0
  134. package/dist/work-index.js +32 -0
  135. package/dist/youiyoux-intake.d.ts +234 -0
  136. package/dist/youiyoux-intake.js +908 -0
  137. 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="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,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 flows = flowPass.moved + flowPass.filed.length + flowPass.problems.length + triggerPass.added + triggerPass.problems.length + stepPass.ran + stepPass.problems.length === 0 && !replied ? {} : { flows: { ...flowPass,
3386
- ...(triggerPass.added + triggerPass.problems.length === 0 ? {} : { triggers: triggerPass }), ...(stepPass.ran + stepPass.problems.length === 0 ? {} : { steps: stepPass }), ...(replied ? { replies: replyPass } : {}) } };
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: one that doesn't
3945
- // answer (a drive away, an iCloud folder macOS is holding) stays unresolved, kept restrictive, and is looked at again
3946
- // by the background pass; the console starts and answers either way (project-identity.ts).
3947
- const identities = projectIdentities();
3948
- await identities.refresh([...options.repos ?? [], ...options.projectRoots ?? [], ...context.store.listProjects().map(one => one.path), ...context.store.knownRepos()]);
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 removed = store.clearPhaseTierAlternates(scope, phase, tier, alsoProvider ?? null);
5071
- return succeed(write, json, "config clear", { scope, phase, tier, also: true, provider: alsoProvider ?? null, cleared: removed }, () => [
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
- store.setPhaseTierAlternate(scope, phase, tier, alsoProvider, alsoModel, acting.name, clock());
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 succeed(write, json, "config set", { scope, phase, tier, also: true, provider: alsoProvider, model: alsoModel }, () => [
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
- const clearedLight = store.clearPhaseTierConfig(scope, phase, "light");
5094
- return succeed(write, json, "config clear", { scope, phase, tier: "light", cleared: clearedLight }, () => [
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 clearedStrong = store.clearPhaseTierConfig(scope, phase, "strong");
5100
- return succeed(write, json, "config clear", { scope, phase, tier: "strong", cleared: clearedStrong }, () => [
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 cleared = store.clearPhaseConfig(scope, phase);
5105
- return succeed(write, json, "config clear", { scope, phase, cleared }, () => [
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
- store.setPhaseTierConfig(scope, phase, "light", providerGiven, modelGiven, acting.name, clock());
5120
- return succeed(write, json, "config set", { scope, phase, tier: "light", provider: providerGiven, model: modelGiven }, () => [
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
- store.setPhaseTierConfig(scope, phase, "strong", providerGiven, modelGiven, acting.name, clock());
5131
- return succeed(write, json, "config set", { scope, phase, tier: "strong", provider: providerGiven, model: modelGiven }, () => [
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
- store.setPhaseConfig(scope, phase, providerGiven, modelGiven, acting.name, clock());
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 succeed(write, json, "config set", { scope, phase, provider: providerGiven, model: modelGiven, warnings }, () => [
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
- const home = homedir();
7497
- const file = approverPlan2.passwordFile === null ? null : approverPlan2.passwordFile.startsWith(`${home}/`) ? `~${approverPlan2.passwordFile.slice(home.length)}` : approverPlan2.passwordFile;
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
- : file === null ? `sign in as ${account} with your password` : `sign in as ${account}: the password is in ${file}`;
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
+ }