aimeat 1.34.0 → 1.36.0

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 (157) hide show
  1. package/dist/.env.example +25 -0
  2. package/dist/locales/en.json +58 -6
  3. package/dist/locales/fi.json +58 -6
  4. package/dist/public/css/views/secretary.css +52 -0
  5. package/dist/public/js/i18n.js +9 -0
  6. package/dist/public/js/services/secretary-helpers.js +8 -4
  7. package/dist/public/js/services/secretary-policy.js +28 -3
  8. package/dist/public/spa.html +5 -1
  9. package/dist/public/views/my-company.js +13 -1
  10. package/dist/public/views/profile/agents/agent-card.js +6 -4
  11. package/dist/public/views/profile/agents/tab-integration.js +5 -2
  12. package/dist/public/views/secretary/cards.js +46 -15
  13. package/dist/public/views/secretary/dashboard.js +68 -2
  14. package/dist/public/views/secretary.js +221 -40
  15. package/dist/scripts/archive-stale-versions.d.ts +33 -0
  16. package/dist/scripts/archive-stale-versions.d.ts.map +1 -0
  17. package/dist/scripts/archive-stale-versions.js +278 -0
  18. package/dist/scripts/archive-stale-versions.js.map +1 -0
  19. package/dist/src/cli/connect/mcp/tools/agent-management.d.ts +4 -1
  20. package/dist/src/cli/connect/mcp/tools/agent-management.d.ts.map +1 -1
  21. package/dist/src/cli/connect/mcp/tools/agent-management.js +1 -1
  22. package/dist/src/cli/connect/mcp/tools/agent-management.js.map +1 -1
  23. package/dist/src/cli/connect/mcp/tools/organisms.d.ts +2 -0
  24. package/dist/src/cli/connect/mcp/tools/organisms.d.ts.map +1 -1
  25. package/dist/src/cli/connect/mcp/tools/organisms.js +9 -1
  26. package/dist/src/cli/connect/mcp/tools/organisms.js.map +1 -1
  27. package/dist/src/cli/connect/onboarding-prompt.d.ts +3 -0
  28. package/dist/src/cli/connect/onboarding-prompt.d.ts.map +1 -1
  29. package/dist/src/cli/connect/onboarding-prompt.js +10 -3
  30. package/dist/src/cli/connect/onboarding-prompt.js.map +1 -1
  31. package/dist/src/config.d.ts +11 -0
  32. package/dist/src/config.d.ts.map +1 -1
  33. package/dist/src/config.js +12 -0
  34. package/dist/src/config.js.map +1 -1
  35. package/dist/src/generated/api-types.d.ts +748 -4
  36. package/dist/src/generated/api-types.d.ts.map +1 -1
  37. package/dist/src/generated/prisma-postgres/edge.js +4 -3
  38. package/dist/src/generated/prisma-postgres/index-browser.js +1 -0
  39. package/dist/src/generated/prisma-postgres/index.d.ts +24 -1
  40. package/dist/src/generated/prisma-postgres/index.js +4 -3
  41. package/dist/src/generated/prisma-postgres/package.json +1 -1
  42. package/dist/src/generated/prisma-postgres/schema.prisma +1 -0
  43. package/dist/src/generated/prisma-postgres/wasm.js +4 -3
  44. package/dist/src/mcp/agent-onboarding.d.ts +4 -0
  45. package/dist/src/mcp/agent-onboarding.d.ts.map +1 -1
  46. package/dist/src/mcp/agent-onboarding.js +28 -2
  47. package/dist/src/mcp/agent-onboarding.js.map +1 -1
  48. package/dist/src/mcp/workspaces.d.ts +4 -0
  49. package/dist/src/mcp/workspaces.d.ts.map +1 -1
  50. package/dist/src/mcp/workspaces.js +16 -3
  51. package/dist/src/mcp/workspaces.js.map +1 -1
  52. package/dist/src/models/agent-onboarding-schemas.d.ts +47 -0
  53. package/dist/src/models/agent-onboarding-schemas.d.ts.map +1 -1
  54. package/dist/src/models/agent-onboarding-schemas.js +117 -0
  55. package/dist/src/models/agent-onboarding-schemas.js.map +1 -1
  56. package/dist/src/routes/agent-onboarding.d.ts +5 -0
  57. package/dist/src/routes/agent-onboarding.d.ts.map +1 -1
  58. package/dist/src/routes/agent-onboarding.js +21 -2
  59. package/dist/src/routes/agent-onboarding.js.map +1 -1
  60. package/dist/src/routes/agents.d.ts +7 -0
  61. package/dist/src/routes/agents.d.ts.map +1 -1
  62. package/dist/src/routes/agents.js +48 -5
  63. package/dist/src/routes/agents.js.map +1 -1
  64. package/dist/src/routes/libs.d.ts +14 -0
  65. package/dist/src/routes/libs.d.ts.map +1 -1
  66. package/dist/src/routes/libs.js +56 -17
  67. package/dist/src/routes/libs.js.map +1 -1
  68. package/dist/src/routes/oauth-login.d.ts +26 -21
  69. package/dist/src/routes/oauth-login.d.ts.map +1 -1
  70. package/dist/src/routes/oauth-login.js +257 -229
  71. package/dist/src/routes/oauth-login.js.map +1 -1
  72. package/dist/src/routes/openrouter.d.ts +3 -0
  73. package/dist/src/routes/openrouter.d.ts.map +1 -1
  74. package/dist/src/routes/openrouter.js +10 -5
  75. package/dist/src/routes/openrouter.js.map +1 -1
  76. package/dist/src/routes/organisms.d.ts +4 -0
  77. package/dist/src/routes/organisms.d.ts.map +1 -1
  78. package/dist/src/routes/organisms.js +32 -5
  79. package/dist/src/routes/organisms.js.map +1 -1
  80. package/dist/src/routes/secretary.d.ts +7 -2
  81. package/dist/src/routes/secretary.d.ts.map +1 -1
  82. package/dist/src/routes/secretary.js +119 -28
  83. package/dist/src/routes/secretary.js.map +1 -1
  84. package/dist/src/routes/site.d.ts +2 -0
  85. package/dist/src/routes/site.d.ts.map +1 -1
  86. package/dist/src/routes/site.js +10 -2
  87. package/dist/src/routes/site.js.map +1 -1
  88. package/dist/src/server-bootstrap/routes-loader.d.ts.map +1 -1
  89. package/dist/src/server-bootstrap/routes-loader.js +8 -15
  90. package/dist/src/server-bootstrap/routes-loader.js.map +1 -1
  91. package/dist/src/services/ai-completion.d.ts +2 -0
  92. package/dist/src/services/ai-completion.d.ts.map +1 -1
  93. package/dist/src/services/ai-completion.js +2 -1
  94. package/dist/src/services/ai-completion.js.map +1 -1
  95. package/dist/src/services/config-schema.d.ts.map +1 -1
  96. package/dist/src/services/config-schema.js +2 -0
  97. package/dist/src/services/config-schema.js.map +1 -1
  98. package/dist/src/services/generator-autopilot.d.ts +2 -0
  99. package/dist/src/services/generator-autopilot.d.ts.map +1 -1
  100. package/dist/src/services/generator-autopilot.js +3 -1
  101. package/dist/src/services/generator-autopilot.js.map +1 -1
  102. package/dist/src/services/notebook-ai.d.ts +2 -0
  103. package/dist/src/services/notebook-ai.d.ts.map +1 -1
  104. package/dist/src/services/notebook-ai.js +1 -1
  105. package/dist/src/services/notebook-ai.js.map +1 -1
  106. package/dist/src/services/oidc-providers.d.ts +94 -0
  107. package/dist/src/services/oidc-providers.d.ts.map +1 -0
  108. package/dist/src/services/oidc-providers.js +181 -0
  109. package/dist/src/services/oidc-providers.js.map +1 -0
  110. package/dist/src/services/onboarding-guide.d.ts +53 -0
  111. package/dist/src/services/onboarding-guide.d.ts.map +1 -0
  112. package/dist/src/services/onboarding-guide.js +81 -0
  113. package/dist/src/services/onboarding-guide.js.map +1 -0
  114. package/dist/src/services/organism-template.d.ts.map +1 -1
  115. package/dist/src/services/organism-template.js +3 -1
  116. package/dist/src/services/organism-template.js.map +1 -1
  117. package/dist/src/services/scheduler.d.ts +9 -0
  118. package/dist/src/services/scheduler.d.ts.map +1 -1
  119. package/dist/src/services/scheduler.js +53 -24
  120. package/dist/src/services/scheduler.js.map +1 -1
  121. package/dist/src/services/secretary-workbrief.d.ts +144 -0
  122. package/dist/src/services/secretary-workbrief.d.ts.map +1 -0
  123. package/dist/src/services/secretary-workbrief.js +315 -0
  124. package/dist/src/services/secretary-workbrief.js.map +1 -0
  125. package/dist/src/services/secretary-worktier.d.ts +118 -0
  126. package/dist/src/services/secretary-worktier.d.ts.map +1 -0
  127. package/dist/src/services/secretary-worktier.js +157 -0
  128. package/dist/src/services/secretary-worktier.js.map +1 -0
  129. package/dist/src/services/skill-bundle/generator.d.ts +5 -0
  130. package/dist/src/services/skill-bundle/generator.d.ts.map +1 -1
  131. package/dist/src/services/skill-bundle/generator.js +22 -1
  132. package/dist/src/services/skill-bundle/generator.js.map +1 -1
  133. package/dist/src/storage/interface.d.ts +2 -1
  134. package/dist/src/storage/interface.d.ts.map +1 -1
  135. package/dist/src/storage/interface.js.map +1 -1
  136. package/dist/src/storage/providers/mongodb/index.d.ts +1 -0
  137. package/dist/src/storage/providers/mongodb/index.d.ts.map +1 -1
  138. package/dist/src/storage/providers/mongodb/index.js +27 -0
  139. package/dist/src/storage/providers/mongodb/index.js.map +1 -1
  140. package/dist/src/storage/providers/sqlite/index.d.ts +1 -0
  141. package/dist/src/storage/providers/sqlite/index.d.ts.map +1 -1
  142. package/dist/src/storage/providers/sqlite/index.js +18 -4
  143. package/dist/src/storage/providers/sqlite/index.js.map +1 -1
  144. package/dist/src/storage/providers/sqlite/schema.d.ts.map +1 -1
  145. package/dist/src/storage/providers/sqlite/schema.js +6 -0
  146. package/dist/src/storage/providers/sqlite/schema.js.map +1 -1
  147. package/dist/src/storage/repositories/identity.repository.d.ts +2 -0
  148. package/dist/src/storage/repositories/identity.repository.d.ts.map +1 -1
  149. package/dist/src/utils/env-config.d.ts.map +1 -1
  150. package/dist/src/utils/env-config.js +11 -0
  151. package/dist/src/utils/env-config.js.map +1 -1
  152. package/dist/src/utils/node-descriptor.d.ts.map +1 -1
  153. package/dist/src/utils/node-descriptor.js +1 -0
  154. package/dist/src/utils/node-descriptor.js.map +1 -1
  155. package/package.json +3 -1
  156. package/prisma/schema.postgres.prisma +1 -0
  157. package/prisma/schema.prisma +1 -0
