@monoes/monomindcli 2.12.0 → 2.14.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 (185) hide show
  1. package/.claude/skills/mastermind/SKILL.md +1 -15
  2. package/README.md +1 -1
  3. package/dist/src/commands/doctor-mcp-probe.d.ts +48 -0
  4. package/dist/src/commands/doctor-mcp-probe.d.ts.map +1 -0
  5. package/dist/src/commands/doctor-mcp-probe.js +205 -0
  6. package/dist/src/commands/doctor-mcp-probe.js.map +1 -0
  7. package/dist/src/commands/doctor-monoes-checks.d.ts +1 -0
  8. package/dist/src/commands/doctor-monoes-checks.d.ts.map +1 -1
  9. package/dist/src/commands/doctor-monoes-checks.js +96 -34
  10. package/dist/src/commands/doctor-monoes-checks.js.map +1 -1
  11. package/dist/src/commands/doctor-project-checks.d.ts +31 -1
  12. package/dist/src/commands/doctor-project-checks.d.ts.map +1 -1
  13. package/dist/src/commands/doctor-project-checks.js +109 -4
  14. package/dist/src/commands/doctor-project-checks.js.map +1 -1
  15. package/dist/src/commands/doctor.d.ts.map +1 -1
  16. package/dist/src/commands/doctor.js +21 -3
  17. package/dist/src/commands/doctor.js.map +1 -1
  18. package/dist/src/commands/init.d.ts.map +1 -1
  19. package/dist/src/commands/init.js +29 -0
  20. package/dist/src/commands/init.js.map +1 -1
  21. package/dist/src/commands/mcp.d.ts.map +1 -1
  22. package/dist/src/commands/mcp.js +4 -3
  23. package/dist/src/commands/mcp.js.map +1 -1
  24. package/dist/src/commands/org-observe.d.ts.map +1 -1
  25. package/dist/src/commands/org-observe.js +14 -0
  26. package/dist/src/commands/org-observe.js.map +1 -1
  27. package/dist/src/commands/org.d.ts.map +1 -1
  28. package/dist/src/commands/org.js +21 -4
  29. package/dist/src/commands/org.js.map +1 -1
  30. package/dist/src/commands/ui.d.ts.map +1 -1
  31. package/dist/src/commands/ui.js +4 -0
  32. package/dist/src/commands/ui.js.map +1 -1
  33. package/dist/src/init/claudemd-generator.d.ts.map +1 -1
  34. package/dist/src/init/claudemd-generator.js +2 -1
  35. package/dist/src/init/claudemd-generator.js.map +1 -1
  36. package/dist/src/init/copy-assets.d.ts.map +1 -1
  37. package/dist/src/init/copy-assets.js +56 -18
  38. package/dist/src/init/copy-assets.js.map +1 -1
  39. package/dist/src/init/executor.d.ts.map +1 -1
  40. package/dist/src/init/executor.js +9 -1
  41. package/dist/src/init/executor.js.map +1 -1
  42. package/dist/src/init/mcp-generator.d.ts.map +1 -1
  43. package/dist/src/init/mcp-generator.js +3 -3
  44. package/dist/src/init/mcp-generator.js.map +1 -1
  45. package/dist/src/init/never-commit.d.ts +39 -0
  46. package/dist/src/init/never-commit.d.ts.map +1 -0
  47. package/dist/src/init/never-commit.js +15 -0
  48. package/dist/src/init/never-commit.js.map +1 -0
  49. package/dist/src/init/shared.d.ts +74 -1
  50. package/dist/src/init/shared.d.ts.map +1 -1
  51. package/dist/src/init/shared.js +166 -0
  52. package/dist/src/init/shared.js.map +1 -1
  53. package/dist/src/init/types.d.ts +11 -0
  54. package/dist/src/init/types.d.ts.map +1 -1
  55. package/dist/src/init/types.js.map +1 -1
  56. package/dist/src/init/upgrade.d.ts.map +1 -1
  57. package/dist/src/init/upgrade.js +1 -0
  58. package/dist/src/init/upgrade.js.map +1 -1
  59. package/dist/src/init/write-kimicode.d.ts.map +1 -1
  60. package/dist/src/init/write-kimicode.js +13 -9
  61. package/dist/src/init/write-kimicode.js.map +1 -1
  62. package/dist/src/init/write-opencode.d.ts.map +1 -1
  63. package/dist/src/init/write-opencode.js +44 -2
  64. package/dist/src/init/write-opencode.js.map +1 -1
  65. package/dist/src/init/write-runtime-config.d.ts +2 -0
  66. package/dist/src/init/write-runtime-config.d.ts.map +1 -1
  67. package/dist/src/init/write-runtime-config.js +65 -38
  68. package/dist/src/init/write-runtime-config.js.map +1 -1
  69. package/dist/src/mastermind/manifest-data.d.ts.map +1 -1
  70. package/dist/src/mastermind/manifest-data.js +14 -0
  71. package/dist/src/mastermind/manifest-data.js.map +1 -1
  72. package/dist/src/mcp/monoes-mcp-entry.mjs +86 -0
  73. package/dist/src/mcp-tools/terminal-tools.d.ts.map +1 -1
  74. package/dist/src/mcp-tools/terminal-tools.js +52 -9
  75. package/dist/src/mcp-tools/terminal-tools.js.map +1 -1
  76. package/dist/src/memory/memory-bridge.d.ts +84 -3
  77. package/dist/src/memory/memory-bridge.d.ts.map +1 -1
  78. package/dist/src/memory/memory-bridge.js +215 -17
  79. package/dist/src/memory/memory-bridge.js.map +1 -1
  80. package/dist/src/orgrt/agent-runner.d.ts +38 -0
  81. package/dist/src/orgrt/agent-runner.d.ts.map +1 -1
  82. package/dist/src/orgrt/agent-runner.js +47 -0
  83. package/dist/src/orgrt/agent-runner.js.map +1 -1
  84. package/dist/src/orgrt/checkpoint.d.ts +12 -1
  85. package/dist/src/orgrt/checkpoint.d.ts.map +1 -1
  86. package/dist/src/orgrt/checkpoint.js +15 -6
  87. package/dist/src/orgrt/checkpoint.js.map +1 -1
  88. package/dist/src/orgrt/completion-gate.d.ts +74 -0
  89. package/dist/src/orgrt/completion-gate.d.ts.map +1 -1
  90. package/dist/src/orgrt/completion-gate.js +45 -0
  91. package/dist/src/orgrt/completion-gate.js.map +1 -1
  92. package/dist/src/orgrt/cost-tier.d.ts +144 -0
  93. package/dist/src/orgrt/cost-tier.d.ts.map +1 -0
  94. package/dist/src/orgrt/cost-tier.js +183 -0
  95. package/dist/src/orgrt/cost-tier.js.map +1 -0
  96. package/dist/src/orgrt/cross-org.d.ts.map +1 -1
  97. package/dist/src/orgrt/cross-org.js +14 -0
  98. package/dist/src/orgrt/cross-org.js.map +1 -1
  99. package/dist/src/orgrt/daemon.d.ts +12 -1
  100. package/dist/src/orgrt/daemon.d.ts.map +1 -1
  101. package/dist/src/orgrt/daemon.js +216 -49
  102. package/dist/src/orgrt/daemon.js.map +1 -1
  103. package/dist/src/orgrt/decisions.d.ts +14 -2
  104. package/dist/src/orgrt/decisions.d.ts.map +1 -1
  105. package/dist/src/orgrt/decisions.js +216 -10
  106. package/dist/src/orgrt/decisions.js.map +1 -1
  107. package/dist/src/orgrt/file-roots.d.ts +7 -0
  108. package/dist/src/orgrt/file-roots.d.ts.map +1 -1
  109. package/dist/src/orgrt/file-roots.js +38 -1
  110. package/dist/src/orgrt/file-roots.js.map +1 -1
  111. package/dist/src/orgrt/forwarder.d.ts.map +1 -1
  112. package/dist/src/orgrt/forwarder.js +16 -2
  113. package/dist/src/orgrt/forwarder.js.map +1 -1
  114. package/dist/src/orgrt/idle-deadline.d.ts +74 -1
  115. package/dist/src/orgrt/idle-deadline.d.ts.map +1 -1
  116. package/dist/src/orgrt/idle-deadline.js +52 -2
  117. package/dist/src/orgrt/idle-deadline.js.map +1 -1
  118. package/dist/src/orgrt/loadouts.d.ts +38 -0
  119. package/dist/src/orgrt/loadouts.d.ts.map +1 -0
  120. package/dist/src/orgrt/loadouts.js +129 -0
  121. package/dist/src/orgrt/loadouts.js.map +1 -0
  122. package/dist/src/orgrt/mailbox.d.ts +17 -1
  123. package/dist/src/orgrt/mailbox.d.ts.map +1 -1
  124. package/dist/src/orgrt/mailbox.js +50 -3
  125. package/dist/src/orgrt/mailbox.js.map +1 -1
  126. package/dist/src/orgrt/policy.d.ts +42 -2
  127. package/dist/src/orgrt/policy.d.ts.map +1 -1
  128. package/dist/src/orgrt/policy.js +53 -8
  129. package/dist/src/orgrt/policy.js.map +1 -1
  130. package/dist/src/orgrt/prompt-vars.d.ts +11 -0
  131. package/dist/src/orgrt/prompt-vars.d.ts.map +1 -0
  132. package/dist/src/orgrt/prompt-vars.js +49 -0
  133. package/dist/src/orgrt/prompt-vars.js.map +1 -0
  134. package/dist/src/orgrt/questions.d.ts +22 -1
  135. package/dist/src/orgrt/questions.d.ts.map +1 -1
  136. package/dist/src/orgrt/questions.js +24 -3
  137. package/dist/src/orgrt/questions.js.map +1 -1
  138. package/dist/src/orgrt/review-packet.d.ts +29 -0
  139. package/dist/src/orgrt/review-packet.d.ts.map +1 -0
  140. package/dist/src/orgrt/review-packet.js +67 -0
  141. package/dist/src/orgrt/review-packet.js.map +1 -0
  142. package/dist/src/orgrt/role-sandbox.d.ts.map +1 -1
  143. package/dist/src/orgrt/role-sandbox.js +16 -3
  144. package/dist/src/orgrt/role-sandbox.js.map +1 -1
  145. package/dist/src/orgrt/session-ledger.d.ts +61 -0
  146. package/dist/src/orgrt/session-ledger.d.ts.map +1 -0
  147. package/dist/src/orgrt/session-ledger.js +113 -0
  148. package/dist/src/orgrt/session-ledger.js.map +1 -0
  149. package/dist/src/orgrt/session.d.ts +45 -5
  150. package/dist/src/orgrt/session.d.ts.map +1 -1
  151. package/dist/src/orgrt/session.js +384 -27
  152. package/dist/src/orgrt/session.js.map +1 -1
  153. package/dist/src/orgrt/task-dag.d.ts +34 -1
  154. package/dist/src/orgrt/task-dag.d.ts.map +1 -1
  155. package/dist/src/orgrt/task-dag.js +58 -7
  156. package/dist/src/orgrt/task-dag.js.map +1 -1
  157. package/dist/src/orgrt/tool-spill.d.ts +94 -0
  158. package/dist/src/orgrt/tool-spill.d.ts.map +1 -0
  159. package/dist/src/orgrt/tool-spill.js +180 -0
  160. package/dist/src/orgrt/tool-spill.js.map +1 -0
  161. package/dist/src/orgrt/types.d.ts +138 -0
  162. package/dist/src/orgrt/types.d.ts.map +1 -1
  163. package/dist/src/orgrt/types.js +148 -0
  164. package/dist/src/orgrt/types.js.map +1 -1
  165. package/dist/src/platform-adapters/renderers/mcp.d.ts +20 -2
  166. package/dist/src/platform-adapters/renderers/mcp.d.ts.map +1 -1
  167. package/dist/src/platform-adapters/renderers/mcp.js +25 -5
  168. package/dist/src/platform-adapters/renderers/mcp.js.map +1 -1
  169. package/dist/src/platform-adapters/renderers/skills.d.ts +0 -6
  170. package/dist/src/platform-adapters/renderers/skills.d.ts.map +1 -1
  171. package/dist/src/platform-adapters/renderers/skills.js +12 -22
  172. package/dist/src/platform-adapters/renderers/skills.js.map +1 -1
  173. package/dist/src/protocol-capabilities.d.ts +2 -1
  174. package/dist/src/protocol-capabilities.d.ts.map +1 -1
  175. package/dist/src/protocol-capabilities.js +2 -1
  176. package/dist/src/protocol-capabilities.js.map +1 -1
  177. package/dist/src/ui/dashboard.html +457 -164
  178. package/dist/src/ui/org-hil.mjs +276 -0
  179. package/dist/src/ui/org-runtime.mjs +350 -0
  180. package/dist/src/ui/orgs.html +22 -4
  181. package/dist/src/ui/routes-monoes.mjs +28 -3
  182. package/dist/src/ui/routes-org.mjs +290 -541
  183. package/dist/src/ui/server.mjs +185 -19
  184. package/dist/tsconfig.tsbuildinfo +1 -1
  185. package/package.json +7 -6
