@monoes/monomindcli 2.9.3 → 2.9.5

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 (99) hide show
  1. package/.claude/skills/mastermind-createorg/SKILL.md +2 -1
  2. package/README.md +9 -5
  3. package/dist/src/autopilot-state.d.ts +16 -4
  4. package/dist/src/autopilot-state.d.ts.map +1 -1
  5. package/dist/src/autopilot-state.js +42 -19
  6. package/dist/src/autopilot-state.js.map +1 -1
  7. package/dist/src/commands/agent-lifecycle.js +4 -4
  8. package/dist/src/commands/agent-lifecycle.js.map +1 -1
  9. package/dist/src/commands/autopilot.d.ts.map +1 -1
  10. package/dist/src/commands/autopilot.js +106 -25
  11. package/dist/src/commands/autopilot.js.map +1 -1
  12. package/dist/src/commands/doctor-project-checks.d.ts.map +1 -1
  13. package/dist/src/commands/doctor-project-checks.js +20 -3
  14. package/dist/src/commands/doctor-project-checks.js.map +1 -1
  15. package/dist/src/commands/hooks-coverage-commands.d.ts.map +1 -1
  16. package/dist/src/commands/hooks-coverage-commands.js +67 -75
  17. package/dist/src/commands/hooks-coverage-commands.js.map +1 -1
  18. package/dist/src/commands/hooks-routing-commands.d.ts.map +1 -1
  19. package/dist/src/commands/hooks-routing-commands.js +12 -26
  20. package/dist/src/commands/hooks-routing-commands.js.map +1 -1
  21. package/dist/src/commands/hooks-workers.d.ts.map +1 -1
  22. package/dist/src/commands/hooks-workers.js +16 -47
  23. package/dist/src/commands/hooks-workers.js.map +1 -1
  24. package/dist/src/commands/hooks.d.ts.map +1 -1
  25. package/dist/src/commands/hooks.js +96 -54
  26. package/dist/src/commands/hooks.js.map +1 -1
  27. package/dist/src/commands/memory-list.d.ts.map +1 -1
  28. package/dist/src/commands/memory-list.js +79 -36
  29. package/dist/src/commands/memory-list.js.map +1 -1
  30. package/dist/src/commands/org.js +2 -2
  31. package/dist/src/commands/org.js.map +1 -1
  32. package/dist/src/commands/security-cve.d.ts.map +1 -1
  33. package/dist/src/commands/security-cve.js +11 -1
  34. package/dist/src/commands/security-cve.js.map +1 -1
  35. package/dist/src/commands/security-misc.d.ts +9 -0
  36. package/dist/src/commands/security-misc.d.ts.map +1 -1
  37. package/dist/src/commands/security-misc.js +54 -33
  38. package/dist/src/commands/security-misc.js.map +1 -1
  39. package/dist/src/commands/security-scan.d.ts.map +1 -1
  40. package/dist/src/commands/security-scan.js +11 -3
  41. package/dist/src/commands/security-scan.js.map +1 -1
  42. package/dist/src/commands/swarm.d.ts.map +1 -1
  43. package/dist/src/commands/swarm.js +137 -96
  44. package/dist/src/commands/swarm.js.map +1 -1
  45. package/dist/src/init/statusline-generator.d.ts.map +1 -1
  46. package/dist/src/init/statusline-generator.js +47 -45
  47. package/dist/src/init/statusline-generator.js.map +1 -1
  48. package/dist/src/init/types.d.ts +1 -1
  49. package/dist/src/init/types.d.ts.map +1 -1
  50. package/dist/src/init/types.js +5 -1
  51. package/dist/src/init/types.js.map +1 -1
  52. package/dist/src/mcp-server.d.ts +6 -0
  53. package/dist/src/mcp-server.d.ts.map +1 -1
  54. package/dist/src/mcp-server.js +47 -22
  55. package/dist/src/mcp-server.js.map +1 -1
  56. package/dist/src/mcp-tools/embeddings-tools.js +2 -2
  57. package/dist/src/mcp-tools/embeddings-tools.js.map +1 -1
  58. package/dist/src/mcp-tools/hive-mind-tools.d.ts.map +1 -1
  59. package/dist/src/mcp-tools/hive-mind-tools.js +13 -11
  60. package/dist/src/mcp-tools/hive-mind-tools.js.map +1 -1
  61. package/dist/src/mcp-tools/hooks-intelligence.d.ts.map +1 -1
  62. package/dist/src/mcp-tools/hooks-intelligence.js +99 -68
  63. package/dist/src/mcp-tools/hooks-intelligence.js.map +1 -1
  64. package/dist/src/mcp-tools/hooks-routing.d.ts.map +1 -1
  65. package/dist/src/mcp-tools/hooks-routing.js +403 -215
  66. package/dist/src/mcp-tools/hooks-routing.js.map +1 -1
  67. package/dist/src/mcp-tools/monograph/impact-tools.d.ts.map +1 -1
  68. package/dist/src/mcp-tools/monograph/impact-tools.js +109 -44
  69. package/dist/src/mcp-tools/monograph/impact-tools.js.map +1 -1
  70. package/dist/src/mcp-tools/performance-tools.d.ts.map +1 -1
  71. package/dist/src/mcp-tools/performance-tools.js +10 -15
  72. package/dist/src/mcp-tools/performance-tools.js.map +1 -1
  73. package/dist/src/mcp-tools/system-tools.d.ts.map +1 -1
  74. package/dist/src/mcp-tools/system-tools.js +100 -38
  75. package/dist/src/mcp-tools/system-tools.js.map +1 -1
  76. package/dist/src/memory/memory-bridge.d.ts.map +1 -1
  77. package/dist/src/memory/memory-bridge.js +172 -69
  78. package/dist/src/memory/memory-bridge.js.map +1 -1
  79. package/dist/src/monovector/diff-classifier.js +3 -3
  80. package/dist/src/monovector/diff-classifier.js.map +1 -1
  81. package/dist/src/orgrt/daemon.d.ts +5 -3
  82. package/dist/src/orgrt/daemon.d.ts.map +1 -1
  83. package/dist/src/orgrt/daemon.js +291 -93
  84. package/dist/src/orgrt/daemon.js.map +1 -1
  85. package/dist/src/orgrt/kimicode-runner.js +1 -1
  86. package/dist/src/orgrt/kimicode-runner.js.map +1 -1
  87. package/dist/src/orgrt/opencode-runner.js +1 -1
  88. package/dist/src/orgrt/opencode-runner.js.map +1 -1
  89. package/dist/src/orgrt/remote.d.ts.map +1 -1
  90. package/dist/src/orgrt/remote.js +12 -6
  91. package/dist/src/orgrt/remote.js.map +1 -1
  92. package/dist/src/orgrt/server.d.ts.map +1 -1
  93. package/dist/src/orgrt/server.js +71 -14
  94. package/dist/src/orgrt/server.js.map +1 -1
  95. package/dist/src/types.d.ts +1 -1
  96. package/dist/src/types.d.ts.map +1 -1
  97. package/dist/src/ui/server.mjs +2647 -838
  98. package/dist/tsconfig.tsbuildinfo +1 -1
  99. package/package.json +7 -7
