@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,1677 +1,14 @@
1
1
  // packages/@monomind/cli/src/commands/org-observe.ts
2
- // Read-side org subcommands (logs / report) + template scaffolding (create).
3
- // Kept out of org.ts to respect the 500-line file ceiling.
4
- import { existsSync, readFileSync, writeFileSync } from 'node:fs';
5
- import { join } from 'node:path';
6
- import { resolveOrgDefBlueprints } from '../catalog/blueprints.js';
7
- import { branchCheckpoint } from '../orgrt/checkpoint-ops.js';
8
- import { checkOrgStructure } from '../orgrt/migrate.js';
9
- import { roleTokensNote } from '../orgrt/report-budget.js';
10
- import { formatEvent, listRunDirs, readHistory, readRunEvents, summarizeRun, utcDateMinute, } from '../orgrt/reporting.js';
11
- import { gitEnforcementFindings } from '../orgrt/role-sandbox.js';
12
- import { resolveModel } from '../orgrt/session.js';
13
- import { buildFromTemplate, ORG_TEMPLATES } from '../orgrt/templates.js';
14
- import { ORG_DIR, OrgDefSchema } from '../orgrt/types.js';
15
- import { output } from '../output.js';
16
- import { listOrgConfigFiles, validateOrgName } from './org.js';
17
- const log = (text) => {
18
- console.log(text);
19
- };
20
- // ─── Agent Exec Protocol JSON output (doc/agent-exec-protocol.md §7) ────────
21
- // Org observe commands emit machine JSON under the global `--format json`
22
- // flag: one JSON object on stdout, diagnostics on stderr only. Envelope for
23
- // lists {v, org, items}; singletons are bare objects carrying v.
24
- /** True when this invocation asked for protocol JSON output. */
25
- export const orgJson = (ctx) => ctx.flags.format === 'json';
26
- /** Print one protocol JSON payload on stdout (compact — one line, NDJSON-safe
27
- * for line-oriented callers) and return a success result. */
28
- export const printOrgJson = (payload) => {
29
- process.stdout.write(`${JSON.stringify(payload)}\n`);
30
- return { success: true, data: payload };
31
- };
32
- /** M5: `--by <resolver>` — who resolved a decision (default `human`). Returns
33
- * an error string for an invalid value. */
34
- const resolverFlag = async (ctx) => {
35
- const raw = ctx.flags.by;
36
- if (raw === undefined || raw === false)
37
- return { ok: true, by: 'human' };
38
- const { normalizeResolver } = await import('../orgrt/approvals.js');
39
- const by = normalizeResolver(raw);
40
- if (!by)
41
- return { ok: false, message: '--by must be 1-128 printable characters' };
42
- return { ok: true, by };
43
- };
44
- /** Validate org config(s) against OrgDefSchema — the exact parse `org run`/`org serve`
45
- * perform — plus the structural invariants the runtime assumes but the schema can't
46
- * express (single root role, resolvable reports_to, unique ids, parseable schedule). */
47
- export const validateAction = async (ctx) => {
48
- const orgsDir = join(ctx.cwd || process.cwd(), ORG_DIR);
49
- let files;
50
- if (ctx.args[0]) {
51
- const validated = validateOrgName(ctx.args[0]);
52
- if (!validated.ok)
53
- return validated.result;
54
- files = [`${validated.name}.json`];
55
- }
56
- else {
57
- if (!existsSync(orgsDir))
58
- return {
59
- success: false,
60
- message: 'no orgs directory — create an org first with /mastermind:createorg',
61
- };
62
- files = listOrgConfigFiles(orgsDir);
63
- if (!files.length)
64
- return { success: false, message: 'no org configs found' };
65
- }
66
- let failed = 0;
67
- for (const f of files) {
68
- const stem = f.replace(/\.json$/, '');
69
- const path = join(orgsDir, f);
70
- const errors = [];
71
- const warnings = [];
72
- if (!existsSync(path)) {
73
- log(output.error(`${stem}: not found (${path})`));
74
- failed++;
75
- continue;
76
- }
77
- try {
78
- const parsedDef = OrgDefSchema.parse(JSON.parse(readFileSync(path, 'utf8')));
79
- const bp = resolveOrgDefBlueprints(parsedDef, ctx.cwd || process.cwd());
80
- const def = bp.def;
81
- errors.push(...bp.errors);
82
- for (const n of bp.notes)
83
- log(output.info(`${stem}: ${n}`));
84
- errors.push(...checkOrgStructure(def));
85
- // ADR-O001 D8: a cost tier that can't resolve a model for a role's
86
- // provider is a config error, not a runtime fallback — surface it here
87
- // as well as at daemon start, so it's caught before a run is attempted.
88
- const { validateCostTiers } = await import('../orgrt/cost-tier.js');
89
- errors.push(...validateCostTiers(def));
90
- // S3: a typo'd placeholder would reach the prompt verbatim.
91
- const { unknownPromptVarErrors } = await import('../orgrt/prompt-vars.js');
92
- errors.push(...unknownPromptVarErrors(def));
93
- // ADR-O001 D7: catalog size (>15 is an error, <5 a warning) and every
94
- // loadout's skills/file must resolve — same checks as daemon start.
95
- const { validateLoadouts } = await import('../orgrt/loadouts.js');
96
- const loadoutFindings = validateLoadouts(def, ctx.cwd || process.cwd());
97
- errors.push(...loadoutFindings.errors);
98
- warnings.push(...loadoutFindings.warnings);
99
- const { validateRoleSkills } = await import('../orgrt/skill-library.js');
100
- errors.push(...def.roles.flatMap((r) => validateRoleSkills(r, ctx.cwd || process.cwd())));
101
- // #258: roles whose policy.git won't have the OS sandbox behind it here
102
- const gitFindings = gitEnforcementFindings(def);
103
- errors.push(...gitFindings.errors);
104
- warnings.push(...gitFindings.warnings);
105
- if (def.name !== stem)
106
- warnings.push(`def.name "${def.name}" differs from filename — the runtime addresses this org as "${stem}"`);
107
- }
108
- catch (err) {
109
- errors.push(err instanceof Error ? err.message : String(err));
110
- }
111
- for (const w of warnings)
112
- log(output.warning(`${stem}: ${w}`));
113
- if (errors.length) {
114
- failed++;
115
- for (const e of errors)
116
- log(output.error(`${stem}: ${e}`));
117
- }
118
- else {
119
- log(output.success(`${stem}: valid${warnings.length ? ` (${warnings.length} warning(s))` : ''}`));
120
- }
121
- }
122
- return failed
123
- ? { success: false, message: `${failed} of ${files.length} org config(s) failed validation` }
124
- : { success: true, message: `${files.length} org config(s) valid` };
125
- };
126
- /** M2: ids of the org's endpoint roles (automations, not agents) — excluded
127
- * from the costs/report/flow role tables. Unreadable config → none. */
128
- const endpointRoleIds = (cwd, name) => {
129
- try {
130
- const raw = JSON.parse(readFileSync(join(cwd, ORG_DIR, `${name}.json`), 'utf8'));
131
- return new Set((Array.isArray(raw.roles) ? raw.roles : [])
132
- .filter((r) => r?.kind === 'endpoint' && typeof r.id === 'string')
133
- .map((r) => r.id));
134
- }
135
- catch {
136
- return new Set();
137
- }
138
- };
139
- // Run ids are joined into filesystem paths — enforce the daemon's own id shape
140
- // so a crafted --run can't traverse out of the org directory (same reason the
141
- // org-name guard exists).
142
- const RUN_ID_RE = /^run-[A-Za-z0-9-]+$/;
143
- const resolveRun = (cwd, name, runFlag) => {
144
- if (typeof runFlag === 'string' && runFlag)
145
- return RUN_ID_RE.test(runFlag) ? runFlag : null;
146
- return listRunDirs(cwd, name)[0] ?? null;
147
- };
148
- /** `org logs <name> [--run id] [--role r] [--filter-tool t] [--filter-role r] [--tools-only] [--audit-filter allow|deny] [--follow]` — formatted bus.jsonl tail. */
149
- export const logsAction = async (ctx, name) => {
150
- const run = resolveRun(ctx.cwd, name, ctx.flags.run);
151
- if (!run)
152
- return {
153
- success: false,
154
- message: `no runs found for org ${name} — start one with: monomind org run ${name}`,
155
- };
156
- const file = join(ctx.cwd, ORG_DIR, name, run, 'bus.jsonl');
157
- const roleFilter = typeof ctx.flags.role === 'string' ? ctx.flags.role : null;
158
- const filterTool = typeof ctx.flags['filter-tool'] === 'string' ? ctx.flags['filter-tool'] : null;
159
- const filterRole = typeof ctx.flags['filter-role'] === 'string' ? ctx.flags['filter-role'] : null;
160
- const auditFilter = typeof ctx.flags['audit-filter'] === 'string' ? ctx.flags['audit-filter'] : null;
161
- const toolsOnly = ctx.flags['tools-only'] === true;
162
- // Protocol JSON mode: full filtered event array, no live tail (§7.2 — the
163
- // streaming form is `org events --ndjson --follow`).
164
- if (orgJson(ctx)) {
165
- if (ctx.flags.follow === true)
166
- return { success: false, message: 'json output cannot --follow — use: org events --ndjson' };
167
- const items = [];
168
- const accept = (e) => (!toolsOnly || e.type === 'tool') &&
169
- (!roleFilter || e.from === roleFilter || e.to === roleFilter) &&
170
- (!filterTool || e.tool === filterTool) &&
171
- (!filterRole || e.from === filterRole) &&
172
- (!auditFilter || e.type !== 'tool' || e.decision === auditFilter);
173
- if (existsSync(file)) {
174
- for (const line of readFileSync(file, 'utf8').split('\n').filter(Boolean)) {
175
- try {
176
- const e = JSON.parse(line);
177
- if (accept(e))
178
- items.push(e);
179
- }
180
- catch {
181
- /* skip corrupt interior lines — same policy as the tail drain */
182
- }
183
- }
184
- }
185
- return printOrgJson({ v: 1, org: name, run, items });
186
- }
187
- const show = (e) => {
188
- if (toolsOnly && e.type !== 'tool')
189
- return;
190
- if (roleFilter && e.from !== roleFilter && e.to !== roleFilter)
191
- return;
192
- if (filterTool && e.tool !== filterTool)
193
- return;
194
- if (filterRole && e.from !== filterRole)
195
- return;
196
- if (auditFilter && e.type === 'tool' && e.decision !== auditFilter)
197
- return;
198
- log(formatEvent(e));
199
- };
200
- log(output.info(`org ${name} — ${run}${roleFilter ? ` (role: ${roleFilter})` : ''}${filterTool ? ` (tool: ${filterTool})` : ''}${filterRole ? ` (filter-role: ${filterRole})` : ''}${auditFilter ? ` (audit-filter: ${auditFilter})` : ''}`));
201
- let seenLines = 0;
202
- const drain = () => {
203
- if (!existsSync(file))
204
- return;
205
- const lines = readFileSync(file, 'utf8').split('\n').filter(Boolean);
206
- for (let i = seenLines; i < lines.length; i++) {
207
- try {
208
- show(JSON.parse(lines[i]));
209
- seenLines = i + 1;
210
- }
211
- catch {
212
- // Only the FINAL line can be a partial mid-append write worth
213
- // retrying; a corrupt interior line would otherwise stall the tail
214
- // forever — skip it and keep going.
215
- if (i === lines.length - 1)
216
- break;
217
- seenLines = i + 1;
218
- }
219
- }
220
- };
221
- drain();
222
- if (ctx.flags.follow !== true)
223
- return { success: true };
224
- log(output.info('following — Ctrl-C to stop'));
225
- await new Promise((resolve) => {
226
- const iv = setInterval(drain, 500);
227
- process.once('SIGINT', () => {
228
- clearInterval(iv);
229
- resolve();
230
- });
231
- process.once('SIGTERM', () => {
232
- clearInterval(iv);
233
- resolve();
234
- });
235
- });
236
- return { success: true };
237
- };
238
- /**
239
- * `org watch <name> <role> [--verbose] [--stats]` — live-tail one role's
240
- * assistant chat text.
241
- *
242
- * Every runner (Claude included — this isn't specific to the subprocess CLI
243
- * runners) funnels through session.ts's shared message loop, which emits
244
- * each assistant-text chunk onto the bus as a `chat` event. This command is
245
- * just `logsAction` pre-filtered to that event type + role and formatted as
246
- * a plain transcript instead of the full annotated event line — a friendlier
247
- * front door onto infrastructure that already exists and already covers
248
- * every runtime uniformly. For the fuller event stream (tool calls, audit
249
- * decisions) use `org logs <name> --role <role> --follow` directly.
250
- *
251
- * --verbose additionally interleaves that role's `status` events (session
252
- * start/end, restart/crash/backoff, state changes) into the transcript, so
253
- * a human watching sees WHY a role went quiet instead of just silence.
254
- * --stats prints a running token/cost line off that role's `usage` events
255
- * (emitted per turn by session.ts, same as --verbose: already-existing bus
256
- * data, no new instrumentation).
257
- */
258
- export const watchAction = async (ctx, name) => {
259
- const role = ctx.args[1];
260
- if (!role)
261
- return { success: false, message: 'usage: monomind org watch <org> <role>' };
262
- const run = resolveRun(ctx.cwd, name, ctx.flags.run);
263
- if (!run)
264
- return {
265
- success: false,
266
- message: `no runs found for org ${name} — start one with: monomind org run ${name}`,
267
- };
268
- const file = join(ctx.cwd, ORG_DIR, name, run, 'bus.jsonl');
269
- const verbose = ctx.flags.verbose === true;
270
- const stats = ctx.flags.stats === true;
271
- let totalTokens = 0;
272
- let totalCostUsd = 0;
273
- const show = (e) => {
274
- if (e.from !== role)
275
- return;
276
- if (e.type === 'chat') {
277
- log(`${output.info(`${role}:`)} ${e.msg ?? ''}`);
278
- return;
279
- }
280
- if (verbose && e.type === 'status') {
281
- log(output.warning(`[${e.reason ?? 'status'}] ${e.msg ?? ''}`));
282
- return;
283
- }
284
- if (stats && e.type === 'usage') {
285
- const tokens = typeof e.data?.tokens === 'number' ? e.data.tokens : 0;
286
- const costDelta = typeof e.data?.cost_usd === 'number' ? e.data.cost_usd : 0;
287
- totalTokens += tokens;
288
- totalCostUsd += costDelta;
289
- log(output.info(`[stats] +${tokens} tokens (total ${totalTokens}) · +$${costDelta.toFixed(4)} (total $${totalCostUsd.toFixed(4)})`));
290
- }
291
- };
292
- log(output.info(`watching ${name}/${role} — ${run} (Ctrl-C to stop; org logs ${name} --role ${role} --follow for the full event stream)`));
293
- let seenLines = 0;
294
- const drain = () => {
295
- if (!existsSync(file))
296
- return;
297
- const lines = readFileSync(file, 'utf8').split('\n').filter(Boolean);
298
- for (let i = seenLines; i < lines.length; i++) {
299
- try {
300
- show(JSON.parse(lines[i]));
301
- seenLines = i + 1;
302
- }
303
- catch {
304
- if (i === lines.length - 1)
305
- break; // only the final line may be a mid-append partial write
306
- seenLines = i + 1;
307
- }
308
- }
309
- };
310
- drain();
311
- if (ctx.flags.follow === false)
312
- return { success: true }; // --follow=false opts out of the default live tail
313
- await new Promise((resolve) => {
314
- const iv = setInterval(drain, 500);
315
- process.once('SIGINT', () => {
316
- clearInterval(iv);
317
- resolve();
318
- });
319
- process.once('SIGTERM', () => {
320
- clearInterval(iv);
321
- resolve();
322
- });
323
- });
324
- return { success: true };
325
- };
326
- /** `org report <name> [--run id] [--all] [--by-role] [--format mermaid]` — summarize a run (or list run history). */
327
- export const reportAction = async (ctx, name) => {
328
- if (ctx.flags.all === true) {
329
- const history = readHistory(ctx.cwd, name);
330
- if (!history.length)
331
- return { success: false, message: `no run history for org ${name}` };
332
- if (orgJson(ctx))
333
- return printOrgJson({ v: 1, org: name, items: history });
334
- log(output.info(`org ${name} — ${history.length} recorded run(s):`));
335
- for (const h of history) {
336
- const dur = h.durationMs != null ? `${Math.round(h.durationMs / 1000)}s` : '?';
337
- // #302: an honest "why", not a bare "no outcome recorded", when the
338
- // truth gate actually knows the real cause (idle-stop, failed-start, a
339
- // boss-restart giving up) and whether work was left outstanding.
340
- const outcome = h.outcome
341
- ? `${h.outcome.status}: ${h.outcome.summary.slice(0, 60)}`
342
- : h.closedBy && h.closedBy !== 'org-complete'
343
- ? `no outcome recorded — ended via ${h.closedBy}${h.runnableTasksAtStop > 0 ? `, ${h.runnableTasksAtStop} task(s) left` : ''}`
344
- : 'no outcome recorded';
345
- log(output.info(` • ${h.run} ${dur} ${h.totalTokens} tokens ${h.messages} msgs — ${outcome}`));
346
- }
347
- return { success: true };
348
- }
349
- const run = resolveRun(ctx.cwd, name, ctx.flags.run);
350
- if (!run)
351
- return { success: false, message: `no runs found for org ${name}` };
352
- const events = readRunEvents(ctx.cwd, name, run);
353
- if (!events.length)
354
- return { success: false, message: `run ${run} has no recorded events` };
355
- const s = summarizeRun(events);
356
- // M2: endpoint roles are automations — they have no row in the role tables.
357
- const endpointIds = endpointRoleIds(ctx.cwd, name);
358
- for (const id of endpointIds)
359
- delete s.roles[id];
360
- // Protocol JSON mode (§7.2): the run summary as a bare object. Emitted
361
- // before the human-only flag modes (mermaid/audit/by-role) — those render
362
- // views of the same summary and have no JSON shape defined by the spec.
363
- if (orgJson(ctx)) {
364
- return printOrgJson({
365
- v: 1,
366
- org: name,
367
- run,
368
- duration_ms: s.durationMs,
369
- events: s.events,
370
- messages: s.messages,
371
- xorg_messages: s.xorgMessages,
372
- total_tokens: s.totalTokens,
373
- total_cost_usd: s.totalCostUsd,
374
- outcome: s.outcome,
375
- blocker: s.blocker,
376
- blocker_detail: s.blockerDetail,
377
- closed_by: s.closedBy,
378
- runnable_tasks_at_stop: s.runnableTasksAtStop,
379
- cut_short: s.cutShort,
380
- crashes: s.crashes,
381
- roles: s.roles,
382
- assets: s.assets,
383
- });
384
- }
385
- // Mermaid flowchart (--format mermaid flag)
386
- if (ctx.flags.format === 'mermaid') {
387
- // Extract message flow from events
388
- const messageEvents = events.filter((e) => e.type === 'message' || e.type === 'xorg');
389
- const roleSet = new Set();
390
- const edges = new Set();
391
- for (const e of messageEvents) {
392
- if (e.from) {
393
- const fromRole = e.from.includes(':') ? e.from.split(':')[1] : e.from;
394
- roleSet.add(fromRole);
395
- if (e.to) {
396
- const toRole = e.to.includes(':') ? e.to.split(':')[1] : e.to;
397
- roleSet.add(toRole);
398
- const edge = `${fromRole} -->|${e.subject || 'msg'}| ${toRole}`;
399
- edges.add(edge);
400
- }
401
- }
402
- }
403
- const roles = Array.from(roleSet).sort();
404
- log(output.info(`flowchart TD`));
405
- log(output.info(` %% Org flow for ${name} / ${run}`));
406
- log(output.info(` %% ${messageEvents.length} messages exchanged`));
407
- log(output.info(` `));
408
- // Define role nodes with styling
409
- for (const role of roles) {
410
- log(output.info(` ${role}[${role}]`));
411
- }
412
- log(output.info(` `));
413
- // Add edges for messages
414
- for (const edge of edges) {
415
- log(output.info(` ${edge}`));
416
- }
417
- log(output.info(` `));
418
- log(output.info(`classDef bossNode fill:#f9f,stroke:#333,stroke-width:2px`));
419
- log(output.info(`classDef workerNode fill:#bbf,stroke:#333,stroke-width:1px`));
420
- return { success: true, message: `Mermaid flowchart exported for ${name} / ${run}` };
421
- }
422
- // Tool audit filter (--audit flag) - show only tool decision events
423
- if (ctx.flags.audit === true) {
424
- let toolEvents = events.filter((e) => e.type === 'tool');
425
- // Optional --tool flag filters to a specific tool name
426
- if (typeof ctx.flags.tool === 'string' && ctx.flags.tool) {
427
- const toolName = ctx.flags.tool;
428
- toolEvents = toolEvents.filter((e) => e.tool === toolName);
429
- }
430
- if (!toolEvents.length) {
431
- const toolFilter = typeof ctx.flags.tool === 'string' ? ` for tool "${ctx.flags.tool}"` : '';
432
- log(output.info(`No tool events found${toolFilter} in ${run}`));
433
- return { success: true };
434
- }
435
- log(output.info(`Tool audit trail for ${name} / ${run} (${toolEvents.length} events):`));
436
- log(output.info('┌──────────────────┬─────────────────────────┬──────────┬──────────────────────────────────────┐'));
437
- log(output.info('│ Role │ Tool │ Decision │ Reason │'));
438
- log(output.info('├──────────────────┼─────────────────────────┼──────────┼──────────────────────────────────────┤'));
439
- for (const e of toolEvents) {
440
- const role = e.from ?? 'system';
441
- const tool = e.tool ?? 'unknown';
442
- const decision = e.decision === 'deny' ? 'DENY' : 'ALLOW';
443
- const reason = e.reason || '-';
444
- log(output.info(`│ ${role.padEnd(16)} │ ${tool.padEnd(23)} │ ${decision.padEnd(8)} │ ${reason.padEnd(38)} │`));
445
- }
446
- log(output.info('└──────────────────┴─────────────────────────┴──────────┴──────────────────────────────────────┘'));
447
- return { success: true };
448
- }
449
- // Per-role cost breakdown (--by-role flag)
450
- if (ctx.flags['by-role'] === true) {
451
- const byRole = new Map();
452
- for (const [roleId, roleStats] of Object.entries(s.roles)) {
453
- const acc = byRole.get(roleId) ?? { cost: 0, tokens: 0, messages: 0 };
454
- acc.cost += roleStats.costUsd ?? 0;
455
- acc.tokens += roleStats.tokens;
456
- acc.messages += roleStats.messagesSent;
457
- byRole.set(roleId, acc);
458
- }
459
- log(output.info(`Per-role cost breakdown for ${name} / ${run}:`));
460
- log(output.info('┌──────────────────┬───────────┬────────────┬───────────┐'));
461
- log(output.info('│ Role │ Cost ($) │ Tokens │ Messages │'));
462
- log(output.info('├──────────────────┼───────────┼────────────┼───────────┤'));
463
- for (const [roleId, data] of byRole) {
464
- log(output.info(`│ ${roleId.padEnd(16)} │ ${(data.cost.toFixed(4)).padStart(9)} │ ${String(data.tokens).padStart(10)} │ ${String(data.messages).padStart(9)} │`));
465
- }
466
- log(output.info('└──────────────────┴───────────┴────────────┴───────────┘'));
467
- return { success: true };
468
- }
469
- // Per-role budget ceiling: same split the daemon applies (budget ÷ role count),
470
- // with any explicit policy.maxTokens override. Missing/unreadable config → no ceilings.
471
- let perRoleBudget = null;
472
- let basis = 'uncached';
473
- const roleCeiling = new Map();
474
- try {
475
- const def = OrgDefSchema.parse(JSON.parse(readFileSync(join(ctx.cwd, ORG_DIR, `${name}.json`), 'utf8')));
476
- const sessionRoles = def.roles.filter((r) => r.kind !== 'endpoint');
477
- perRoleBudget = Math.floor((def.run_config.budget_tokens ?? 1_000_000) / Math.max(1, sessionRoles.length));
478
- basis = def.run_config.budget_tokens_basis ?? 'uncached';
479
- for (const r of sessionRoles) {
480
- const max = r.policy?.maxTokens;
481
- roleCeiling.set(r.id, max ?? r.budget_tokens ?? perRoleBudget);
482
- }
483
- }
484
- catch {
485
- /* config gone or invalid — report without budget context */
486
- }
487
- log(output.info(`ORG REPORT — ${name} / ${run}`));
488
- log(output.info(` Duration: ${s.durationMs != null ? `${Math.round(s.durationMs / 1000)}s` : '?'} Events: ${s.events} Messages: ${s.messages}${s.xorgMessages ? ` (+${s.xorgMessages} cross-org)` : ''}`));
489
- log(output.info(` Tokens: ${s.totalTokens}${perRoleBudget ? ` (budget: ${perRoleBudget}/role ${basis === 'billable' ? 'billable' : 'in+out'})` : ''}${s.totalCostUsd ? ` Cost: $${s.totalCostUsd.toFixed(4)}` : ''}`));
490
- if (s.outcome) {
491
- // #302: blocker/blockerDetail are top-level on RunSummary (a sibling of
492
- // outcome, not nested in it — see reporting.ts), rendered here alongside
493
- // the outcome they were attached to.
494
- const blockerSuffix = s.blocker
495
- ? ` [blocker: ${s.blocker}${s.blockerDetail ? ` — ${s.blockerDetail}` : ''}]`
496
- : '';
497
- log(output.success(` Outcome: ${s.outcome.status} (by ${s.outcome.by}) — ${s.outcome.summary}${blockerSuffix}`));
498
- }
499
- else if (s.closedBy && s.closedBy !== 'org-complete') {
500
- // #302 truth gate: a run that never called org_complete still has a real,
501
- // recorded cause — never leave an operator reading "not recorded" when
502
- // we actually know the run was idle-stopped, failed to start, or gave up
503
- // after a boss-restart, and how much runnable work was left behind.
504
- const pending = s.runnableTasksAtStop > 0 ? `, ${s.runnableTasksAtStop} task(s) left` : '';
505
- log(output.warning(` Outcome: not recorded — ended via ${s.closedBy}${pending}`));
506
- }
507
- else {
508
- log(output.warning(' Outcome: not recorded (coordinator never called org_complete)'));
509
- }
510
- log(output.info(' Roles:'));
511
- for (const [id, r] of Object.entries(s.roles)) {
512
- const wasCutShort = s.cutShort.includes(id);
513
- const icon = r.crashed ? '✗' : wasCutShort ? '⊘' : '•';
514
- const suffix = r.crashed ? ' — CRASHED' : wasCutShort ? ' — cut short by stop' : '';
515
- log(output.info(` ${icon} ${id}: ${r.messagesSent} msgs, ${r.toolsAllowed} tools${r.toolsDenied ? ` (${r.toolsDenied} denied)` : ''}, ${roleTokensNote(r, roleCeiling.get(id), basis)}${suffix}`));
516
- }
517
- if (s.assets.length) {
518
- log(output.info(` Assets (${s.assets.length}):`));
519
- for (const a of s.assets.slice(0, 20))
520
- log(output.info(` 📄 ${a}`));
521
- if (s.assets.length > 20)
522
- log(output.info(` … and ${s.assets.length - 20} more`));
523
- }
524
- return { success: true };
525
- };
526
- /** Read questions.json. A MISSING file legitimately means "no questions" → [].
527
- * Any other failure (unreadable, malformed — e.g. a partial daemon write) THROWS:
528
- * answerAction rewrites this file from what this returns, so silently coercing a
529
- * failed read to [] would atomically replace every recorded question with one. */
530
- const readQuestions = (cwd, name) => {
531
- const path = join(cwd, ORG_DIR, name, 'questions.json');
532
- let raw;
533
- try {
534
- raw = readFileSync(path, 'utf8');
535
- }
536
- catch (err) {
537
- if (err.code === 'ENOENT')
538
- return [];
539
- throw new Error(`cannot read ${path}: ${err instanceof Error ? err.message : String(err)}`);
540
- }
541
- let parsed;
542
- try {
543
- parsed = JSON.parse(raw);
544
- }
545
- catch (err) {
546
- throw new Error(`${path} is not valid JSON (${err instanceof Error ? err.message : String(err)})`);
547
- }
548
- if (parsed?.questions === undefined || parsed.questions === null)
549
- return [];
550
- if (!Array.isArray(parsed.questions))
551
- throw new Error(`${path}: "questions" is not an array`);
552
- return parsed.questions;
553
- };
554
- /** `org questions <name> [--all]` — list pending (or all) ask_human questions. */
555
- export const questionsAction = async (ctx, name) => {
556
- let all;
557
- try {
558
- all = readQuestions(ctx.cwd, name);
559
- }
560
- catch (err) {
561
- log(output.error(`Cannot read questions for org ${name}: ${err instanceof Error ? err.message : String(err)}`));
562
- return { success: false, message: 'questions.json unreadable' };
563
- }
564
- const shown = ctx.flags.all === true ? all : all.filter((q) => q.answer === null);
565
- if (orgJson(ctx))
566
- return printOrgJson({ v: 1, org: name, items: shown });
567
- if (!shown.length) {
568
- log(output.info(all.length
569
- ? `No pending questions for org ${name} (${all.length} answered — use --all).`
570
- : `No questions recorded for org ${name}.`));
571
- return { success: true };
572
- }
573
- for (const q of shown) {
574
- const when = utcDateMinute(q.ts);
575
- log(output.info(`${q.answer === null ? '❓' : '✓'} [${q.questionId}] ${when} ${q.role}: ${q.question}`));
576
- if (q.answer !== null)
577
- log(output.info(` ↳ ${q.answer}`));
578
- }
579
- if (shown.some((q) => q.answer === null))
580
- log(output.info(`\nAnswer with: monomind org answer ${name} <question-id> "your answer"`));
581
- return { success: true };
582
- };
583
- /** Read approvals.json — the tool/action-approval queue checked by
584
- * checkApproval for Bash/WebFetch/WebSearch/org_complete (session.ts).
585
- * Distinct from questions.json/gates.json: approving those does NOT touch
586
- * this file or grant anything here. A MISSING file legitimately means "no
587
- * pending approvals" → []. Any other failure THROWS, same rationale as
588
- * readQuestions above — approveAction/denyAction rewrite this file from
589
- * what this returns. */
590
- const readApprovals = (cwd, name) => {
591
- const path = join(cwd, ORG_DIR, name, 'approvals.json');
592
- let raw;
593
- try {
594
- raw = readFileSync(path, 'utf8');
595
- }
596
- catch (err) {
597
- if (err.code === 'ENOENT')
598
- return [];
599
- throw new Error(`cannot read ${path}: ${err instanceof Error ? err.message : String(err)}`);
600
- }
601
- let parsed;
602
- try {
603
- parsed = JSON.parse(raw);
604
- }
605
- catch (err) {
606
- throw new Error(`${path} is not valid JSON (${err instanceof Error ? err.message : String(err)})`);
607
- }
608
- if (parsed?.approvals === undefined || parsed.approvals === null)
609
- return [];
610
- if (!Array.isArray(parsed.approvals))
611
- throw new Error(`${path}: "approvals" is not an array`);
612
- return parsed.approvals;
613
- };
614
- /** `org approvals <name> [--all]` — list pending (or all) tool/action approval requests. */
615
- export const approvalsAction = async (ctx, name) => {
616
- let all;
617
- try {
618
- all = readApprovals(ctx.cwd, name);
619
- }
620
- catch (err) {
621
- log(output.error(`Cannot read approvals for org ${name}: ${err instanceof Error ? err.message : String(err)}`));
622
- return { success: false, message: 'approvals.json unreadable' };
623
- }
624
- const shown = ctx.flags.all === true ? all : all.filter((a) => a.approved === null);
625
- if (orgJson(ctx))
626
- return printOrgJson({
627
- v: 1,
628
- org: name,
629
- // M5: requestId / resolvedBy / input are always present (null for
630
- // entries recorded before they existed).
631
- items: shown.map((a) => ({
632
- ...a,
633
- requestId: a.requestId ?? null,
634
- resolvedBy: a.resolvedBy ?? null,
635
- input: a.input ?? null,
636
- })),
637
- });
638
- if (!shown.length) {
639
- log(output.info(all.length
640
- ? `No pending approvals for org ${name} (${all.length} resolved — use --all).`
641
- : `No approval requests recorded for org ${name}.`));
642
- return { success: true };
643
- }
644
- for (const a of shown) {
645
- const when = utcDateMinute(a.ts);
646
- const mark = a.approved === null ? '❓' : a.approved ? '✓' : '✗';
647
- const ref = a.requestId ? ` [${a.requestId}]` : '';
648
- const by = a.resolvedBy ? ` (by ${a.resolvedBy})` : '';
649
- log(output.info(`${mark} ${when} ${a.roleId}: ${a.action}${ref}${by}`));
650
- }
651
- if (shown.some((a) => a.approved === null))
652
- log(output.info(`\nApprove with: monomind org approve ${name} <role> <action> [--request <id>] [--by <resolver>]\nDeny with: monomind org deny ${name} <role> <action> [--request <id>] [--by <resolver>]`));
653
- return { success: true };
654
- };
655
- /** `org answer <name> <question-id> <answer...>` — answer a pending ask_human question.
656
- * Delivers live via the hosting daemon's /api/answer-question when the org is running
657
- * (broker lookup); otherwise records the answer and queues it for the next run. */
658
- export const answerAction = async (ctx, name) => {
659
- const questionId = ctx.args[1];
660
- const answer = ctx.args.slice(2).join(' ').trim();
661
- if (!questionId || !answer)
662
- return {
663
- success: false,
664
- message: `usage: monomind org answer ${name} <question-id> "answer text"`,
665
- };
666
- let questions;
667
- try {
668
- questions = readQuestions(ctx.cwd, name);
669
- }
670
- catch (err) {
671
- log(output.error(`Cannot read questions for org ${name}: ${err instanceof Error ? err.message : String(err)}`));
672
- return { success: false, message: 'questions.json unreadable — answer not recorded' };
673
- }
674
- const q = questions.find((x) => x.questionId === questionId);
675
- if (!q) {
676
- log(output.error(`Question "${questionId}" not found for org ${name} — list with: monomind org questions ${name}`));
677
- return { success: false, message: 'question not found' };
678
- }
679
- if (q.answer !== null)
680
- return { success: false, message: `question "${questionId}" was already answered` };
681
- const byFlag = await resolverFlag(ctx);
682
- if (!byFlag.ok)
683
- return { success: false, message: byFlag.message };
684
- const resolvedBy = byFlag.by;
685
- // Live path: the hosting daemon updates questions.json and pushes into the role's mailbox.
686
- // SEC: answering a role's question is a human decision — authenticate with
687
- // the operator credential, not the agent-facing one in the broker entry.
688
- const { lookupOrg, readOperatorCredential } = await import('../orgrt/broker.js');
689
- const remote = lookupOrg(name);
690
- if (remote) {
691
- const cred = readOperatorCredential(name);
692
- try {
693
- const res = await fetch(`${remote.url}/api/answer-question`, {
694
- method: 'POST',
695
- headers: {
696
- 'Content-Type': 'application/json',
697
- ...(cred ? { 'x-monomind-cred': cred } : {}),
698
- },
699
- body: JSON.stringify({ org: name, role: q.role, questionId, answer, resolvedBy }),
700
- signal: AbortSignal.timeout(10_000),
701
- });
702
- const data = (await res.json().catch(() => ({})));
703
- if (res.ok && data.ok) {
704
- if (orgJson(ctx))
705
- return printOrgJson({
706
- v: 1,
707
- org: name,
708
- question_id: questionId,
709
- role: q.role,
710
- delivery: 'live',
711
- answered: true,
712
- resolvedBy,
713
- });
714
- log(output.success(`Answer delivered to ${name}:${q.role} (live).`));
715
- return { success: true };
716
- }
717
- log(output.warning(`Live delivery rejected (${data.error ?? res.status}) — falling back to offline queue.`));
718
- }
719
- catch (err) {
720
- log(output.warning(`Hosting daemon unreachable (${err instanceof Error ? err.message : 'error'}) — falling back to offline queue.`));
721
- }
722
- }
723
- // Offline path: mirror daemon.answerQuestion's org-not-running branch.
724
- // RE-READ and merge by questionId just before writing — the pre-fetch
725
- // snapshot can be up to 10s stale (live-delivery timeout), and rewriting
726
- // from it would delete questions the daemon appended meanwhile and revert
727
- // answers it recorded (atomic rename prevents torn writes, not lost updates).
728
- // A FAILED re-read must abort the write: rewriting from [] would atomically
729
- // rename a single-question file over every other recorded question.
730
- let fresh;
731
- try {
732
- fresh = readQuestions(ctx.cwd, name);
733
- }
734
- catch (err) {
735
- log(output.error(`Refusing to rewrite questions.json — ${err instanceof Error ? err.message : String(err)}`));
736
- log(output.warning(`The answer was NOT recorded. Fix or restore ${join(ctx.cwd, ORG_DIR, name, 'questions.json')}, then retry.`));
737
- return { success: false, message: 'questions.json unreadable — answer not recorded' };
738
- }
739
- const freshQ = fresh.find((x) => x.questionId === questionId);
740
- if (freshQ && freshQ.answer !== null) {
741
- return {
742
- success: false,
743
- message: `question "${questionId}" was answered while this command was running`,
744
- };
745
- }
746
- // Queue BEFORE marking answered (same rule as daemon.answerQuestion): if the
747
- // append fails, the question must stay pending and answerable. Marking first meant
748
- // a failed queueMessage recorded the answer as delivered while nothing was queued,
749
- // and the `already answered` guard then rejected every retry.
750
- const { queueMessage } = await import('../orgrt/inbox.js');
751
- try {
752
- queueMessage(ctx.cwd, name, {
753
- fromQualified: 'human',
754
- toRole: q.role,
755
- subject: `answer:${questionId}`,
756
- body: `question: ${q.question}\n\nanswer: ${answer}`,
757
- ts: Date.now(),
758
- });
759
- }
760
- catch (err) {
761
- log(output.error(`Could not queue the answer for ${name}:${q.role} (${err instanceof Error ? err.message : String(err)}) — answer NOT recorded, retry it.`));
762
- return { success: false, message: 'queueing failed — answer not recorded' };
763
- }
764
- const merged = fresh.some((x) => x.questionId === questionId)
765
- ? fresh.map((x) => x.questionId === questionId ? { ...x, answer, answeredAt: Date.now(), resolvedBy } : x)
766
- : [...fresh, { ...q, answer, answeredAt: Date.now(), resolvedBy }];
767
- const dest = join(ctx.cwd, ORG_DIR, name, 'questions.json');
768
- const tmp = `${dest}.${process.pid}.tmp`;
769
- writeFileSync(tmp, JSON.stringify({ questions: merged }, null, 2));
770
- const { renameSync } = await import('node:fs');
771
- renameSync(tmp, dest);
772
- if (orgJson(ctx))
773
- return printOrgJson({
774
- v: 1,
775
- org: name,
776
- question_id: questionId,
777
- role: q.role,
778
- delivery: 'queued',
779
- answered: true,
780
- resolvedBy,
781
- });
782
- log(output.success(`Answer recorded — ${name}:${q.role} receives it when the org next runs.`));
783
- return { success: true };
784
- };
785
- /** `org inbox <name> --json '{"from":"orgA:role","subject":"...","body":"..."}' [--to role]`
786
- * (or `--from/--subject/--body`) `[--format json]`.
787
- * Inbound entrypoint for cross-org/remote delivery — orgrt/remote.ts's deliverRemote()
788
- * shells out to exactly this command over SSH, and mono-agent replies through it.
789
- *
790
- * Live path (M3): POST to the hosting daemon's /api/xdeliver with the OPERATOR
791
- * credential (readOperatorCredential), which lets the daemon trust `from` as
792
- * given — the sender org need not be registered (`workflow`, an automation
793
- * role). Without an operator credential it presents the sender org's own
794
- * broker credential, if that org is registered. Offline path: spool into
795
- * inbox.jsonl, drained into the role's mailbox on the org's next start.
796
- *
797
- * `--format json` prints `{"v":1,"org","to","from","delivery":"live"|"queued",
798
- * "receipt","messageId"}`. Exit 0 for live and queued; non-zero only for
799
- * invalid input or an unknown org. */
800
- export const inboxAction = async (ctx, name) => {
801
- const json = orgJson(ctx);
802
- // JSON mode keeps stdout to the single payload — diagnostics go to stderr.
803
- const note = (text) => {
804
- if (json)
805
- process.stderr.write(`${text}\n`);
806
- else
807
- log(text);
808
- };
809
- const fail = (message) => {
810
- if (json)
811
- process.stdout.write(`${JSON.stringify({ v: 1, org: name, error: message })}\n`);
812
- else
813
- log(output.error(message));
814
- return { success: false, message };
815
- };
816
- let payload = {};
817
- const rawJson = ctx.flags.json;
818
- if (typeof rawJson === 'string') {
819
- try {
820
- payload = JSON.parse(rawJson);
821
- }
822
- catch {
823
- return fail('org inbox: --json is not valid JSON');
824
- }
825
- }
826
- else {
827
- payload = { from: ctx.flags.from, subject: ctx.flags.subject, body: ctx.flags.body };
828
- }
829
- const from = typeof payload.from === 'string' ? payload.from.trim() : '';
830
- const subject = typeof payload.subject === 'string' ? payload.subject : '';
831
- const body = typeof payload.body === 'string' ? payload.body : '';
832
- if (!from || !body)
833
- return fail('org inbox: payload requires "from" and "body" (via --json or --from/--body)');
834
- if (!/^[^\s:]{1,128}(:[^\s:]{1,128})?$/.test(from))
835
- return fail(`org inbox: invalid sender "${from}" — use "<org>:<role>" or "<role>"`);
836
- const { lookupOrg, normalizeCredential, readOperatorCredential } = await import('../orgrt/broker.js');
837
- const remote = lookupOrg(name);
838
- const defPath = join(ctx.cwd, ORG_DIR, `${name}.json`);
839
- if (!remote && !existsSync(defPath))
840
- return fail(`Org not found: ${name}`);
841
- // Target role: explicit --to, else the org's coordinator (reports_to == null),
842
- // else the first role — matching where a role-less cross-org message should land.
843
- let toRole = typeof ctx.flags.to === 'string' ? ctx.flags.to : '';
844
- if (toRole && !/^[a-z0-9][a-z0-9_-]*$/i.test(toRole))
845
- return fail(`Invalid role id: ${toRole}`);
846
- if (!toRole) {
847
- if (!existsSync(defPath))
848
- return fail(`Org "${name}" config is not in this project — pass --to <role>.`);
849
- try {
850
- const def = JSON.parse(readFileSync(defPath, 'utf8'));
851
- const roles = Array.isArray(def.roles) ? def.roles : [];
852
- toRole = roles.find((r) => r.reports_to == null)?.id ?? roles[0]?.id ?? '';
853
- }
854
- catch (err) {
855
- return fail(`Could not read org config for ${name}: ${err instanceof Error ? err.message : String(err)}`);
856
- }
857
- if (!toRole)
858
- return fail(`Org "${name}" has no roles to deliver to — pass --to <role>.`);
859
- }
860
- const { newMessageId, queueMessage } = await import('../orgrt/inbox.js');
861
- const messageId = newMessageId();
862
- const to = `${name}:${toRole}`;
863
- const done = (delivery, receipt) => {
864
- if (json)
865
- return printOrgJson({ v: 1, org: name, to, from, delivery, receipt, messageId });
866
- log(output.success(receipt));
867
- return { success: true, message: receipt };
868
- };
869
- // Live path: a hosting daemon registered this org with the broker.
870
- if (remote) {
871
- const [fromOrg, fromRole] = from.includes(':') ? from.split(':', 2) : ['external', from];
872
- const operatorCred = readOperatorCredential(name);
873
- const agentCred = normalizeCredential(remote.credential);
874
- const cred = operatorCred ?? agentCred;
875
- const fromCredential = operatorCred ? undefined : lookupOrg(fromOrg)?.credential;
876
- try {
877
- const res = await fetch(`${remote.url}/api/xdeliver`, {
878
- method: 'POST',
879
- headers: {
880
- 'Content-Type': 'application/json',
881
- ...(cred ? { 'x-monomind-cred': cred } : {}),
882
- },
883
- body: JSON.stringify({
884
- fromOrg,
885
- fromRole,
886
- ...(fromCredential ? { fromCredential } : {}),
887
- toOrg: name,
888
- toRole,
889
- subject,
890
- body,
891
- messageId,
892
- }),
893
- signal: AbortSignal.timeout(10_000),
894
- });
895
- const data = (await res.json().catch(() => ({})));
896
- if (res.ok && data.ok) {
897
- const receipt = data.receipt ?? `delivered to ${to}`;
898
- // The daemon itself may have queued it (role starting, org waking).
899
- return done(/^queued/.test(receipt) ? 'queued' : 'live', receipt);
900
- }
901
- note(output.warning(`Live delivery rejected (${data.error ?? res.status}) — falling back to offline queue.`));
902
- }
903
- catch (err) {
904
- note(output.warning(`Hosting daemon unreachable (${err instanceof Error ? err.message : 'error'}) — falling back to offline queue.`));
905
- }
906
- }
907
- // Offline path: spool; drained into the role's mailbox when the org next starts.
908
- const queued = queueMessage(ctx.cwd, name, {
909
- fromQualified: from,
910
- toRole,
911
- subject,
912
- body,
913
- ts: Date.now(),
914
- messageId,
915
- });
916
- if (!queued) {
917
- const message = `Could not queue the message for ${to} (disk full or permissions).`;
918
- if (json)
919
- process.stdout.write(`${JSON.stringify({ v: 1, org: name, error: message })}\n`);
920
- else
921
- log(output.error(message));
922
- return { success: false, message: 'queueing failed' };
923
- }
924
- return done('queued', `queued for ${to} (delivered when the org next runs)`);
925
- };
926
- /** `org create <name> --template <t> [--goal g] [--schedule s]` — scaffold a config from a template. */
927
- export const createAction = async (ctx, name) => {
928
- const templateName = typeof ctx.flags.template === 'string' ? ctx.flags.template : '';
929
- if (!templateName) {
930
- log(output.info(`Available templates: ${Object.keys(ORG_TEMPLATES).join(', ')}`));
931
- return {
932
- success: false,
933
- message: 'usage: monomind org create <name> --template <template> [--goal "..."] [--schedule 30m]',
934
- };
935
- }
936
- const def = buildFromTemplate(templateName, name, typeof ctx.flags.goal === 'string' ? ctx.flags.goal : undefined);
937
- if (!def) {
938
- log(output.error(`Unknown template "${templateName}" — available: ${Object.keys(ORG_TEMPLATES).join(', ')}`));
939
- return { success: false, message: 'unknown template' };
940
- }
941
- if (typeof ctx.flags.schedule === 'string')
942
- def.schedule = ctx.flags.schedule;
943
- const file = join(ctx.cwd, ORG_DIR, `${name}.json`);
944
- if (existsSync(file) && ctx.flags.force !== true) {
945
- log(output.error(`Org "${name}" already exists — pass --force to overwrite.`));
946
- return { success: false, message: 'org exists' };
947
- }
948
- OrgDefSchema.parse(def); // templates must always produce a runnable config
949
- // Per-role model — the single most consequential setting the template picked on
950
- // the user's behalf. Mirror resolveModel() (same helper `org run`'s cost estimate
951
- // uses) so a role relying on its runtime/vendor default isn't mislabeled here.
952
- const modelRows = def.roles.map((r) => {
953
- const explicit = r.adapter_config?.model;
954
- return {
955
- id: r.id,
956
- model: String(explicit ?? resolveModel(r, r.runtime ?? def.runtime, r.provider?.vendor)),
957
- explicit: !!explicit,
958
- };
959
- });
960
- const printModels = () => {
961
- log(output.bold(' Models:'));
962
- for (const r of modelRows) {
963
- log(` ${r.id.padEnd(20)} ${r.model}${r.explicit ? '' : ' (default)'}`);
964
- }
965
- };
966
- if (ctx.interactive && ctx.flags.yes !== true) {
967
- log(output.bold(`\nAbout to create org "${name}" from template "${templateName}" (${def.roles.length} roles):`));
968
- printModels();
969
- const { confirm } = await import('../prompt.js');
970
- const proceed = await confirm({ message: 'Create this org?', default: true });
971
- if (!proceed) {
972
- log(output.info('Cancelled — no file written. Adjust --template/--goal, or edit the template, then retry (pass --yes to skip this prompt).'));
973
- return { success: false, message: 'cancelled by user' };
974
- }
975
- }
976
- const { mkdirSync } = await import('node:fs');
977
- mkdirSync(join(ctx.cwd, ORG_DIR), { recursive: true });
978
- writeFileSync(file, `${JSON.stringify(def, null, 2)}\n`, 'utf8');
979
- log(output.success(`Org "${name}" created from template "${templateName}" (${def.roles.length} roles).`));
980
- log(output.info(` Budget: ${def.run_config.budget_tokens} tokens · Turn limit: ${def.run_config.max_turns_per_message} per message (effectively unlimited by default — set run_config.max_turns_per_message, or a role's own max_turns_per_message, to cap it).`));
981
- if (!ctx.interactive || ctx.flags.yes === true)
982
- printModels();
983
- log(output.info(` Edit the goal/roles in ${file}, then: monomind org run ${name}`));
984
- return { success: true };
985
- };
986
- /** `org costs <name> [--run id]` — show per-role cost tracking from runtime.json */
987
- export const costsAction = async (ctx, name) => {
988
- const run = resolveRun(ctx.cwd, name, ctx.flags.run);
989
- if (!run)
990
- return {
991
- success: false,
992
- message: `no runs found for org ${name} — start one with: monomind org run ${name}`,
993
- };
994
- // Read runtime.json for the per-role metrics
995
- const rtPath = join(ctx.cwd, ORG_DIR, name, 'runtime.json');
996
- if (!existsSync(rtPath)) {
997
- return { success: false, message: `no runtime state found for org ${name}` };
998
- }
999
- let rt;
1000
- try {
1001
- rt = JSON.parse(readFileSync(rtPath, 'utf8'));
1002
- }
1003
- catch (err) {
1004
- log(output.error(`Cannot read runtime.json: ${err instanceof Error ? err.message : String(err)}`));
1005
- return { success: false, message: 'runtime.json unreadable' };
1006
- }
1007
- if (rt?.run !== run && !orgJson(ctx)) {
1008
- log(output.warning(`Note: runtime.json shows run ${rt?.run ?? 'unknown'} — showing metrics for requested run ${run} from history`));
1009
- }
1010
- // Also check the run summary for cost data
1011
- const events = readRunEvents(ctx.cwd, name, run);
1012
- const summary = events.length ? summarizeRun(events) : null;
1013
- if (!orgJson(ctx))
1014
- log(output.info(`Per-role cost breakdown for ${name} / ${run}:`));
1015
- // Combine data from runtime.json (live metrics) and summary (historical)
1016
- const roleData = new Map();
1017
- // M2: endpoint roles are automations — no row in the cost table.
1018
- const endpointIds = endpointRoleIds(ctx.cwd, name);
1019
- // Add live metrics from runtime.json
1020
- if (rt?.roleMetrics) {
1021
- for (const [roleId, metrics] of Object.entries(rt.roleMetrics)) {
1022
- if (endpointIds.has(roleId))
1023
- continue;
1024
- roleData.set(roleId, {
1025
- tokens: metrics.tokens,
1026
- costUsd: metrics.costUsd,
1027
- messages: 0,
1028
- });
1029
- }
1030
- }
1031
- // Add historical data from summary if available
1032
- if (summary?.roles) {
1033
- for (const [roleId, roleStats] of Object.entries(summary.roles)) {
1034
- if (endpointIds.has(roleId))
1035
- continue;
1036
- const existing = roleData.get(roleId) ?? { tokens: 0, costUsd: 0, messages: 0 };
1037
- roleData.set(roleId, {
1038
- tokens: existing.tokens || roleStats.tokens,
1039
- costUsd: existing.costUsd || roleStats.costUsd,
1040
- messages: roleStats.messagesSent,
1041
- });
1042
- }
1043
- }
1044
- if (orgJson(ctx)) {
1045
- const items = [];
1046
- for (const [roleId, data] of roleData) {
1047
- items.push({
1048
- role: roleId,
1049
- tokens: data.tokens,
1050
- cost_usd: data.costUsd,
1051
- messages: data.messages,
1052
- });
1053
- }
1054
- const totals = items.reduce((acc, i) => ({
1055
- tokens: acc.tokens + i.tokens,
1056
- cost_usd: acc.cost_usd + i.cost_usd,
1057
- messages: acc.messages + i.messages,
1058
- }), { tokens: 0, cost_usd: 0, messages: 0 });
1059
- return printOrgJson({ v: 1, org: name, run, items, totals });
1060
- }
1061
- if (roleData.size === 0) {
1062
- log(output.info(`No role metrics available yet — metrics populate as agents use tokens.`));
1063
- return { success: true };
1064
- }
1065
- log(output.info('┌──────────────────┬───────────┬────────────┬───────────┐'));
1066
- log(output.info('│ Role │ Cost ($) │ Tokens │ Messages │'));
1067
- log(output.info('├──────────────────┼───────────┼────────────┼───────────┤'));
1068
- let totalCost = 0;
1069
- let totalTokens = 0;
1070
- let totalMessages = 0;
1071
- for (const [roleId, data] of roleData) {
1072
- log(output.info(`│ ${roleId.padEnd(16)} │ ${(data.costUsd.toFixed(4)).padStart(9)} │ ${String(data.tokens).padStart(10)} │ ${String(data.messages).padStart(9)} │`));
1073
- totalCost += data.costUsd;
1074
- totalTokens += data.tokens;
1075
- totalMessages += data.messages;
1076
- }
1077
- log(output.info('├──────────────────┼───────────┼────────────┼───────────┤'));
1078
- log(output.info(`│ ${('TOTAL').padEnd(16)} │ ${(totalCost.toFixed(4)).padStart(9)} │ ${String(totalTokens).padStart(10)} │ ${String(totalMessages).padStart(9)} │`));
1079
- log(output.info('└──────────────────┴───────────┴────────────┴───────────┘'));
1080
- return { success: true };
1081
- };
1082
- /** `org flow <name> [--run id]` — export org flow as Mermaid diagram */
1083
- export const flowAction = async (ctx, name) => {
1084
- const run = resolveRun(ctx.cwd, name, ctx.flags.run);
1085
- if (!run)
1086
- return {
1087
- success: false,
1088
- message: `no runs found for org ${name} — start one with: monomind org run ${name}`,
1089
- };
1090
- const events = readRunEvents(ctx.cwd, name, run);
1091
- if (!events.length)
1092
- return { success: false, message: `run ${run} has no recorded events` };
1093
- // Extract message flow from events
1094
- const messageEvents = events.filter((e) => e.type === 'message' || e.type === 'xorg');
1095
- const roleSet = new Set();
1096
- const edges = new Set();
1097
- const edgeObjects = [];
1098
- for (const e of messageEvents) {
1099
- if (e.from) {
1100
- const fromRole = e.from.includes(':') ? e.from.split(':')[1] : e.from;
1101
- roleSet.add(fromRole);
1102
- if (e.to) {
1103
- const toRole = e.to.includes(':') ? e.to.split(':')[1] : e.to;
1104
- roleSet.add(toRole);
1105
- const edge = `${fromRole} -->|${e.subject || 'msg'}| ${toRole}`;
1106
- edges.add(edge);
1107
- edgeObjects.push({ from: fromRole, to: toRole, subject: e.subject || 'msg' });
1108
- }
1109
- }
1110
- }
1111
- // M2: endpoint roles are automations — not listed as role nodes.
1112
- const endpointIds = endpointRoleIds(ctx.cwd, name);
1113
- const roles = Array.from(roleSet)
1114
- .filter((r) => !endpointIds.has(r))
1115
- .sort();
1116
- // Protocol JSON mode (§7.2): structured roles + edges instead of Mermaid.
1117
- if (orgJson(ctx))
1118
- return printOrgJson({ v: 1, org: name, run, roles, edges: edgeObjects });
1119
- log(output.info(`flowchart TD`));
1120
- log(output.info(` %% Org flow for ${name} / ${run}`));
1121
- log(output.info(` %% ${messageEvents.length} messages exchanged`));
1122
- log(output.info(` `));
1123
- // Define role nodes with styling
1124
- for (const role of roles) {
1125
- log(output.info(` ${role}[${role}]`));
1126
- }
1127
- log(output.info(` `));
1128
- // Add edges for messages
1129
- for (const edge of edges) {
1130
- log(output.info(` ${edge}`));
1131
- }
1132
- log(output.info(` `));
1133
- log(output.info(`classDef bossNode fill:#f9f,stroke:#333,stroke-width:2px`));
1134
- log(output.info(`classDef workerNode fill:#bbf,stroke:#333,stroke-width:1px`));
1135
- return { success: true, message: `Mermaid flowchart exported for ${name} / ${run}` };
1136
- };
1137
- /** `org approve <org> <role> <action>` — approve a pending tool/action approval */
1138
- /** Shared by approveAction/denyAction: try the live daemon first (updates its
1139
- * in-memory state and notifies the waiting agent's mailbox immediately), and
1140
- * fall back to writing approvals.json directly when the org isn't running or
1141
- * the daemon is unreachable — mirrors answerAction's live-then-offline shape. */
1142
- async function resolveApproval(ctx, name, role, action, approved) {
1143
- const verb = approved ? 'approved' : 'denied';
1144
- // M5: attribution and request scoping.
1145
- const byFlag = await resolverFlag(ctx);
1146
- if (!byFlag.ok)
1147
- return { success: false, message: byFlag.message };
1148
- const resolvedBy = byFlag.by;
1149
- const requestId = typeof ctx.flags.request === 'string' && ctx.flags.request ? ctx.flags.request : undefined;
1150
- if (ctx.flags.request !== undefined && !requestId)
1151
- return { success: false, message: '--request needs an approval request id (apr-…)' };
1152
- // SEC: approvals carry human authority — the operator credential, never the
1153
- // broker entry's agent credential (which any agent subprocess can read).
1154
- const { lookupOrg, readOperatorCredential } = await import('../orgrt/broker.js');
1155
- const remote = lookupOrg(name);
1156
- if (remote) {
1157
- const cred = readOperatorCredential(name);
1158
- try {
1159
- const res = await fetch(`${remote.url}/api/set-approval`, {
1160
- method: 'POST',
1161
- headers: {
1162
- 'Content-Type': 'application/json',
1163
- ...(cred ? { 'x-monomind-cred': cred } : {}),
1164
- },
1165
- body: JSON.stringify({
1166
- org: name,
1167
- role,
1168
- action,
1169
- approved,
1170
- resolvedBy,
1171
- ...(requestId ? { requestId } : {}),
1172
- }),
1173
- signal: AbortSignal.timeout(10_000),
1174
- });
1175
- const data = (await res.json().catch(() => ({})));
1176
- if (res.ok && data.ok) {
1177
- if (orgJson(ctx))
1178
- return printOrgJson({
1179
- v: 1,
1180
- org: name,
1181
- role,
1182
- action,
1183
- approved,
1184
- delivery: 'live',
1185
- resolvedBy,
1186
- requestId: requestId ?? null,
1187
- });
1188
- log(approved
1189
- ? output.success(`Approved: ${role} may execute ${action} (live, by ${resolvedBy}).`)
1190
- : output.info(`Denied: ${role} may NOT execute ${action} (live, by ${resolvedBy}).`));
1191
- return { success: true, message: `${verb} ${action} for ${role}` };
1192
- }
1193
- log(output.warning(`Live delivery rejected (${data.error ?? res.status}) — falling back to offline queue.`));
1194
- }
1195
- catch (err) {
1196
- log(output.warning(`Hosting daemon unreachable (${err instanceof Error ? err.message : 'error'}) — falling back to offline queue.`));
1197
- }
1198
- }
1199
- // Offline path: org isn't live (or the live call failed) — write approvals.json directly.
1200
- const approvalsPath = join(ctx.cwd, ORG_DIR, name, 'approvals.json');
1201
- if (!existsSync(approvalsPath)) {
1202
- return { success: false, message: `no pending approvals for org ${name}` };
1203
- }
1204
- const data = JSON.parse(readFileSync(approvalsPath, 'utf8'));
1205
- const pending = data.approvals ?? [];
1206
- // Same selection as the daemon's setApproval: the one request named by
1207
- // --request, else every still-pending entry for the (role, action) pair.
1208
- const items = pending.filter((a) => a.roleId === role &&
1209
- a.action === action &&
1210
- a.approved === null &&
1211
- (requestId === undefined || a.requestId === requestId));
1212
- if (items.length === 0) {
1213
- return {
1214
- success: false,
1215
- message: requestId
1216
- ? `no pending approval ${requestId} found for role ${role} action ${action}`
1217
- : `no pending approval found for role ${role} action ${action}`,
1218
- };
1219
- }
1220
- const now = Date.now();
1221
- for (const item of items) {
1222
- item.approved = approved;
1223
- item.ts = now;
1224
- item.resolvedBy = resolvedBy;
1225
- item.resolvedAt = now;
1226
- }
1227
- writeFileSync(approvalsPath, JSON.stringify({ approvals: pending }, null, 2));
1228
- if (orgJson(ctx))
1229
- return printOrgJson({
1230
- v: 1,
1231
- org: name,
1232
- role,
1233
- action,
1234
- approved,
1235
- delivery: 'recorded',
1236
- resolvedBy,
1237
- requestId: requestId ?? null,
1238
- });
1239
- log(approved
1240
- ? output.success(`Approved: ${role} may execute ${action} (by ${resolvedBy})`)
1241
- : output.info(`Denied: ${role} may NOT execute ${action} (by ${resolvedBy})`));
1242
- return { success: true, message: `${verb} ${action} for ${role}` };
1243
- }
1244
- /** `org approve <org> <role> <action> [--request <id>] [--by <resolver>]` — approve a pending tool/action approval */
1245
- export const approveAction = async (ctx, name) => {
1246
- const role = ctx.args[1];
1247
- const action = ctx.args[2];
1248
- if (!role || !action) {
1249
- return { success: false, message: 'usage: org approve <org> <role> <action>' };
1250
- }
1251
- return resolveApproval(ctx, name, role, action, true);
1252
- };
1253
- /** `org deny <org> <role> <action> [--request <id>] [--by <resolver>]` — deny a pending tool/action approval */
1254
- export const denyAction = async (ctx, name) => {
1255
- const role = ctx.args[1];
1256
- const action = ctx.args[2];
1257
- if (!role || !action) {
1258
- return { success: false, message: 'usage: org deny <org> <role> <action>' };
1259
- }
1260
- return resolveApproval(ctx, name, role, action, false);
1261
- };
1262
- /** `org replay <org> <run-id>` — time-travel debugging: re-emit a past run's bus
1263
- * events into a fresh replay run for inspection. This is event-log replay only —
1264
- * it does not restart agent execution or restore live sessions. To actually
1265
- * resume an org's execution from where it left off, use `org run --resume`. */
1266
- export const replayAction = async (ctx, name) => {
1267
- const run = ctx.args[1];
1268
- if (!run) {
1269
- return { success: false, message: 'usage: org replay <org> <run-id>' };
1270
- }
1271
- const runDir = join(ctx.cwd, ORG_DIR, name, run);
1272
- if (!existsSync(runDir)) {
1273
- return { success: false, message: `run ${run} not found for org ${name}` };
1274
- }
1275
- const busFile = join(runDir, 'bus.jsonl');
1276
- if (!existsSync(busFile)) {
1277
- return { success: false, message: `no bus events found for run ${run}` };
1278
- }
1279
- log(output.info(`Replaying org ${name} events from checkpoint ${run}...`));
1280
- // Create daemon and replay the bus events for debugging/inspection
1281
- const { OrgDaemon } = await import('../orgrt/daemon.js');
1282
- const daemon = new OrgDaemon(ctx.cwd, { forward: false });
1283
- const resumed = await daemon.replayFrom(name, run);
1284
- if (!resumed) {
1285
- return {
1286
- success: false,
1287
- message: `replay failed - check bus.jsonl and org config for ${name} are valid`,
1288
- };
1289
- }
1290
- log(output.success(`Org ${name} events replayed from ${run} as run ${resumed.run}`));
1291
- log(output.info(`Use: monomind org logs ${name} --run ${resumed.run} to inspect events.`));
1292
- log(output.info(`This is debug replay only — it does not restart agent execution. To resume live execution, use: monomind org run ${name} --resume`));
1293
- return { success: true, message: `replayed events from checkpoint ${run} as ${resumed.run}` };
1294
- };
1295
- /** PID of a live `org serve` daemon for this project, or null. Mirrors
1296
- * runAction's `liveServeDaemonPid` (commands/org.ts, ~line 107) — duplicated
1297
- * rather than imported because that function isn't exported there and this
1298
- * file is scoped to not touch org.ts. Liveness is confirmed against the pid
1299
- * itself, not just heartbeat-file presence, since a SIGKILLed daemon leaves
1300
- * a stale heartbeat behind; a stamp older than a few beats (daemon beats
1301
- * every 30s) means it's gone or wedged. */
1302
- function liveServeDaemonPid(cwd) {
1303
- try {
1304
- const hb = JSON.parse(readFileSync(join(cwd, '.monomind', 'serve-heartbeat.json'), 'utf8'));
1305
- if (typeof hb.pid !== 'number' || hb.pid === process.pid)
1306
- return null;
1307
- const age = Date.now() - Date.parse(hb.updatedAt ?? '');
1308
- if (!Number.isFinite(age) || age > 3 * 60_000)
1309
- return null;
1310
- process.kill(hb.pid, 0); // throws if the process is gone
1311
- return hb.pid;
1312
- }
1313
- catch {
1314
- return null;
1315
- }
1316
- }
1317
- /** `org resume-from <org>` — resume live execution from the org's persisted
1318
- * checkpoint (runtime.json): restores mailbox queues, policy/token counters,
1319
- * and session state, subject to checkpoint TTL and checksum validation. Unlike
1320
- * `replay`, this restarts real agent execution via `startOrg(..., { resume: true })`. */
1321
- export const resumeFromAction = async (ctx, name) => {
1322
- // A live `org serve` daemon (or another `org run`) may already own this
1323
- // org's execution. Building a fresh in-process OrgDaemon and resuming
1324
- // unconditionally — as this action used to — puts two processes on one
1325
- // runtime.json/broker lease and spawns a second set of role sessions
1326
- // against the same shared git workspace, since the fresh daemon's
1327
- // duplicate-start guard (`this.orgs.has(name)`) is scoped to its own empty
1328
- // in-memory map and never sees the other process. `runAction` (org.ts)
1329
- // already guards this by handing the request to the live daemon via a
1330
- // runfile instead of racing it — but that runfile mechanism only carries a
1331
- // plain "start" request (pollRunfiles/org.ts calls `daemon.startOrg(name,
1332
- // task)` with no resume option), so it can't be reused for a resume
1333
- // request without also touching org.ts. Refuse instead of racing the live
1334
- // daemon.
1335
- const serveOwner = liveServeDaemonPid(ctx.cwd);
1336
- if (serveOwner != null) {
1337
- log(output.error(`org ${name}: a live serve daemon (pid ${serveOwner}) already owns this project's orgs — refusing to start a second execution here.`));
1338
- log(output.info(`Check it against the live daemon instead: monomind org status ${name} / org decisions ${name} / org gates ${name}. ` +
1339
- `To resume from checkpoint yourself, stop the live daemon first (monomind org stop) or let it pick the org back up on its own schedule.`));
1340
- return {
1341
- success: false,
1342
- message: `org ${name}: already owned by live serve daemon (pid ${serveOwner}) — refused to avoid a duplicate execution`,
1343
- };
1344
- }
1345
- log(output.info(`Resuming org ${name} from checkpoint...`));
1346
- const { OrgDaemon } = await import('../orgrt/daemon.js');
1347
- const daemon = new OrgDaemon(ctx.cwd, { forward: false });
1348
- const resumed = await daemon.resumeOrg(name);
1349
- if (!resumed) {
1350
- return {
1351
- success: false,
1352
- message: `resume failed for ${name} - check runtime.json checkpoint is present, unexpired, and valid`,
1353
- };
1354
- }
1355
- log(output.success(`Org ${name} resumed - ${resumed.agents.size} role(s) restored`));
1356
- log(output.info(`Stop with: monomind org stop ${name}`));
1357
- return { success: true, message: `resumed ${name} - ${resumed.agents.size} role(s) restored` };
1358
- };
1359
- /** `org branch <org> <run-id> <label> [--format json]` — snapshot a run's event
1360
- * log for replay. This is NOT an executable what-if scenario: it copies
1361
- * bus.jsonl into a new run directory tagged with a `.branch-source` marker so
1362
- * it can be inspected or replayed later; it does not fork or re-run agent
1363
- * execution. Delegates to the shared, atomic (tmp+rename) implementation in
1364
- * checkpoint-ops.ts.
1365
- *
1366
- * `<label>` is a note recorded in `.branch-source`, not the new run's name —
1367
- * the run id is generated. `--format json` reports that generated id as a
1368
- * field so a scripted caller can feed it to `org replay` without scraping the
1369
- * human-readable line (#292). */
1370
- export const branchAction = async (ctx, name) => {
1371
- const run = ctx.args[1];
1372
- const label = ctx.args[2];
1373
- if (!run || !label) {
1374
- return { success: false, message: 'usage: org branch <org> <run-id> <label>' };
1375
- }
1376
- const result = branchCheckpoint(ctx.cwd, name, run, label);
1377
- if (!result.ok) {
1378
- return { success: false, message: result.error };
1379
- }
1380
- if (orgJson(ctx))
1381
- return printOrgJson({ v: 1, org: name, run: result.branchRun, from: run, label });
1382
- log(output.success(`Snapshotted ${run} as run ${result.branchRun} (label: "${label}")`));
1383
- log(output.info(`Replay it with: monomind org replay ${name} ${result.branchRun}`));
1384
- return { success: true, message: `snapshotted ${run} as run ${result.branchRun}` };
1385
- };
1386
- /** `org decisions <org> [--run id]` — show Rifft-style decision traces */
1387
- export const decisionsAction = async (ctx, name) => {
1388
- const run = resolveRun(ctx.cwd, name, ctx.flags.run);
1389
- if (!run) {
1390
- return { success: false, message: `no runs found for org ${name}` };
1391
- }
1392
- const events = readRunEvents(ctx.cwd, name, run);
1393
- if (!events.length) {
1394
- return { success: false, message: `run ${run} has no recorded events` };
1395
- }
1396
- // Filter decision trace events
1397
- const decisionEvents = events.filter((e) => e.type === 'audit' && e.reason === 'decision-trace' && e.data && typeof e.data === 'object');
1398
- if (orgJson(ctx)) {
1399
- return printOrgJson({
1400
- v: 1,
1401
- org: name,
1402
- run,
1403
- items: decisionEvents.map((e) => ({
1404
- ts: e.ts,
1405
- role: e.from ?? 'system',
1406
- ...e.data,
1407
- })),
1408
- });
1409
- }
1410
- if (!decisionEvents.length) {
1411
- log(output.info(`No decision traces found in ${run}`));
1412
- return { success: true };
1413
- }
1414
- log(output.info(`Decision traces for ${name} / ${run} (${decisionEvents.length} decisions):`));
1415
- log(output.info('┌──────────────────┬─────────────┬────────────────────────────────────────┐'));
1416
- log(output.info('│ Role │ Type │ Context │'));
1417
- log(output.info('├──────────────────┼─────────────┼────────────────────────────────────────┤'));
1418
- for (const e of decisionEvents) {
1419
- const role = e.from ?? 'system';
1420
- const type = e.data.decisionType ?? 'unknown';
1421
- const context = e.data.context ?? '-';
1422
- log(output.info(`│ ${role.padEnd(16)} │ ${type.padEnd(11)} │ ${context.padEnd(38)} │`));
1423
- }
1424
- log(output.info('└──────────────────┴─────────────┴────────────────────────────────────────┘'));
1425
- return { success: true, message: `${decisionEvents.length} decision traces` };
1426
- };
1427
- // ── Decision gates ──────────────────────────────────────────────────────
1428
- function readGatesFile(cwd, org) {
1429
- try {
1430
- return JSON.parse(readFileSync(join(cwd, ORG_DIR, org, 'gates.json'), 'utf8'));
1431
- }
1432
- catch {
1433
- return { gates: [] };
1434
- }
1435
- }
1436
- /** Read gates.json for a write path. A MISSING file legitimately means "no gates" → [].
1437
- * Any other failure (unreadable, malformed — e.g. a partial daemon write) THROWS:
1438
- * gateResolveAction rewrites this file from what this returns, so silently coercing a
1439
- * failed read to [] would surface a real I/O error as "gate not found" and a
1440
- * subsequent write would replace every recorded gate with just this one. */
1441
- function readGatesFileStrict(cwd, org) {
1442
- const path = join(cwd, ORG_DIR, org, 'gates.json');
1443
- let raw;
1444
- try {
1445
- raw = readFileSync(path, 'utf8');
1446
- }
1447
- catch (err) {
1448
- if (err.code === 'ENOENT')
1449
- return { gates: [] };
1450
- throw new Error(`cannot read ${path}: ${err instanceof Error ? err.message : String(err)}`);
1451
- }
1452
- let parsed;
1453
- try {
1454
- parsed = JSON.parse(raw);
1455
- }
1456
- catch (err) {
1457
- throw new Error(`${path} is not valid JSON (${err instanceof Error ? err.message : String(err)})`);
1458
- }
1459
- if (parsed?.gates === undefined || parsed.gates === null)
1460
- return { gates: [] };
1461
- if (!Array.isArray(parsed.gates))
1462
- throw new Error(`${path}: "gates" is not an array`);
1463
- return { gates: parsed.gates };
1464
- }
1465
- export const gatesAction = async (ctx, name) => {
1466
- const data = readGatesFile(ctx.cwd, name);
1467
- const showAll = ctx.flags.all === true;
1468
- const gates = showAll ? data.gates : data.gates.filter((g) => g.status === 'pending');
1469
- if (orgJson(ctx))
1470
- return printOrgJson({ v: 1, org: name, items: gates });
1471
- if (!gates.length) {
1472
- log(output.info(showAll
1473
- ? `No gates for org "${name}"`
1474
- : `No pending gates for org "${name}" (use --all to include resolved)`));
1475
- return { success: true };
1476
- }
1477
- log(output.info(`${showAll ? 'All' : 'Pending'} gates for org "${name}" (${gates.length}):\n`));
1478
- for (const g of gates) {
1479
- const status = g.status === 'pending'
1480
- ? '⏳ pending'
1481
- : g.status === 'approved'
1482
- ? '✅ approved'
1483
- : '❌ rejected';
1484
- log(output.info(` ${g.id} ${status} role:${g.roleId}`));
1485
- log(output.info(` name: ${g.name}`));
1486
- log(output.info(` desc: ${g.description}`));
1487
- if (g.resolution)
1488
- log(output.info(` resolution: ${g.resolution}`));
1489
- log('');
1490
- }
1491
- return { success: true, message: `${gates.length} gate(s)` };
1492
- };
1493
- export const gateResolveAction = async (ctx, name, approved) => {
1494
- const gateId = ctx.args[1];
1495
- const resolution = ctx.args.slice(2).join(' ') || undefined;
1496
- if (!gateId)
1497
- return {
1498
- success: false,
1499
- message: `usage: monomind org gate-${approved ? 'approve' : 'reject'} ${name} <gate-id> [resolution]`,
1500
- };
1501
- let data;
1502
- try {
1503
- data = readGatesFileStrict(ctx.cwd, name);
1504
- }
1505
- catch (err) {
1506
- log(output.error(`Cannot read gates for org ${name}: ${err instanceof Error ? err.message : String(err)}`));
1507
- return { success: false, message: 'gates.json unreadable — gate not resolved' };
1508
- }
1509
- const gate = data.gates.find((g) => g.id === gateId);
1510
- if (!gate)
1511
- return { success: false, message: `gate "${gateId}" not found for org "${name}"` };
1512
- if (gate.status !== 'pending')
1513
- return { success: false, message: `gate "${gateId}" already resolved (${gate.status})` };
1514
- const byFlag = await resolverFlag(ctx);
1515
- if (!byFlag.ok)
1516
- return { success: false, message: byFlag.message };
1517
- const resolvedBy = byFlag.by;
1518
- // SEC: gate resolution is a human decision — operator credential only.
1519
- const { lookupOrg, readOperatorCredential } = await import('../orgrt/broker.js');
1520
- const remote = lookupOrg(name);
1521
- if (remote) {
1522
- const cred = readOperatorCredential(name);
1523
- try {
1524
- const res = await fetch(`${remote.url}/api/resolve-gate`, {
1525
- method: 'POST',
1526
- headers: {
1527
- 'Content-Type': 'application/json',
1528
- ...(cred ? { 'x-monomind-cred': cred } : {}),
1529
- },
1530
- body: JSON.stringify({ org: name, gateId, approved, resolution, resolvedBy }),
1531
- signal: AbortSignal.timeout(10_000),
1532
- });
1533
- const d = (await res.json().catch(() => ({})));
1534
- if (res.ok && d.ok) {
1535
- if (orgJson(ctx))
1536
- return printOrgJson({
1537
- v: 1,
1538
- org: name,
1539
- gate_id: gateId,
1540
- status: approved ? 'approved' : 'rejected',
1541
- delivery: 'live',
1542
- resolvedBy,
1543
- });
1544
- log(output.success(`Gate ${gateId} ${approved ? 'approved' : 'rejected'} (live).`));
1545
- return { success: true, message: `gate ${approved ? 'approved' : 'rejected'}` };
1546
- }
1547
- log(output.warning(`Live resolution rejected (${d.error ?? res.status}) — falling back to offline queue.`));
1548
- }
1549
- catch (err) {
1550
- log(output.warning(`Hosting daemon unreachable (${err instanceof Error ? err.message : 'error'}) — falling back to offline queue.`));
1551
- }
1552
- }
1553
- // Offline path: no live daemon hosts this org (or the live call failed) —
1554
- // resolve gates.json directly, same as resolveApproval's offline branch.
1555
- // Re-read fresh (mirrors answerAction/resolveApproval) in case a live
1556
- // daemon resolved this gate concurrently while the live call above was
1557
- // in flight or timing out.
1558
- const { writeGates } = await import('../orgrt/decisions.js');
1559
- let fresh;
1560
- try {
1561
- fresh = readGatesFileStrict(ctx.cwd, name);
1562
- }
1563
- catch (err) {
1564
- log(output.error(`Refusing to rewrite gates.json — ${err instanceof Error ? err.message : String(err)}`));
1565
- log(output.warning(`The gate was NOT resolved. Fix or restore ${join(ctx.cwd, ORG_DIR, name, 'gates.json')}, then retry.`));
1566
- return { success: false, message: 'gates.json unreadable — gate not resolved' };
1567
- }
1568
- const idx = fresh.gates.findIndex((g) => g.id === gateId);
1569
- if (idx === -1)
1570
- return { success: false, message: `gate "${gateId}" not found for org "${name}"` };
1571
- if (fresh.gates[idx].status !== 'pending') {
1572
- return {
1573
- success: false,
1574
- message: `gate "${gateId}" already resolved (${fresh.gates[idx].status})`,
1575
- };
1576
- }
1577
- fresh.gates[idx] = {
1578
- ...fresh.gates[idx],
1579
- status: approved ? 'approved' : 'rejected',
1580
- resolvedAt: Date.now(),
1581
- resolvedBy,
1582
- resolution,
1583
- };
1584
- writeGates(ctx.cwd, name, fresh);
1585
- if (orgJson(ctx))
1586
- return printOrgJson({
1587
- v: 1,
1588
- org: name,
1589
- gate_id: gateId,
1590
- status: approved ? 'approved' : 'rejected',
1591
- delivery: 'recorded',
1592
- resolvedBy,
1593
- });
1594
- log(output.success(`Gate ${gateId} ${approved ? 'approved' : 'rejected'} — ${name} picks it up on its next cycle.`));
1595
- return { success: true, message: `gate ${approved ? 'approved' : 'rejected'} (queued)` };
1596
- };
1597
- // ─── org events — the only genuinely new org command (plan D12) ─────────────
1598
- /** Resolve a `--since` cursor: an event id (skip everything at/before it) or
1599
- * an ISO-8601 timestamp (skip older events). Returns null when unusable. */
1600
- function parseSinceCursor(raw) {
1601
- if (typeof raw !== 'string' || !raw)
1602
- return null;
1603
- if (/^run-[A-Za-z0-9-]+/.test(raw))
1604
- return { id: raw };
1605
- const ts = Date.parse(raw);
1606
- return Number.isFinite(ts) ? { iso: raw } : null;
1607
- }
1608
- /** `org events <name> [--run id] [--follow] [--since <eventId|iso>] [--ndjson]`
1609
- * — live tail of a run's bus.jsonl as NDJSON, one BusEvent per line
1610
- * (protocol §7.3). This is the machine streaming surface; `org logs` stays
1611
- * the human one. NDJSON is the only output mode (--ndjson accepted for
1612
- * spec symmetry). */
1613
- export const eventsAction = async (ctx, name) => {
1614
- const run = resolveRun(ctx.cwd, name, ctx.flags.run);
1615
- if (!run)
1616
- return {
1617
- success: false,
1618
- message: `no runs found for org ${name} — start one with: monomind org run ${name}`,
1619
- };
1620
- const file = join(ctx.cwd, ORG_DIR, name, run, 'bus.jsonl');
1621
- const since = parseSinceCursor(ctx.flags.since);
1622
- let skippedPastId = !since?.id; // true = no id cursor, nothing to skip
1623
- let seenLines = 0;
1624
- const emitLine = (line) => {
1625
- let e;
1626
- try {
1627
- e = JSON.parse(line);
1628
- }
1629
- catch {
1630
- return; // corrupt interior line — skip, same policy as logsAction
1631
- }
1632
- if (since?.id) {
1633
- if (!skippedPastId) {
1634
- if (e.id === since.id)
1635
- skippedPastId = true;
1636
- return; // everything strictly before the cursor is replay-suppressed
1637
- }
1638
- }
1639
- if (since?.iso && Date.parse(since.iso) && new Date(e.ts).getTime() < Date.parse(since.iso))
1640
- return;
1641
- process.stdout.write(`${JSON.stringify(e)}\n`);
1642
- };
1643
- const drain = () => {
1644
- if (!existsSync(file))
1645
- return;
1646
- const lines = readFileSync(file, 'utf8').split('\n').filter(Boolean);
1647
- for (let i = seenLines; i < lines.length; i++) {
1648
- // Only the FINAL line can be a partial mid-append write worth retrying.
1649
- if (i === lines.length - 1) {
1650
- try {
1651
- JSON.parse(lines[i]);
1652
- }
1653
- catch {
1654
- break;
1655
- }
1656
- }
1657
- emitLine(lines[i]);
1658
- seenLines = i + 1;
1659
- }
1660
- };
1661
- drain();
1662
- if (ctx.flags.follow !== true)
1663
- return { success: true };
1664
- await new Promise((resolve) => {
1665
- const iv = setInterval(drain, 500);
1666
- process.once('SIGINT', () => {
1667
- clearInterval(iv);
1668
- resolve();
1669
- });
1670
- process.once('SIGTERM', () => {
1671
- clearInterval(iv);
1672
- resolve();
1673
- });
1674
- });
1675
- return { success: true };
1676
- };
2
+ // Read-side org subcommand actions. Each subcommand family lives in its own
3
+ // org-observe-*.ts module; this module re-exports them so existing import
4
+ // paths (tests, and the org subcommand tables' lazy imports) keep working.
5
+ export { approvalsAction, approveAction, denyAction, } from './org-observe-approvals.js';
6
+ export { branchAction, decisionsAction, replayAction, resumeFromAction, } from './org-observe-checkpoint.js';
7
+ export { createAction, validateAction, } from './org-observe-config.js';
8
+ export { gateResolveAction, gatesAction, } from './org-observe-gates.js';
9
+ export { inboxAction } from './org-observe-inbox.js';
10
+ export { eventsAction, logsAction, watchAction, } from './org-observe-logs.js';
11
+ export { answerAction, questionsAction, } from './org-observe-questions.js';
12
+ export { costsAction, flowAction, reportAction, } from './org-observe-report.js';
13
+ export { orgJson, printOrgJson, } from './org-observe-shared.js';
1677
14
  //# sourceMappingURL=org-observe.js.map