@monoes/monomindcli 2.16.13 → 2.16.14

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 (171) hide show
  1. package/README.md +1 -1
  2. package/dist/src/commands/org-control.d.ts +117 -0
  3. package/dist/src/commands/org-control.d.ts.map +1 -0
  4. package/dist/src/commands/org-control.js +274 -0
  5. package/dist/src/commands/org-control.js.map +1 -0
  6. package/dist/src/commands/org-lifecycle.d.ts +7 -0
  7. package/dist/src/commands/org-lifecycle.d.ts.map +1 -0
  8. package/dist/src/commands/org-lifecycle.js +298 -0
  9. package/dist/src/commands/org-lifecycle.js.map +1 -0
  10. package/dist/src/commands/org-manage.d.ts +7 -0
  11. package/dist/src/commands/org-manage.d.ts.map +1 -0
  12. package/dist/src/commands/org-manage.js +357 -0
  13. package/dist/src/commands/org-manage.js.map +1 -0
  14. package/dist/src/commands/org-memory-command.d.ts +3 -0
  15. package/dist/src/commands/org-memory-command.d.ts.map +1 -0
  16. package/dist/src/commands/org-memory-command.js +178 -0
  17. package/dist/src/commands/org-memory-command.js.map +1 -0
  18. package/dist/src/commands/org-observe-approvals.d.ts +8 -0
  19. package/dist/src/commands/org-observe-approvals.d.ts.map +1 -0
  20. package/dist/src/commands/org-observe-approvals.js +211 -0
  21. package/dist/src/commands/org-observe-approvals.js.map +1 -0
  22. package/dist/src/commands/org-observe-checkpoint.d.ts +26 -0
  23. package/dist/src/commands/org-observe-checkpoint.d.ts.map +1 -0
  24. package/dist/src/commands/org-observe-checkpoint.js +180 -0
  25. package/dist/src/commands/org-observe-checkpoint.js.map +1 -0
  26. package/dist/src/commands/org-observe-config.d.ts +8 -0
  27. package/dist/src/commands/org-observe-config.d.ts.map +1 -0
  28. package/dist/src/commands/org-observe-config.js +160 -0
  29. package/dist/src/commands/org-observe-config.js.map +1 -0
  30. package/dist/src/commands/org-observe-gates.d.ts +4 -0
  31. package/dist/src/commands/org-observe-gates.d.ts.map +1 -0
  32. package/dist/src/commands/org-observe-gates.js +182 -0
  33. package/dist/src/commands/org-observe-gates.js.map +1 -0
  34. package/dist/src/commands/org-observe-inbox.d.ts +18 -0
  35. package/dist/src/commands/org-observe-inbox.d.ts.map +1 -0
  36. package/dist/src/commands/org-observe-inbox.js +154 -0
  37. package/dist/src/commands/org-observe-inbox.js.map +1 -0
  38. package/dist/src/commands/org-observe-logs.d.ts +31 -0
  39. package/dist/src/commands/org-observe-logs.d.ts.map +1 -0
  40. package/dist/src/commands/org-observe-logs.js +272 -0
  41. package/dist/src/commands/org-observe-logs.js.map +1 -0
  42. package/dist/src/commands/org-observe-questions.d.ts +8 -0
  43. package/dist/src/commands/org-observe-questions.d.ts.map +1 -0
  44. package/dist/src/commands/org-observe-questions.js +201 -0
  45. package/dist/src/commands/org-observe-questions.js.map +1 -0
  46. package/dist/src/commands/org-observe-report.d.ts +8 -0
  47. package/dist/src/commands/org-observe-report.d.ts.map +1 -0
  48. package/dist/src/commands/org-observe-report.js +366 -0
  49. package/dist/src/commands/org-observe-report.js.map +1 -0
  50. package/dist/src/commands/org-observe-shared.d.ts +20 -0
  51. package/dist/src/commands/org-observe-shared.d.ts.map +1 -0
  52. package/dist/src/commands/org-observe-shared.js +55 -0
  53. package/dist/src/commands/org-observe-shared.js.map +1 -0
  54. package/dist/src/commands/org-observe.d.ts +9 -101
  55. package/dist/src/commands/org-observe.d.ts.map +1 -1
  56. package/dist/src/commands/org-observe.js +12 -1675
  57. package/dist/src/commands/org-observe.js.map +1 -1
  58. package/dist/src/commands/org-poll.d.ts +57 -0
  59. package/dist/src/commands/org-poll.d.ts.map +1 -0
  60. package/dist/src/commands/org-poll.js +194 -0
  61. package/dist/src/commands/org-poll.js.map +1 -0
  62. package/dist/src/commands/org-run.d.ts +3 -0
  63. package/dist/src/commands/org-run.d.ts.map +1 -0
  64. package/dist/src/commands/org-run.js +449 -0
  65. package/dist/src/commands/org-run.js.map +1 -0
  66. package/dist/src/commands/org-serve.d.ts +19 -0
  67. package/dist/src/commands/org-serve.d.ts.map +1 -0
  68. package/dist/src/commands/org-serve.js +395 -0
  69. package/dist/src/commands/org-serve.js.map +1 -0
  70. package/dist/src/commands/org-subcommands-config.d.ts +8 -0
  71. package/dist/src/commands/org-subcommands-config.d.ts.map +1 -0
  72. package/dist/src/commands/org-subcommands-config.js +93 -0
  73. package/dist/src/commands/org-subcommands-config.js.map +1 -0
  74. package/dist/src/commands/org-subcommands-hil.d.ts +11 -0
  75. package/dist/src/commands/org-subcommands-hil.d.ts.map +1 -0
  76. package/dist/src/commands/org-subcommands-hil.js +216 -0
  77. package/dist/src/commands/org-subcommands-hil.js.map +1 -0
  78. package/dist/src/commands/org-subcommands-observe.d.ts +12 -0
  79. package/dist/src/commands/org-subcommands-observe.d.ts.map +1 -0
  80. package/dist/src/commands/org-subcommands-observe.js +249 -0
  81. package/dist/src/commands/org-subcommands-observe.js.map +1 -0
  82. package/dist/src/commands/org-subcommands-runtime.d.ts +12 -0
  83. package/dist/src/commands/org-subcommands-runtime.d.ts.map +1 -0
  84. package/dist/src/commands/org-subcommands-runtime.js +163 -0
  85. package/dist/src/commands/org-subcommands-runtime.js.map +1 -0
  86. package/dist/src/commands/org.d.ts +3 -151
  87. package/dist/src/commands/org.d.ts.map +1 -1
  88. package/dist/src/commands/org.js +43 -2735
  89. package/dist/src/commands/org.js.map +1 -1
  90. package/dist/src/orgrt/daemon-types.d.ts +196 -0
  91. package/dist/src/orgrt/daemon-types.d.ts.map +1 -0
  92. package/dist/src/orgrt/daemon-types.js +38 -0
  93. package/dist/src/orgrt/daemon-types.js.map +1 -0
  94. package/dist/src/orgrt/daemon.d.ts +70 -300
  95. package/dist/src/orgrt/daemon.d.ts.map +1 -1
  96. package/dist/src/orgrt/daemon.js +41 -2536
  97. package/dist/src/orgrt/daemon.js.map +1 -1
  98. package/dist/src/orgrt/idle-watchdog.d.ts +39 -0
  99. package/dist/src/orgrt/idle-watchdog.d.ts.map +1 -0
  100. package/dist/src/orgrt/idle-watchdog.js +289 -0
  101. package/dist/src/orgrt/idle-watchdog.js.map +1 -0
  102. package/dist/src/orgrt/org-reload.d.ts +7 -0
  103. package/dist/src/orgrt/org-reload.d.ts.map +1 -0
  104. package/dist/src/orgrt/org-reload.js +111 -0
  105. package/dist/src/orgrt/org-reload.js.map +1 -0
  106. package/dist/src/orgrt/org-start-steps.d.ts +21 -0
  107. package/dist/src/orgrt/org-start-steps.d.ts.map +1 -0
  108. package/dist/src/orgrt/org-start-steps.js +305 -0
  109. package/dist/src/orgrt/org-start-steps.js.map +1 -0
  110. package/dist/src/orgrt/org-start.d.ts +7 -0
  111. package/dist/src/orgrt/org-start.d.ts.map +1 -0
  112. package/dist/src/orgrt/org-start.js +435 -0
  113. package/dist/src/orgrt/org-start.js.map +1 -0
  114. package/dist/src/orgrt/org-state-file.d.ts +8 -0
  115. package/dist/src/orgrt/org-state-file.d.ts.map +1 -0
  116. package/dist/src/orgrt/org-state-file.js +116 -0
  117. package/dist/src/orgrt/org-state-file.js.map +1 -0
  118. package/dist/src/orgrt/org-stop.d.ts +6 -0
  119. package/dist/src/orgrt/org-stop.d.ts.map +1 -0
  120. package/dist/src/orgrt/org-stop.js +332 -0
  121. package/dist/src/orgrt/org-stop.js.map +1 -0
  122. package/dist/src/orgrt/org-tools.d.ts +11 -0
  123. package/dist/src/orgrt/org-tools.d.ts.map +1 -0
  124. package/dist/src/orgrt/org-tools.js +305 -0
  125. package/dist/src/orgrt/org-tools.js.map +1 -0
  126. package/dist/src/orgrt/role-incarnation.d.ts +18 -0
  127. package/dist/src/orgrt/role-incarnation.d.ts.map +1 -0
  128. package/dist/src/orgrt/role-incarnation.js +374 -0
  129. package/dist/src/orgrt/role-incarnation.js.map +1 -0
  130. package/dist/src/orgrt/role-respawn.d.ts +7 -0
  131. package/dist/src/orgrt/role-respawn.d.ts.map +1 -0
  132. package/dist/src/orgrt/role-respawn.js +318 -0
  133. package/dist/src/orgrt/role-respawn.js.map +1 -0
  134. package/dist/src/orgrt/role-session-opts.d.ts +389 -0
  135. package/dist/src/orgrt/role-session-opts.d.ts.map +1 -0
  136. package/dist/src/orgrt/role-session-opts.js +289 -0
  137. package/dist/src/orgrt/role-session-opts.js.map +1 -0
  138. package/dist/src/orgrt/runner-resolve.d.ts +43 -0
  139. package/dist/src/orgrt/runner-resolve.d.ts.map +1 -0
  140. package/dist/src/orgrt/runner-resolve.js +77 -0
  141. package/dist/src/orgrt/runner-resolve.js.map +1 -0
  142. package/dist/src/orgrt/session-gate.d.ts +34 -0
  143. package/dist/src/orgrt/session-gate.d.ts.map +1 -0
  144. package/dist/src/orgrt/session-gate.js +74 -0
  145. package/dist/src/orgrt/session-gate.js.map +1 -0
  146. package/dist/src/orgrt/session-prompt.d.ts +31 -0
  147. package/dist/src/orgrt/session-prompt.d.ts.map +1 -0
  148. package/dist/src/orgrt/session-prompt.js +115 -0
  149. package/dist/src/orgrt/session-prompt.js.map +1 -0
  150. package/dist/src/orgrt/session-run.d.ts +18 -0
  151. package/dist/src/orgrt/session-run.d.ts.map +1 -0
  152. package/dist/src/orgrt/session-run.js +429 -0
  153. package/dist/src/orgrt/session-run.js.map +1 -0
  154. package/dist/src/orgrt/session-stream.d.ts +21 -0
  155. package/dist/src/orgrt/session-stream.d.ts.map +1 -0
  156. package/dist/src/orgrt/session-stream.js +173 -0
  157. package/dist/src/orgrt/session-stream.js.map +1 -0
  158. package/dist/src/orgrt/session-types.d.ts +219 -0
  159. package/dist/src/orgrt/session-types.d.ts.map +1 -0
  160. package/dist/src/orgrt/session-types.js +2 -0
  161. package/dist/src/orgrt/session-types.js.map +1 -0
  162. package/dist/src/orgrt/session-usage.d.ts +29 -0
  163. package/dist/src/orgrt/session-usage.d.ts.map +1 -0
  164. package/dist/src/orgrt/session-usage.js +64 -0
  165. package/dist/src/orgrt/session-usage.js.map +1 -0
  166. package/dist/src/orgrt/session.d.ts +5 -281
  167. package/dist/src/orgrt/session.d.ts.map +1 -1
  168. package/dist/src/orgrt/session.js +8 -1117
  169. package/dist/src/orgrt/session.js.map +1 -1
  170. package/dist/tsconfig.tsbuildinfo +1 -1
  171. package/package.json +6 -6