@@ -1,7 +1,7 @@
1
1
  // packages/@monomind/cli/src/orgrt/daemon.ts
2
2
  // monolean: single-process inter-org — upgrade path = daemon-to-daemon HTTP when multi-host is real
3
3
  import { readFileSync, mkdirSync, existsSync, unlinkSync } from 'node:fs';
4
- import { execSync } from 'node:child_process';
4
+ import { execFileSync } from 'node:child_process';
5
5
  import { join, isAbsolute } from 'node:path';
6
6
  import { writeJsonFileAtomic } from '../utils/json-file.js';
7
7
  import { reapOrphanedSdkProcesses } from '../utils/resource-governor.js';
@@ -12,9 +12,9 @@ import { runAgentSession } from './session.js';
12
12
  import { attachForwarder } from './forwarder.js';
13
13
  import { BrokerLease, normalizeCredential } from './broker.js';
14
14
  import { drainInbox } from './inbox.js';
15
- import { OrgDefSchema, ORG_DIR } from './types.js';
15
+ import { OrgDefSchema, ORG_DIR, } from './types.js';
16
16
  import { TaskDag } from './task-dag.js';
17
- import { summarizeRun, readRunEvents, readHistory, historyFile } from './reporting.js';
17
+ import { summarizeRun, readRunEvents, readHistory, historyFile, } from './reporting.js';
18
18
  import { getResourceLimits, configureResourceLimits } from '../utils/resource-governor.js';
19
19
  import { OpencodeAgentRunner } from './opencode-runner.js';
20
20
  import { KimiCodeAgentRunner } from './kimicode-runner.js';
@@ -22,7 +22,7 @@ import { VercelAgentRunner } from './vercel-runner.js';
22
22
  import { CodexAgentRunner } from './codex-runner.js';
23
23
  import { AntigravityAgentRunner } from './antigravity-runner.js';
24
24
  import { captureCheckpoint } from './checkpoint.js';
25
- import { loadGlobalFenceConfig, mergeFenceConfigs, createFenceForRole } from './fence.js';
25
+ import { loadGlobalFenceConfig, mergeFenceConfigs, createFenceForRole, } from './fence.js';
26
26
  // ── Extracted module imports ────────────────────────────────────────────
27
27
  import * as approvalOps from './approvals.js';
28
28
  import * as questionOps from './questions.js';