@@ -12,10 +12,15 @@ import { StateDetector } from './state-detector.js';
12
12
  * "boss appears hung". */
13
13
  const SILENT_SESSION_MS = 4 * 60_000;
14
14
  const CONTEXT_LIMIT_RE = /context.window.limit|context.length.exceeded|maximum.context/i;
15
+ import { createHash } from 'node:crypto';
15
16
  import { readFileSync } from 'node:fs';
17
+ import { join } from 'node:path';
18
+ import { resolveRoleCostTier } from './cost-tier.js';
19
+ import { expandRolePromptVars, promptVarsFor } from './prompt-vars.js';
16
20
  import { resolveProviderEnv, resolveRoleProvider } from './provider.js';
17
21
  import { resolveRoleGitEnforcement } from './role-sandbox.js';
18
22
  import { loadBuiltinRoleSkill } from './role-skills.js';
23
+ import { ROLE_SESSION_KEY, resolveSessionScope, SessionLedger, taskKeyOf, } from './session-ledger.js';
19
24
  import { DEFAULT_CLAUDE_MODEL, VERCEL_PROVIDERS } from './vercel-providers.js';
20
25
  /**
21
26
  * Resolves the extra system-prompt block for a role: built-in archetype
@@ -189,6 +194,14 @@ endpointBriefing) {
189
194
  .filter(Boolean)
190
195
  .join('\n\n');
191
196
  }
197
+ /** The system prompt one session of this role is built with. */
198
+ function rolePromptFor(opts) {
199
+ return buildRolePrompt(expandRolePromptVars(opts.role, promptVarsFor(opts.orgRoot ?? opts.cwd)), (opts.def ?? { name: opts.org, goal: '' }), opts.def?.roles.map((r) => r.id) ?? [opts.role.id], opts.glossary,
200
+ // D7: the loadout's text follows the role's own guidance. With no
201
+ // loadout this is exactly resolveRoleExtraGuidance(role), as before.
202
+ [resolveRoleExtraGuidance(opts.role), opts.loadout?.guidance].filter(Boolean).join('\n\n') ||
203
+ undefined, opts.onComplete ? endpointBriefingLines(opts.def) : undefined);
204
+ }
192
205
  /**
193
206
  * Runs a role for the life of the org, transparently restarting the
194
207
  * underlying SDK session whenever it ends on its own (`maxTurns` reached)
@@ -248,23 +261,138 @@ async function runAgentSessionLoop(opts) {
248
261
  // SDK session id here (it must outlive individual runOneSession calls,
249
262
  // since resume continues the same billing session) and emit only deltas.
250
263
  const sessionCostTotals = new Map();
264
+ // ADR-O001 D1: modelUsage is cumulative per session exactly like
265
+ // total_cost_usd, so it needs the same prev-value map to become a delta.
266
+ const sessionTokenTotals = new Map();
267
+ // ADR-O001 D3. 'role' scope (the default) keeps the pre-D3 loop exactly: one
268
+ // model session for the role's life, resumed across maxTurns restarts via
269
+ // resumeSessionId. 'task' scope keys model sessions by the task a message
270
+ // belongs to: the process exits at a task boundary (or on idle) and the next
271
+ // one resumes that task's session from the ledger. Either way every session
272
+ // run is recorded with its session id before and after.
273
+ const scope = resolveSessionScope(opts.role, opts.def);
274
+ const idleExitMs = opts.def?.run_config
275
+ ?.session_idle_exit_ms;
276
+ const ledger = opts.sessionLedger ?? new SessionLedger();
277
+ const runtimeKey = opts.role.runtime ?? opts.def?.runtime ?? 'claude';
278
+ let taskKey = ROLE_SESSION_KEY;
279
+ // Why the next fresh session for a key is fresh, when the loop itself threw
280
+ // the record away (stale resume, turn-limit error) — recorded, not guessed.
281
+ const droppedBecause = new Map();
282
+ const staleTried = new Set();
283
+ // The options THIS session is built from — the role's own, except that a
284
+ // task-scoped session carries its task's loadout (D7).
285
+ let sessionOpts = opts;
286
+ let promptHash = '*';
251
287
  // Always run at least once: a mailbox can be closed with queued items still
252
288
  // pending (stream() drains the queue before honoring `closed`), which is a
253
289
  // normal, valid starting state - checking isClosed before the first run
254
290
  // would skip that drain entirely.
255
291
  while (true) {
292
+ // Opt-in only: keep the process DOWN until there is mail, instead of
293
+ // starting a query() that parks on an empty mailbox. waitForMessage()
294
+ // still returns true for a closed mailbox with queued items.
295
+ if (scope !== 'role' || idleExitMs !== undefined) {
296
+ if (!(await mailbox.waitForMessage()))
297
+ return;
298
+ }
299
+ let startReason;
300
+ if (scope === 'cold') {
301
+ // D6: nothing carries over — not a checkpointed session, not the last
302
+ // message's. Paying the cache miss here is the point.
303
+ resumeSessionId = undefined;
304
+ startReason = 'fresh-cold';
305
+ }
306
+ else if (scope === 'task') {
307
+ // An untagged message (mail, an answer, a continuation) belongs to the
308
+ // session the role is already in.
309
+ taskKey = taskKeyOf(mailbox.peek() ?? '') ?? taskKey;
310
+ // D7: this task's session is built with this task's loadout.
311
+ sessionOpts =
312
+ taskKey !== ROLE_SESSION_KEY && opts.loadoutFor
313
+ ? { ...opts, loadout: opts.loadoutFor(taskKey) }
314
+ : opts;
315
+ promptHash = createHash('sha256')
316
+ .update(rolePromptFor(sessionOpts))
317
+ .digest('hex')
318
+ .slice(0, 16);
319
+ const pick = ledger.resumeFor({
320
+ role: opts.role.id,
321
+ runtime: runtimeKey,
322
+ taskKey,
323
+ cwd: opts.cwd,
324
+ promptHash,
325
+ });
326
+ resumeSessionId = pick.sessionId;
327
+ startReason =
328
+ pick.reason === 'fresh-no-record'
329
+ ? (droppedBecause.get(taskKey) ?? pick.reason)
330
+ : pick.reason;
331
+ }
332
+ else {
333
+ startReason = resumeSessionId ? 'resumed' : 'fresh-no-record';
334
+ }
335
+ const sessionKey = taskKey;
336
+ const streamOpts = scope === 'cold'
337
+ ? { stopBefore: () => true, idleExitMs }
338
+ : scope === 'task'
339
+ ? {
340
+ stopBefore: (next) => {
341
+ const k = taskKeyOf(next);
342
+ return k !== undefined && k !== sessionKey;
343
+ },
344
+ idleExitMs,
345
+ }
346
+ : idleExitMs !== undefined
347
+ ? { idleExitMs }
348
+ : undefined;
349
+ const sessionIdBefore = resumeSessionId;
350
+ const startedAt = Date.now();
351
+ const recordRun = (after, error) => {
352
+ const run = ledger.recordRun({
353
+ role: opts.role.id,
354
+ runtime: runtimeKey,
355
+ taskKey: sessionKey,
356
+ sessionIdBefore,
357
+ sessionIdAfter: after,
358
+ reason: startReason,
359
+ startedAt,
360
+ endedAt: Date.now(),
361
+ ...(error ? { error } : {}),
362
+ });
363
+ opts.bus.emit({
364
+ type: 'audit',
365
+ from: opts.role.id,
366
+ reason: 'session-run',
367
+ msg: `session ${run.resumed ? 'resumed' : 'started fresh'} (${startReason}) for ${sessionKey}`,
368
+ data: run,
369
+ });
370
+ };
256
371
  const realBefore = mailbox.consumedRealCount;
257
372
  let sessionId;
258
373
  let hitTurnLimit = false;
259
374
  const attempt = { replied: false };
260
375
  try {
261
- const res = await runOneSession(opts, resumeSessionId, sessionCostTotals, attempt);
376
+ const res = await runOneSession(sessionOpts, resumeSessionId, sessionCostTotals, attempt, sessionTokenTotals, streamOpts);
262
377
  sessionId = res.sessionId;
263
378
  hitTurnLimit = res.hitTurnLimit;
264
379
  resumeSessionId = sessionId;
380
+ recordRun(sessionId);
381
+ if (scope === 'task' && sessionId) {
382
+ droppedBecause.delete(sessionKey);
383
+ ledger.set({
384
+ role: opts.role.id,
385
+ runtime: runtimeKey,
386
+ taskKey: sessionKey,
387
+ cwd: opts.cwd,
388
+ promptHash,
389
+ sessionId,
390
+ });
391
+ }
265
392
  }
266
393
  catch (err) {
267
394
  const errMsg = err instanceof Error ? err.message : String(err);
395
+ recordRun(undefined, errMsg);
268
396
  // #304 (review round 3): an org's own stop aborts whatever this attempt
269
397
  // was doing — max-turns and stale-resume below are diagnoses for a
270
398
  // genuinely failed attempt, not for one the org itself just cut off.
@@ -285,8 +413,36 @@ async function runAgentSessionLoop(opts) {
285
413
  sessionId = undefined;
286
414
  resumeSessionId = undefined;
287
415
  hitTurnLimit = true;
416
+ if (scope === 'task') {
417
+ ledger.drop({ role: opts.role.id, runtime: runtimeKey, taskKey: sessionKey });
418
+ droppedBecause.set(sessionKey, 'fresh-after-turn-limit');
419
+ }
288
420
  }
289
421
  else if (!stopping &&
422
+ scope === 'task' &&
423
+ sessionIdBefore !== undefined &&
424
+ !staleTried.has(sessionKey) &&
425
+ !attempt.replied) {
426
+ // D3's per-key form of #149 below: a recorded session that fails
427
+ // before replying is treated as expired — forget it and retry that
428
+ // task fresh once. Anything it had already pulled goes back first.
429
+ staleTried.add(sessionKey);
430
+ ledger.drop({ role: opts.role.id, runtime: runtimeKey, taskKey: sessionKey });
431
+ droppedBecause.set(sessionKey, 'fresh-after-stale-resume');
432
+ mailbox.reclaimInFlight();
433
+ sessionId = undefined;
434
+ resumeSessionId = undefined;
435
+ hitTurnLimit = false;
436
+ opts.bus.emit({
437
+ type: 'status',
438
+ from: opts.role.id,
439
+ reason: 'resume-session-stale',
440
+ msg: `agent "${opts.role.id}" could not resume its session for ${sessionKey} — retrying with a fresh session`,
441
+ data: { error: errMsg },
442
+ });
443
+ }
444
+ else if (!stopping &&
445
+ scope === 'role' &&
290
446
  resumeSessionId &&
291
447
  resumeSessionId === initialResumeSessionId &&
292
448
  !triedFreshAfterResumeFailure &&
@@ -357,6 +513,17 @@ async function runAgentSessionLoop(opts) {
357
513
  });
358
514
  }
359
515
  }
516
+ else if (mailbox.lastStreamEnd) {
517
+ // D3: the process ended on purpose — a task boundary or idle — and the
518
+ // model session is kept for the next wake.
519
+ opts.bus.emit({
520
+ type: 'status',
521
+ from: opts.role.id,
522
+ reason: 'session-cycled',
523
+ msg: `process cycled (${mailbox.lastStreamEnd}); model session kept for ${sessionKey}`,
524
+ data: { taskKey: sessionKey, end: mailbox.lastStreamEnd, sessionId },
525
+ });
526
+ }
360
527
  else {
361
528
  opts.bus.emit({
362
529
  type: 'status',
@@ -366,11 +533,68 @@ async function runAgentSessionLoop(opts) {
366
533
  }
367
534
  }
368
535
  }
536
+ /** ADR-O001 D1 — token-metering helpers.
537
+ *
538
+ * `cache_read_input_tokens` and `cache_creation_input_tokens` are siblings
539
+ * of `input_tokens` in the Anthropic API, not subsets of it, and both are
540
+ * billable. Everything below therefore sums all four. */
541
+ function totalTokens(u) {
542
+ return u.input + u.output + u.cacheRead + u.cacheCreation;
543
+ }
544
+ function addTo(target, add) {
545
+ target.input += add.input;
546
+ target.output += add.output;
547
+ target.cacheRead += add.cacheRead;
548
+ target.cacheCreation += add.cacheCreation;
549
+ }
550
+ /** One model turn's own usage, off an 'assistant' (or per-turn 'result')
551
+ * message. */
552
+ function turnBreakdown(m) {
553
+ return {
554
+ input: m.input_tokens ?? 0,
555
+ output: m.output_tokens ?? 0,
556
+ cacheRead: m.cache_read_input_tokens ?? 0,
557
+ cacheCreation: m.cache_creation_input_tokens ?? 0,
558
+ };
559
+ }
560
+ /** What a 'result' message says this mailbox message consumed.
561
+ *
562
+ * When the runner reports `cumulative_tokens` (the Claude SDK's whole-pipeline
563
+ * `modelUsage`, which unlike `usage` includes Task subagents and sidechains),
564
+ * that value is CUMULATIVE per session — the same lifecycle as
565
+ * `total_cost_usd` — so it is converted to a delta against the previous value
566
+ * for the same session_id. A fresh/restarted session has no prior entry and
567
+ * correctly yields its full value; a value that ticks down (a provider-side
568
+ * correction) floors at 0 rather than re-adding the whole cumulative total.
569
+ * Without `cumulative_tokens` the per-turn fields are used as before. */
570
+ function resultBreakdown(m, tokenTotals, sid) {
571
+ const cum = m.cumulative_tokens;
572
+ if (!cum)
573
+ return turnBreakdown(m);
574
+ const now = {
575
+ input: cum.input,
576
+ output: cum.output,
577
+ cacheRead: cum.cache_read,
578
+ cacheCreation: cum.cache_creation,
579
+ };
580
+ if (!tokenTotals)
581
+ return now;
582
+ const prev = tokenTotals.get(sid);
583
+ tokenTotals.set(sid, now);
584
+ if (!prev)
585
+ return now;
586
+ return {
587
+ input: Math.max(0, now.input - prev.input),
588
+ output: Math.max(0, now.output - prev.output),
589
+ cacheRead: Math.max(0, now.cacheRead - prev.cacheRead),
590
+ cacheCreation: Math.max(0, now.cacheCreation - prev.cacheCreation),
591
+ };
592
+ }
369
593
  /** One bounded SDK session for a role; resolves with the SDK's session_id (for
370
594
  * resuming on restart) and whether it ended by hitting the turn limit (so the
371
595
  * caller can push a continuation) when the stream ends (mailbox closed or
372
596
  * maxTurns reached). */