@@ -30,7 +30,7 @@ const html = htm.bind(h);
30
30
  import { t } from '/js/i18n.js';
31
31
  import { CopyButton } from '/components/CopyButton.js';
32
32
  import { Markdown } from '/components/Markdown.js';
33
- import { SECRETARY_CAPABILITIES, BANDS } from '/js/services/secretary-policy.js';
33
+ import { SECRETARY_CAPABILITIES, BANDS, autonomyLevelOf } from '/js/services/secretary-policy.js';
34
34
  import { buildInterviewPrompt, groupFeedByDay } from '/js/services/secretary-helpers.js';
35
35
 
36
36
  /** Context "hats" switcher chips + add. */
@@ -227,9 +227,22 @@ export function brainCard(p) {
227
227
  /** Operating model: stop-spending + daily budget + per-capability autonomy bands. */
228
228
  export function operatingCard(p) {
229
229
  const policy = p.policy;
230
+ const level = autonomyLevelOf(policy.bands); // 'suggest' | 'assist' — the single toggle's state
230
231
  return html`
231
232
  <section class="sec-card">
232
233
  <h2 class="sec-h2">${t('secretary.howIWork')}</h2>
234
+
235
+ ${/* ONE autonomy toggle (collapses the per-capability band sprawl). Heavy work is always a Work
236
+ Brief a Builder executes — never something the cheap Secretary does on its own. */ ''}
237
+ <div class="sec-autonomy">
238
+ ${[['suggest', 'secretary.autonomy.suggest', 'secretary.autonomy.suggestHint'],
239
+ ['assist', 'secretary.autonomy.assist', 'secretary.autonomy.assistHint']].map(([val, label, hint]) => html`
240
+ <label class=${'sec-auto-opt' + (level === val ? ' sec-auto-opt--on' : '')} key=${val}>
241
+ <input type="radio" name="sec-autonomy" checked=${level === val} onChange=${() => p.setAutonomy(val)} />
242
+ <span><strong>${t(label)}</strong><br/><span class="sec-hint">${t(hint)}</span></span>
243
+ </label>`)}
244
+ </div>
245
+
233
246
  <label class="sec-stop ${policy.stopSpending ? 'on' : ''}">
234
247
  <input type="checkbox" checked=${policy.stopSpending} onChange=${p.toggleStop} />
235
248
  <span><strong>${t('secretary.stopSpending')}</strong><br/><span class="sec-hint">${t('secretary.stopSpendingHint')}</span></span>
@@ -239,18 +252,24 @@ export function operatingCard(p) {
239
252
  <input type="number" min="0" class="sec-budget-in" value=${policy.dailyMorselBudget == null ? '' : policy.dailyMorselBudget} placeholder=${t('secretary.noLimit')} onChange=${(e) => p.setBudget(e.target.value)} />
240
253
  <span class="sec-hint">${t('secretary.dailyBudgetHint')}</span>
241
254
  </div>
242
- <p class="sec-hint sec-bands-note">${t('secretary.bandsNote')}</p>
243
- <ul class="sec-caps">
244
- ${SECRETARY_CAPABILITIES.map((c) => html`
245
- <li class="sec-cap" key=${c.id}>
246
- <span class="sec-cap-label">${t('secretary.cap.' + c.id)}${c.costs ? html`<span class="sec-cap-tag">${t('secretary.capCosts')}</span>` : null}</span>
247
- ${c.enterprise
248
- ? html`<span class="sec-cap-locked sec-cap-ent">${t('secretary.enterprise')}</span>`
249
- : c.locked
250
- ? html`<span class="sec-cap-locked">${t('secretary.band.' + policy.bands[c.id])} · ${t('secretary.locked')}</span>`
251
- : html`<select class="sec-band" value=${policy.bands[c.id]} onChange=${(e) => p.setBand(c.id, e.target.value)}>${BANDS.map((b) => html`<option value=${b} selected=${policy.bands[c.id] === b}>${t('secretary.band.' + b)}</option>`)}</select>`}
252
- </li>`)}
253
- </ul>
255
+
256
+ ${/* Per-capability bands are still here for power users, tucked behind an Advanced disclosure so the
257
+ default surface is just the one toggle above. */ ''}
258
+ <details class="sec-adv">
259
+ <summary>${t('secretary.advancedBands')}</summary>
260
+ <p class="sec-hint sec-bands-note">${t('secretary.bandsNote')}</p>
261
+ <ul class="sec-caps">
262
+ ${SECRETARY_CAPABILITIES.map((c) => html`
263
+ <li class="sec-cap" key=${c.id}>
264
+ <span class="sec-cap-label">${t('secretary.cap.' + c.id)}${c.costs ? html`<span class="sec-cap-tag">${t('secretary.capCosts')}</span>` : null}</span>
265
+ ${c.enterprise
266
+ ? html`<span class="sec-cap-locked sec-cap-ent">${t('secretary.enterprise')}</span>`
267
+ : c.locked
268
+ ? html`<span class="sec-cap-locked">${t('secretary.band.' + policy.bands[c.id])} · ${t('secretary.locked')}</span>`
269
+ : html`<select class="sec-band" value=${policy.bands[c.id]} onChange=${(e) => p.setBand(c.id, e.target.value)}>${BANDS.map((b) => html`<option value=${b} selected=${policy.bands[c.id] === b}>${t('secretary.band.' + b)}</option>`)}</select>`}
270
+ </li>`)}
271
+ </ul>
272
+ </details>
254
273
  </section>`;
255
274
  }
256
275
 
@@ -278,13 +297,25 @@ function routineStepRow(p, r, s) {
278
297
  const isDelegate = s.capability === 'delegate';
279
298
  const agents = p.agents || [];
280
299
  const workflows = p.workflows || [];
300
+ // Unblur the delegation target by ROLE: node-local Specialists vs connected big-AI Builders. The Secretary
301
+ // (the watcher) is never a delegation target. Same handler/value (agent name) — the optgroups just label
302
+ // which kind of agent each is, so "pick a specialist" no longer lumps Builders + specialists together.
303
+ const isSpecialist = (a) => (a.tags || []).includes('system:specialist');
304
+ const isSecretary = (a) => (a.tags || []).includes('system:secretary') || a.name === 'secretary';
305
+ const specialistAgents = agents.filter((a) => isSpecialist(a));
306
+ const builderAgents = agents.filter((a) => !isSpecialist(a) && !isSecretary(a));
281
307
  // B4: delegate to one agent (agent task) · G7: OR run an Agent Workflow chaining specialists.
282
308
  const delegateControl = html`
283
309
  <div class="sec-delegate">
284
- ${agents.length > 0 ? html`
310
+ ${(specialistAgents.length + builderAgents.length) > 0 ? html`
285
311
  <select class="sec-band" value=${p.delegateAgent} onChange=${(e) => p.setDelegateAgent(e.target.value)}>
286
312
  <option value="" selected=${!p.delegateAgent}>${t('secretary.next.delegatePick')}</option>
287
- ${agents.map((a) => html`<option value=${a.name} selected=${p.delegateAgent === a.name}>${(a.name)}</option>`)}
313
+ ${specialistAgents.length ? html`<optgroup label=${t('secretary.next.delegateSpecialists')}>
314
+ ${specialistAgents.map((a) => html`<option value=${a.name} selected=${p.delegateAgent === a.name}>${(a.name)}</option>`)}
315
+ </optgroup>` : null}
316
+ ${builderAgents.length ? html`<optgroup label=${t('secretary.next.delegateBuilders')}>
317
+ ${builderAgents.map((a) => html`<option value=${a.name} selected=${p.delegateAgent === a.name}>${(a.name)}</option>`)}
318
+ </optgroup>` : null}
288
319
  </select>
289
320
  <button class="btn-primary btn-sm" disabled=${busy || !p.delegateAgent} onClick=${() => p.approveStep(r, s, { agentName: p.delegateAgent })}>${busy ? t('secretary.next.running') : t('secretary.next.delegateGo')}</button>` : null}
290
321
  ${workflows.length > 0 ? html`
@@ -7,9 +7,13 @@
7
7
  * collapsible "Manage & setup" disclosure header that tucks the set-up-once config cards away.
8
8
  * The view (views/secretary.js) owns all state/handlers and passes them in; the config cards
9
9
  * themselves still live in ./cards.js / ./cards-reach.js. Redesign: docs/internal/2026-06-25-secretary-view-redesign.md.
10
- * @structure quickActionRow · dashStatus · standPanel · actionItemsCard · routinesCard · quickActionsManager · manageHeader (one render function each)
11
- * @usage import { quickActionRow, dashStatus, standPanel, actionItemsCard, routinesCard, quickActionsManager, manageHeader } from '/views/secretary/dashboard.js';
10
+ * @structure quickActionRow · dashStatus · standPanel · briefTrayCard · actionItemsCard · routinesCard · quickActionsManager · manageHeader (one render function each)
11
+ * @usage import { quickActionRow, dashStatus, standPanel, briefTrayCard, actionItemsCard, routinesCard, quickActionsManager, manageHeader } from '/views/secretary/dashboard.js';
12
12
  * @version-history
13
+ * v0.10.0 — 2026-07-01 — Light-vs-heavy seam: briefTrayCard — the Work Brief tray. The cheap Secretary
14
+ * detects structural gaps and proposes grounded briefs; this renders each with "Open in Claude Desktop"
15
+ * (CopyButton with an MCP preface + the brief) and "Send to a connected Builder" (lane b dispatch), so
16
+ * heavy work goes to a big AI via MCP and never gets cheap-authored here.
13
17
  * v0.9.0 — 2026-06-28 — strategyCard: optional steering frame (vision/mission/principles/risks +
14
18
  * current→target + ordered milestone gates with status/focus/reorder/goal-linking + AI re-plan).
15
19
  * v0.8.0 — 2026-06-28 — secretaryDangerCard: a guarded "Reset Secretary" control (type RESET to wipe
@@ -33,6 +37,7 @@ import htm from 'htm';
33
37
  const html = htm.bind(h);
34
38
  import { t } from '/js/i18n.js';
35
39
  import { Markdown } from '/components/Markdown.js';
40
+ import { CopyButton } from '/components/CopyButton.js';
36
41
 
37
42
  /** Core (+ later dynamic) quick actions: a button row above the chat. `items` are descriptors
38
43
  * `{ key, label, title?, primary?, disabled?, hidden?, onClick }` so B3 can extend with dynamic actions. */
@@ -106,6 +111,67 @@ export function standPanel(p) {
106
111
  </section>`;
107
112
  }
108
113
 
114
+ /** Compose the paste a Builder receives in Claude Desktop: a connect-first preface + the full brief.
115
+ * The preface tells the assistant to connect via the appdev MCP if it isn't already, so pasting into a
116
+ * Claude Desktop that has NOT been wired to the node still leads somewhere instead of silently failing. */
117
+ function heavyWorkPrompt(b) {
118
+ return `I'd like you to carry out a work brief on my AIMEAT node using the appdev MCP tools (the aimeat_* tools).
119
+
120
+ FIRST — make sure you can reach my node: if you are not already connected to my AIMEAT node via the appdev MCP, connect now. In the AIMEAT app open Profile → Agents → "Connect a New Agent" for the exact device-authorization steps (or read the node's /llms.txt). You need the aimeat_* tools available before you can do any of the work below.
121
+
122
+ THEN — carry out the brief end-to-end with those tools: build/fill via aimeat_workspace_* / aimeat_organism_*, ground everything in the real sources listed, and never invent facts. If a records space has no real data, leave it empty rather than make something up.
123
+
124
+ ${b.brief || ''}`;
125
+ }
126
+
127
+ /** One Work Brief row: objective + ws + a markdown preview + the two heavy-work lanes (Open in Claude
128
+ * Desktop / Send to a connected Builder) + Done. The seam made physical — the cheap Secretary proposed
129
+ * this; a Builder (a big AI via MCP) executes it. */
130
+ function briefRow(b, p) {
131
+ const builders = p.builders || [];
132
+ const dispatched = b.status === 'dispatched';
133
+ return html`
134
+ <div class="sec-brief" key=${b.id}>
135
+ <div class="sec-brief-top">
136
+ <span class="sec-brief-badge ${dispatched ? 'is-dispatched' : ''}">${dispatched ? t('secretary.brief.dispatchedBadge') : t('secretary.brief.heavyBadge')}</span>
137
+ <span class="sec-brief-obj">${b.objective}</span>
138
+ ${b.wsName ? html`<span class="sec-hint sec-brief-ws">${b.wsName}</span>` : null}
139
+ </div>
140
+ <details class="sec-brief-detail">
141
+ <summary>${t('secretary.brief.preview')}</summary>
142
+ <div class="sec-brief-md"><${Markdown} text=${b.brief || ''} /></div>
143
+ </details>
144
+ ${dispatched ? html`<div class="sec-hint sec-brief-wait">${t('secretary.brief.dispatchedNote')}</div>` : null}
145
+ <div class="sec-brief-actions">
146
+ <${CopyButton} text=${heavyWorkPrompt(b)} className="btn-primary btn-sm" label=${t('secretary.brief.openClaude')} />
147
+ ${builders.length ? html`
148
+ <select class="sec-select" disabled=${dispatched || p.dispatching === b.id}
149
+ onChange=${(e) => { const v = e.target.value; e.target.value = ''; if (v) p.onDispatch(b.id, v); }}>
150
+ <option value="">${dispatched ? t('secretary.brief.waiting') : t('secretary.brief.sendBuilder')}</option>
151
+ ${builders.map((a) => html`<option value=${a.gaii} key=${a.gaii}>${a.name}</option>`)}
152
+ </select>` : html`<span class="sec-hint">${t('secretary.brief.noBuilder')}</span>`}
153
+ <button class="btn-ghost btn-sm" onClick=${() => p.onResolve(b.id)}>${t('secretary.brief.done')}</button>
154
+ </div>
155
+ </div>`;
156
+ }
157
+
158
+ /** The Work Brief tray: heavy work the Secretary detected and prepared for a Builder. Null when empty
159
+ * (no gaps → nothing to hand off). The clear divide between "watch + propose" (cheap, here) and
160
+ * "build + author" (a big AI via MCP). */
161
+ export function briefTrayCard(p) {
162
+ const briefs = p.briefs || [];
163
+ if (!briefs.length) return null;
164
+ return html`
165
+ <section class="sec-card sec-brief-tray">
166
+ <div class="sec-card-head">
167
+ <h2 class="sec-h2">${t('secretary.brief.startHere')} ${t('secretary.brief.title')} <span class="sec-count">${briefs.length}</span></h2>
168
+ <span class="sec-hint">${t('secretary.brief.subtitle')}</span>
169
+ </div>
170
+ <p class="sec-brief-banner">${t('secretary.brief.mcpNote')} <a class="sec-linkbtn" href="/v1/profile">${t('secretary.brief.connectLink')}</a></p>
171
+ ${briefs.map((b) => briefRow(b, p))}
172
+ </section>`;
173
+ }
174
+
109
175
  /** "What's next" — the Secretary's proposed next actions, each with Do it / Skip; doing one runs it. */
110
176
  export function whatsNextPanel(p) {
111
177
  const n = p.answer;
@@ -11,6 +11,34 @@
11
11
  * @structure SECRETARY_ICON · SecretaryView (default) — state, effects, handlers, layout (cards in ./secretary/cards.js + ./secretary/cards-reach.js + dashboard chrome in ./secretary/dashboard.js)
12
12
  * @usage routed at /v1/secretary by spa.html (+ portal.ts spaRoutes).
13
13
  * @version-history
14
+ * v0.26.0 — 2026-07-01 — Opt-in feature: the Secretary is now gated on AIMEAT_SECRETARY_ENABLED (off by
15
+ * default). The header nav link is hidden and this view redirects to /v1/profile when the node has it
16
+ * disabled (flag read from /v1/site/header-nav `features.secretary`).
17
+ * v0.25.0 — 2026-07-01 — Light-vs-heavy seam: setup no longer cheap-authors content (that hallucinated).
18
+ * applyResult now CREATES the workspace structure and then proposes grounded Work Briefs (POST
19
+ * /v1/secretary/brief) the user hands to a Builder (a connected big AI via the appdev MCP tools). Drops
20
+ * the per-workspace author-now fill loop + the author_content band. The dashboard brief tray (cards)
21
+ * renders the briefs with "Open in Claude Desktop" / "Send to a connected Builder".
22
+ * v0.24.0 — 2026-06-30 — Never-empty + honest fill result. normalizeInlineManifest now GUARANTEES every
23
+ * workspace has a document space (appends a "Muistiinpanot" doc space when the design gave only records
24
+ * spaces) so the setup fill always has a thinking-home, and is genuinely document-first for unspecified
25
+ * spaces. The per-workspace fill loop counts a workspace as filled ONLY on a real 🖊️ authored line (not
26
+ * a ⚠️ summary), retries the HTTP call once (flaky owl-alpha), and the toast names workspaces it
27
+ * couldn't fill instead of a bare count.
28
+ * v0.23.0 — 2026-06-30 — Autonomy chosen up front: "Set up my Secretary" first asks HOW autonomous it
29
+ * should be (Prepare/Draft · Run/Act · Only-when-I-ask/Off), each explained in plain words incl. the
30
+ * AI-credit cost, BEFORE anything runs — so nobody stumbles into Act bands later. The choice sets the
31
+ * new context's author_content band and, for Draft/Act, turns the daily check-in on. Browser-verified.
32
+ * v0.22.0 — 2026-06-30 — Live setup progress: the fill runs ONE workspace at a time (author-now?ws=)
33
+ * and a top-level overlay (survives the hire→dashboard switch) shows "Filling workspace N/total ·
34
+ * <name>" + a progress bar through the whole slow fill, instead of a couple of fleeting toasts.
35
+ * Browser-verified: overlay cycled 1/2 → 2/2 with the bar advancing.
36
+ * v0.21.0 — 2026-06-30 — Deterministic workspace structure (NO AI): genAndApplyManifest normalizes any
37
+ * design-provided inline manifest into a valid one (fixes namespace form + backfills schemaRef/backing/
38
+ * writeRole/mode, rekeys schemas, derives a records schema when missing) and falls back to a plain
39
+ * document space — instead of a flaky per-workspace AI generator call that left some workspaces as
40
+ * "set up when opened". Every workspace now gets its structure reliably, every time. Verified on the
41
+ * dev server: a 6-workspace design (with non-conforming komento.* namespaces) all came out structured.
14
42
  * v0.20.0 — 2026-06-30 — Content at handshake: a fresh hire now fills its workspaces with REAL content
15
43
  * immediately (POST /v1/secretary/author-now, long timeout) and drops any left empty; the workspace
16
44
  * generator runs with skipExamples so no fake "example-N" placeholder records are written.
@@ -73,12 +101,12 @@ import { t, getLocale } from '/js/i18n.js';
73
101
  import { api, apiGet, apiPost, apiPut, apiDelete } from '/js/api.js';
74
102
  import { useViewCSS } from '/components/useViewCSS.js';
75
103
  import { useToast } from '/components/Toast.js';
76
- import { createOrganism, createWorkspace, generateRaw, parseGenerated, validateGenerated, applyGeneratedWorkspace } from '/js/services/organisms.js';
77
- import { defaultPolicy, mergePolicy } from '/js/services/secretary-policy.js';
104
+ import { createOrganism, createWorkspace, validateGenerated, applyGeneratedWorkspace } from '/js/services/organisms.js';
105
+ import { defaultPolicy, mergePolicy, autonomyPreset } from '/js/services/secretary-policy.js';
78
106
  import { buildDesignPrompt, extractJson, snapshotOf, genCtxId, migrateConfig, suggestContextId, computeBudgetInfo, computeReliability, sanitizeQuickActions, normalizeStrategy, MILESTONE_STATUSES, buildStrategyReplanPrompt, linkMilestoneGoals, keepKnownGoalRefs, SECRETARY_AIMEAT_PRIMER } from '/js/services/secretary-helpers.js';
79
107
  import { contextSwitcher, hirePanel, chatCard, findCard, noteCard, decisionsCard, brainCard, operatingCard, historyCard, metaCard, whatsNextCard, feedCard, automationCard, goalsCard, decisionLogCard } from '/views/secretary/cards.js';
80
108
  import { createResourceCard, knowledgeCard, accessCard, crewCard } from '/views/secretary/cards-reach.js';
81
- import { quickActionRow, dashStatus, standPanel, whatsNextPanel, actionItemsCard, routinesCard, triggersCard, quickActionsManager, manageHeader, workflowDesignPanel, secretaryDangerCard, strategyCard } from '/views/secretary/dashboard.js';
109
+ import { quickActionRow, dashStatus, standPanel, briefTrayCard, whatsNextPanel, actionItemsCard, routinesCard, triggersCard, quickActionsManager, manageHeader, workflowDesignPanel, secretaryDangerCard, strategyCard } from '/views/secretary/dashboard.js';
82
110
  import { fetchWorkflowOffers, designWorkflow, saveDesignedWorkflow, slugifyWorkflowId } from '/views/secretary/workflow-design.js';
83
111
  import { useSpecialists, specialistsCard } from '/views/secretary/use-specialists.js';
84
112
  import { useIntake } from '/views/secretary/use-intake.js';
@@ -106,13 +134,29 @@ export const SECRETARY_ICON = html`
106
134
 
107
135
  export default function SecretaryView() {
108
136
  useViewCSS('/css/views/secretary.css');
137
+ // Secretary is opt-in per node (AIMEAT_SECRETARY_ENABLED). If it's disabled but this view is reached
138
+ // via a direct URL/bookmark (no nav link is rendered when off), bounce back to the profile.
139
+ useEffect(() => {
140
+ let cancelled = false;
141
+ fetch('/v1/site/header-nav')
142
+ .then(r => r.json())
143
+ .then(d => { if (!cancelled && d?.ok && d.data?.features && d.data.features.secretary === false) window.location.replace('/v1/profile'); })
144
+ .catch(() => {});
145
+ return () => { cancelled = true; };
146
+ }, []);
109
147
  const { showToast, ToastContainer } = useToast();
110
148
  const [secretary, setSecretary] = useState(undefined); // undefined=loading, null=not provisioned
111
149
  const [hasOpenRouterKey, setHasOpenRouterKey] = useState(false); // owner has an OpenRouter key configured
112
150
  const [config, setConfig] = useState(null); // { contexts:[...], activeContextId }
151
+ const [briefs, setBriefs] = useState([]); // open Work Briefs (heavy work to hand a Builder)
152
+ const [builders, setBuilders] = useState([]); // connected big-AI agents (not Secretary / specialists)
153
+ const [dispatching, setDispatching] = useState(''); // briefId currently being dispatched
113
154
  const [result, setResult] = useState('');
114
155
  const [needs, setNeeds] = useState('');
115
156
  const [applying, setApplying] = useState(false);
157
+ const [setupStep, setSetupStep] = useState(null); // live setup progress: { phase, done, total, name }
158
+ const [chooseMode, setChooseMode] = useState(false); // showing the "how should it work?" autonomy step
159
+ const [setupMode, setSetupMode] = useState('draft'); // chosen autonomy: 'off' | 'draft' | 'act'
116
160
  const [generating, setGenerating] = useState(false);
117
161
  const [showHire, setShowHire] = useState(false);
118
162
  const [hireMode, setHireMode] = useState('new'); // 'new' (add context) | 'edit' (re-run active)
@@ -161,17 +205,42 @@ export default function SecretaryView() {
161
205
  const sec = agents.find((a) => (a.tags || []).includes('system:secretary'))
162
206
  || agents.find((a) => a.name === 'secretary') || null;
163
207
  setSecretary(sec);
208
+ // Builders = connected big-AI agents the heavy work is dispatched to — anything that is NOT the
209
+ // Secretary (the watcher) and NOT a node-local specialist. These are visually distinct in the brief tray.
210
+ setBuilders(agents.filter((a) => a && a.name !== 'secretary'
211
+ && !(a.tags || []).includes('system:secretary') && !(a.tags || []).includes('system:specialist')));
164
212
  if (!sec) return;
165
- const [dir, cfg] = await Promise.all([
213
+ const [dir, cfg, briefResp] = await Promise.all([
166
214
  apiGet('/v1/agents/secretary/directives').catch(() => null),
167
215
  apiGet('/v1/memory/secretary.config').catch(() => null),
216
+ apiGet('/v1/secretary/briefs').catch(() => null),
168
217
  ]);
218
+ setBriefs((briefResp && briefResp.data && briefResp.data.briefs) || []);
169
219
  const rawCfg = (cfg && cfg.data && cfg.data.value) || null;
170
220
  const { config: normalized, changed } = migrateConfig(rawCfg, dir && dir.data);
171
221
  if (changed) await apiPost('/v1/memory', { key: 'secretary.config', value: normalized, visibility: 'private' }).catch(() => {});
172
222
  setConfig(normalized);
173
223
  }, []);
174
224
 
225
+ // Dispatch a Work Brief to a connected Builder (lane b): queues a task the Builder picks up + runs via
226
+ // the appdev MCP tools. Marks the brief 'dispatched' so the tray reflects "waiting on the Builder".
227
+ const dispatchBrief = useCallback(async (briefId, agentGaii) => {
228
+ if (!briefId || !agentGaii) return;
229
+ setDispatching(briefId);
230
+ try {
231
+ const r = await apiPost(`/v1/secretary/brief/${encodeURIComponent(briefId)}/dispatch`, { targetAgent: agentGaii }).catch(() => null);
232
+ if (r && r.data) { showToast(t('secretary.brief.dispatched')); load(); }
233
+ else showToast(t('secretary.brief.dispatchFailed'), true);
234
+ } finally { setDispatching(''); }
235
+ }, [load, showToast]);
236
+
237
+ // Resolve a Work Brief (the owner handled it themselves, or it's no longer relevant). Marks it 'done'.
238
+ const resolveBrief = useCallback(async (briefId) => {
239
+ if (!briefId) return;
240
+ await apiPost(`/v1/secretary/brief/${encodeURIComponent(briefId)}/done`, {}).catch(() => null);
241
+ load();
242
+ }, [load]);
243
+
175
244
  useEffect(() => { load(); }, [load]);
176
245
 
177
246
  // Re-fetch config on server-reported changes: the autonomous tick writes pending decisions + the
@@ -282,9 +351,14 @@ export default function SecretaryView() {
282
351
  return ids;
283
352
  }, []);
284
353
 
285
- // Give a freshly-created workspace a PROPER manifest (AI-designed object types) so it's immediately
286
- // usable, not an empty "set up workspace" shell. Reuses the proven organism workspace generator.
287
- // Best-effort: a manifest-less workspace still exists and can be set up manually.
354
+ // ── Deterministic workspace structure (NO AI) ──
355
+ // The design JSON's inline manifests routinely fail the workspace validator (wrong namespace form, or
356
+ // missing schemaRef/backing/writeRole), which used to push EACH workspace into a flaky per-workspace AI
357
+ // generator call — owl-alpha failing some of those left workspaces as "set up when opened". Instead we
358
+ // NORMALIZE any inline manifest into a valid one ourselves, and fall back to a plain document space.
359
+ // Zero AI calls → every workspace gets its structure reliably, every time.
360
+ const WS_TYPE_MAP = { text: 'string', number: 'number', date: 'string', select: 'string', boolean: 'boolean', reference: 'string' };
361
+ const wsSlug = (s) => String(s || '').trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'tila';
288
362
  const backfillEnvelope = (mf, wsId, name) => {
289
363
  if (!mf) return;
290
364
  mf.manifestVersion = mf.manifestVersion || '1.0';
@@ -293,25 +367,76 @@ export default function SecretaryView() {
293
367
  mf.kind = mf.kind || 'project';
294
368
  mf.status = ['active', 'paused', 'done', 'archived'].includes(mf.status) ? mf.status : 'active';
295
369
  };
370
+ // Make ANY inline manifest valid: backfill the envelope + each objectType's required fields, normalize
371
+ // the namespace to the required shared.* form (rekeying its schema), and derive a schema for a records
372
+ // type that lacks one. Mutates `manifest`; may add keys to `schemas`.
373
+ const normalizeInlineManifest = (manifest, schemas, wsId, name) => {
374
+ backfillEnvelope(manifest, wsId, name);
375
+ const ots = Array.isArray(manifest.objectTypes) ? manifest.objectTypes : [];
376
+ manifest.objectTypes = ots.filter(Boolean).map((ot, i) => {
377
+ const o = { ...ot };
378
+ o.name = String(o.name || ('tila' + (i + 1))).trim() || ('tila' + (i + 1));
379
+ let ns = String(o.namespace || ('shared.' + wsSlug(o.name)));
380
+ if (!/^(meta|shared|member)\./.test(ns)) {
381
+ const tail = ns.includes('.') ? ns.slice(ns.indexOf('.') + 1) : ns;
382
+ const newNs = 'shared.' + (wsSlug(tail) || wsSlug(o.name));
383
+ if (schemas[o.namespace] && !schemas[newNs]) { schemas[newNs] = schemas[o.namespace]; delete schemas[o.namespace]; }
384
+ ns = newNs;
385
+ }
386
+ o.namespace = ns;
387
+ o.backing = o.backing === 'tasks' ? 'tasks' : 'memory';
388
+ o.writeRole = ['owner', 'admin', 'member'].includes(o.writeRole) ? o.writeRole : 'member';
389
+ o.cardinality = o.cardinality === 'one' ? 'one' : 'many';
390
+ o.versioned = o.versioned !== false;
391
+ o.schemaRef = o.schemaRef || ('schema:' + wsSlug(o.name) + '@1');
392
+ // Document-first: keep an explicit mode as the design gave it; for an UNSPECIFIED space default to a
393
+ // DOCUMENT (flexible information — the common case) unless the design clearly describes a record list
394
+ // (real fields, or a schema already supplied for it). A records space is for same-shaped, list-like
395
+ // data (actions/tasks/bugs/a pipeline/a tracker) only.
396
+ if (o.mode !== 'document' && o.mode !== 'records') {
397
+ o.mode = ((Array.isArray(o.fields) && o.fields.length) || schemas[ns]) ? 'records' : 'document';
398
+ }
399
+ if (o.mode !== 'document' && o.backing === 'memory' && !schemas[ns]) {
400
+ const props = {}; (o.fields || []).forEach((f) => { if (f && f.name) props[f.name] = { type: WS_TYPE_MAP[f.type] || 'string' }; });
401
+ if (!props.id) props.id = { type: 'string' };
402
+ schemas[ns] = { type: 'object', properties: props, required: ['id'] };
403
+ }
404
+ return o;
405
+ });
406
+ // Never-empty guarantee: every workspace needs at least one DOCUMENT space — a free-markdown "thinking
407
+ // home" the setup fill can always author grounded content into, even when the design gave only records
408
+ // spaces (records are real-data-only and legitimately start empty). Append a default document space when
409
+ // the design provided none. Additive + idempotent: only when no document space exists.
410
+ if (!manifest.objectTypes.some((o) => o.mode === 'document')) {
411
+ let dns = 'shared.muistiinpanot';
412
+ if (manifest.objectTypes.some((o) => o.namespace === dns)) dns = 'shared.muistiinpanot-' + wsSlug(name);
413
+ manifest.objectTypes.push({ name: 'Muistiinpanot', namespace: dns, backing: 'memory', writeRole: 'member', cardinality: 'many', versioned: true, schemaRef: 'schema:doc@1', mode: 'document' });
414
+ }
415
+ };
416
+ // A plain document space (free markdown) — the deterministic fallback so a workspace with no usable
417
+ // manifest still gets a valid structure at setup, without any AI call.
418
+ const docSpaceManifest = (wsId, name) => ({
419
+ manifest: { manifestVersion: '1.0', id: wsId, name, kind: 'notes', status: 'active',
420
+ objectTypes: [{ name: 'Dokumentit', namespace: 'shared.dokumentit', backing: 'memory', writeRole: 'member', cardinality: 'many', versioned: true, schemaRef: 'schema:doc@1', mode: 'document' }] },
421
+ schemas: {},
422
+ });
296
423
  const genAndApplyManifest = useCallback(async (orgId, wsId, ws) => {
297
- const name = String(ws.name || '').trim();
424
+ const name = String(ws.name || '').trim() || 'Työtila';
298
425
  try {
299
- // skipExamples: the Secretary fills workspaces with REAL content at setup (author-now), so it never
300
- // wants fake "example-N" placeholder records (they read as disinformation, not examples).
301
- // 1) Use a manifest the design JSON already carried (the prompt-driven path can include it inline).
302
- if (ws.manifest) {
303
- const provided = { manifest: ws.manifest, schemas: ws.schemas || {} };
304
- backfillEnvelope(provided.manifest, wsId, name);
426
+ // skipExamples: the Secretary fills workspaces with REAL content at setup (author-now), never fakes.
427
+ if (ws.manifest && typeof ws.manifest === 'object') {
428
+ const schemas = { ...(ws.schemas || {}) };
429
+ const manifest = JSON.parse(JSON.stringify(ws.manifest));
430
+ normalizeInlineManifest(manifest, schemas, wsId, name);
431
+ const provided = { manifest, schemas };
305
432
  if (!validateGenerated(provided).length) { await applyGeneratedWorkspace(orgId, wsId, provided, { skipExamples: true }); return; }
306
433
  }
307
- // 2) Otherwise (or if the inline one was invalid) design it in-app with the workspace generator.
308
- const generated = parseGenerated(await generateRaw(`${name} — ${ws.purpose || ''}`.trim(), null));
309
- backfillEnvelope(generated && generated.manifest, wsId, name);
310
- if (generated && !validateGenerated(generated).length) await applyGeneratedWorkspace(orgId, wsId, generated, { skipExamples: true });
434
+ // No usable inline manifest → a plain document space, applied deterministically (no AI).
435
+ await applyGeneratedWorkspace(orgId, wsId, docSpaceManifest(wsId, name), { skipExamples: true });
311
436
  } catch { /* leave it manifest-less; the owner can set it up manually */ }
312
437
  }, []);
313
438
 
314
- const applyResult = useCallback(async () => {
439
+ const applyResult = useCallback(async (mode = 'off') => {
315
440
  setApplying(true);
316
441
  try {
317
442
  const json = extractJson(result);
@@ -326,6 +451,8 @@ export default function SecretaryView() {
326
451
  }
327
452
  const newBrain = { purpose: b.purpose, rules };
328
453
  const isNew = hireMode === 'new' || !active;
454
+ setSetupStep({ phase: 'structure' }); // live progress: building the space + workspaces
455
+ let fillTargets = []; // [{ id, name }] of new workspaces to fill one at a time
329
456
 
330
457
  let next;
331
458
  if (isNew) {
@@ -347,6 +474,7 @@ export default function SecretaryView() {
347
474
  }
348
475
  // Design + apply each workspace's manifest in parallel so they come out usable, not empty shells.
349
476
  await Promise.all(created.map((c) => genAndApplyManifest(orgId, c.wsId, c.ws)));
477
+ fillTargets = created.map((c) => ({ id: c.wsId, name: String((c.ws && c.ws.name) || '').trim() || c.wsId }));
350
478
  }
351
479
  // B3: seed the brain-proposed quick actions — active on hire (run verbs are dropped by the sanitizer).
352
480
  const quickActions = sanitizeQuickActions(json.quickActions, 'brain', 'active');
@@ -357,7 +485,10 @@ export default function SecretaryView() {
357
485
  // the owner ticking the linked goals by hand after setup.
358
486
  const goalIds = await seedGoals(json.goals, ctxId, org.name);
359
487
  const strategy = linkMilestoneGoals(normalizeStrategy(json.strategy), goalIds);
360
- const ctx = { id: ctxId, name: org.name, brain: newBrain, organismId: orgId || null, organismName: org.name, workspaces: wsSummary, policy: defaultPolicy(), brainHistory: [], quickActions, strategy };
488
+ // Autonomy chosen in the setup step controls only how active the DAILY tick is (it watches, briefs,
489
+ // and proposes Work Briefs — it never authors content itself, so there is no "fill" band any more).
490
+ const policy = defaultPolicy();
491
+ const ctx = { id: ctxId, name: org.name, brain: newBrain, organismId: orgId || null, organismName: org.name, workspaces: wsSummary, policy, brainHistory: [], quickActions, strategy };
361
492
  next = { contexts: [...contexts, ctx], activeContextId: ctx.id };
362
493
  } else {
363
494
  // Re-run on the active context (reshape): update its brain, snapshot the old one, KEEP its
@@ -391,31 +522,44 @@ export default function SecretaryView() {
391
522
  await persistConfig(next);
392
523
  await syncDirectives(newBrain);
393
524
  window.dispatchEvent(new CustomEvent('aimeat-live-update')); // refresh goals/learning + cards
394
- showToast(t('secretary.hireDone'));
395
- setResult(''); setNeeds(''); setShowHire(false);
396
- // Fill the brand-new workspaces with REAL content right at handshake (documents first), and drop any
397
- // that stay empty ("empty workspace = useless"). A slow model can take a while — long timeout, no retry
398
- // (the server already retries flaky models). Only on a fresh hire, so it never touches existing content.
399
- if (isNew) {
400
- showToast(t('secretary.fillingWorkspaces'));
401
- try {
402
- const r = await api('/v1/secretary/author-now', { method: 'POST', body: JSON.stringify({}), timeoutMs: 1_800_000, retries: 0 });
403
- const d = (r && r.data) || {};
404
- const filled = (d.authored || []).length;
405
- const removed = (d.removedEmptyWorkspaces || []).length;
406
- showToast(`${t('secretary.workspacesFilled')} (${filled})${removed ? ` — ${t('secretary.emptyRemoved')}: ${removed}` : ''}`);
407
- window.dispatchEvent(new CustomEvent('aimeat-live-update'));
408
- } catch (e) {
409
- showToast(`${t('secretary.fillError')}: ${e.message}`, true);
410
- }
525
+ // The Secretary NO LONGER authors content one-shot at handshake (the cheap model hallucinated and went
526
+ // "onto the Builders' turf"). Instead it now proposes grounded Work Briefs for the new (empty)
527
+ // workspaces — heavy work a Builder (a connected big AI via the appdev MCP tools) executes. Fast +
528
+ // deterministic (no slow model call); the dashboard's brief tray then offers "Open in Claude Desktop"
529
+ // / "Send to a connected Builder". Only on a fresh hire, so it never touches existing content.
530
+ if (isNew && fillTargets.length) {
531
+ setSetupStep({ phase: 'fill', done: 0, total: fillTargets.length, name: fillTargets[0].name });
532
+ const r = await api('/v1/secretary/brief', { method: 'POST', body: '{}', timeoutMs: 120000, retries: 1 }).catch(() => null);
533
+ const proposed = (r && r.data && typeof r.data.proposed === 'number') ? r.data.proposed : 0;
534
+ setSetupStep({ phase: 'done', done: fillTargets.length, total: fillTargets.length });
535
+ window.dispatchEvent(new CustomEvent('aimeat-live-update')); // briefs show up in the tray live
536
+ showToast(proposed ? `${t('secretary.briefsReady')} (${proposed})` : t('secretary.hireDone'));
537
+ } else {
538
+ showToast(t('secretary.hireDone'));
539
+ }
540
+ // If the owner chose an autonomous mode, turn the daily check-in ON for them — so it actually keeps
541
+ // working on its own (the band alone does nothing without a schedule). 'off' stays fully manual.
542
+ if (isNew && mode !== 'off') {
543
+ await apiPost('/v1/schedules', { kind: 'secretary', cron: '0 8 * * *', display_name: t('secretary.auto.scheduleName') }).catch(() => {});
411
544
  }
545
+ setResult(''); setNeeds(''); setShowHire(false);
412
546
  } catch (e) {
413
547
  showToast(`${t('secretary.hireError')}: ${e.message}`, true);
414
548
  } finally {
415
- setApplying(false);
549
+ setApplying(false); setSetupStep(null);
416
550
  }
417
551
  }, [result, hireMode, active, contexts, config, persistConfig, syncDirectives, seedGoals, genAndApplyManifest, showToast]);
418
552
 
553
+ // "Set up my Secretary" no longer runs immediately — it first asks (one step, up front) HOW autonomous
554
+ // the owner wants it, explained in plain words, so nobody stumbles into Act bands later. The chosen mode
555
+ // is passed to applyResult. Only for a brand-new hire; re-running an existing context keeps its policy.
556
+ const startSetup = useCallback(() => {
557
+ if (!result.trim()) return;
558
+ try { extractJson(result); } catch { showToast(t('secretary.hireBadShape'), true); return; }
559
+ if (hireMode === 'new' || !active) { setSetupMode('draft'); setChooseMode(true); } else { applyResult('off'); }
560
+ }, [result, hireMode, active, applyResult, showToast]);
561
+ const confirmSetup = useCallback(() => { setChooseMode(false); applyResult(setupMode); }, [applyResult, setupMode]);
562
+
419
563
  // C2 — direct brain editor: edit the active context's purpose + rules in place (no AI re-run).
420
564
  const startBrainEdit = useCallback(() => {
421
565
  if (!active) return;
@@ -811,6 +955,9 @@ ${SECRETARY_AIMEAT_PRIMER}`;
811
955
  persistConfig({ ...config, contexts: contexts.map((c) => (c.id === active.id ? updated : c)) });
812
956
  };
813
957
  const setBand = (capId, band) => updateActivePolicy({ bands: { ...policy.bands, [capId]: band } });
958
+ // One autonomy toggle: 'suggest' (surface only) | 'assist' (handle the small stuff). Sets the band preset
959
+ // underneath, so the band machinery + parity tests are unchanged — the toggle is just the simple surface.
960
+ const setAutonomy = (level) => updateActivePolicy({ bands: autonomyPreset(level) });
814
961
  const toggleStop = () => updateActivePolicy({ stopSpending: !policy.stopSpending });
815
962
  const setBudget = (v) => updateActivePolicy({ dailyMorselBudget: v === '' ? null : Math.max(0, Math.floor(Number(v) || 0)) });
816
963
 
@@ -898,7 +1045,7 @@ ${SECRETARY_AIMEAT_PRIMER}`;
898
1045
  ? html`<div class="sec-empty">${hasOpenRouterKey ? t('secretary.provisioning') : t('secretary.notReady')}</div>`
899
1046
  : html`
900
1047
  ${hired ? contextSwitcher({ contexts, activeId, switchContext, openAdd }) : null}
901
- ${showHirePanel ? hirePanel({ firstEver, hireMode, owner, current: hireMode === 'edit' ? active : null, needs, setNeeds, result, setResult, applying, generating, generateInApp, applyResult, onCancel: cancelHire }) : null}
1048
+ ${showHirePanel ? hirePanel({ firstEver, hireMode, owner, current: hireMode === 'edit' ? active : null, needs, setNeeds, result, setResult, applying, setupStep, generating, generateInApp, applyResult: startSetup, onCancel: cancelHire }) : null}
902
1049
  ${hired && !showHire && active ? html`
903
1050
  ${/* Quick-action row (core verbs; dynamic actions arrive in B3) */ ''}
904
1051
  ${quickActionRow({ items: [
@@ -917,6 +1064,9 @@ ${SECRETARY_AIMEAT_PRIMER}`;
917
1064
  ${/* Today — full-width status band, always on top */ ''}
918
1065
  ${dashStatus({ reliability, budgetInfo, schedule: auto.schedule, lastScan, stale, onReconcile: reconcile, scanning })}
919
1066
 
1067
+ ${/* Heavy work to hand off — the Work Brief tray (the light-vs-heavy seam made physical). */ ''}
1068
+ ${briefTrayCard({ briefs, builders, dispatching, onDispatch: dispatchBrief, onResolve: resolveBrief })}
1069
+
920
1070
  ${/* Customizable two-column dashboard (pin to column / reorder / hide per card; persisted). */ ''}
921
1071
  ${(() => {
922
1072
  const nodes = {
@@ -966,7 +1116,7 @@ ${SECRETARY_AIMEAT_PRIMER}`;
966
1116
  crewSummary: crew.agents && crew.agents.length ? `${crew.agents.length} ${t('secretary.dash.crewAgents')}` : '' })}
967
1117
  ${manageOpen ? html`
968
1118
  ${brainCard({ brain, active, openEdit, brainDraft, setBrainDraft, startBrainEdit, saveBrain, cancelBrainEdit, savingBrain })}
969
- ${operatingCard({ policy, toggleStop, setBudget, setBand })}
1119
+ ${operatingCard({ policy, toggleStop, setBudget, setBand, setAutonomy })}
970
1120
  ${!strategy ? html`<section class="sec-card">
971
1121
  <h2 class="sec-h2">${t('secretary.strat.title')}</h2>
972
1122
  <p class="sec-hint">${t('secretary.strat.enableHint')}</p>
@@ -987,6 +1137,37 @@ ${SECRETARY_AIMEAT_PRIMER}`;
987
1137
  ` : null}
988
1138
  ` : null}`}
989
1139
  ${calEvent ? html`<${CalendarEventDialog} event=${calEvent} onClose=${() => setCalEvent(null)} triggers=${{ update: trig.updateTrigger, togglePause: trig.togglePause, remove: trig.removeTrigger }} workflows=${trig.workflows} specialists=${spec.list} routines=${{ setCadence: next.setRoutineCadence, setStatus: next.setRoutineStatus }} tick=${{ toggle: auto.toggleTick, run: auto.runTick }} feed=${{ remove: auto.deleteFeedItem }} />` : null}
1140
+ ${chooseMode ? html`
1141
+ <div class="sec-setup-overlay" onClick=${(e) => { if (e.target === e.currentTarget) setChooseMode(false); }}>
1142
+ <div class="sec-setup-box sec-mode-box">
1143
+ <h3 class="sec-h2">${t('secretary.mode.title')}</h3>
1144
+ <p class="sec-desc">${t('secretary.mode.intro')}</p>
1145
+ ${[['draft', t('secretary.mode.draftTitle'), t('secretary.mode.draftDesc')], ['act', t('secretary.mode.actTitle'), t('secretary.mode.actDesc')], ['off', t('secretary.mode.offTitle'), t('secretary.mode.offDesc')]].map(([val, title, desc]) => html`
1146
+ <label class=${'sec-mode-opt' + (setupMode === val ? ' sec-mode-opt--on' : '')}>
1147
+ <input type="radio" name="setupmode" checked=${setupMode === val} onChange=${() => setSetupMode(val)} />
1148
+ <span><span class="sec-mode-opt-t">${title}</span><span class="sec-desc">${desc}</span></span>
1149
+ </label>`)}
1150
+ <p class="sec-hint">${t('secretary.mode.changeLater')}</p>
1151
+ <div class="sec-actions">
1152
+ <button class="btn-ghost btn-sm" onClick=${() => setChooseMode(false)}>${t('secretary.cancel')}</button>
1153
+ <button class="btn-primary" onClick=${confirmSetup}>${t('secretary.apply')}</button>
1154
+ </div>
1155
+ </div>
1156
+ </div>` : null}
1157
+ ${applying && setupStep ? (() => {
1158
+ // Top-level setup-progress overlay — stays visible across the hire→dashboard switch (persistConfig
1159
+ // makes the Secretary "exist" before the slow fill, so a panel-local progress view would unmount).
1160
+ const s = setupStep, pct = s.total ? Math.round(((s.done || 0) / s.total) * 100) : null;
1161
+ const msg = s.phase === 'fill'
1162
+ ? `${t('secretary.stepFilling')} ${(s.done || 0) + 1}/${s.total}${s.name ? ' · ' + s.name : ''}`
1163
+ : (s.phase === 'done' ? t('secretary.stepDone') : t('secretary.stepStructure'));
1164
+ return html`<div class="sec-setup-overlay"><div class="sec-setup-box">
1165
+ <h3 class="sec-h2">${t('secretary.settingUpTitle')}</h3>
1166
+ <div class="sec-setup-row"><span class="sec-spin"></span><span>${msg}</span></div>
1167
+ ${pct != null ? html`<div class="sec-progress-bar"><div class="sec-progress-fill" style=${`width:${pct}%`}></div></div>` : null}
1168
+ <p class="sec-desc">${t('secretary.settingUpHint')}</p>
1169
+ </div></div>`;
1170
+ })() : null}
990
1171
  <${ToastContainer} />
991
1172
  </div>`;
992
1173
  }