@@ -35,7 +35,9 @@ import * as checkpointOps from './checkpoint-ops.js';
35
35
  class OtelTracer {
36
36
  enabled = false;
37
37
  spans = new Map();
38
- enable() { this.enabled = true; }
38
+ enable() {
39
+ this.enabled = true;
40
+ }
39
41
  startSpan(name, metadata = {}) {
40
42
  if (!this.enabled)
41
43
  return;
@@ -72,7 +74,9 @@ function autoRuntimeFromProvider(kind) {
72
74
  return undefined;
73
75
  }
74
76
  export function resolveRunner(orgRuntime, providerKind) {
75
- const selected = orgRuntime ?? autoRuntimeFromProvider(providerKind) ?? process.env.MONOMIND_RUNTIME;
77
+ const selected = orgRuntime ??
78
+ autoRuntimeFromProvider(providerKind) ??
79
+ process.env.MONOMIND_RUNTIME;
76
80
  if (selected === 'opencode')
77
81
  return new OpencodeAgentRunner();
78
82
  if (selected === 'kimicode')
@@ -99,7 +103,7 @@ export function resolveRoleRunner(roleRuntime, orgRuntime, roleProviderKind, org
99
103
  /** Per-role token budget: a role's own `budget_tokens` wins; otherwise the
100
104
  * even split of run_config.budget_tokens across all roles. */
101
105
  export function roleTokenBudget(role, def) {
102
- return role.budget_tokens ?? Math.floor((def.run_config.budget_tokens ?? 1_000_000) / def.roles.length);
106
+ return (role.budget_tokens ?? Math.floor((def.run_config.budget_tokens ?? 1_000_000) / def.roles.length));
103
107
  }
104
108
  /** Bounded ring buffer for agent terminal scrollback. */
105
109
  export class ScrollbackBuffer {
@@ -113,8 +117,12 @@ export class ScrollbackBuffer {
113
117
  if (this.lines.length > this.maxLines)
114
118
  this.lines.splice(0, this.lines.length - this.maxLines);
115
119
  }
116
- snapshot() { return [...this.lines]; }
117
- clear() { this.lines.length = 0; }
120
+ snapshot() {
121
+ return [...this.lines];
122
+ }
123
+ clear() {
124
+ this.lines.length = 0;
125
+ }
118
126
  }
119
127
  export class OrgDaemon {
120
128
  root;
@@ -142,7 +150,9 @@ export class OrgDaemon {
142
150
  /** @internal */ recallUsage = new Map();
143
151
  /** @internal */ orgLearnedRuns = new Set();
144
152
  /** @internal */ abandoned = new Map();
145
- constructor(/** @internal */ root, /** @internal */ opts = {}) {
153
+ constructor(
154
+ /** @internal */ root,
155
+ /** @internal */ opts = {}) {
146
156
  this.root = root;
147
157
  this.opts = opts;
148
158
  }
@@ -157,8 +167,12 @@ export class OrgDaemon {
157
167
  this.globalSubscribers.add(fn);
158
168
  return () => this.globalSubscribers.delete(fn);
159
169
  }
160
- listOrgs() { return [...this.orgs.values()]; }
161
- getOrg(name) { return this.orgs.get(name); }
170
+ listOrgs() {
171
+ return [...this.orgs.values()];
172
+ }
173
+ getOrg(name) {
174
+ return this.orgs.get(name);
175
+ }
162
176
  /** Hot-reload an org definition from disk without stopping running sessions.
163
177
  * Applies: goal, run_config, schedule. New roles are added as pending (lazy-spawnable).
164
178
  * Removed roles are NOT killed — they finish their current work and won't be re-spawned.
@@ -184,8 +198,8 @@ export class OrgDaemon {
184
198
  changed.push(`run_config.${key}`);
185
199
  }
186
200
  }
187
- const existingRoleIds = new Set(running.def.roles.map(r => r.id));
188
- const newRoleIds = new Set(newDef.roles.map(r => r.id));
201
+ const existingRoleIds = new Set(running.def.roles.map((r) => r.id));
202
+ const newRoleIds = new Set(newDef.roles.map((r) => r.id));
189
203
  for (const role of newDef.roles) {
190
204
  if (!existingRoleIds.has(role.id)) {
191
205
  running.def.roles.push(role);
@@ -199,16 +213,23 @@ export class OrgDaemon {
199
213
  if (!newRoleIds.has(id))
200
214
  removedRoles.push(id);
201
215
  }
202
- running.bus.emit({ type: 'audit', reason: 'hot-reload',
216
+ running.bus.emit({
217
+ type: 'audit',
218
+ reason: 'hot-reload',
203
219
  msg: `org def reloaded: ${changed.length} fields changed, ${newRoles.length} new roles, ${removedRoles.length} removed roles`,
204
- data: { changed, newRoles, removedRoles } });
220
+ data: { changed, newRoles, removedRoles },
221
+ });
205
222
  return { changed, newRoles, removedRoles };
206
223
  }
207
224
  /** Names of the orgs this daemon currently has running. Snapshot — safe to
208
225
  * iterate while stopOrg() mutates the underlying map. */
209
- listRunning() { return [...this.orgs.keys()]; }
226
+ listRunning() {
227
+ return [...this.orgs.keys()];
228
+ }
210
229
  /** Hook for the SSE server — registers a listener for all bus events across all orgs. */
211
- onBusEvent = (fn) => { this.subscribe(fn); };
230
+ onBusEvent = (fn) => {
231
+ this.subscribe(fn);
232
+ };
212
233
  /** Snapshot of all running orgs for dashboard initial load. */
213
234
  getStatusSnapshot = () => {
214
235
  const orgs = [];
@@ -216,13 +237,15 @@ export class OrgDaemon {
216
237
  const roles = [];
217
238
  for (const [roleId, agent] of running.agents) {
218
239
  roles.push({
219
- id: roleId, status: agent.status,
240
+ id: roleId,
241
+ status: agent.status,
220
242
  worktree: agent.worktreePath ?? null,
221
243
  metrics: agent.metrics,
222
244
  });
223
245
  }
224
246
  orgs.push({
225
- name, run: running.run,
247
+ name,
248
+ run: running.run,
226
249
  roles,
227
250
  pendingRoles: running.pendingRoles ? [...running.pendingRoles.keys()] : [],
228
251
  tasks: running.taskDag?.all() ?? [],
@@ -275,23 +298,37 @@ export class OrgDaemon {
275
298
  let worktreePath;
276
299
  if (ws === 'worktree') {
277
300
  worktreePath = join(this.root, ORG_DIR, name, 'worktree');
278
- const { execSync } = await import('node:child_process');
301
+ const { execFileSync } = await import('node:child_process');
279
302
  try {
280
303
  // Remove stale worktree from a previous run
281
304
  if (existsSync(worktreePath)) {
282
305
  // R4: bound the call — a wedged git hook (git-lfs, gc lock, gpg sign
283
306
  // prompt) would otherwise hang the whole daemon indefinitely.
284
- execSync(`git worktree remove --force "${worktreePath}"`, { cwd: this.root, stdio: 'ignore', timeout: 30_000 });
307
+ // SEC-5: execFileSync + argv — no shell interpolation of worktreePath.
308
+ execFileSync('git', ['worktree', 'remove', '--force', worktreePath], {
309
+ cwd: this.root,
310
+ stdio: 'ignore',
311
+ timeout: 30_000,
312
+ });
285
313
  }
286
314
  }
287
- catch { /* best-effort cleanup */ }
288
- execSync(`git worktree add "${worktreePath}" HEAD --detach`, { cwd: this.root, stdio: 'ignore', timeout: 30_000 });
315
+ catch {
316
+ /* best-effort cleanup */
317
+ }
318
+ execFileSync('git', ['worktree', 'add', worktreePath, 'HEAD', '--detach'], {
319
+ cwd: this.root,
320
+ stdio: 'ignore',
321
+ timeout: 30_000,
322
+ });
289
323
  cwd = worktreePath;
290
324
  }
291
325
  else {
292
- cwd = ws === 'repo' ? this.root
293
- : ws === 'isolated' ? join(this.root, ORG_DIR, name, 'workspace')
294
- : ws;
326
+ cwd =
327
+ ws === 'repo'
328
+ ? this.root
329
+ : ws === 'isolated'
330
+ ? join(this.root, ORG_DIR, name, 'workspace')
331
+ : ws;
295
332
  }
296
333
  mkdirSync(cwd, { recursive: true });
297
334
  // An org must be able to seat its whole roster. maxSdkProcesses is sized for
@@ -306,7 +343,8 @@ export class OrgDaemon {
306
343
  // OpencodeAgentRunner, and CodexAgentRunner each spawn one subprocess per role.
307
344
  // The current sizing (def.roles.length) is therefore safe — it over-provisions
308
345
  // for in-process runners but never under-provisions for subprocess runners.
309
- if (!process.env.MONOMIND_MAX_SDK_PROCS && getResourceLimits().maxSdkProcesses < def.roles.length) {
346
+ if (!process.env.MONOMIND_MAX_SDK_PROCS &&
347
+ getResourceLimits().maxSdkProcesses < def.roles.length) {
310
348
  configureResourceLimits({ maxSdkProcesses: def.roles.length });
311
349
  }
312
350
  // Validate per-role providers before spawning anything (fail-fast: a
@@ -329,10 +367,8 @@ export class OrgDaemon {
329
367
  const MAX_COLLECTED = 1000;
330
368
  const collected = [];
331
369
  let lastActivity = Date.now();
332
- bus.subscribe(e => {
333
- const slim = e.data?.content != null
334
- ? { ...e, data: { ...e.data, content: undefined } }
335
- : e;
370
+ bus.subscribe((e) => {
371
+ const slim = e.data?.content != null ? { ...e, data: { ...e.data, content: undefined } } : e;
336
372
  collected.push(slim);
337
373
  if (collected.length > MAX_COLLECTED)
338
374
  collected.splice(0, collected.length - MAX_COLLECTED);
@@ -347,7 +383,7 @@ export class OrgDaemon {
347
383
  // against a concurrent manual stop.
348
384
  if (e.type === 'status' && e.reason === 'org-complete') {
349
385
  const t = setTimeout(() => {
350
- this.stopOrg(name, { drainMs: COMPLETE_DRAIN_MS }).catch(err => console.error(`org ${name}: auto-stop after org_complete failed:`, err instanceof Error ? err.message : err));
386
+ this.stopOrg(name, { drainMs: COMPLETE_DRAIN_MS }).catch((err) => console.error(`org ${name}: auto-stop after org_complete failed:`, err instanceof Error ? err.message : err));
351
387
  }, 1000);
352
388
  t.unref?.();
353
389
  }
@@ -381,7 +417,14 @@ export class OrgDaemon {
381
417
  });
382
418
  if (this.opts.forward !== false)
383
419
  this.forwarders.set(name, attachForwarder(bus, this.opts.controlJson ?? join(this.root, '.monomind/control.json')));
384
- const running = { def, run, bus, agents: new Map(), busEvents: () => [...collected], workdir: cwd };
420
+ const running = {
421
+ def,
422
+ run,
423
+ bus,
424
+ agents: new Map(),
425
+ busEvents: () => [...collected],
426
+ workdir: cwd,
427
+ };
385
428
  this.orgs.set(name, running);
386
429
  // ── MonoFence guardrail: pre-create per-role instances ────────────────
387
430
  const globalFence = loadGlobalFenceConfig(this.root);
@@ -404,7 +447,9 @@ export class OrgDaemon {
404
447
  });
405
448
  }
406
449
  }
407
- catch { /* monofence-ai not installed — skip silently */ }
450
+ catch {
451
+ /* monofence-ai not installed — skip silently */
452
+ }
408
453
  }
409
454
  if (roleFences.size > 0)
410
455
  running.fences = roleFences;
@@ -415,7 +460,7 @@ export class OrgDaemon {
415
460
  // kickoff went to (type==='boss' || reports_to===null || roles[0]), so a
416
461
  // fallback-selected boss could be told to call org_complete without having
417
462
  // the tool.
418
- const bossRole = def.roles.find(r => r.type === 'boss' || r.reports_to === null) ?? def.roles[0];
463
+ const bossRole = def.roles.find((r) => r.type === 'boss' || r.reports_to === null) ?? def.roles[0];
419
464
  // Canonical entity names from the org KG — injected into the coordinator
420
465
  // prompt so org_learn extractions reuse them instead of minting duplicates.
421
466
  const glossary = await (async () => {
@@ -443,26 +488,53 @@ export class OrgDaemon {
443
488
  if (ws === 'worktree-per-role' && role.id !== bossRole.id) {
444
489
  const wtPath = join(this.root, ORG_DIR, name, `worktree-${role.id}`);
445
490
  try {
446
- // Q7: top-level `import { execSync }` replaces the inlined
491
+ // Q7: top-level `import { execFileSync }` replaces the inlined
447
492
  // `require('node:child_process')` that broke ESM at runtime —
448
493
  // vitest's CJS shim masked it in tests but the built package
449
494
  // threw "require is not defined" in real Node ESM execution.
495
+ // SEC-5: argv-array form, no shell.
450
496
  if (existsSync(wtPath)) {
451
497
  try {
452
- execSync(`git worktree remove --force "${wtPath}"`, { cwd: this.root, stdio: 'ignore', timeout: 30_000 });
498
+ execFileSync('git', ['worktree', 'remove', '--force', wtPath], {
499
+ cwd: this.root,
500
+ stdio: 'ignore',
501
+ timeout: 30_000,
502
+ });
503
+ }
504
+ catch {
505
+ /* best-effort */
453
506
  }
454
- catch { /* best-effort */ }
455
507
  }
456
- execSync(`git worktree add "${wtPath}" HEAD --detach`, { cwd: this.root, stdio: 'ignore', timeout: 30_000 });
508
+ execFileSync('git', ['worktree', 'add', wtPath, 'HEAD', '--detach'], {
509
+ cwd: this.root,
510
+ stdio: 'ignore',
511
+ timeout: 30_000,
512
+ });
457
513
  roleCwd = wtPath;
458
514
  }
459
- catch { /* fallback to shared cwd if git worktree fails */ }
515
+ catch {
516
+ /* fallback to shared cwd if git worktree fails */
517
+ }
460
518
  }
461
519
  const mailbox = new Mailbox();
462
520
  const policy = new PolicyEngine(role.id, { maxTokens: role.budget_tokens ?? perRoleBudget, ...(role.policy ?? {}) }, bus, roleCwd);
463
- const runtime = { mailbox, policy, status: 'running', done: Promise.resolve(), metrics: { tokens: 0, costUsd: 0 }, worktreePath: roleCwd !== cwd ? roleCwd : undefined, scrollback: new ScrollbackBuffer() };
521
+ const runtime = {
522
+ mailbox,
523
+ policy,
524
+ status: 'running',
525
+ done: Promise.resolve(),
526
+ metrics: { tokens: 0, costUsd: 0 },
527
+ worktreePath: roleCwd !== cwd ? roleCwd : undefined,
528
+ scrollback: new ScrollbackBuffer(),
529
+ };
464
530
  const sessionOpts = {
465
- org: name, role, bus, policy, mailbox, cwd: roleCwd, def,
531
+ org: name,
532
+ role,
533
+ bus,
534
+ policy,
535
+ mailbox,
536
+ cwd: roleCwd,
537
+ def,
466
538
  // Pass the org state directory so runners that persist per-role state
467
539
  // (VercelAgentRunner session files) write under .monomind/orgs/<name>
468
540
  // instead of polluting the workspace cwd.
@@ -483,7 +555,13 @@ export class OrgDaemon {
483
555
  fence: roleFences.get(role.id),
484
556
  onComplete: role.id === bossRole.id
485
557
  ? (r, outcome, summary) => {
486
- bus.emit({ type: 'status', from: r, reason: 'org-complete', msg: `run outcome: ${outcome}`, data: { outcome, summary } });
558
+ bus.emit({
559
+ type: 'status',
560
+ from: r,
561
+ reason: 'org-complete',
562
+ msg: `run outcome: ${outcome}`,
563
+ data: { outcome, summary },
564
+ });
487
565
  }
488
566
  : undefined,
489
567
  // #11: a boss that overflows its context window isn't a crash (it keeps
@@ -493,25 +571,50 @@ export class OrgDaemon {
493
571
  onContextLimit: role.id === bossRole.id ? () => this.scheduleBossRestart(name) : undefined,
494
572
  recall: async (r, q) => {
495
573
  const answer = await this.recallOrgMemory(name, def, q, r);
496
- bus.emit({ type: 'status', from: r, reason: 'org-recall', msg: `recall: ${q.slice(0, 80)}`, data: { hits: answer.hits } });
574
+ bus.emit({
575
+ type: 'status',
576
+ from: r,
577
+ reason: 'org-recall',
578
+ msg: `recall: ${q.slice(0, 80)}`,
579
+ data: { hits: answer.hits },
580
+ });
497
581
  return answer.text;
498
582
  },
499
583
  searchKnowledge: async (r, q) => {
500
584
  const answer = await this.searchProjectKnowledge(q);
501
- bus.emit({ type: 'status', from: r, reason: 'knowledge-search', msg: `knowledge: ${q.slice(0, 80)}`, data: { hits: answer.hits } });
585
+ bus.emit({
586
+ type: 'status',
587
+ from: r,
588
+ reason: 'knowledge-search',
589
+ msg: `knowledge: ${q.slice(0, 80)}`,
590
+ data: { hits: answer.hits },
591
+ });
502
592
  return answer.text;
503
593
  },
504
594
  glossary,
505
595
  remember: async (r, content, scope) => {
506
596
  const text = await this.rememberOrgMemory(name, def, r, content, scope, run);
507
- bus.emit({ type: 'status', from: r, reason: 'org-remember', msg: `remember (${scope}): ${content.slice(0, 80)}`, data: { scope } });
597
+ bus.emit({
598
+ type: 'status',
599
+ from: r,
600
+ reason: 'org-remember',
601
+ msg: `remember (${scope}): ${content.slice(0, 80)}`,
602
+ data: { scope },
603
+ });
508
604
  return text;
509
605
  },
510
606
  learn: async (r, payload) => {
511
607
  const text = await this.learnOrgKnowledge(name, run, payload);
512
608
  bus.emit({
513
- type: 'status', from: r, reason: 'org-learn', msg: `learn: ${text.slice(0, 120)}`,
514
- data: { nodes: payload.nodes?.length ?? 0, edges: payload.edges?.length ?? 0, rules: payload.rules?.length ?? 0 },
609
+ type: 'status',
610
+ from: r,
611
+ reason: 'org-learn',
612
+ msg: `learn: ${text.slice(0, 120)}`,
613
+ data: {
614
+ nodes: payload.nodes?.length ?? 0,
615
+ edges: payload.edges?.length ?? 0,
616
+ rules: payload.rules?.length ?? 0,
617
+ },
515
618
  });
516
619
  return text;
517
620
  },
@@ -532,7 +635,8 @@ export class OrgDaemon {
532
635
  // Leaving it undefined for the default path is what keeps
533
636
  // Claude/Antigravity orgs byte-for-byte unchanged. Session opts are
534
637
  // built per role here in spawnRole, so each role gets its own runner.
535
- runner: this.opts.runner ?? resolveRoleRunner(role.runtime, def.runtime, role.provider?.kind, undefined),
638
+ runner: this.opts.runner ??
639
+ resolveRoleRunner(role.runtime, def.runtime, role.provider?.kind, undefined),
536
640
  };
537
641
  // Supervised session: transient crashes (provider blips, network) restart
538
642
  // with backoff; a crash with the mailbox already closed, or one that
@@ -562,7 +666,8 @@ export class OrgDaemon {
562
666
  if (killedByStop) {
563
667
  runtime.status = 'ended';
564
668
  bus.emit({
565
- type: 'status', from: role.id,
669
+ type: 'status',
670
+ from: role.id,
566
671
  msg: `agent "${role.id}" terminated by stop (was still working when drain window expired)`,
567
672
  reason: 'terminated-by-stop',
568
673
  });
@@ -576,10 +681,16 @@ export class OrgDaemon {
576
681
  mailbox.close();
577
682
  const isContextLimit = OrgDaemon.CONTEXT_LIMIT_RE.test(message);
578
683
  bus.emit({
579
- type: 'audit', from: role.id,
684
+ type: 'audit',
685
+ from: role.id,
580
686
  msg: `agent "${role.id}" crashed: ${message}`,
581
687
  reason: isContextLimit ? 'agent-context-limit' : 'agent-session-crash',
582
- data: { agentId: role.id, error: message, restarts: attempt, contextLimit: isContextLimit },
688
+ data: {
689
+ agentId: role.id,
690
+ error: message,
691
+ restarts: attempt,
692
+ contextLimit: isContextLimit,
693
+ },
583
694
  });
584
695
  if (role.id !== bossRole.id) {
585
696
  // #2/#3: a worker is gone for the rest of this run. Without this
@@ -594,8 +705,12 @@ export class OrgDaemon {
594
705
  ? ' This was a context-window overflow — re-dispatching the same task verbatim will fail identically. Break the work into smaller pieces (one file or section at a time) and do not paste large file contents in a single message.'
595
706
  : '';
596
707
  bossRt.mailbox.push(`[system] Worker "${role.id}" crashed and will not recover this run (${message}). It can no longer receive messages — stop messaging it. Reassign its outstanding work to another agent or take it on yourself.${guidance}`);
597
- bus.emit({ type: 'audit', from: bossRole.id, reason: 'worker-crashed',
598
- msg: `worker "${role.id}" crashed (contextLimit=${isContextLimit}); coordinator notified to reassign` });
708
+ bus.emit({
709
+ type: 'audit',
710
+ from: bossRole.id,
711
+ reason: 'worker-crashed',
712
+ msg: `worker "${role.id}" crashed (contextLimit=${isContextLimit}); coordinator notified to reassign`,
713
+ });
599
714
  }
600
715
  }
601
716
  else {
@@ -613,7 +728,9 @@ export class OrgDaemon {
613
728
  const fatal = err?.fatal === true;
614
729
  if (fatal) {
615
730
  bus.emit({
616
- type: 'status', from: role.id, reason: 'agent-fatal',
731
+ type: 'status',
732
+ from: role.id,
733
+ reason: 'agent-fatal',
617
734
  msg: `agent "${role.id}" hit a fatal (non-retryable) error — not restarting`,
618
735
  });
619
736
  crash();
@@ -624,10 +741,15 @@ export class OrgDaemon {
624
741
  return;
625
742
  }
626
743
  bus.emit({
627
- type: 'status', from: role.id, reason: 'agent-restart',
744
+ type: 'status',
745
+ from: role.id,
746
+ reason: 'agent-restart',
628
747
  msg: `agent "${role.id}" crashed (${message}) — restarting in ${BACKOFFS_MS[attempt]}ms (attempt ${attempt + 1}/${BACKOFFS_MS.length})`,
629
748
  });
630
- await new Promise(r => { const t = setTimeout(r, BACKOFFS_MS[attempt]); t.unref?.(); });
749
+ await new Promise((r) => {
750
+ const t = setTimeout(r, BACKOFFS_MS[attempt]);
751
+ t.unref?.();
752
+ });
631
753
  if (mailbox.isClosed) {
632
754
  crash();
633
755
  return;
@@ -661,7 +783,9 @@ export class OrgDaemon {
661
783
  // no-cjs-require-in-esm.test.ts.
662
784
  reapOrphanedSdkProcesses(new Set(), process.pid);
663
785
  }
664
- catch { /* best-effort */ }
786
+ catch {
787
+ /* best-effort */
788
+ }
665
789
  };
666
790
  process.on('exit', crashCleanup);
667
791
  running._crashCleanup = crashCleanup;
@@ -672,17 +796,24 @@ export class OrgDaemon {
672
796
  if (staleThreshold && staleThreshold > 0 && cwd === this.root) {
673
797
  try {
674
798
  const { execSync } = await import('node:child_process');
675
- const behind = execSync('git rev-list --count HEAD..@{upstream} 2>/dev/null', { cwd, encoding: 'utf8', timeout: 10_000 }).trim();
799
+ const behind = execSync('git rev-list --count HEAD..@{upstream} 2>/dev/null', {
800
+ cwd,
801
+ encoding: 'utf8',
802
+ timeout: 10_000,
803
+ }).trim();
676
804
  const count = parseInt(behind, 10);
677
805
  if (!isNaN(count) && count > staleThreshold) {
678
806
  bus.emit({
679
- type: 'audit', reason: 'stale-base',
807
+ type: 'audit',
808
+ reason: 'stale-base',
680
809
  msg: `working tree is ${count} commits behind upstream (threshold: ${staleThreshold}) — consider pulling before running`,
681
810
  data: { behind: count, threshold: staleThreshold },
682
811
  });
683
812
  }
684
813
  }
685
- catch { /* no upstream tracking or git unavailable — skip silently */ }
814
+ catch {
815
+ /* no upstream tracking or git unavailable — skip silently */
816
+ }
686
817
  }
687
818
  const boss = bossRole;
688
819
  // Cross-run memory: brief the coordinator on the previous run so scheduled
@@ -695,9 +826,15 @@ export class OrgDaemon {
695
826
  : `no recorded outcome (${prev.messages} messages, ${prev.assets.length} assets${prev.crashes.length ? `, ${prev.crashes.length} crashed agent(s)` : ''})`) +
696
827
  `\nBuild on that work — do not redo what is already done.`
697
828
  : '';
698
- running.agents.get(boss.id).mailbox.push(`Org "${name}" started (run ${run}).\nGoal: ${taskOverride ?? def.goal}\n` +
829
+ running.agents
830
+ .get(boss.id)
831
+ .mailbox.push(`Org "${name}" started (run ${run}).\nGoal: ${taskOverride ?? def.goal}\n` +
699
832
  `Coordinate your team via org_send. When the goal is achieved (or clearly can't be), record it with org_complete, then end your turn.${prevBrief}`);
700
- bus.emit({ type: 'status', msg: `org started (${def.roles.length} agents)`, data: { goal: taskOverride ?? def.goal } });
833
+ bus.emit({
834
+ type: 'status',
835
+ msg: `org started (${def.roles.length} agents)`,
836
+ data: { goal: taskOverride ?? def.goal },
837
+ });
701
838
  this.persistState(name, 'running', run);
702
839
  // Idle watchdog: a hung tool call (or a run that quietly finished without
703
840
  // org_complete) produces no bus events, and every agent just waits. After
@@ -712,13 +849,13 @@ export class OrgDaemon {
712
849
  let nudges = 0;
713
850
  const idleStop = (msg) => {
714
851
  bus.emit({ type: 'audit', reason: 'idle-stop', msg });
715
- this.stopOrg(name).catch(err => console.error(`org ${name}: idle-stop failed:`, err instanceof Error ? err.message : err));
852
+ this.stopOrg(name).catch((err) => console.error(`org ${name}: idle-stop failed:`, err instanceof Error ? err.message : err));
716
853
  };
717
854
  const wd = setInterval(() => {
718
855
  if (this.restarting.has(name))
719
856
  return; // boss auto-restart in flight — don't nudge or stop
720
857
  // A pending gate means the org is legitimately waiting for human input
721
- const pendingGates = this.readGates(name).gates.filter(g => g.status === 'pending');
858
+ const pendingGates = this.readGates(name).gates.filter((g) => g.status === 'pending');
722
859
  if (pendingGates.length > 0)
723
860
  return;
724
861
  const idleFor = Date.now() - lastActivity;
@@ -738,7 +875,12 @@ export class OrgDaemon {
738
875
  }
739
876
  nudges++;
740
877
  nudgedAt = Date.now();
741
- bus.emit({ type: 'audit', from: bossRole.id, reason: 'idle-nudge', msg: `no org activity for ${Math.round(idleFor / 60_000)}m — nudging boss (${nudges}/${MAX_IDLE_NUDGES})` });
878
+ bus.emit({
879
+ type: 'audit',
880
+ from: bossRole.id,
881
+ reason: 'idle-nudge',
882
+ msg: `no org activity for ${Math.round(idleFor / 60_000)}m — nudging boss (${nudges}/${MAX_IDLE_NUDGES})`,
883
+ });
742
884
  bossRt.mailbox.push(`[watchdog] No activity in org "${name}" for ${Math.round(idleFor / 60_000)} minute(s). ` +
743
885
  `If the goal is achieved (or clearly can't be), call org_complete now. Otherwise check on your team via org_send and reassign stalled work.`);
744
886
  }
@@ -769,7 +911,13 @@ export class OrgDaemon {
769
911
  }
770
912
  const agent = running.agents.get(msg.toRole);
771
913
  if (agent && !agent.mailbox.isClosed) {
772
- bus.emit({ type: 'xorg', from: msg.fromQualified, to: `${name}:${msg.toRole}`, subject: msg.subject, msg: msg.body });
914
+ bus.emit({
915
+ type: 'xorg',
916
+ from: msg.fromQualified,
917
+ to: `${name}:${msg.toRole}`,
918
+ subject: msg.subject,
919
+ msg: msg.body,
920
+ });
773
921
  agent.mailbox.push(this.mailBody(name, running, `[message from ${msg.fromQualified}] subject: ${msg.subject}`, msg.body, `inbox-${msg.ts}-${Math.random().toString(36).slice(2, 8)}`));
774
922
  }
775
923
  }
@@ -847,11 +995,15 @@ export class OrgDaemon {
847
995
  // "crashed") and threw the work away. allSettled resolves as soon as every
848
996
  // session ends, so a long drain is a ceiling, not a delay.
849
997
  const stopWaitMs = drainMs ?? this.opts.stopWaitMs ?? 15_000;
850
- const allDone = Promise.allSettled([...org.agents.values()].map(a => a.done)).then(() => false);
851
- const timedOut = await Promise.race([allDone, new Promise(r => setTimeout(() => r(true), stopWaitMs))]);
998
+ const allDone = Promise.allSettled([...org.agents.values()].map((a) => a.done)).then(() => false);
999
+ const timedOut = await Promise.race([
1000
+ allDone,
1001
+ new Promise((r) => setTimeout(() => r(true), stopWaitMs)),
1002
+ ]);
852
1003
  if (timedOut) {
853
1004
  org.bus.emit({
854
- type: 'audit', msg: `org stop timed out after ${stopWaitMs}ms waiting for agent sessions to finish — proceeding anyway`,
1005
+ type: 'audit',
1006
+ msg: `org stop timed out after ${stopWaitMs}ms waiting for agent sessions to finish — proceeding anyway`,
855
1007
  reason: 'stop-timeout',
856
1008
  });
857
1009
  // Reap only SDK processes spawned by THIS node process — ownerPid filter
@@ -859,9 +1011,15 @@ export class OrgDaemon {
859
1011
  try {
860
1012
  const reaped = reapOrphanedSdkProcesses(new Set(), process.pid);
861
1013
  if (reaped > 0)
862
- org.bus.emit({ type: 'audit', reason: 'orphan-reap', msg: `reaped ${reaped} orphaned SDK process(es) after stop timeout` });
1014
+ org.bus.emit({
1015
+ type: 'audit',
1016
+ reason: 'orphan-reap',
1017
+ msg: `reaped ${reaped} orphaned SDK process(es) after stop timeout`,
1018
+ });
1019
+ }
1020
+ catch {
1021
+ /* best-effort */
863
1022
  }
864
- catch { /* best-effort */ }
865
1023
  }
866
1024
  org.bus.emit({ type: 'status', msg: 'org stopped' });
867
1025
  await org.bus.flush();
@@ -893,7 +1051,10 @@ export class OrgDaemon {
893
1051
  if (forwarder) {
894
1052
  await Promise.race([
895
1053
  forwarder.settle(),
896
- new Promise(r => { const t = setTimeout(r, 5_000); t.unref?.(); }),
1054
+ new Promise((r) => {
1055
+ const t = setTimeout(r, 5_000);
1056
+ t.unref?.();
1057
+ }),
897
1058
  ]);
898
1059
  forwarder.unsubscribe();
899
1060
  // Only remove from the map if it's still OURS — an autoWake-restart may
@@ -908,27 +1069,41 @@ export class OrgDaemon {
908
1069
  this.persistState(name, 'stopped', org.run, org, stopCheckpoint);
909
1070
  // Clean up git worktrees — shared (workspace: 'worktree') and per-role.
910
1071
  try {
911
- const { execSync } = await import('node:child_process');
1072
+ const { execFileSync } = await import('node:child_process');
912
1073
  if (org.worktreePath) {
913
1074
  try {
914
- execSync(`git worktree remove --force "${org.worktreePath}"`, { cwd: this.root, stdio: 'ignore', timeout: 30_000 });
1075
+ execFileSync('git', ['worktree', 'remove', '--force', org.worktreePath], {
1076
+ cwd: this.root,
1077
+ stdio: 'ignore',
1078
+ timeout: 30_000,
1079
+ });
1080
+ }
1081
+ catch {
1082
+ /* best-effort */
915
1083
  }
916
- catch { /* best-effort */ }
917
1084
  }
918
1085
  for (const agent of org.agents.values()) {
919
1086
  if (agent.worktreePath) {
920
1087
  try {
921
- execSync(`git worktree remove --force "${agent.worktreePath}"`, { cwd: this.root, stdio: 'ignore', timeout: 30_000 });
1088
+ execFileSync('git', ['worktree', 'remove', '--force', agent.worktreePath], {
1089
+ cwd: this.root,
1090
+ stdio: 'ignore',
1091
+ timeout: 30_000,
1092
+ });
1093
+ }
1094
+ catch {
1095
+ /* best-effort */
922
1096
  }
923
- catch { /* best-effort */ }
924
1097
  }
925
1098
  }
926
1099
  }
927
- catch { /* node:child_process unavailable — skip */ }
1100
+ catch {
1101
+ /* node:child_process unavailable — skip */
1102
+ }
928
1103
  }
929
1104
  async stopAll() {
930
1105
  await Promise.all([
931
- ...[...this.orgs.keys()].map(n => this.stopOrg(n)),
1106
+ ...[...this.orgs.keys()].map((n) => this.stopOrg(n)),
932
1107
  ...this.stopping.values(), // detached self-stops still flushing
933
1108
  ]);
934
1109
  }
@@ -949,7 +1124,10 @@ export class OrgDaemon {
949
1124
  // would brick every subsequent `org status` / isOrgRunning / scheduler
950
1125
  // call. The state files in 6 other daemon paths already use this helper.
951
1126
  writeJsonFileAtomic(p, {
952
- status, run, pid: process.pid, updated: new Date().toISOString(),
1127
+ status,
1128
+ run,
1129
+ pid: process.pid,
1130
+ updated: new Date().toISOString(),
953
1131
  ...(missing.length ? { abandonedRoles: missing } : {}),
954
1132
  ...(checkpoint ? { checkpoint } : {}),
955
1133
  });
@@ -963,11 +1141,16 @@ export class OrgDaemon {
963
1141
  // C4: atomic write — crash handler is the most likely place to hit
964
1142
  // a partial write since the process is mid-teardown.
965
1143
  writeJsonFileAtomic(p, {
966
- status: 'crashed', run: org.run, pid: process.pid,
967
- updated: new Date().toISOString(), closedBy: 'crash-handler',
1144
+ status: 'crashed',
1145
+ run: org.run,
1146
+ pid: process.pid,
1147
+ updated: new Date().toISOString(),
1148
+ closedBy: 'crash-handler',
968
1149
  });
969
1150
  }
970
- catch { /* best effort — filesystem may be unavailable */ }
1151
+ catch {
1152
+ /* best effort — filesystem may be unavailable */
1153
+ }
971
1154
  }
972
1155
  }
973
1156
  heartbeatPath() {
@@ -982,17 +1165,22 @@ export class OrgDaemon {
982
1165
  // C4: atomic write — heartbeat corruption is how `org status` reports
983
1166
  // a phantom daemon after a crash.
984
1167
  writeJsonFileAtomic(p, {
985
- pid: process.pid, updatedAt: new Date().toISOString(),
1168
+ pid: process.pid,
1169
+ updatedAt: new Date().toISOString(),
986
1170
  running: this.listRunning(),
987
1171
  });
988
1172
  }
989
- catch { /* best effort */ }
1173
+ catch {
1174
+ /* best effort */
1175
+ }
990
1176
  }
991
1177
  clearHeartbeat() {
992
1178
  try {
993
1179
  unlinkSync(this.heartbeatPath());
994
1180
  }
995
- catch { /* already gone or never written */ }
1181
+ catch {
1182
+ /* already gone or never written */
1183
+ }
996
1184
  }
997
1185
  // ── Delegated methods — extracted to focused modules ──────────────────
998
1186
  // approvals.ts
@@ -1043,16 +1231,26 @@ export class OrgDaemon {
1043
1231
  }
1044
1232
  // scheduler-integration.ts
1045
1233
  /** @internal */
1046
- autoWake(name) { scheduler.autoWake(this, name); }
1047
- scheduleBossRestart(name) { scheduler.scheduleBossRestart(this, name); }
1234
+ autoWake(name) {
1235
+ scheduler.autoWake(this, name);
1236
+ }
1237
+ scheduleBossRestart(name) {
1238
+ scheduler.scheduleBossRestart(this, name);
1239
+ }
1048
1240
  /** @internal */
1049
1241
  scheduleDeferredSpawn(name, running, role, spawnRole) {
1050
1242
  scheduler.scheduleDeferredSpawn(this, name, running, role, spawnRole);
1051
1243
  }
1052
1244
  // org-memory.ts
1053
- orgMemoryNamespace(name, def) { return orgMemory.orgMemoryNamespace(name, def); }
1054
- orgMemoryDbPath() { return orgMemory.orgMemoryDbPath(this.root); }
1055
- orgMemoryUsable() { return orgMemory.orgMemoryUsable(this.root); }
1245
+ orgMemoryNamespace(name, def) {
1246
+ return orgMemory.orgMemoryNamespace(name, def);
1247
+ }
1248
+ orgMemoryDbPath() {
1249
+ return orgMemory.orgMemoryDbPath(this.root);
1250
+ }
1251
+ orgMemoryUsable() {
1252
+ return orgMemory.orgMemoryUsable(this.root);
1253
+ }
1056
1254
  async rememberOrgMemory(name, def, role, content, scope, run) {
1057
1255
  return orgMemory.rememberOrgMemory(this.root, name, def, role, content, scope, run);
1058
1256
  }