@@ -1,218 +1,14 @@
1
1
  // packages/@monomind/cli/src/orgrt/session.ts
2
- import { z } from 'zod';
3
- import { ClaudeAgentRunner, defaultClaudeRunner } from './agent-runner.js';
4
- import { CLAUDE_SANDBOX_CWD_ENV, claudeBashTimeoutEnv, claudeSandboxCwdNote, } from './bash-timeout.js';
5
- import { endpointBriefingLines } from './endpoint-roles.js';
6
- import { scanInput } from './fence.js';
7
- import { Mailbox } from './mailbox.js';
8
- import { summarizeToolOutput } from './policy.js';
9
- import { FaultRestarts, ProcessFaultError } from './sandbox-fault.js';
10
- import { sandboxStubPaths, sandboxStubs } from './sandbox-stubs.js';
11
- import { StateDetector } from './state-detector.js';
12
- import { linkAbort, queueCancelNotice, TaskCancelledError, trackTaskProcess, } from './task-cancel.js';
13
- import { MAX_TASK_BRIEF } from './task-dag.js';
14
- import { orgTaskTool } from './task-tools.js';
15
- import { MAX_BLOCK_RECHECK_MINUTES, } from './types.js';
16
- /** How long an SDK stream may stay open with zero messages before we say so.
17
- * Comfortably longer than a slow first turn, shorter than the idle watchdog's
18
- * 10-minute window so the specific cause is reported before the generic
19
- * "boss appears hung". */
20
- const SILENT_SESSION_MS = 4 * 60_000;
21
- const CONTEXT_LIMIT_RE = /context.window.limit|context.length.exceeded|maximum.context/i;
22
2
  import { createHash } from 'node:crypto';
23
- import { readFileSync } from 'node:fs';
24
- import { homedir } from 'node:os';
25
- import { join } from 'node:path';
26
- import { ensureAuthorityDirs } from './authority-mask.js';
27
- import { resolveRoleCostTier } from './cost-tier.js';
28
3
  import { CumulativeMeter } from './cumulative-meter.js';
29
- import { expandRolePromptVars, promptVarsFor } from './prompt-vars.js';
30
- import { resolveProviderEnv, resolveRoleProvider } from './provider.js';
31
- import { resolveRoleGitEnforcement, roleAuthorityMask } from './role-sandbox.js';
4
+ import { Mailbox } from './mailbox.js';
5
+ import { FaultRestarts, ProcessFaultError } from './sandbox-fault.js';
32
6
  import { mailRouteKey, ROLE_SESSION_KEY, resolveSessionScope, SessionLedger, } from './session-ledger.js';