373
- async function runOneSession(opts, resume, costTotals, progress) {
597
+ async function runOneSession(opts, resume, costTotals, progress, tokenTotals, streamOpts) {
374
598
  const { org, role, bus, policy, mailbox, cwd } = opts;
375
599
  // Read lastMessageId live from opts instead of capturing at session start
376
600
  // This ensures chat responses link to the most recent message delivered
@@ -389,7 +613,24 @@ async function runOneSession(opts, resume, costTotals, progress) {
389
613
  // The named provider's default model fills in adapter_config.model when the
390
614
  // role didn't pin one.
391
615
  const prov = resolveRoleProvider(role, opts.orgRoot ?? opts.cwd);
616
+ // ADR-O001 D8: the role's cost tier, when the org declares one. Resolved
617
+ // here — the single choke point where a role's model is decided — so the
618
+ // documented precedence holds in exactly one place:
619
+ // explicit adapter_config.model > tier > named-provider default > runtime
620
+ // The tier's EFFORT is applied even when the model came from an explicit
621
+ // pin: which model to run and how hard to think are separate axes, and
622
+ // silently dropping the effort because a model was pinned would be the
623
+ // "silent downgrade" this decision exists to prevent.
624
+ // Throws (fails the session) rather than guessing when the tier has no
625
+ // entry for this role's provider — daemon.ts validates the whole roster
626
+ // up front so that is normally caught before any token is spent.
627
+ const tier = resolveRoleCostTier({
628
+ role,
629
+ def: opts.def,
630
+ vendor: role.provider?.vendor ?? prov.cfg?.vendor,
631
+ });
392
632
  const model = role.adapter_config?.model ??
633
+ tier?.model ??
393
634
  prov.defaultModel ??
394
635
  resolveModel(role, role.runtime, role.provider?.vendor ?? prov.cfg?.vendor);
395
636
  bus.emit({ type: 'status', from: role.id, msg: 'session starting' });
@@ -401,7 +642,7 @@ async function runOneSession(opts, resume, costTotals, progress) {
401
642
  // time a 'result' message ends one mailbox message and the next one starts.
402
643
  // Exists purely so the 'result' branch never re-adds what this branch
403
644
  // already added (see there for why it can't just always add).
404
- let messageTurnTokens = 0;
645
+ let messageTurnTokens = { input: 0, output: 0, cacheRead: 0, cacheCreation: 0 };
405
646
  // Abort hook for the runner (AgentRunArgs.signal): the silent-stream
406
647
  // abort below used to call iterator.return() only, which queues behind a
407
648
  // subprocess runner blocked in `for await (child.stdout)` — the child was
@@ -438,12 +679,18 @@ async function runOneSession(opts, resume, costTotals, progress) {
438
679
  });
439
680
  const stream = runner.run({
440
681
  tools,
441
- prompt: mailbox.stream(),
442
- systemPrompt: buildRolePrompt(role, (opts.def ?? { name: org, goal: '' }), opts.def?.roles.map((r) => r.id) ?? [role.id], opts.glossary, resolveRoleExtraGuidance(role), opts.onComplete ? endpointBriefingLines(opts.def) : undefined),
682
+ // No options = the pre-D3 stream, exactly.
683
+ prompt: streamOpts ? mailbox.stream('', streamOpts) : mailbox.stream(),
684
+ systemPrompt: rolePromptFor(opts),
443
685
  model,
444
686
  cwd,
687
+ effort: tier?.effort,
445
688
  env: {
446
689
  ...resolveProviderEnv(prov.cfg),
690
+ // D8: how a NON-Claude provider expresses the tier's effort level.
691
+ // Empty for Claude (handled natively by ClaudeAgentRunner) and for a
692
+ // provider that declares no mechanism — which simply ignores effort.
693
+ ...(tier?.env ?? {}),
447
694
  // Custom-endpoint providers (named-provider path): pin the engine's
448
695
  // model env so background/haiku tasks also route to the endpoint's
449
696
  // model instead of erroring on an Anthropic-only default.
@@ -475,6 +722,13 @@ async function runOneSession(opts, resume, costTotals, progress) {
475
722
  maxTurns: opts.maxTurns ?? 30,
476
723
  resume,
477
724
  claudeRestrictions: gitEnforcement.claudeRestrictions,
725
+ // ADR-O001 D2: tool results are 76% of a role's context mass and nothing
726
+ // bounded them. Under the ORG STATE dir (never the workspace cwd, which
727
+ // may be the repo), and under orgRoot — which file-roots.ts already
728
+ // makes readable to the role's file tools and role-sandbox.ts already
729
+ // makes readable to Bash — so the path in the digest actually resolves
730
+ // when the role decides it needs the full text.
731
+ toolSpillDir: join(opts.orgDir ?? opts.cwd, 'tool-results', role.id.replace(/[^a-zA-Z0-9_.-]/g, '_')),
478
732
  canUseTool: gatedCanUseTool(policy, opts.beforeTool, role.id, opts.fence, opts.onDecision
479
733
  ? (toolName, _input, decision, kind) => opts.onDecision?.(role.id, toolName, decision.message ?? 'denied', kind)
480
734
  : undefined, opts.hasPendingGate),
@@ -624,10 +878,18 @@ async function runOneSession(opts, resume, costTotals, progress) {
624
878
  // .d.ts, which puts `usage`/token counts on BetaMessage but cost only
625
879
  // on SDKResultSuccess.total_cost_usd/modelUsage. overBudgetUsd is
626
880
  // still checked below, once per message, same as before this fix.)
627
- const turnTokens = (m.input_tokens ?? 0) + (m.output_tokens ?? 0);
881
+ //
882
+ // ADR-O001 D1: the sum must include BOTH cache fields. They are
883
+ // siblings of input_tokens in the Anthropic API, not subsets of it —
884
+ // `input_tokens` is the uncached remainder — and both are billable
885
+ // (~0.1x and ~1.25x input). Omitting them meant the better the cache
886
+ // worked the less the meter saw: on one measured run, 2,765M tokens
887
+ // billed against 8.1M recorded, with input_tokens at 0.0M.
888
+ const turn = turnBreakdown(m);
889
+ const turnTokens = totalTokens(turn);
628
890
  if (turnTokens > 0) {
629
- messageTurnTokens += turnTokens;
630
- policy.addUsage(turnTokens);
891
+ addTo(messageTurnTokens, turn);
892
+ policy.addTokenUsage(turn);
631
893
  if (policy.overBudget) {
632
894
  bus.emit({
633
895
  type: 'status',
@@ -664,20 +926,48 @@ async function runOneSession(opts, resume, costTotals, progress) {
664
926
  });
665
927
  }
666
928
  else if (m.type === 'result') {
667
- const tokens = (m.input_tokens ?? 0) + (m.output_tokens ?? 0);
929
+ // ADR-O001 D1: prefer the SDK's `modelUsage` over `usage`. The SDK
930
+ // documents `usage` as "MAIN AGENT LOOP ONLY — excludes Task
931
+ // subagent, sidechain, and auxiliary model calls ... Prefer
932
+ // modelUsage for token/cost accounting"; the measured run made 46
933
+ // subagent calls this counter never saw. modelUsage is CUMULATIVE per
934
+ // session (same lifecycle as total_cost_usd, per its own type doc),
935
+ // so it is converted to a delta here rather than added, exactly as
936
+ // cost is below. A runner that reports no modelUsage falls back to
937
+ // the per-turn `usage` fields, which keep their old semantics.
938
+ const resultTokens = resultBreakdown(m, tokenTotals, m.session_id ?? sessionId ?? '');
668
939
  // Per the SDK's own type docs, a 'result' message's usage is that
669
940
  // message's own (effectively last-turn) usage in streaming-input mode,
670
941
  // NOT a cumulative total across every turn of the mailbox message —
671
942
  // and that last turn was already counted above via its own 'assistant'
672
943
  // message, specifically so overBudget could trip mid-message. Adding
673
- // `tokens` again here unconditionally would double-count it. Only make
944
+ // the result's own usage again unconditionally would double-count it.
945
+ // (A modelUsage-derived delta is per-session-cumulative, so the same
946
+ // subtraction is exactly right there too: it removes what the
947
+ // assistant turns of THIS message already contributed and leaves the
948
+ // subagent/auxiliary volume the main loop never reported.) Only make
674
949
  // up the shortfall (never negative) so a turn whose usage somehow
675
950
  // never reached the 'assistant' branch (e.g. a runner/test double that
676
951
  // doesn't emit per-turn usage) still gets counted at least once.
677
- const shortfall = Math.max(0, tokens - messageTurnTokens);
678
- if (shortfall > 0)
679
- policy.addUsage(shortfall);
680
- messageTurnTokens = 0;
952
+ const shortfall = {
953
+ input: Math.max(0, resultTokens.input - messageTurnTokens.input),
954
+ output: Math.max(0, resultTokens.output - messageTurnTokens.output),
955
+ cacheRead: Math.max(0, resultTokens.cacheRead - messageTurnTokens.cacheRead),
956
+ cacheCreation: Math.max(0, resultTokens.cacheCreation - messageTurnTokens.cacheCreation),
957
+ };
958
+ if (totalTokens(shortfall) > 0)
959
+ policy.addTokenUsage(shortfall);
960
+ // What this whole mailbox message actually added to the meter: the
961
+ // per-turn accounting above plus whatever the result topped up. This
962
+ // is what the 'usage' event reports, so a consumer summing events
963
+ // lands on the same number as policy.usage.
964
+ const messageTokens = {
965
+ input: messageTurnTokens.input + shortfall.input,
966
+ output: messageTurnTokens.output + shortfall.output,
967
+ cacheRead: messageTurnTokens.cacheRead + shortfall.cacheRead,
968
+ cacheCreation: messageTurnTokens.cacheCreation + shortfall.cacheCreation,
969
+ };
970
+ messageTurnTokens = { input: 0, output: 0, cacheRead: 0, cacheCreation: 0 };
681
971
  // Convert the SDK's cumulative-per-session total_cost_usd into a
682
972
  // per-result delta before emitting - downstream sums usage events.
683
973
  // costTotals is keyed by session_id, so a genuinely new/restarted
@@ -701,7 +991,19 @@ async function runOneSession(opts, resume, costTotals, progress) {
701
991
  bus.emit({
702
992
  type: 'usage',
703
993
  from: role.id,
704
- data: { tokens, cost_usd: costDelta, subtype: m.subtype },
994
+ // ADR-O001 D1: the four quantities travel separately so every
995
+ // downstream consumer (forwarder → dashboard state.json, reporting,
996
+ // `org costs`) can record real values instead of the 0s they used
997
+ // to persist. `tokens` stays the single billable total.
998
+ data: {
999
+ tokens: totalTokens(messageTokens),
1000
+ cost_usd: costDelta,
1001
+ subtype: m.subtype,
1002
+ tokens_in: messageTokens.input,
1003
+ tokens_out: messageTokens.output,
1004
+ cache_read: messageTokens.cacheRead,
1005
+ cache_creation: messageTokens.cacheCreation,
1006
+ },
705
1007
  });
706
1008
  if (m.subtype && m.subtype !== 'success') {
707
1009
  if (m.subtype === 'error_max_turns')
@@ -787,6 +1089,13 @@ async function runOneSession(opts, resume, costTotals, progress) {
787
1089
  providerSet?.close();
788
1090
  }
789
1091
  }
1092
+ /** ADR-O001 D7: org_task's description suffix for an org with a catalog. */
1093
+ function loadoutHelp(catalog) {
1094
+ const list = catalog
1095
+ .map((l) => (l.description ? `${l.name} (${l.description})` : l.name))
1096
+ .join(', ');
1097
+ return ` Optionally select a "loadout" — the named, stable specialisation the assignee's session is built with: ${list}. Select by kind of work; put everything specific to this task (which diff, criteria, what failed last time) in the title or a message, not in the choice of loadout. The selection is recorded on the task and reused on every retry.`;
1098
+ }
790
1099
  /** Build the org tool surface as platform-agnostic OrgToolDef[]. The handlers
791
1100
  * close over sessionOpts callbacks (deliver, recall, remember, …) — same
792
1101
  * wiring as the previous inline createSdkMcpServer block, just decoupled from
@@ -912,22 +1221,64 @@ export function buildOrgTools(opts) {
912
1221
  handler: async (args) => text(await onGate(role.id, args.name, args.description)),
913
1222
  });
914
1223
  }
1224
+ // ADR-O001 D7: the `loadout` argument exists only for an org with a
1225
+ // catalog, so every other org's tool list stays byte-identical.
1226
+ const catalog = opts.loadoutCatalog?.length ? opts.loadoutCatalog : undefined;
1227
+ const loadoutArg = catalog
1228
+ ? { loadout: z.enum(catalog.map((l) => l.name)).optional() }
1229
+ : {};
915
1230
  const createTask = opts.createTask;
916
1231
  if (createTask) {
917
1232
  tools.push({
918
1233
  name: 'org_task',
919
- description: 'Create a task in the DAG with optional dependencies. Dependencies must be existing task IDs. Tasks become ready when all deps are done, then get dispatched to the assignee.',
920
- schema: { title: z.string(), assignee: z.string(), deps: z.array(z.string()).default([]) },
921
- handler: async (args) => text(createTask(role.id, args.title, args.assignee, args.deps ?? [])),
1234
+ description: 'Create a task in the DAG with optional dependencies. Dependencies must be existing task IDs. Tasks become ready when all deps are done, then get dispatched to the assignee.' +
1235
+ (catalog ? loadoutHelp(catalog) : ''),
1236
+ schema: {
1237
+ title: z.string(),
1238
+ assignee: z.string(),
1239
+ deps: z.array(z.string()).default([]),
1240
+ ...loadoutArg,
1241
+ },
1242
+ handler: async (args) => text(createTask(role.id, args.title, args.assignee, args.deps ?? [], args.loadout)),
922
1243
  });
923
1244
  }
924
1245
  const completeTask = opts.completeTask;
925
1246
  if (completeTask) {
926
1247
  tools.push({
927
1248
  name: 'org_task_done',
928
- description: 'Mark a task as completed and optionally provide a result summary. Any downstream tasks whose deps are now all done will become ready and be dispatched.',
929
- schema: { taskId: z.string(), result: z.string().optional() },
930
- handler: async (args) => text(completeTask(role.id, args.taskId, args.result)),
1249
+ description: opts.requireTaskEvidence
1250
+ ? 'Mark a task as completed. This org requires EVIDENCE (run_config.completion_evidence): pass `evidence` with the current commit sha and one entry per acceptance criterion — the command you actually ran, its real exit code, and its output. Evidence pinned to an older commit is stale and will be refused, and a refused completion puts the task back in your queue with the reason — but only up to run_config.max_evidence_attempts times (default 3), after which the task is recorded as failed and escalated to the boss instead of returned to you. Any downstream tasks whose deps are now all done become ready and are dispatched.'
1251
+ : 'Mark a task as completed and optionally provide a result summary. Any downstream tasks whose deps are now all done will become ready and be dispatched.',
1252
+ schema: {
1253
+ taskId: z.string(),
1254
+ result: z.string().optional(),
1255
+ evidence: z
1256
+ .object({
1257
+ headSha: z.string(),
1258
+ checks: z
1259
+ .array(z.object({
1260
+ command: z.string(),
1261
+ exitCode: z.number().int(),
1262
+ output: z.string().optional(),
1263
+ }))
1264
+ .default([]),
1265
+ })
1266
+ .optional(),
1267
+ },
1268
+ handler: async (args) => text(completeTask(role.id, args.taskId, args.result, args.evidence)),
1269
+ });
1270
+ }
1271
+ const requestReview = opts.requestReview;
1272
+ if (requestReview) {
1273
+ tools.push({
1274
+ name: 'org_review',
1275
+ description: "Ask an artifact-only reviewer for a verdict on a task. You pass ids only: the runtime builds the review from the task's text, its assignee's latest org_task_done evidence (commands, exit codes, output) and its own git diff of base...headSha — nothing you write is added, so there is no summary to give. The reviewer starts cold every time and replies to you with org_send. Refused if the task has no evidence yet.",
1276
+ schema: {
1277
+ taskId: z.string(),
1278
+ reviewer: z.string(),
1279
+ base: z.string().optional().describe("git ref to diff against (default 'main')"),
1280
+ },
1281
+ handler: async (args) => text(requestReview(role.id, args.taskId, args.reviewer, args.base)),
931
1282
  });
932
1283
  }
933
1284
  const listTasks = opts.listTasks;
@@ -982,7 +1333,8 @@ export function buildOrgTools(opts) {
982
1333
  if (planGraph) {
983
1334
  tools.push({
984
1335
  name: 'org_plan_graph',
985
- description: 'Propose a full work graph in one call. Each task spec uses a local "name" and references other specs by name in "after".',
1336
+ description: 'Propose a full work graph in one call. Each task spec uses a local "name" and references other specs by name in "after".' +
1337
+ (catalog ? ' Each spec may select a "loadout" exactly as org_task does.' : ''),
986
1338
  schema: {
987
1339
  tasks: z
988
1340
  .array(z.object({
@@ -990,11 +1342,11 @@ export function buildOrgTools(opts) {
990
1342
  title: z.string(),
991
1343
  assignee: z.string(),
992
1344
  after: z.array(z.string()).default([]),
1345
+ ...loadoutArg,
993
1346
  }))
994
1347
  .min(1),
995
1348
  },
996
- handler: async (args) => text(planGraph(role.id, args.tasks ??
997
- [])),
1349
+ handler: async (args) => text(planGraph(role.id, args.tasks ?? [])),
998
1350
  });
999
1351
  }
1000
1352
  tools.push({
@@ -1015,12 +1367,17 @@ export function buildOrgTools(opts) {
1015
1367
  });
1016
1368
  tools.push({
1017
1369
  name: 'ask_human',
1018
- description: 'Ask a human a free-form question and pause for their answer. Use only when you genuinely need human judgment.',
1019
- schema: { question: z.string() },
1370
+ description: 'Ask a human a free-form question. Use only when you genuinely need human judgment. ' +
1371
+ 'Set blocking: true ONLY if you cannot continue until it is answered — a blocking question pauses the ' +
1372
+ "org's idle watchdog (for up to an hour; after that the run resumes its normal idle checks either way). " +
1373
+ 'If you can keep working while you wait — an FYI, a preference, anything you would describe as "not blocking on this" — ' +
1374
+ 'pass blocking: false and carry on; the question is still recorded and answered, it just does not freeze the run. ' +
1375
+ 'Defaults to blocking.',
1376
+ schema: { question: z.string(), blocking: z.boolean().optional() },
1020
1377
  handler: async (args) => {
1021
1378
  if (!opts.askHuman)
1022
1379
  return text('ask_human is not available in this session');
1023
- const receipt = await opts.askHuman(role.id, args.question);
1380
+ const receipt = await opts.askHuman(role.id, args.question, args.blocking);
1024
1381
  return text(receipt);
1025
1382
  },
1026
1383
  });