@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
@@ -25,9 +25,10 @@ import { fileToolRoots } from './file-roots.js';
25
25
  import { attachForwarder } from './forwarder.js';
26
26
  import { GrokAgentRunner } from './grok-runner.js';
27
27
  import { HermesAgentRunner } from './hermes-runner.js';
28
- import { clearIdleRecord, projectIdleStop, writeIdleRecord, } from './idle-deadline.js';
28
+ import { advanceHold, clearIdleRecord, noProgressRoles, projectIdleStop, writeIdleRecord, } from './idle-deadline.js';
29
29
  import { drainInbox, newMessageId, queueMessage } from './inbox.js';
30
30
  import { KimiCodeAgentRunner } from './kimicode-runner.js';
31
+ import { loadoutCatalog, resolveLoadout, sessionLoadoutFor, taskTag } from './loadouts.js';
31
32
  import { isRecoverableCloseReason, Mailbox } from './mailbox.js';
32
33
  import { OpencodeAgentRunner } from './opencode-runner.js';
33
34
  import * as orgMemory from './org-memory.js';
@@ -43,6 +44,7 @@ import { buildRespawnReceipt, computeReplacementBudget, mergeEffectiveRoleConfig
43
44
  import { buildRuntimeOptions } from './runtime-options.js';
44
45
  import * as scheduler from './scheduler-integration.js';
45
46
  import { runAgentSession } from './session.js';
47
+ import { SessionLedger } from './session-ledger.js';
46
48
  import { TaskDag } from './task-dag.js';
47
49
  import { freshChainId, roleProviderPrefixes, ToolProviderHub, } from './tool-providers.js';
48
50
  import { ORG_DIR, OrgDefSchema, } from './types.js';
@@ -624,6 +626,22 @@ export class OrgDaemon {
624
626
  getResourceLimits().maxSdkProcesses < sessionRoleCount) {
625
627
  configureResourceLimits({ maxSdkProcesses: sessionRoleCount });
626
628
  }
629
+ // ADR-O001 D8: a cost tier that can't resolve a model for a role's
630
+ // provider must stop the run here. The alternative — resolving it at
631
+ // session start — would either crash one role ten minutes in or, worse,
632
+ // quietly leave that role on a different model than the tier claimed.
633
+ const { validateCostTiers } = await import('./cost-tier.js');
634
+ const tierErrors = validateCostTiers(def);
635
+ if (tierErrors.length) {
636
+ throw new Error(`org ${name}: ${tierErrors.join('; ')}`);
637
+ }
638
+ // ADR-O001 D7: an oversized or unresolvable loadout catalog stops the run
639
+ // here, for the same reason — not ten minutes in, at some role's spawn.
640
+ const { validateLoadouts } = await import('./loadouts.js');
641
+ const loadoutErrors = validateLoadouts(def, this.root).errors;
642
+ if (loadoutErrors.length) {
643
+ throw new Error(`org ${name}: ${loadoutErrors.join('; ')}`);
644
+ }
627
645
  // Validate per-role providers before spawning anything (fail-fast: a
628
646
  // missing env var discovered 10 minutes into a run wastes the entire run).
629
647
  const { resolveProviderEnv: validateProvider, resolveRoleProvider } = await import('./provider.js');
@@ -674,6 +692,13 @@ export class OrgDaemon {
674
692
  // boss that's genuinely out of ideas can loop forever making zero
675
693
  // progress without ever tripping the watchdog.
676
694
  let lastToolActivity = 0;
695
+ // ADR-O001 D4 (no-progress detector): the idle clock above is org-wide and
696
+ // says nothing at all while a hold is in force — the 8.3-hour stall looked
697
+ // perfectly healthy from it. Per-role last-activity is what tells a role
698
+ // that is nominally working but producing nothing from one that is simply
699
+ // waiting its turn.
700
+ const roleActivity = new Map();
701
+ const noProgressAlarmed = new Set();
677
702
  // Org-wide budget ceiling (bug 1): tracks whether run_config.budget_tokens
678
703
  // has already been enforced this run, so the close-all-mailboxes sweep
679
704
  // below only fires once instead of on every subsequent usage event.
@@ -683,12 +708,18 @@ export class OrgDaemon {
683
708
  collected.push(slim);
684
709
  if (collected.length > MAX_COLLECTED)
685
710
  collected.splice(0, collected.length - MAX_COLLECTED);
686
- // The watchdog's own nudge event must not count as org activity, or a
687
- // hung boss would never trip the "nudge produced no activity" stop.
688
- if (e.reason !== 'idle-nudge')
711
+ // The watchdog's own events must not count as org activity, or a hung
712
+ // boss would never trip the "nudge produced no activity" stop and a
713
+ // silent role would clear its own no-progress alarm.
714
+ const selfEmitted = e.reason === 'idle-nudge' || e.reason === 'no-progress' || e.reason === 'hold-expired';
715
+ if (!selfEmitted)
689
716
  lastActivity = Date.now();
690
717
  if (e.type === 'tool')
691
718
  lastToolActivity = Date.now();
719
+ if (e.from && !selfEmitted) {
720
+ roleActivity.set(e.from, Date.now());
721
+ noProgressAlarmed.delete(e.from);
722
+ }
692
723
  // org_complete IS the end of the run — self-stop instead of sitting
693
724
  // "running" forever after a recorded outcome. Deferred (unref'd) so the
694
725
  // tool call's receipt reaches the boss and its final turn text still
@@ -727,8 +758,11 @@ export class OrgDaemon {
727
758
  const orgBudget = def.run_config.budget_tokens;
728
759
  if (orgBudget != null) {
729
760
  let orgUsage = 0;
761
+ // ADR-O001 D1: budgetedUsage, not usage — the org-wide ceiling is
762
+ // the same declared number as the per-role one and must be compared
763
+ // on the same basis.
730
764
  for (const rt of running.agents.values())
731
- orgUsage += rt.policy.usage;
765
+ orgUsage += rt.policy.budgetedUsage;
732
766
  // Mid-run role replacement retires a policy engine's usage into the
733
767
  // slot instead of discarding it (see role-slot.ts / respawnRole) -
734
768
  // include it here or a replacement could silently reset spend and
@@ -773,6 +807,7 @@ export class OrgDaemon {
773
807
  const running = {
774
808
  def,
775
809
  run,
810
+ sessionLedger: new SessionLedger(join(dir, 'sessions.json')),
776
811
  bus,
777
812
  agents: new Map(),
778
813
  roleSlots: new Map(),
@@ -1075,7 +1110,7 @@ export class OrgDaemon {
1075
1110
  idleMs,
1076
1111
  bossReachable: bossRt?.status === 'running' && !bossRt.mailbox.isClosed,
1077
1112
  })).toISOString();
1078
- const key = `${at}|${hold}`;
1113
+ const key = `${at}|${hold?.reason ?? null}|${hold?.until ?? null}`;
1079
1114
  if (key === published)
1080
1115
  return;
1081
1116
  try {
@@ -1091,7 +1126,32 @@ export class OrgDaemon {
1091
1126
  console.error(`org ${name}: could not write the idle deadline:`, err instanceof Error ? err.message : err);
1092
1127
  }
1093
1128
  };
1094
- // The legitimate waits the watchdog never nudges or stops through.
1129
+ // ADR-O001 D4 — Gas Town's "30 minutes hooked without progress" alarm.
1130
+ // The org-wide idle clock is silent while a hold is in force, so a role
1131
+ // that is nominally running and producing nothing gets its own, loud
1132
+ // audit event. Once per spell: the bus subscriber clears the flag as
1133
+ // soon as the role emits anything.
1134
+ const alarmNoProgress = (now) => {
1135
+ const stalled = noProgressRoles([...running.agents].map(([id, rt]) => ({
1136
+ id,
1137
+ working: rt.status === 'running' && !rt.mailbox.isClosed,
1138
+ lastActivity: roleActivity.get(id) ?? lastActivity,
1139
+ alarmed: noProgressAlarmed.has(id),
1140
+ })), now);
1141
+ for (const role of stalled) {
1142
+ noProgressAlarmed.add(role.id);
1143
+ bus.emit({
1144
+ type: 'audit',
1145
+ from: role.id,
1146
+ reason: 'no-progress',
1147
+ msg: `role "${role.id}" has been running for ${Math.round(role.silentMs / 60_000)}m ` +
1148
+ `without a single bus event — it is hooked but producing nothing`,
1149
+ data: { role: role.id, silentMinutes: Math.round(role.silentMs / 60_000) },
1150
+ });
1151
+ }
1152
+ };
1153
+ // The legitimate waits the watchdog holds through — every one of them
1154
+ // with a deadline attached by advanceHold (ADR-O001 D4).
1095
1155
  const holdReason = () => {
1096
1156
  if (this.restarting.has(name))
1097
1157
  return 'restarting'; // boss auto-restart in flight
@@ -1106,10 +1166,12 @@ export class OrgDaemon {
1106
1166
  // agent. Without this check the watchdog nudges (and, after enough
1107
1167
  // nudges, idle-stops) an org that's simply waiting on a human answer
1108
1168
  // that's already on its way.
1109
- const pendingQuestions = questionOps
1110
- .readQuestions(this.root, name)
1111
- .questions.filter((q) => q.answer === null);
1112
- if (pendingQuestions.length > 0)
1169
+ //
1170
+ // ADR-O001 D4: only a question the asker declared BLOCKING counts. The
1171
+ // 8.3-hour stall was held open by a question whose own text opened
1172
+ // with "no answer needed for the run to continue; I am not blocking on
1173
+ // this" — it suppressed the watchdog exactly like a real blocker.
1174
+ if (questionOps.pendingBlockingQuestions(this.root, name).length > 0)
1113
1175
  return 'pending-question';
1114
1176
  // M1 (C-41): a pending tool approval is the same kind of legitimate
1115
1177
  // wait — the role was told to wait for `org approve/deny`.
@@ -1118,23 +1180,28 @@ export class OrgDaemon {
1118
1180
  // M2 (C-41): a delivered endpoint message whose reply is still due.
1119
1181
  if (hasActiveEndpointWait(running))
1120
1182
  return 'endpoint-reply-due';
1183
+ // A task blocked on a real-world time still in the future is
1184
+ // legitimate waiting, same as a pending gate — don't nudge about it.
1185
+ // Its deadline is the time the asker actually named, not the default
1186
+ // hold TTL, so a block set hours out is honoured exactly.
1187
+ const blockedUntil = running.taskDag?.activeBlockUntil(Date.now()) ?? null;
1188
+ if (blockedUntil !== null)
1189
+ return { reason: 'task-blocked', until: blockedUntil };
1121
1190
  return null;
1122
1191
  };
1123
- const check = () => {
1124
- const hold = holdReason();
1125
- if (hold)
1126
- return hold;
1127
- // Auto-resume any task whose org_task_block time has passed: flip it
1128
- // back to 'running' and re-push it into the assignee's mailbox, same
1129
- // as a fresh dispatch. This IS real activity, so fall through to the
1130
- // normal idleFor check below rather than returning early — an
1131
- // unblocked task should reset the idle clock, not just silently
1132
- // update state nobody notices until the next nudge.
1192
+ // Auto-resume any task whose org_task_block time has passed: flip it
1193
+ // back to 'running' and re-push it into the assignee's mailbox, same as
1194
+ // a fresh dispatch. This IS real activity, so it feeds the normal
1195
+ // idleFor check rather than short-circuiting it — an unblocked task
1196
+ // should reset the idle clock, not just silently update state nobody
1197
+ // notices until the next nudge. Runs on every tick, a hold in force
1198
+ // included: a block that expires mid-wait is real work again.
1199
+ const resumeExpiredBlocks = () => {
1133
1200
  const unblocked = running.taskDag?.unblockExpired(Date.now()) ?? [];
1134
1201
  for (const task of unblocked) {
1135
1202
  const agent = running.agents.get(task.assignee);
1136
1203
  if (agent && !agent.mailbox.isClosed) {
1137
- agent.mailbox.push(`[task:${task.id}] Block expired — resuming: ${task.title}`);
1204
+ agent.mailbox.push(`${taskTag(task)} Block expired — resuming: ${task.title}`);
1138
1205
  }
1139
1206
  bus.emit({
1140
1207
  type: 'status',
@@ -1144,20 +1211,20 @@ export class OrgDaemon {
1144
1211
  data: { taskId: task.id, assignee: task.assignee },
1145
1212
  });
1146
1213
  }
1147
- // A task blocked on a real-world time still in the future is
1148
- // legitimate waiting, same as a pending gate — don't nudge about it.
1149
- if (running.taskDag?.hasActiveBlock(Date.now()))
1150
- return 'task-blocked';
1214
+ };
1215
+ // The normal idle path: nudge the boss, then stop if the nudge produced
1216
+ // nothing. Runs only when nothing (still) holds the watchdog.
1217
+ const check = () => {
1151
1218
  const idleFor = Date.now() - lastActivity;
1152
1219
  if (idleFor < idleMs) {
1153
1220
  nudges = resolvedIdleNudgeCount(nudgedAt, nudges, lastToolActivity);
1154
1221
  nudgedAt = 0;
1155
- return null;
1222
+ return;
1156
1223
  }
1157
1224
  if (nudgedAt === 0) {
1158
1225
  if (nudges >= MAX_IDLE_NUDGES) {
1159
1226
  idleStop(`org idle again after ${nudges} nudges — stopping run`);
1160
- return null;
1227
+ return;
1161
1228
  }
1162
1229
  const bossRt = running.agents.get(bossRole.id);
1163
1230
  // #205: a budget-exhausted boss closed its own mailbox on
@@ -1170,11 +1237,11 @@ export class OrgDaemon {
1170
1237
  idleStop(`org idle for ${Math.round(idleFor / 60_000)}m and boss "${bossRole.id}" is over its ` +
1171
1238
  `${budgetReason === 'token-budget' ? 'token' : 'USD'} budget — raise the role's ` +
1172
1239
  `${budgetReason === 'token-budget' ? 'budget_tokens' : 'budget_usd'} (or run_config's) and resume from checkpoint — stopping run`);
1173
- return null;
1240
+ return;
1174
1241
  }
1175
1242
  if (bossRt?.status !== 'running' || bossRt.mailbox.isClosed) {
1176
1243
  idleStop(`org idle for ${Math.round(idleFor / 60_000)}m and boss "${bossRole.id}" is unreachable — stopping run`);
1177
- return null;
1244
+ return;
1178
1245
  }
1179
1246
  nudges++;
1180
1247
  nudgedAt = Date.now();
@@ -1193,10 +1260,35 @@ export class OrgDaemon {
1193
1260
  else if (Date.now() - nudgedAt >= idleMs) {
1194
1261
  idleStop(`nudge produced no activity for another ${Math.round(idleMs / 60_000)}m — boss appears hung, stopping run`);
1195
1262
  }
1263
+ };
1264
+ // One watchdog tick. The hold is resolved FIRST and with a deadline, so
1265
+ // a wait that outlives its deadline hands the run back to the idle path
1266
+ // instead of suppressing it forever (ADR-O001 D4).
1267
+ let holdTrack = null;
1268
+ const tick = () => {
1269
+ const now = Date.now();
1270
+ alarmNoProgress(now);
1271
+ resumeExpiredBlocks();
1272
+ const step = advanceHold(holdTrack, holdReason(), now);
1273
+ holdTrack = step.track;
1274
+ if (step.expired) {
1275
+ bus.emit({
1276
+ type: 'audit',
1277
+ reason: 'hold-expired',
1278
+ msg: `the "${step.expired}" hold on org "${name}" outlived its deadline — the idle ` +
1279
+ `watchdog is running again and will nudge, then stop the run if nothing happens`,
1280
+ data: { hold: step.expired },
1281
+ });
1282
+ }
1283
+ if (step.hold)
1284
+ return step.hold;
1285
+ check();
1196
1286
  return null;
1197
1287
  };
1198
- publishDeadline(holdReason());
1199
- const wd = setInterval(() => publishDeadline(check()), Math.max(200, Math.min(idleMs / 2, 30_000)));
1288
+ const initialHold = advanceHold(null, holdReason(), Date.now());
1289
+ holdTrack = initialHold.track;
1290
+ publishDeadline(initialHold.hold);
1291
+ const wd = setInterval(() => publishDeadline(tick()), Math.max(200, Math.min(idleMs / 2, 30_000)));
1200
1292
  wd.unref?.();
1201
1293
  this.watchdogs.set(name, wd);
1202
1294
  }
@@ -1206,7 +1298,7 @@ export class OrgDaemon {
1206
1298
  run,
1207
1299
  idle_minutes: 0,
1208
1300
  idle_stop_at: null,
1209
- hold: 'disabled',
1301
+ hold: { reason: 'disabled', until: null },
1210
1302
  });
1211
1303
  }
1212
1304
  catch (err) {
@@ -1344,6 +1436,11 @@ export class OrgDaemon {
1344
1436
  }
1345
1437
  const policy = new PolicyEngine(role.id, {
1346
1438
  maxTokens: role.budget_tokens ?? perRoleBudget,
1439
+ // ADR-O001 D1: which basis that ceiling is enforced on. Defaults to
1440
+ // the historical uncached basis so the honest (cache-aware) meter
1441
+ // introduced alongside it cannot exhaust an existing budget_tokens —
1442
+ // including the schema's 1M default — roughly 100x early.
1443
+ maxTokensBasis: def.run_config.budget_tokens_basis ?? 'uncached',
1347
1444
  maxUsd: role.budget_usd,
1348
1445
  ...(role.policy ?? {}),
1349
1446
  }, bus, roleCwd,
@@ -1356,7 +1453,13 @@ export class OrgDaemon {
1356
1453
  providerPrefixes: () => roleProviderPrefixes(role),
1357
1454
  trace: () => this.roleTrace(name, role.id),
1358
1455
  });
1359
- if (roleCheckpoint?.tokensUsed) {
1456
+ // ADR-O001 D1: prefer the persisted four-quantity breakdown; a checkpoint
1457
+ // written before it existed still resumes via the scalar, on the uncached
1458
+ // basis it was recorded on.
1459
+ if (roleCheckpoint?.tokenUsage) {
1460
+ policy.setTokenUsage(roleCheckpoint.tokenUsage);
1461
+ }
1462
+ else if (roleCheckpoint?.tokensUsed) {
1360
1463
  policy.setUsage(roleCheckpoint.tokensUsed);
1361
1464
  }
1362
1465
  // ORG-7: restore accumulated USD spend across resume so a stop/resume
@@ -1364,6 +1467,31 @@ export class OrgDaemon {
1364
1467
  if (roleCheckpoint?.costUsd) {
1365
1468
  policy.setUsageUsd(roleCheckpoint.costUsd);
1366
1469
  }
1470
+ // ADR-O001 D7: the loadout this incarnation's session is built with, fixed
1471
+ // for its life. A checkpointed role keeps what it had (its SDK session was
1472
+ // built with it); a replacement keeps its predecessor's; a new role takes
1473
+ // the loadout of the first ready task it is being spawned for.
1474
+ const loadoutName = roleCheckpoint
1475
+ ? roleCheckpoint.loadout
1476
+ : (existingSlot?.runtime?.loadout ?? sessionLoadoutFor(running.taskDag, role.id));
1477
+ let loadout;
1478
+ if (loadoutName) {
1479
+ try {
1480
+ loadout = resolveLoadout(def, loadoutName, this.root);
1481
+ }
1482
+ catch (err) {
1483
+ // Validated at start, so only a config hot-reload or a deleted
1484
+ // instructions_file lands here. Spawn without it, loudly: a role that
1485
+ // fails to spawn would strand its task, which is worse.
1486
+ bus.emit({
1487
+ type: 'audit',
1488
+ from: role.id,
1489
+ reason: 'loadout-unresolvable',
1490
+ msg: `role "${role.id}" spawned without loadout "${loadoutName}": ${err instanceof Error ? err.message : err}`,
1491
+ data: { loadout: loadoutName },
1492
+ });
1493
+ }
1494
+ }
1367
1495
  const runtime = {
1368
1496
  mailbox,
1369
1497
  policy,
@@ -1375,6 +1503,7 @@ export class OrgDaemon {
1375
1503
  sessionId: roleCheckpoint?.sessionId,
1376
1504
  worktreePath: roleCwd !== cwd ? roleCwd : undefined,
1377
1505
  scrollback: new ScrollbackBuffer(),
1506
+ ...(loadout ? { loadout: loadout.name } : {}),
1378
1507
  };
1379
1508
  if (roleCheckpoint?.scrollback?.length) {
1380
1509
  for (const line of roleCheckpoint.scrollback)
@@ -1412,13 +1541,34 @@ export class OrgDaemon {
1412
1541
  },
1413
1542
  maxTurns: role.max_turns_per_message ?? def.run_config.max_turns_per_message,
1414
1543
  resumeSessionId: roleCheckpoint?.sessionId,
1544
+ sessionLedger: running.sessionLedger,
1545
+ // ADR-O001 D3 x D7: a task-scoped session is built with its task's own
1546
+ // recorded loadout. Unresolvable → no loadout, loudly (as at spawn).
1547
+ loadoutFor: (taskId) => {
1548
+ const name = running.taskDag?.get(taskId)?.loadout;
1549
+ if (!name)
1550
+ return undefined;
1551
+ try {
1552
+ return resolveLoadout(def, name, this.root);
1553
+ }
1554
+ catch (err) {
1555
+ bus.emit({
1556
+ type: 'audit',
1557
+ from: role.id,
1558
+ reason: 'loadout-unresolvable',
1559
+ msg: `task ${taskId} session built without loadout "${name}": ${err instanceof Error ? err.message : err}`,
1560
+ data: { loadout: name, taskId },
1561
+ });
1562
+ return undefined;
1563
+ }
1564
+ },
1415
1565
  lastMessageId: () => runtime.lastMessageId,
1416
1566
  onOutput: (line) => runtime.scrollback.push(line),
1417
1567
  onSessionId: (id) => {
1418
1568
  runtime.sessionId = id;
1419
1569
  },
1420
1570
  deliver: (from, to, subject, body) => this.deliver(name, from, to, subject, body),
1421
- askHuman: (r, question) => this.askHuman(name, r, question),
1571
+ askHuman: (r, question, blocking) => this.askHuman(name, r, question, blocking),
1422
1572
  onGate: (r, gateName, gateDesc) => this.createGate(name, r, gateName, gateDesc),
1423
1573
  circuitBreaker: (() => {
1424
1574
  const cb = def.run_config.circuit_breaker;
@@ -1463,9 +1613,7 @@ export class OrgDaemon {
1463
1613
  // unanswered question is the same "genuinely waiting on a
1464
1614
  // human" fact either way.
1465
1615
  const pendingHumanWaits = this.listGates(name, 'pending').length +
1466
- questionOps
1467
- .readQuestions(this.root, name)
1468
- .questions.filter((q) => q.answer === null).length;
1616
+ questionOps.pendingBlockingQuestions(this.root, name).length;
1469
1617
  return resolveOrgComplete(bus, r, outcome, summary, blocker, blockerDetail, {
1470
1618
  mode: def.run_config.completion ?? 'boss',
1471
1619
  maxBudgetFraction,
@@ -1535,12 +1683,24 @@ export class OrgDaemon {
1535
1683
  });
1536
1684
  return text;
1537
1685
  },
1538
- createTask: (r, title, assignee, deps) => {
1539
- return this.dagCreateTask(name, r, title, assignee, deps);
1686
+ createTask: (r, title, assignee, deps, loadout) => {
1687
+ return this.dagCreateTask(name, r, title, assignee, deps, loadout);
1540
1688
  },
1541
- completeTask: (r, taskId, result) => {
1542
- return this.dagCompleteTask(name, r, taskId, result);
1689
+ // ADR-O001 D7: only an org with a catalog gets the `loadout` argument;
1690
+ // the session itself is built with the loadout frozen above.
1691
+ loadoutCatalog: loadoutCatalog(def),
1692
+ loadout,
1693
+ completeTask: (r, taskId, result, evidence) => {
1694
+ return this.dagCompleteTask(name, r, taskId, result, evidence);
1543
1695
  },
1696
+ // ADR-O001 D5: only an org that opted in advertises the evidence
1697
+ // argument, so every other org's tool list stays byte-identical.
1698
+ requireTaskEvidence: def.run_config.completion_evidence === true,
1699
+ // ADR-O001 D6: only an org with an artifact-only reviewer gets org_review,
1700
+ // so every other org's tool list stays byte-identical.
1701
+ requestReview: def.roles.some((r) => r.review_input === 'artifact-only')
1702
+ ? (r, taskId, reviewer, base) => this.dagRequestReview(name, r, taskId, reviewer, base)
1703
+ : undefined,
1544
1704
  listTasks: () => {
1545
1705
  const running = this.orgs.get(name);
1546
1706
  return JSON.stringify(running?.taskDag?.all() ?? [], null, 2);
@@ -1986,7 +2146,10 @@ export class OrgDaemon {
1986
2146
  slot.queuedDuringSwap.push(...reclaimedQueue);
1987
2147
  // Step 9: retire accounting BEFORE replacing the runtime.
1988
2148
  slot.retiredUsage = {
1989
- tokens: slot.retiredUsage.tokens + oldRuntime.policy.usage,
2149
+ // Budgeted basis: this total is summed with live policy.budgetedUsage
2150
+ // against the org-wide budget_tokens ceiling (ADR-O001 D1), so the two
2151
+ // terms must share a basis.
2152
+ tokens: slot.retiredUsage.tokens + oldRuntime.policy.budgetedUsage,
1990
2153
  costUsd: slot.retiredUsage.costUsd + oldRuntime.metrics.costUsd,
1991
2154
  };
1992
2155
  // Step 10: spawn generation N+1 (generation already bumped in step 6).
@@ -2552,8 +2715,8 @@ export class OrgDaemon {
2552
2715
  return approvalOps.setApproval(this, org, role, action, approved, opts);
2553
2716
  }
2554
2717
  // questions.ts
2555
- async askHuman(org, role, question) {
2556
- return questionOps.askHuman(this, org, role, question);
2718
+ async askHuman(org, role, question, blocking) {
2719
+ return questionOps.askHuman(this, org, role, question, blocking);
2557
2720
  }
2558
2721
  async answerQuestion(org, role, questionId, answer, resolvedBy) {
2559
2722
  return questionOps.answerQuestion(this, org, role, questionId, answer, resolvedBy);
@@ -2571,11 +2734,15 @@ export class OrgDaemon {
2571
2734
  listGates(org, status) {
2572
2735
  return decisionOps.listGates(this, org, status);
2573
2736
  }
2574
- dagCreateTask(org, role, title, assignee, deps) {
2575
- return decisionOps.dagCreateTask(this, org, role, title, assignee, deps);
2737
+ dagCreateTask(org, role, title, assignee, deps, loadout) {
2738
+ return decisionOps.dagCreateTask(this, org, role, title, assignee, deps, loadout);
2739
+ }
2740
+ dagCompleteTask(org, role, taskId, result, evidence) {
2741
+ return decisionOps.dagCompleteTask(this, org, role, taskId, result, evidence);
2576
2742
  }
2577
- dagCompleteTask(org, role, taskId, result) {
2578
- return decisionOps.dagCompleteTask(this, org, role, taskId, result);
2743
+ /** ADR-O001 D6 — see decisions.ts's dagRequestReview. */
2744
+ dagRequestReview(org, role, taskId, reviewer, base) {
2745
+ return decisionOps.dagRequestReview(this, org, role, taskId, reviewer, base);
2579
2746
  }
2580
2747
  dagSplitTask(org, role, parentId, children) {
2581
2748
  return decisionOps.dagSplitTask(this, org, role, parentId, children);