33
- import { roleSkillGuidance } from './skill-library.js';
34
- import { skillTools } from './skill-tools.js';
35
- import { strictArgs } from './tool-fence.js';
36
- import { DEFAULT_CLAUDE_MODEL, VERCEL_PROVIDERS } from './vercel-providers.js';
37
- /**
38
- * Resolves the extra system-prompt block for a role: its pinned library
39
- * skills and on-demand skill catalog (skill-library.ts) plus the role's own
40
- * `instructions_file`, if any — all optional and independent. A missing or
41
- * unreadable `instructions_file` is skipped (not an error): a role shouldn't
42
- * fail to start a session over a stale/typo'd custom-file path.
43
- */
44
- export function resolveRoleExtraGuidance(role, projectRoot) {
45
- const parts = [];
46
- const skills = roleSkillGuidance(role, projectRoot);
47
- if (skills)
48
- parts.push(skills);
49
- if (role.instructions_file) {
50
- try {
51
- const custom = readFileSync(role.instructions_file, 'utf-8').trim();
52
- if (custom)
53
- parts.push(custom);
54
- }
55
- catch {
56
- // missing/unreadable custom instructions file — skip, don't crash session start
57
- }
58
- }
59
- return parts.length ? parts.join('\n\n') : undefined;
60
- }
61
- /** Resolve the model string for a role: explicit adapter_config.model wins;
62
- * otherwise fall back to the vendor/runtime default.
63
- *
64
- * Vendor defaults are read straight off VERCEL_PROVIDERS, which already
65
- * carries a defaultModel per vendor. A second hand-kept table lived here and
66
- * restated all sixteen of them — two lists of per-vendor defaults that would
67
- * eventually disagree, which is precisely the drift #252 was. An empty
68
- * registry default (openai-compatible, which serves arbitrary endpoints) is
69
- * falsy and so falls through to the runtime switch, as it always did. */
70
- export function resolveModel(role, runtime, vendor) {
71
- const explicit = role.adapter_config?.model;
72
- if (explicit)
73
- return explicit;
74
- const vendorDefault = vendor ? VERCEL_PROVIDERS[vendor]?.defaultModel : undefined;
75
- if (vendorDefault)
76
- return vendorDefault;
77
- switch (runtime) {
78
- case 'claude':
79
- return DEFAULT_CLAUDE_MODEL;
80
- // Kimi Code CLI namespaces model ids as <provider>/<model> (its own
81
- // default_model is "kimi-code/kimi-for-coding-highspeed") — a bare "k3"
82
- // 404s with "Model \"k3\" is not configured in config.toml".
83
- case 'kimicode':
84
- return 'kimi-code/k3';
85
- case 'opencode':
86
- return 'glm-5.2'; // opencode is typically paired with a vendor; this is the bare-runtime fallback
87
- case 'codex':
88
- return 'gpt-5.6-terra';
89
- case 'antigravity':
90
- return 'gemini-3.6-flash-high';
91
- case 'vercel':
92
- return 'gpt-5.5';
93
- default:
94
- return DEFAULT_CLAUDE_MODEL;
95
- }
96
- }
97
- /** The SDK's `canUseTool` gate, composed from two independent layers: PolicyEngine's
98
- * static config checks (deny/allow lists, path scoping, git level, web allowlist,
99
- * budget), then — only for calls policy would allow — the human-approval guardrail
100
- * (`beforeTool`, i.e. daemon.checkApproval) for whatever action names it treats as
101
- * sensitive (Bash/WebFetch/WebSearch/org_complete). Exported standalone so this
102
- * composition is unit-testable without spinning up a real SDK session: previously
103
- * `beforeTool` was wired into SessionOpts but never actually called from here, so
104
- * none of those sensitive actions ever paused for a human. */
105
- export function gatedCanUseTool(policy, beforeTool, roleId, fence,
106
- /** Optional hook invoked whenever this gate denies a tool call — wired to
107
- * daemon.recordDecision() so denials show up in `org decisions` traces.
108
- * #290: `kind` names WHICH of this function's four deny paths fired, so a
109
- * consumer never has to tell a fence block from a routine pending approval
110
- * by matching the English in the message. */
111
- onDeny,
112
- /** ORG-9: reports whether this role has a pending (unresolved) decision gate.
113
- * org_gate is documented as creating a "hard-blocking" checkpoint, but until
114
- * this was wired in nothing actually stopped tool use while a gate sat
115
- * pending — only approvals did that. When set and true, ALL tool calls are
116
- * denied (not just the sensitive subset approvals gate) until the gate is
117
- * resolved, matching the "hard-blocking" description. */
118
- hasPendingGate) {
119
- return async (toolName, input, meta) => {
120
- if (hasPendingGate?.()) {
121
- const decision = {
122
- behavior: 'deny',
123
- message: `Tool "${toolName}" is blocked — awaiting gate resolution. A decision gate is pending; wait for a human to approve or reject it via 'monomind org gate-approve/gate-reject'.`,
124
- };
125
- onDeny?.(toolName, input, decision, 'gate-pending');
126
- return decision;
127
- }
128
- if (fence) {
129
- const text = typeof input.command === 'string'
130
- ? input.command
131
- : typeof input.content === 'string'
132
- ? input.content
133
- : typeof input.url === 'string'
134
- ? input.url
135
- : JSON.stringify(input);
136
- const fenceDecision = await scanInput(fence.instance, text, fence.abortThreshold);
137
- if (fenceDecision.behavior === 'deny') {
138
- onDeny?.(toolName, input, fenceDecision, 'fence-block');
139
- return fenceDecision;
140
- }
141
- }
142
- const decision = await policy.decide(toolName, input, meta?.toolUseId);
143
- if (decision.behavior === 'deny') {
144
- onDeny?.(toolName, input, decision, 'policy-deny');
145
- return decision;
146
- }
147
- if (!beforeTool)
148
- return decision;
149
- const approved = await beforeTool(roleId, toolName, input);
150
- if (approved === false) {
151
- const denied = {
152
- behavior: 'deny',
153
- message: `Tool "${toolName}" was denied by guardrail approval`,
154
- };
155
- onDeny?.(toolName, input, denied, 'approval-denied');
156
- return denied;
157
- }
158
- if (approved === null) {
159
- const pending = {
160
- behavior: 'deny',
161
- message: `Tool "${toolName}" is pending human approval — it will be available once approved or denied via 'monomind org approve/deny'.`,
162
- };
163
- onDeny?.(toolName, input, pending, 'approval-pending');
164
- return pending;
165
- }
166
- return decision;
167
- };
168
- }
169
- /** Role briefing given to each agent session (SDK systemPrompt option).
170
- * `extraGuidance` carries pre-resolved text the caller already loaded from
171
- * disk — the role's library skills and/or its own `instructions_file`,
172
- * if either resolved to something. Kept as a plain string param (not read here)
173
- * so this function stays synchronous/pure and trivially testable. */
174
- export function buildRolePrompt(role, def, roster, glossary, extraGuidance,
175
- /** M2: one line per endpoint role (endpointBriefingLines) — boss only. */
176
- endpointBriefing) {
177
- const isCoordinator = role.reports_to == null;
178
- return [
179
- `You are agent "${role.id}" (${role.title || role.type}) in the org "${def.name}".`,
180
- `Org goal: ${def.goal}`,
181
- isCoordinator ? `You are the coordinator of this org.` : `You report to "${role.reports_to}".`,
182
- role.responsibilities?.length
183
- ? `Your responsibilities:\n- ${role.responsibilities.join('\n- ')}`
184
- : '',
185
- extraGuidance || '',
186
- `## Communication protocol`,
187
- `The ONLY way to communicate with other agents is the org_send tool.`,
188
- `Roster: ${roster.join(', ')}. Address another org's agent as "<org-name>:<role-id>".`,
189
- endpointBriefing?.length ? `Automations in this org:\n${endpointBriefing.join('\n')}` : '',
190
- `If you need a human decision, call ask_human with your question, then end your turn - you'll receive the human's answer as a new message when it arrives. Do not call ask_human for anything you can resolve yourself.`,
191
- `For irreversible or high-risk actions (deployments, deletions, external communications), call org_gate to create a decision gate — a hard-blocking approval checkpoint. End your turn and wait for the human's approval or rejection before proceeding.`,
192
- `You can structure work as a task DAG: use org_task to create tasks with dependencies, org_task_done to mark them complete, and org_tasks to see the full DAG. Tasks with satisfied dependencies are automatically dispatched to their assignee.`,
193
- `The work graph is dynamic: call org_task_split when scope expands, org_task_merge when parallel branches converge early, or org_task_cancel when evidence makes a planned task moot. Use org_plan_graph to propose a full work graph in one call when you know the plan upfront. If a task genuinely can't proceed until a specific real-world time — a scheduled long-running process, a deadline someone gave you, anything with a known future unblock time — call org_task_block instead of leaving it idle: it stops the idle watchdog from nudging you about it and automatically resumes the task when the time arrives, instead of you repeatedly re-confirming "still waiting" every idle cycle.`,
194
- `Before starting substantial work, call org_recall to check what previous runs already learned or delivered - do not redo finished work.`,
195
- `The user's documents (notes, handbooks, specs) are searchable with knowledge_search - ground your work in them instead of guessing; results labeled [global] come from the user's personal cross-project brain.`,
196
- `When you receive a message, act on it, then org_send your result to the requester.`,
197
- isCoordinator
198
- ? `When the org's goal for this run is achieved (or clearly can't be): first call org_learn ONCE with the durable knowledge this run produced, then call org_complete exactly once with the outcome and a concise summary. Then end your turn.`
199
- : `When your current work is complete and no reply is needed, end your turn without further tool calls.`,
200
- isCoordinator && glossary?.length
201
- ? `Known entities (reuse these EXACT names in org_learn instead of near-duplicates): ${glossary.slice(0, 40).join(', ')}`
202
- : '',
203
- ]
204
- .filter(Boolean)
205
- .join('\n\n');
206
- }
207
- /** The system prompt one session of this role is built with. */
208
- function rolePromptFor(opts) {
209
- 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,
210
- // D7: the loadout's text follows the role's own guidance. With no
211
- // loadout this is exactly resolveRoleExtraGuidance(role), as before.
212
- [resolveRoleExtraGuidance(opts.role, opts.orgRoot ?? opts.cwd), opts.loadout?.guidance]
213
- .filter(Boolean)
214
- .join('\n\n') || undefined, opts.onComplete ? endpointBriefingLines(opts.def) : undefined);
215
- }
7
+ import { rolePromptFor } from './session-prompt.js';
8
+ import { runOneSession } from './session-run.js';
9
+ import { queueCancelNotice, TaskCancelledError, trackTaskProcess } from './task-cancel.js';
10
+ export { gatedCanUseTool } from './session-gate.js';
11
+ export { buildRolePrompt, resolveModel, resolveRoleExtraGuidance, } from './session-prompt.js';
216
12
  /**
217
13
  * Runs a role for the life of the org, transparently restarting the
218
14
  * underlying SDK session whenever it ends on its own (`maxTurns` reached)
@@ -594,911 +390,6 @@ async function runAgentSessionLoop(opts) {
594
390
  }
595
391
  }
596
392
  }
597
- /** ADR-O001 D1 — token-metering helpers.
598
- *
599
- * `cache_read_input_tokens` and `cache_creation_input_tokens` are siblings
600
- * of `input_tokens` in the Anthropic API, not subsets of it, and both are
601
- * billable. Everything below therefore sums all four. */
602
- function totalTokens(u) {
603
- return u.input + u.output + u.cacheRead + u.cacheCreation;
604
- }
605
- function addTo(target, add) {
606
- target.input += add.input;
607
- target.output += add.output;
608
- target.cacheRead += add.cacheRead;
609
- target.cacheCreation += add.cacheCreation;
610
- }
611
- /** One model turn's own usage, off an 'assistant' (or per-turn 'result')
612
- * message. */
613
- function turnBreakdown(m) {
614
- return {
615
- input: m.input_tokens ?? 0,
616
- output: m.output_tokens ?? 0,
617
- cacheRead: m.cache_read_input_tokens ?? 0,
618
- cacheCreation: m.cache_creation_input_tokens ?? 0,
619
- };
620
- }
621
- /** What a 'result' message says this mailbox message consumed.
622
- *
623
- * When the runner reports `cumulative_tokens` (the Claude SDK's whole-pipeline
624
- * `modelUsage`, which unlike `usage` includes Task subagents and sidechains),
625
- * that value is CUMULATIVE per session — the same lifecycle as
626
- * `total_cost_usd` — so it is converted to a delta by the meter (see
627
- * cumulative-meter.ts). Without `cumulative_tokens` the per-turn fields are
628
- * used as before. */
629
- function resultBreakdown(m, tokenTotals, sid) {
630
- const cum = m.cumulative_tokens;
631
- if (!cum)
632
- return turnBreakdown(m);
633
- const now = {
634
- input: cum.input,
635
- output: cum.output,
636
- cacheRead: cum.cache_read,
637
- cacheCreation: cum.cache_creation,
638
- };
639
- return tokenTotals ? tokenTotals.delta(sid, now) : now;
640
- }
641
- /** ADR-O001 D1: the four quantities travel separately so every downstream
642
- * consumer (forwarder → dashboard state.json, reporting, `org costs`) can
643
- * record real values instead of the 0s they used to persist. `tokens` stays
644
- * the single billable total. */
645
- function emitUsage(bus, from, t, costUsd, subtype) {
646
- bus.emit({
647
- type: 'usage',
648
- from,
649
- data: {
650
- tokens: totalTokens(t),
651
- cost_usd: costUsd,
652
- subtype,
653
- tokens_in: t.input,
654
- tokens_out: t.output,
655
- cache_read: t.cacheRead,
656
- cache_creation: t.cacheCreation,
657
- },
658
- });
659
- }
660
- /** One bounded SDK session for a role; resolves with the SDK's session_id (for
661
- * resuming on restart) and whether it ended by hitting the turn limit (so the
662
- * caller can push a continuation) when the stream ends (mailbox closed or
663
- * maxTurns reached). */
664
- async function runOneSession(opts, resume, costTotals, progress, tokenTotals, streamOpts, faultWatch, cancelled) {
665
- const { org, role, bus, policy, mailbox, cwd } = opts;
666
- // Each call starts a new runner process, whose cumulative totals may or may
667
- // not continue the previous one's (cumulative-meter.ts).
668
- costTotals?.newProcess();
669
- tokenTotals?.newProcess();
670
- // Read lastMessageId live from opts instead of capturing at session start
671
- // This ensures chat responses link to the most recent message delivered
672
- const getLastMessageId = () => (opts.lastMessageId ? opts.lastMessageId() : undefined);
673
- // Resolve runner. Precedence: explicit runner > queryFn-wrapped > default.
674
- // queryFn stays supported so daemon.ts / test-loop.ts need no changes.
675
- const runner = opts.runner ?? (opts.queryFn ? new ClaudeAgentRunner(opts.queryFn) : defaultClaudeRunner);
676
- const tools = buildOrgTools(opts);
677
- // M1: provider tools are listed per session start, so a hot-reloaded
678
- // tool_providers block takes effect at the role's next session.
679
- const providerSet = opts.buildProviderTools ? await opts.buildProviderTools() : undefined;
680
- if (providerSet)
681
- tools.push(...providerSet.tools);
682
- // Named-provider resolution (`adapter_config.provider`): explicit role
683
- // provider wins, else the named entry from `monomind providers configure`.
684
- // The named provider's default model fills in adapter_config.model when the
685
- // role didn't pin one.
686
- const prov = resolveRoleProvider(role, opts.orgRoot ?? opts.cwd);
687
- // ADR-O001 D8: the role's cost tier, when the org declares one. Resolved
688
- // here — the single choke point where a role's model is decided — so the
689
- // documented precedence holds in exactly one place:
690
- // explicit adapter_config.model > tier > named-provider default > runtime
691
- // The tier's EFFORT is applied even when the model came from an explicit
692
- // pin: which model to run and how hard to think are separate axes, and
693
- // silently dropping the effort because a model was pinned would be the
694
- // "silent downgrade" this decision exists to prevent.
695
- // Throws (fails the session) rather than guessing when the tier has no
696
- // entry for this role's provider — daemon.ts validates the whole roster
697
- // up front so that is normally caught before any token is spent.
698
- const tier = resolveRoleCostTier({
699
- role,
700
- def: opts.def,
701
- vendor: role.provider?.vendor ?? prov.cfg?.vendor,
702
- });
703
- const model = role.adapter_config?.model ??
704
- tier?.model ??
705
- prov.defaultModel ??
706
- resolveModel(role, role.runtime, role.provider?.vendor ?? prov.cfg?.vendor);
707
- bus.emit({ type: 'status', from: role.id, msg: 'session starting' });
708
- let sessionId = resume;
709
- let hitTurnLimit = false;
710
- let contextLimitFired = false;
711
- // #budget-realtime: real tokens already accounted for the message CURRENTLY
712
- // in flight, via the per-assistant-turn accounting below — reset to 0 each
713
- // time a 'result' message ends one mailbox message and the next one starts.
714
- // Exists purely so the 'result' branch never re-adds what this branch
715
- // already added (see there for why it can't just always add).
716
- let messageTurnTokens = { input: 0, output: 0, cacheRead: 0, cacheCreation: 0 };
717
- // Abort hook for the runner (AgentRunArgs.signal): the silent-stream
718
- // abort below used to call iterator.return() only, which queues behind a
719
- // subprocess runner blocked in `for await (child.stdout)` — the child was
720
- // never killed, so every supervisor retry stacked another live CLI.
721
- //
722
- // Per attempt, linked one way to the caller's externalAbort (#256): the
723
- // silent-stream abort used to fire the daemon's slot controller itself,
724
- // permanently. Every retry then started on an already-aborted signal (a
725
- // runner honoring it kills its child at once) and the daemon's crash
726
- // backoff, which races that controller to notice an org stop, resolved
727
- // immediately - the role burned its retries and crashed. An org stop still
728
- // aborts the attempt; the attempt's own abort stays its own.
729
- const abort = new AbortController();
730
- const external = opts.externalAbort?.signal;
731
- const onExternalAbort = () => abort.abort(external?.reason);
732
- if (external?.aborted)
733
- onExternalAbort();
734
- else
735
- external?.addEventListener('abort', onExternalAbort, { once: true });
736
- // org_task_cancel for this process's task: end it the same way (task-cancel.ts).
737
- // Already aborted when it landed during the setup awaits above.
738
- const unlinkCancelled = linkAbort(cancelled, abort);
739
- try {
740
- // #258: policy.git enforced where git runs, not only by Bash text
741
- // classification — guard env for every runtime, OS sandbox + file-tool
742
- // deny rules for Claude. Throws (session fails) when the role requires
743
- // the sandbox and it can't start.
744
- // Before the sandbox is built: it can only mask directories that exist.
745
- ensureAuthorityDirs(homedir(), process.env);
746
- const gitEnforcement = resolveRoleGitEnforcement({
747
- org,
748
- role,
749
- cwd,
750
- orgRoot: opts.orgRoot,
751
- orgDir: opts.orgDir,
752
- bus,
753
- claudeRuntime: runner instanceof ClaudeAgentRunner,
754
- runtime: role.runtime ?? opts.def?.runtime,
755
- // The sandbox's mount-point stubs, created once and kept until the run
756
- // ends, so no other role's process deletes one mid-bind (sandbox-stubs.ts).
757
- // Held before the deny list is built, which keeps a denied cwd read-only
758
- // when all of them are in place (sandbox-deny-write.ts).
759
- holdStubs: (writableRoots) => {
760
- const paths = sandboxStubPaths({ cwd, home: homedir(), writableRoots, env: process.env });
761
- sandboxStubs.hold(`${org}:${opts.run ?? ''}`, paths);
762
- return sandboxStubs.missing(paths);
763
- },
764
- });
765
- // What this session really got, not what the config asked for (policy-git.ts).
766
- policy.setOsSandboxed(!!gitEnforcement.claudeRestrictions?.sandbox);
767
- const authorityMask = roleAuthorityMask({
768
- bus,
769
- roleId: role.id,
770
- inSdkSandbox: !!gitEnforcement.claudeRestrictions?.sandbox,
771
- // vercel runs in-process with no shell; its file tools go through the policy engine.
772
- inProcess: (role.runtime ?? opts.def?.runtime) === 'vercel',
773
- cwd,
774
- orgRoot: opts.orgRoot,
775
- });
776
- const stream = runner.run({
777
- tools,
778
- // No options = the pre-D3 stream, exactly.
779
- prompt: streamOpts ? mailbox.stream('', streamOpts) : mailbox.stream(),
780
- systemPrompt: gitEnforcement.claudeRestrictions?.sandbox
781
- ? `${rolePromptFor(opts)}\n\n${claudeSandboxCwdNote(cwd)}`
782
- : rolePromptFor(opts),
783
- model,
784
- cwd,
785
- effort: tier?.effort,
786
- env: {
787
- ...resolveProviderEnv(prov.cfg),
788
- // D8: how a NON-Claude provider expresses the tier's effort level.
789
- // Empty for Claude (handled natively by ClaudeAgentRunner) and for a
790
- // provider that declares no mechanism — which simply ignores effort.
791
- ...(tier?.env ?? {}),
792
- // Claude Code's 2-minute Bash default is too short for org work.
793
- ...(runner instanceof ClaudeAgentRunner
794
- ? claudeBashTimeoutEnv(opts.def?.run_config?.bash_timeout_ms)
795
- : {}),
796
- // Custom-endpoint providers (named-provider path): pin the engine's
797
- // model env so background/haiku tasks also route to the endpoint's
798
- // model instead of erroring on an Anthropic-only default.
799
- ...(prov.cfg?.authToken
800
- ? { ANTHROPIC_MODEL: model, ANTHROPIC_SMALL_FAST_MODEL: model }
801
- : {}),
802
- ...gitEnforcement.env,
803
- ...(gitEnforcement.claudeRestrictions?.sandbox ? CLAUDE_SANDBOX_CWD_ENV : {}),
804
- // No MONOMIND_HOOK_QUIET / MONOMIND_GRAPH_GATE / MONOMIND_SDK_AGENT
805
- // here (#249): every CLI hands this env to its shell tool, so they
806
- // reached every command the role ran and silently muted monomind's
807
- // own hooks, graph gate and tests inside the role. ClaudeAgentRunner
808
- // loads no filesystem hooks (settingSources: []). The codex/kimi/
809
- // opencode hook bridges generated by `monomind init` read the
810
- // MONOMIND_ORG_ROLE marker below and set the quieting vars on the
811
- // hook-handler process they spawn — hooks stay quiet, commands don't.
812
- //
813
- // Per-role scoping for runners that persist state under the org dir
814
- // (VercelAgentRunner session files). Without these, session files would
815
- // land in args.cwd (project root for workspace:'repo') under the literal
816
- // 'default' roleId, polluting the repo and making files unattributable.
817
- MONOMIND_ORG_DIR: opts.orgDir ?? opts.cwd,
818
- MONOMIND_ROLE_ID: role.id,
819
- // M1: attribution for anything the role runs (C-16).
820
- MONOMIND_ORG_NAME: org,
821
- MONOMIND_ORG_ROLE: role.id,
822
- ...(opts.run ? { MONOMIND_ORG_RUN: opts.run } : {}),
823
- ...(opts.orgRoot ? { MONOMIND_ORG_ROOT: opts.orgRoot } : {}),
824
- },
825
- maxTurns: opts.maxTurns ?? 30,
826
- maxToolRounds: role.max_tool_rounds ?? opts.def?.run_config?.max_tool_rounds,
827
- resume,
828
- claudeRestrictions: gitEnforcement.claudeRestrictions,
829
- authorityMask,
830
- // ADR-O001 D2: tool results are 76% of a role's context mass and nothing
831
- // bounded them. Under the ORG STATE dir (never the workspace cwd, which
832
- // may be the repo), and under orgRoot — which file-roots.ts already
833
- // makes readable to the role's file tools and role-sandbox.ts already
834
- // makes readable to Bash — so the path in the digest actually resolves
835
- // when the role decides it needs the full text.
836
- toolSpillDir: join(opts.orgDir ?? opts.cwd, 'tool-results', role.id.replace(/[^a-zA-Z0-9_.-]/g, '_')),
837
- canUseTool: gatedCanUseTool(policy, opts.beforeTool, role.id, opts.fence, opts.onDecision
838
- ? (toolName, _input, decision, kind) => opts.onDecision?.(role.id, toolName, decision.message ?? 'denied', kind)
839
- : undefined, opts.hasPendingGate),
840
- // test seam forwarded through extras: lets the scripted fake SDK
841
- // (test-loop.ts) drive org_send and tool calls through the real
842
- // deliver/policy paths; the real SDK ignores it.
843
- extras: opts.runner
844
- ? undefined
845
- : {
846
- _orgTest: {
847
- deliver: (to, subject, body) => opts.deliver(role.id, to, subject, body),
848
- callTool: (name, input) => policy.decide(name, input),
849
- },
850
- },
851
- signal: abort.signal,
852
- // VercelAgentRunner-only fields — ignored by other runners.
853
- vendor: role.provider?.vendor,
854
- providerConfig: role.provider,
855
- });
856
- // A silent session is its own failure mode, and until now an unnameable
857
- // one: nine consecutive cycles of a scheduled org opened all seven streams
858
- // and yielded NOTHING - no assistant message, no result, no error, and no
859
- // stream end. The only symptom was the idle watchdog reporting the boss
860
- // "appears hung" twenty minutes later, which described neither the scope
861
- // (every role) nor the cause.
862
- //
863
- // Naming it used to be all this did: log an audit event at 4 minutes and
864
- // then keep waiting on the same stuck `for await`, so recovery still
865
- // depended on the org-wide idle watchdog (10m nudge + 10m stop = 20m of
866
- // dead time per cycle - and it kills the WHOLE run, not just the stuck
867
- // session). Only the FIRST pull from the stream is raced against the
868
- // timeout: once any message has arrived the session is demonstrably
869
- // alive, so a slow-but-working tool call is never mistaken for a stall.
870
- // On silence, abandon this attempt (best-effort iterator.return() to
871
- // signal the SDK) and throw - the caller's crash-retry-with-backoff loop
872
- // (daemon.ts's `runtime.done`) already knows how to retry a failed
873
- // session with a fresh query() call and, for the boss, escalate to a
874
- // whole-org restart if it keeps failing. That gives the SDK several
875
- // fresh attempts within a single cycle instead of one silent attempt
876
- // followed by twenty minutes of nothing.
877
- const openedAt = Date.now();
878
- const detector = new StateDetector();
879
- const iterator = stream[Symbol.asyncIterator]();
880
- const SILENT = Symbol('silent');
881
- let silentTimer;
882
- const silentMs = opts.silentSessionMs ?? SILENT_SESSION_MS;
883
- const firstPull = await Promise.race([
884
- iterator.next(),
885
- new Promise((resolve) => {
886
- silentTimer = setTimeout(() => resolve(SILENT), silentMs);
887
- silentTimer.unref?.();
888
- }),
889
- ]);
890
- clearTimeout(silentTimer);
891
- if (firstPull === SILENT) {
892
- bus.emit({
893
- type: 'audit',
894
- from: role.id,
895
- reason: 'session-silent',
896
- msg: `SDK stream open ${Math.round((Date.now() - openedAt) / 1000)}s with zero messages - aborting this attempt and retrying. Set MONOMIND_DEBUG=1 to log raw message types.`,
897
- });
898
- // Kill the runner's subprocess FIRST: iterator.return() below cannot
899
- // reach a runner blocked in its stdout loop, and the retry would
900
- // otherwise spawn a second CLI next to the still-running first one.
901
- abort.abort();
902
- try {
903
- await Promise.race([
904
- iterator.return?.(undefined) ?? Promise.resolve(),
905
- new Promise((r) => {
906
- const t = setTimeout(() => r(), 2_000);
907
- t.unref?.();
908
- }),
909
- ]);
910
- }
911
- catch {
912
- /* best-effort */
913
- }
914
- throw new Error(`org "${org}" role "${role.id}": SDK stream silent for ${Math.round(silentMs / 1000)}s with zero messages`);
915
- }
916
- const first = firstPull;
917
- // Replay the first pulled message, then continue draining normally.
918
- async function* rest() {
919
- if (!first.done)
920
- yield first.value;
921
- while (true) {
922
- const r = await iterator.next();
923
- if (r.done)
924
- return;
925
- yield r.value;
926
- }
927
- }
928
- for await (const m of rest()) {
929
- if (process.env.MONOMIND_DEBUG) {
930
- console.error(`[orgrt:${org}/${role.id}] runner message type=${m.type} subtype=${String(m.subtype ?? '-')}`);
931
- }
932
- if (cancelled?.aborted)
933
- throw cancelled.reason;
934
- mailbox.observeTurn(m.type); // the prompt stream outlives a live turn (#331)
935
- if (m.session_id) {
936
- sessionId = m.session_id;
937
- // P2-13: propagate the session ID back to the daemon so checkpoints
938
- // can resume the SDK session after a crash/restart.
939
- opts.onSessionId?.(sessionId);
940
- }
941
- const prevState = detector.current();
942
- const textForDetect = m.type === 'assistant' ? m.text || '' : undefined;
943
- const newState = detector.onMessage(m.type, m.subtype, textForDetect);
944
- if (newState !== prevState) {
945
- bus.emit({
946
- type: 'status',
947
- from: role.id,
948
- reason: 'state-change',
949
- msg: `${prevState} → ${newState}`,
950
- data: { from: prevState, to: newState },
951
- });
952
- }
953
- if (m.type === 'assistant') {
954
- if (progress)
955
- progress.replied = true;
956
- const text = m.text || '';
957
- if (text.trim()) {
958
- opts.onOutput?.(text);
959
- bus.emit({ type: 'chat', from: role.id, msg: text, parentId: getLastMessageId() });
960
- if (opts.onContextLimit && !contextLimitFired && CONTEXT_LIMIT_RE.test(text)) {
961
- contextLimitFired = true;
962
- bus.emit({
963
- type: 'audit',
964
- from: role.id,
965
- reason: 'boss-context-limit',
966
- msg: 'coordinator context window exhausted — requesting whole-org restart with fresh sessions',
967
- });
968
- opts.onContextLimit();
969
- }
970
- }
971
- // #budget-realtime (HIGH): the SDK's 'result' message arrives once per
972
- // WHOLE mailbox message in streaming-input mode — with
973
- // max_turns_per_message defaulting to 100,000, a single message can
974
- // internally loop through hundreds/thousands of tool-use turns before
975
- // that 'result' ever arrives, during which policy.used never moved and
976
- // policy.decide() allowed every one of those turns' tool calls
977
- // regardless of real spend (the overspend was already done by the time
978
- // overBudget could ever trip). Each 'assistant' SDK message DOES carry
979
- // that ONE model turn's real usage (agent-runner.ts reads it off
980
- // BetaMessage.usage) — accumulate it as turns actually happen and
981
- // enforce the budget immediately, so overBudget can close the mailbox
982
- // DURING a runaway message instead of only once it finally completes.
983
- // (USD budget can't get the same real-time treatment: the SDK only
984
- // exposes cost as a cumulative total on 'result', not per-turn on
985
- // 'assistant' — verified against this project's Claude Agent SDK
986
- // .d.ts, which puts `usage`/token counts on BetaMessage but cost only
987
- // on SDKResultSuccess.total_cost_usd/modelUsage. overBudgetUsd is
988
- // still checked below, once per message, same as before this fix.)
989
- //
990
- // ADR-O001 D1: the sum must include BOTH cache fields. They are
991
- // siblings of input_tokens in the Anthropic API, not subsets of it —
992
- // `input_tokens` is the uncached remainder — and both are billable
993
- // (~0.1x and ~1.25x input). Omitting them meant the better the cache
994
- // worked the less the meter saw: on one measured run, 2,765M tokens
995
- // billed against 8.1M recorded, with input_tokens at 0.0M.
996
- const turn = turnBreakdown(m);
997
- const turnTokens = totalTokens(turn);
998
- if (turnTokens > 0) {
999
- addTo(messageTurnTokens, turn);
1000
- policy.addTokenUsage(turn);
1001
- if (policy.overBudget) {
1002
- bus.emit({
1003
- type: 'status',
1004
- from: role.id,
1005
- reason: 'budget-exhausted',
1006
- msg: 'token budget exhausted - closing session',
1007
- });
1008
- mailbox.close('token-budget');
1009
- }
1010
- }
1011
- }
1012
- else if (m.type === 'tool_result') {
1013
- // #289: the tool call's outcome. Until this event existed, a Bash
1014
- // running a test suite looked identical on the bus whether the suite
1015
- // passed, failed, or the binary was missing — one 'allow' at the moment
1016
- // it started — so every consumer inferred success from the agent's own
1017
- // narration. `call_id` joins this back to that invocation event; the
1018
- // body is redacted and capped (policy.ts) so a megabyte of output, or a
1019
- // credential echoed by a command, never lands in bus.jsonl.
1020
- const body = summarizeToolOutput(m.text ?? '');
1021
- const data = {
1022
- ...(m.tool_use_id ? { call_id: m.tool_use_id } : {}),
1023
- ok: m.is_error !== true,
1024
- ...(typeof m.duration_ms === 'number' ? { duration_ms: m.duration_ms } : {}),
1025
- output: body.output,
1026
- ...(body.truncated ? { truncated: true } : {}),
1027
- output_chars: body.output_chars,
1028
- };
1029
- bus.emit({
1030
- type: 'tool_result',
1031
- from: role.id,
1032
- ...(m.tool ? { tool: m.tool } : {}),
1033
- data: data,
1034
- });
1035
- faultWatch?.observe(m, sessionId);
1036
- }
1037
- else if (m.type === 'result') {
1038
- // ADR-O001 D1: prefer the SDK's `modelUsage` over `usage`. The SDK
1039
- // documents `usage` as "MAIN AGENT LOOP ONLY — excludes Task
1040
- // subagent, sidechain, and auxiliary model calls ... Prefer
1041
- // modelUsage for token/cost accounting"; the measured run made 46
1042
- // subagent calls this counter never saw. modelUsage is CUMULATIVE per
1043
- // session (same lifecycle as total_cost_usd, per its own type doc),
1044
- // so it is converted to a delta here rather than added, exactly as
1045
- // cost is below. A runner that reports no modelUsage falls back to
1046
- // the per-turn `usage` fields, which keep their old semantics.
1047
- const resultTokens = resultBreakdown(m, tokenTotals, m.session_id ?? sessionId ?? '');
1048
- // Per the SDK's own type docs, a 'result' message's usage is that
1049
- // message's own (effectively last-turn) usage in streaming-input mode,
1050
- // NOT a cumulative total across every turn of the mailbox message —
1051
- // and that last turn was already counted above via its own 'assistant'
1052
- // message, specifically so overBudget could trip mid-message. Adding
1053
- // the result's own usage again unconditionally would double-count it.
1054
- // (A modelUsage-derived delta is per-session-cumulative, so the same
1055
- // subtraction is exactly right there too: it removes what the
1056
- // assistant turns of THIS message already contributed and leaves the
1057
- // subagent/auxiliary volume the main loop never reported.) Only make
1058
- // up the shortfall (never negative) so a turn whose usage somehow
1059
- // never reached the 'assistant' branch (e.g. a runner/test double that
1060
- // doesn't emit per-turn usage) still gets counted at least once.
1061
- const shortfall = {
1062
- input: Math.max(0, resultTokens.input - messageTurnTokens.input),
1063
- output: Math.max(0, resultTokens.output - messageTurnTokens.output),
1064
- cacheRead: Math.max(0, resultTokens.cacheRead - messageTurnTokens.cacheRead),
1065
- cacheCreation: Math.max(0, resultTokens.cacheCreation - messageTurnTokens.cacheCreation),
1066
- };
1067
- if (totalTokens(shortfall) > 0)
1068
- policy.addTokenUsage(shortfall);
1069
- // What this whole mailbox message actually added to the meter: the
1070
- // per-turn accounting above plus whatever the result topped up. This
1071
- // is what the 'usage' event reports, so a consumer summing events
1072
- // lands on the same number as policy.usage.
1073
- const messageTokens = {
1074
- input: messageTurnTokens.input + shortfall.input,
1075
- output: messageTurnTokens.output + shortfall.output,
1076
- cacheRead: messageTurnTokens.cacheRead + shortfall.cacheRead,
1077
- cacheCreation: messageTurnTokens.cacheCreation + shortfall.cacheCreation,
1078
- };
1079
- messageTurnTokens = { input: 0, output: 0, cacheRead: 0, cacheCreation: 0 };
1080
- // Convert the SDK's cumulative total_cost_usd into a per-result
1081
- // delta before emitting - downstream sums usage events. A new session
1082
- // id counts in full; a same-process dip (rounding, a provider-side
1083
- // correction) floors at 0 rather than re-adding the cumulative cost,
1084
- // which feeds USD budget enforcement (ORG-7). A resume in a new
1085
- // process is judged by the meter (cumulative-meter.ts).
1086
- let costDelta = m.cost_usd;
1087
- if (costTotals && typeof m.cost_usd === 'number' && Number.isFinite(m.cost_usd)) {
1088
- const sid = m.session_id ?? sessionId ?? '';
1089
- costDelta = costTotals.delta(sid, { usd: m.cost_usd }).usd;
1090
- }
1091
- // ORG-7: accumulate real USD cost so policy.overBudgetUsd (role.budget_usd) is enforceable.
1092
- if (typeof costDelta === 'number' && Number.isFinite(costDelta))
1093
- policy.addUsageUsd(costDelta);
1094
- emitUsage(bus, role.id, messageTokens, costDelta, m.subtype);
1095
- if (m.subtype && m.subtype !== 'success') {
1096
- if (m.subtype === 'error_max_turns')
1097
- hitTurnLimit = true;
1098
- bus.emit({
1099
- type: 'audit',
1100
- from: role.id,
1101
- reason: 'session-result-error',
1102
- msg: `turn ended with subtype "${m.subtype}"${m.is_error ? ' (is_error)' : ''} - the role produced no usable output`,
1103
- });
1104
- if (opts.circuitBreaker && m.subtype !== 'error_max_turns') {
1105
- const cb = opts.circuitBreaker;
1106
- cb.state.failures++;
1107
- if (cb.state.failures >= cb.threshold) {
1108
- cb.state.tripped = true;
1109
- bus.emit({
1110
- type: 'audit',
1111
- from: role.id,
1112
- reason: 'circuit-breaker-tripped',
1113
- msg: `circuit breaker tripped after ${cb.state.failures} consecutive failures — closing role`,
1114
- data: { failures: cb.state.failures, threshold: cb.threshold },
1115
- });
1116
- mailbox.close();
1117
- }
1118
- }
1119
- }
1120
- else if (m.subtype === 'success' && opts.circuitBreaker) {
1121
- opts.circuitBreaker.state.failures = 0;
1122
- }
1123
- if (policy.overBudget) {
1124
- bus.emit({
1125
- type: 'status',
1126
- from: role.id,
1127
- reason: 'budget-exhausted',
1128
- msg: 'token budget exhausted - closing session',
1129
- });
1130
- // #205: tag WHY the mailbox closed. A budget-exhausted boss is
1131
- // recoverable (raise the budget, resume) — the idle watchdog reads
1132
- // this to stop reporting it as generic "unreachable" (crash-like).
1133
- mailbox.close('token-budget');
1134
- }
1135
- // ORG-7: parallel USD-budget enforcement, same pattern as the token check above.
1136
- if (policy.overBudgetUsd) {
1137
- bus.emit({
1138
- type: 'status',
1139
- from: role.id,
1140
- reason: 'budget-exhausted',
1141
- msg: 'USD budget exhausted - closing session',
1142
- });
1143
- mailbox.close('usd-budget');
1144
- }
1145
- // The turn is over: whatever tool calls it was going to make, it has
1146
- // made. What the role left open is knowable here (decisions.ts's
1147
- // nudgeOpenTasksAtTurnEnd) instead of only to the idle watchdog.
1148
- opts.onTurnEnd?.();
1149
- }
1150
- }
1151
- if (cancelled?.aborted)
1152
- throw cancelled.reason;
1153
- bus.emit({ type: 'status', from: role.id, msg: 'session ended' });
1154
- return { sessionId, hitTurnLimit };
1155
- }
1156
- catch (err) {
1157
- // The turn in flight never got its 'result', so its metered turns (already
1158
- // in policy) have no usage event yet. Cost is only on 'result': unknown.
1159
- if (totalTokens(messageTurnTokens) > 0)
1160
- emitUsage(bus, role.id, messageTurnTokens, undefined, 'aborted');
1161
- // Ending the process was the point; sandbox-fault.ts already audited it.
1162
- if (err instanceof ProcessFaultError)
1163
- throw err;
1164
- if (cancelled?.aborted)
1165
- throw cancelled.reason;
1166
- // org_complete / org stop close the mailbox and abort every session: a
1167
- // normal stop, not a failure. The daemon still classifies it.
1168
- const message = err instanceof Error ? err.message : String(err);
1169
- if ((mailbox.isClosed || (external?.aborted ?? false)) &&
1170
- (err?.name === 'AbortError' || /\baborted\b/i.test(message))) {
1171
- bus.emit({
1172
- type: 'status',
1173
- from: role.id,
1174
- reason: 'session-stopped',
1175
- msg: 'session stopped',
1176
- });
1177
- throw err;
1178
- }
1179
- // #304: the daemon's role loop catches this same error one step later and
1180
- // emits the authoritative CLASSIFIED status — crashed / stopped with the
1181
- // org / terminated by stop — carrying the real error text when it is a
1182
- // genuine crash (daemon.ts's 'agent-session-crash' audit). This
1183
- // breadcrumb must not pre-empt that with the raw SDK string: on a
1184
- // planned stop it announced "Claude Code process aborted by user" about
1185
- // a stop nobody requested. It deliberately does NOT classify —
1186
- // session.ts relays, the daemon decides — and it carries a `reason` so
1187
- // it is filterable; its absence is why every #304/#251 test was
1188
- // structurally unable to see this event.
1189
- bus.emit({
1190
- type: 'status',
1191
- from: role.id,
1192
- reason: 'session-error',
1193
- msg: 'session ended with an error — see the classified status that follows',
1194
- });
1195
- throw err;
1196
- }
1197
- finally {
1198
- // Unlink so a long-lived role doesn't pile a listener per attempt onto the
1199
- // slot controller. Aborting the finished attempt keeps a runner abandoned
1200
- // mid-stream by a throw from outliving it now that an org stop can no
1201
- // longer reach it.
1202
- external?.removeEventListener('abort', onExternalAbort);
1203
- unlinkCancelled();
1204
- abort.abort();
1205
- providerSet?.close();
1206
- }
1207
- }
1208
- /** Build the org tool surface as platform-agnostic OrgToolDef[]. The handlers
1209
- * close over sessionOpts callbacks (deliver, recall, remember, …) — same
1210
- * wiring as the previous inline createSdkMcpServer block, just decoupled from
1211
- * the Claude SDK's tool() shape so any AgentRunner can host them.
1212
- *
1213
- * Behaviour is identical to the old inline definitions: conditional tools are
1214
- * gated on their callback being present, org_send/ask_human are always added. */
393
+ export { buildOrgTools } from './org-tools.js';
1215
394
  export { AUTO_ASSIGNEE } from './task-tools.js';
1216
- export function buildOrgTools(opts) {
1217
- const { role, deliver } = opts;
1218
- const tools = [];
1219
- const text = (t) => ({ text: t });
1220
- const searchKnowledge = opts.searchKnowledge;
1221
- if (searchKnowledge) {
1222
- tools.push({
1223
- name: 'knowledge_search',
1224
- description: "Semantic search over the user's Second Brain: this project's indexed documents plus their personal cross-project global brain. Use to ground work in the user's actual notes, handbooks, and documents.",
1225
- schema: { query: z.string() },
1226
- handler: async (args) => text(await searchKnowledge(role.id, args.query)),
1227
- });
1228
- }
1229
- tools.push(...skillTools(role, opts.orgRoot ?? opts.cwd, opts.onSkillLoad));
1230
- const recall = opts.recall;
1231
- if (recall) {
1232
- tools.push({
1233
- name: 'org_recall',
1234
- description: "Search this org's accumulated memory from previous runs (outcomes, decisions, learnings). Use before starting work that may already have been done.",
1235
- schema: { query: z.string() },
1236
- handler: async (args) => text(await recall(role.id, args.query)),
1237
- });
1238
- }
1239
- const remember = opts.remember;
1240
- if (remember) {
1241
- tools.push({
1242
- name: 'org_remember',
1243
- description: 'Save a memory for future runs. scope "org" (default) shares it with the whole org; scope "agent" keeps it private to your role. Use for decisions, findings, and state worth recalling later - org_recall searches both.',
1244
- schema: { content: z.string(), scope: z.enum(['org', 'agent']).optional() },
1245
- handler: async (args) => text(await remember(role.id, args.content, args.scope ?? 'org')),
1246
- });
1247
- }
1248
- const learn = opts.learn;
1249
- if (learn) {
1250
- tools.push({
1251
- name: 'org_learn',
1252
- description: "Persist durable knowledge from this run into the org's knowledge graph: entities ({name, type?, description?}), relationships ({source, target, relation, description?}) and reusable rules ({rule, context?}). Entities merge by name across runs - reuse the exact names listed in your briefing. Call once, before org_complete.",
1253
- schema: {
1254
- nodes: z
1255
- .array(strictArgs({
1256
- name: z.string(),
1257
- type: z.string().optional(),
1258
- description: z.string().optional(),
1259
- }))
1260
- .optional(),
1261
- edges: z
1262
- .array(strictArgs({
1263
- source: z.string(),
1264
- target: z.string(),
1265
- relation: z.string(),
1266
- description: z.string().optional(),
1267
- }))
1268
- .optional(),
1269
- rules: z.array(strictArgs({ rule: z.string(), context: z.string().optional() })).optional(),
1270
- },
1271
- handler: async (args) => text(await learn(role.id, args)),
1272
- });
1273
- }
1274
- // Gate purely on onComplete: the daemon passes it only to the role its
1275
- // boss-selection rule picked, so tool availability always matches the
1276
- // kickoff instruction (reports_to may be non-null for a fallback boss).
1277
- if (opts.onComplete) {
1278
- tools.push({
1279
- name: 'org_complete',
1280
- description: '⚠️ Ends the run — every agent session shuts down after this. Call exactly once, and only against the org\'s actual, full stated goal (see your briefing), never against just the current batch of dispatched tasks. "achieved" means the WHOLE goal is done, not "everyone assigned so far finished their piece" — a multi-phase or open-ended goal is very rarely achieved in a single run. If the current task batch is clearly done but the goal has more scope left: do NOT call this — use org_task/createTask to dispatch the next phase\'s work instead, so the org keeps making progress instead of stopping short. "achieved" only when the full goal is met; "failed" only when it clearly cannot be — neither requires a blocker (though if this org\'s run_config.completion is set to \'dag\', "achieved" is ALSO refused while org_tasks still has runnable work that is not blocked on a real-world time — check org_tasks first if that setting applies to you). Use outcome "partial" only when you are ending the run with real scope still remaining, and you MUST name why with `blocker`: \'budget\' (a role is genuinely near its token/USD ceiling), \'human\' (an ask_human question or decision gate is actually pending), \'time\' (if the only thing blocking further work is a scheduled process or deadline you know the time of, call org_task_block instead of ending the run so a future run can pick back up automatically — this blocker is refused unless a task is actually blocked that way), or \'external\' (anything else — `blockerDetail` must be a real, substantive explanation; placeholders like "none"/"n/a" and anything under 10 characters are refused, because this is the one blocker nothing else can check, and it is recorded verbatim in the run history under your name). A refusal names which of these to use instead. Before calling, check org_tasks — siblings with in-progress work only get a short drain window to finish before being cut off, so do not call this while others are still mid-build or mid-edit unless the run genuinely cannot continue. The outcome and summary are persisted to the org run history and briefed to the next run. If the run produced a concrete deliverable (a post, document, message, piece of content, code, etc.), summary MUST include that deliverable\'s full text verbatim — not a meta-description of what happened. Someone reading only summary should be able to see the actual result, not just that "a result was produced".',
1281
- schema: {
1282
- outcome: z.enum(['achieved', 'partial', 'failed']),
1283
- summary: z.string(),
1284
- blocker: z.enum(['budget', 'human', 'external', 'time']).optional(),
1285
- blockerDetail: z.string().optional(),
1286
- },
1287
- handler: async (args) => {
1288
- const refusal = opts.onComplete?.(role.id, args.outcome, args.summary, args.blocker, args.blockerDetail);
1289
- // #302: a refusal must not claim the outcome was recorded — nothing
1290
- // was, and the daemon never emitted the event that would stop the run.
1291
- if (refusal)
1292
- return text(refusal);
1293
- return text(`outcome "${args.outcome}" recorded`);
1294
- },
1295
- });
1296
- }
1297
- const onRespawnRole = opts.onRespawnRole;
1298
- if (onRespawnRole) {
1299
- tools.push({
1300
- name: 'org_respawn_role',
1301
- description: "Replace one crashed, exhausted, or unsuitable WORKER role with a fresh session — keeping its role id, workspace, task ownership, and queued messages. Cannot target the coordinator (yourself) or an unknown/removed/not-yet-started role. Omit runtime/model/providerName to keep their current values. Omitted budgetTokens uses the normal per-role allocation for this run; it cannot raise the org-wide token budget. reason is a short operational reason for the audit log; briefing is what the replacement should know to continue the work — it starts a FRESH model session with no memory of the old one's conversation, so include everything it needs.",
1302
- schema: {
1303
- roleId: z.string(),
1304
- runtime: z.string().optional(),
1305
- model: z.string().optional(),
1306
- providerName: z.string().optional(),
1307
- budgetTokens: z.number().optional(),
1308
- reason: z.string(),
1309
- briefing: z.string(),
1310
- },
1311
- handler: async (args) => {
1312
- const receipt = await onRespawnRole(role.id, args);
1313
- return text(JSON.stringify(receipt));
1314
- },
1315
- });
1316
- }
1317
- const onListRuntimeOptions = opts.onListRuntimeOptions;
1318
- if (onListRuntimeOptions) {
1319
- tools.push({
1320
- name: 'org_list_runtime_options',
1321
- description: 'List every runtime this daemon knows how to run a role on (availability, detected binary/version, install hint) and every named provider configured for this project (name and default model only — never keys, tokens, or endpoints). Use before org_respawn_role to pick a real runtime/providerName. "available" means locally resolvable/detected, not that login, quota, or a remote model is valid.',
1322
- schema: {},
1323
- handler: async () => text(JSON.stringify(await onListRuntimeOptions())),
1324
- });
1325
- }
1326
- const onGate = opts.onGate;
1327
- if (onGate) {
1328
- tools.push({
1329
- name: 'org_gate',
1330
- description: 'Create a decision gate — a hard-blocking human-approval checkpoint. Use before irreversible actions (deployments, deletions, external comms). The gate pauses your work until a human approves or rejects it. End your turn after calling this; you will receive the resolution as a new message.',
1331
- schema: { name: z.string(), description: z.string() },
1332
- handler: async (args) => text(await onGate(role.id, args.name, args.description)),
1333
- });
1334
- }
1335
- // ADR-O001 D7: the `loadout` argument exists only for an org with a
1336
- // catalog, so every other org's tool list stays byte-identical.
1337
- const catalog = opts.loadoutCatalog?.length ? opts.loadoutCatalog : undefined;
1338
- const loadoutArg = catalog
1339
- ? { loadout: z.enum(catalog.map((l) => l.name)).optional() }
1340
- : {};
1341
- // Sent with the dispatch itself (task-provenance.ts dispatchLine), so the
1342
- // instructions arrive with the task instead of in a follow-up message.
1343
- const briefArg = z.string().max(MAX_TASK_BRIEF).optional();
1344
- const orgTask = orgTaskTool(opts, loadoutArg, briefArg);
1345
- if (orgTask)
1346
- tools.push(orgTask);
1347
- const completeTask = opts.completeTask;
1348
- if (completeTask) {
1349
- tools.push({
1350
- name: 'org_task_done',
1351
- description: opts.requireTaskEvidence
1352
- ? 'Mark a task as completed. This org requires EVIDENCE (run_config.completion_evidence): pass `evidence` with the current commit sha of the work (the HEAD of any worktree of this repository, or any local branch tip), `worktree` naming the worktree you ran in when it is not the org workspace, and one entry per acceptance criterion — the command you actually ran, its real exit code, and its output. A check passes when its exit code equals `expectExit` (default 0): when the criterion is met by a non-zero exit (a lookup that must find nothing → 1, a timeout that must fire → 124), set `expectExit` instead of appending `|| true`, which erases the exit code, and always say why in a one-line `expectReason` ("404 = branch not protected") — `expectExit` without it is refused. `expectExit` is only for a SINGLE-PURPOSE command: on a test suite or any other aggregate runner (vitest, jest, `pnpm test`, `pnpm -r`, a verify script) it is refused outright, because a suite exit code means "at least one of many things failed" and declaring it expected accepts every other failure too — run the one failing test file on its own and declare `expectExit` on that, or exclude the known failure and record the exclusion in `result`. A task whose job is to REPORT (QA, an audit) closes on commands that prove the report exists and is complete (e.g. `test -s <report file>`); the failures it found are findings — put them in `result` and send them to the coordinator, not in `checks`. If you tested something outside a git worktree (a scratch dir, an installed tarball), pin `headSha`/`worktree` to the worktree the artifact was built from. Evidence pinned to a commit that is no longer the head of that work 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 refused proofs (default 3; a call with no `evidence` at all is refused without counting), 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.'
1353
- : '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.',
1354
- schema: {
1355
- taskId: z.string(),
1356
- result: z.string().optional(),
1357
- evidence: strictArgs({
1358
- headSha: z.string(),
1359
- worktree: z.string().optional(),
1360
- checks: z
1361
- .array(strictArgs({
1362
- command: z.string(),
1363
- exitCode: z.number().int(),
1364
- expectExit: z.number().int().optional(),
1365
- expectReason: z.string().optional(),
1366
- output: z.string().optional(),
1367
- }))
1368
- .default([]),
1369
- }).optional(),
1370
- },
1371
- handler: async (args) => text(completeTask(role.id, args.taskId, args.result, args.evidence)),
1372
- });
1373
- }
1374
- const requestReview = opts.requestReview;
1375
- if (requestReview) {
1376
- tools.push({
1377
- name: 'org_review',
1378
- 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.",
1379
- schema: {
1380
- taskId: z.string(),
1381
- reviewer: z.string(),
1382
- base: z.string().optional().describe("git ref to diff against (default 'main')"),
1383
- },
1384
- handler: async (args) => text(requestReview(role.id, args.taskId, args.reviewer, args.base)),
1385
- });
1386
- }
1387
- const listTasks = opts.listTasks;
1388
- if (listTasks) {
1389
- tools.push({
1390
- name: 'org_tasks',
1391
- description: 'List all tasks in the DAG with their current status and dependencies. Pass `taskId` to get just that task, including its result and latest evidence.',
1392
- schema: { taskId: z.string().optional() },
1393
- handler: async (args) => text(listTasks(args.taskId)),
1394
- });
1395
- }
1396
- const splitTask = opts.splitTask;
1397
- if (splitTask) {
1398
- tools.push({
1399
- name: 'org_task_split',
1400
- description: 'Split a task into parallel children when scope expands. The parent becomes "split"; children inherit its deps; downstream tasks are rewired to depend on all children.',
1401
- schema: {
1402
- parentId: z.string(),
1403
- children: z.array(strictArgs({ title: z.string(), assignee: z.string() })).min(1),
1404
- },
1405
- handler: async (args) => text(splitTask(role.id, args.parentId, args.children ?? [])),
1406
- });
1407
- }
1408
- const mergeTask = opts.mergeTask;
1409
- if (mergeTask) {
1410
- tools.push({
1411
- name: 'org_task_merge',
1412
- description: 'Merge one task into another when parallel branches converge early. The source becomes "merged"; downstream deps are rewired to the target.',
1413
- schema: { sourceId: z.string(), targetId: z.string() },
1414
- handler: async (args) => text(mergeTask(role.id, args.sourceId, args.targetId)),
1415
- });
1416
- }
1417
- const cancelTask = opts.cancelTask;
1418
- if (cancelTask) {
1419
- tools.push({
1420
- name: 'org_task_cancel',
1421
- description: 'Cancel a task as moot. The task becomes "cancelled" and unblocks downstream work. Its assignee is told to stop and not to commit or report further work for it; a role that runs one session per task has that session\'s process ended at once.',
1422
- schema: { taskId: z.string(), reason: z.string().optional() },
1423
- handler: async (args) => text(cancelTask(role.id, args.taskId, args.reason)),
1424
- });
1425
- }
1426
- const blockTask = opts.blockTask;
1427
- if (blockTask) {
1428
- tools.push({
1429
- name: 'org_task_block',
1430
- description: `Mark a task you are actively working (status "running") as blocked on a real-world time, not on other tasks — e.g. a scheduled soak test, a CI run that takes hours, a human-set deadline. Use this INSTEAD of leaving the task "running" with nothing actually happening, and instead of calling org_complete just because there is genuinely nothing to do right now: the idle watchdog will stop nudging you about this task until the time you give arrives, then automatically re-dispatch it to you. Give untilIso as an ISO 8601 date/time (e.g. "2026-08-19T09:00:00Z"). Nothing external wakes a blocked task — not a background command finishing, not a Monitor event, not npm propagation — so never block on a command you started: run waits in the foreground instead, with a command timeout long enough for them. Until the deadline you are woken periodically (every run_config.block_recheck_minutes, default 5; recheckAfterMinutes sets it for this block, 1-${MAX_BLOCK_RECHECK_MINUTES}) to re-check: close the task, re-block it, or report.`,
1431
- schema: {
1432
- taskId: z.string(),
1433
- untilIso: z.string(),
1434
- reason: z.string().optional(),
1435
- recheckAfterMinutes: z.number().positive().max(MAX_BLOCK_RECHECK_MINUTES).optional(),
1436
- },
1437
- handler: async (args) => text(blockTask(role.id, args.taskId, args.untilIso, args.reason, args.recheckAfterMinutes)),
1438
- });
1439
- }
1440
- const planGraph = opts.planGraph;
1441
- if (planGraph) {
1442
- tools.push({
1443
- name: 'org_plan_graph',
1444
- description: 'Propose a full work graph in one call. Each task spec uses a local "name" and references other specs by name in "after", and may carry a "brief" with its instructions exactly as org_task does.' +
1445
- (catalog ? ' Each spec may select a "loadout" exactly as org_task does.' : ''),
1446
- schema: {
1447
- tasks: z
1448
- .array(strictArgs({
1449
- name: z.string(),
1450
- title: z.string(),
1451
- assignee: z.string(),
1452
- after: z.array(z.string()).default([]),
1453
- brief: briefArg,
1454
- ...loadoutArg,
1455
- }, { deps: 'use `after` with node names' }))
1456
- .min(1),
1457
- },
1458
- handler: async (args) => text(planGraph(role.id, args.tasks ?? [])),
1459
- });
1460
- }
1461
- tools.push({
1462
- name: 'org_send',
1463
- description: 'Send a message to another agent (role id) or another org ("org:role"). This is the only inter-agent channel.' +
1464
- // D3: only orgs with a task-scoped role see this, so every other org's
1465
- // tool list (prefix position 0) is unchanged.
1466
- (opts.def?.roles.some((r) => resolveSessionScope(r, opts.def) === 'task')
1467
- ? " When a message is about a task, start its subject with [task:<id>] so a task-scoped recipient reads it in that task's session."
1468
- : ''),
1469
- schema: { to: z.string(), subject: z.string(), message: z.string() },
1470
- handler: async (args) => {
1471
- if (opts.beforeTool) {
1472
- const approved = await opts.beforeTool(role.id, 'org_send', args);
1473
- if (approved === false)
1474
- return text('Tool "org_send" was denied by guardrail approval');
1475
- if (approved === null)
1476
- return text('Tool "org_send" is pending human approval - you will receive the result when it is approved or denied.');
1477
- }
1478
- const receipt = await deliver(role.id, args.to, args.subject, args.message);
1479
- return text(receipt);
1480
- },
1481
- });
1482
- tools.push({
1483
- name: 'ask_human',
1484
- description: 'Ask a human a free-form question. Use only when you genuinely need human judgment. ' +
1485
- 'Set blocking: true ONLY if you cannot continue until it is answered — a blocking question pauses the ' +
1486
- "org's idle watchdog (for up to an hour; after that the run resumes its normal idle checks either way). " +
1487
- 'If you can keep working while you wait — an FYI, a preference, anything you would describe as "not blocking on this" — ' +
1488
- 'pass blocking: false and carry on; the question is still recorded and answered, it just does not freeze the run. ' +
1489
- 'Defaults to blocking.',
1490
- schema: { question: z.string(), blocking: z.boolean().optional() },
1491
- handler: async (args) => {
1492
- if (!opts.askHuman)
1493
- return text('ask_human is not available in this session');
1494
- const receipt = await opts.askHuman(role.id, args.question, args.blocking);
1495
- return text(receipt);
1496
- },
1497
- });
1498
- // Built-in org tools reject undeclared keys instead of stripping them: a
1499
- // stripped `deps` on an org_plan_graph node silently dropped every edge.
1500
- for (const t of tools)
1501
- t.strict ??= {};
1502
- return tools;
1503
- }
1504
395
  //# sourceMappingURL=session.js.map