@monoes/monomindcli 2.10.30 → 2.10.31

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 (114) hide show
  1. package/.claude/helpers/graphify-freshen.cjs +11 -12
  2. package/.claude/helpers/handlers/capture-handler.cjs +1 -1
  3. package/dist/src/commands/analyze-diff.d.ts.map +1 -1
  4. package/dist/src/commands/analyze-diff.js +6 -0
  5. package/dist/src/commands/analyze-diff.js.map +1 -1
  6. package/dist/src/commands/doctor-project-checks.d.ts.map +1 -1
  7. package/dist/src/commands/doctor-project-checks.js +2 -2
  8. package/dist/src/commands/doctor-project-checks.js.map +1 -1
  9. package/dist/src/commands/init.d.ts.map +1 -1
  10. package/dist/src/commands/init.js.map +1 -1
  11. package/dist/src/commands/mcp.d.ts.map +1 -1
  12. package/dist/src/commands/mcp.js.map +1 -1
  13. package/dist/src/commands/org-observe.d.ts +15 -7
  14. package/dist/src/commands/org-observe.d.ts.map +1 -1
  15. package/dist/src/commands/org-observe.js +208 -63
  16. package/dist/src/commands/org-observe.js.map +1 -1
  17. package/dist/src/commands/org.d.ts.map +1 -1
  18. package/dist/src/commands/org.js +57 -4
  19. package/dist/src/commands/org.js.map +1 -1
  20. package/dist/src/commands/status.d.ts.map +1 -1
  21. package/dist/src/commands/status.js +11 -2
  22. package/dist/src/commands/status.js.map +1 -1
  23. package/dist/src/mcp-tools/system-tools.d.ts.map +1 -1
  24. package/dist/src/mcp-tools/system-tools.js.map +1 -1
  25. package/dist/src/memory/memory-kg.d.ts.map +1 -1
  26. package/dist/src/memory/memory-kg.js +10 -2
  27. package/dist/src/memory/memory-kg.js.map +1 -1
  28. package/dist/src/orgrt/agent-exec.d.ts.map +1 -1
  29. package/dist/src/orgrt/agent-exec.js +7 -3
  30. package/dist/src/orgrt/agent-exec.js.map +1 -1
  31. package/dist/src/orgrt/antigravity-runner.d.ts.map +1 -1
  32. package/dist/src/orgrt/antigravity-runner.js +6 -2
  33. package/dist/src/orgrt/antigravity-runner.js.map +1 -1
  34. package/dist/src/orgrt/approvals.d.ts +15 -1
  35. package/dist/src/orgrt/approvals.d.ts.map +1 -1
  36. package/dist/src/orgrt/approvals.js +76 -11
  37. package/dist/src/orgrt/approvals.js.map +1 -1
  38. package/dist/src/orgrt/broker.d.ts +10 -2
  39. package/dist/src/orgrt/broker.d.ts.map +1 -1
  40. package/dist/src/orgrt/broker.js +20 -5
  41. package/dist/src/orgrt/broker.js.map +1 -1
  42. package/dist/src/orgrt/checkpoint.d.ts.map +1 -1
  43. package/dist/src/orgrt/checkpoint.js.map +1 -1
  44. package/dist/src/orgrt/cross-org.d.ts +15 -2
  45. package/dist/src/orgrt/cross-org.d.ts.map +1 -1
  46. package/dist/src/orgrt/cross-org.js +169 -16
  47. package/dist/src/orgrt/cross-org.js.map +1 -1
  48. package/dist/src/orgrt/daemon.d.ts +31 -3
  49. package/dist/src/orgrt/daemon.d.ts.map +1 -1
  50. package/dist/src/orgrt/daemon.js +132 -21
  51. package/dist/src/orgrt/daemon.js.map +1 -1
  52. package/dist/src/orgrt/decisions.d.ts.map +1 -1
  53. package/dist/src/orgrt/decisions.js +13 -1
  54. package/dist/src/orgrt/decisions.js.map +1 -1
  55. package/dist/src/orgrt/endpoint-roles.d.ts +86 -0
  56. package/dist/src/orgrt/endpoint-roles.d.ts.map +1 -0
  57. package/dist/src/orgrt/endpoint-roles.js +364 -0
  58. package/dist/src/orgrt/endpoint-roles.js.map +1 -0
  59. package/dist/src/orgrt/inbox.d.ts +18 -0
  60. package/dist/src/orgrt/inbox.d.ts.map +1 -1
  61. package/dist/src/orgrt/inbox.js +73 -0
  62. package/dist/src/orgrt/inbox.js.map +1 -1
  63. package/dist/src/orgrt/migrate.d.ts.map +1 -1
  64. package/dist/src/orgrt/migrate.js +3 -0
  65. package/dist/src/orgrt/migrate.js.map +1 -1
  66. package/dist/src/orgrt/opencode-runner.d.ts.map +1 -1
  67. package/dist/src/orgrt/opencode-runner.js +2 -1
  68. package/dist/src/orgrt/opencode-runner.js.map +1 -1
  69. package/dist/src/orgrt/pi-rpc-runner.d.ts.map +1 -1
  70. package/dist/src/orgrt/pi-rpc-runner.js +6 -2
  71. package/dist/src/orgrt/pi-rpc-runner.js.map +1 -1
  72. package/dist/src/orgrt/policy.d.ts +22 -1
  73. package/dist/src/orgrt/policy.d.ts.map +1 -1
  74. package/dist/src/orgrt/policy.js +36 -4
  75. package/dist/src/orgrt/policy.js.map +1 -1
  76. package/dist/src/orgrt/questions.d.ts +3 -1
  77. package/dist/src/orgrt/questions.d.ts.map +1 -1
  78. package/dist/src/orgrt/questions.js +19 -3
  79. package/dist/src/orgrt/questions.js.map +1 -1
  80. package/dist/src/orgrt/qwen-rpc-runner.d.ts.map +1 -1
  81. package/dist/src/orgrt/qwen-rpc-runner.js +3 -1
  82. package/dist/src/orgrt/qwen-rpc-runner.js.map +1 -1
  83. package/dist/src/orgrt/role-slot.d.ts +1 -2
  84. package/dist/src/orgrt/role-slot.d.ts.map +1 -1
  85. package/dist/src/orgrt/role-slot.js +2 -1
  86. package/dist/src/orgrt/role-slot.js.map +1 -1
  87. package/dist/src/orgrt/scheduler-integration.d.ts.map +1 -1
  88. package/dist/src/orgrt/scheduler-integration.js +8 -1
  89. package/dist/src/orgrt/scheduler-integration.js.map +1 -1
  90. package/dist/src/orgrt/server.d.ts.map +1 -1
  91. package/dist/src/orgrt/server.js +43 -8
  92. package/dist/src/orgrt/server.js.map +1 -1
  93. package/dist/src/orgrt/session.d.ts +12 -1
  94. package/dist/src/orgrt/session.d.ts.map +1 -1
  95. package/dist/src/orgrt/session.js +20 -3
  96. package/dist/src/orgrt/session.js.map +1 -1
  97. package/dist/src/orgrt/tool-providers.d.ts +110 -0
  98. package/dist/src/orgrt/tool-providers.d.ts.map +1 -0
  99. package/dist/src/orgrt/tool-providers.js +617 -0
  100. package/dist/src/orgrt/tool-providers.js.map +1 -0
  101. package/dist/src/orgrt/types.d.ts +72 -0
  102. package/dist/src/orgrt/types.d.ts.map +1 -1
  103. package/dist/src/orgrt/types.js +56 -0
  104. package/dist/src/orgrt/types.js.map +1 -1
  105. package/dist/src/protocol-capabilities.d.ts +7 -1
  106. package/dist/src/protocol-capabilities.d.ts.map +1 -1
  107. package/dist/src/protocol-capabilities.js +15 -1
  108. package/dist/src/protocol-capabilities.js.map +1 -1
  109. package/dist/src/ui/routes-org.mjs +9 -7
  110. package/dist/src/utils/native-error.d.ts.map +1 -1
  111. package/dist/src/utils/native-error.js +9 -5
  112. package/dist/src/utils/native-error.js.map +1 -1
  113. package/dist/tsconfig.tsbuildinfo +1 -1
  114. package/package.json +2 -2
@@ -26,6 +26,18 @@ export const printOrgJson = (payload) => {
26
26
  process.stdout.write(`${JSON.stringify(payload)}\n`);
27
27
  return { success: true, data: payload };
28
28
  };
29
+ /** M5: `--by <resolver>` — who resolved a decision (default `human`). Returns
30
+ * an error string for an invalid value. */
31
+ const resolverFlag = async (ctx) => {
32
+ const raw = ctx.flags.by;
33
+ if (raw === undefined || raw === false)
34
+ return { ok: true, by: 'human' };
35
+ const { normalizeResolver } = await import('../orgrt/approvals.js');
36
+ const by = normalizeResolver(raw);
37
+ if (!by)
38
+ return { ok: false, message: '--by must be 1-128 printable characters' };
39
+ return { ok: true, by };
40
+ };
29
41
  /** Validate org config(s) against OrgDefSchema — the exact parse `org run`/`org serve`
30
42
  * perform — plus the structural invariants the runtime assumes but the schema can't
31
43
  * express (single root role, resolvable reports_to, unique ids, parseable schedule). */
@@ -83,6 +95,19 @@ export const validateAction = async (ctx) => {
83
95
  ? { success: false, message: `${failed} of ${files.length} org config(s) failed validation` }
84
96
  : { success: true, message: `${files.length} org config(s) valid` };
85
97
  };
98
+ /** M2: ids of the org's endpoint roles (automations, not agents) — excluded
99
+ * from the costs/report/flow role tables. Unreadable config → none. */
100
+ const endpointRoleIds = (cwd, name) => {
101
+ try {
102
+ const raw = JSON.parse(readFileSync(join(cwd, ORG_DIR, `${name}.json`), 'utf8'));
103
+ return new Set((Array.isArray(raw.roles) ? raw.roles : [])
104
+ .filter((r) => r?.kind === 'endpoint' && typeof r.id === 'string')
105
+ .map((r) => r.id));
106
+ }
107
+ catch {
108
+ return new Set();
109
+ }
110
+ };
86
111
  // Run ids are joined into filesystem paths — enforce the daemon's own id shape
87
112
  // so a crafted --run can't traverse out of the org directory (same reason the
88
113
  // org-name guard exists).
@@ -295,6 +320,10 @@ export const reportAction = async (ctx, name) => {
295
320
  if (!events.length)
296
321
  return { success: false, message: `run ${run} has no recorded events` };
297
322
  const s = summarizeRun(events);
323
+ // M2: endpoint roles are automations — they have no row in the role tables.
324
+ const endpointIds = endpointRoleIds(ctx.cwd, name);
325
+ for (const id of endpointIds)
326
+ delete s.roles[id];
298
327
  // Protocol JSON mode (§7.2): the run summary as a bare object. Emitted
299
328
  // before the human-only flag modes (mermaid/audit/by-role) — those render
300
329
  // views of the same summary and have no JSON shape defined by the spec.
@@ -406,8 +435,9 @@ export const reportAction = async (ctx, name) => {
406
435
  const roleCeiling = new Map();
407
436
  try {
408
437
  const def = OrgDefSchema.parse(JSON.parse(readFileSync(join(ctx.cwd, ORG_DIR, `${name}.json`), 'utf8')));
409
- perRoleBudget = Math.floor((def.run_config.budget_tokens ?? 1_000_000) / def.roles.length);
410
- for (const r of def.roles) {
438
+ const sessionRoles = def.roles.filter((r) => r.kind !== 'endpoint');
439
+ perRoleBudget = Math.floor((def.run_config.budget_tokens ?? 1_000_000) / Math.max(1, sessionRoles.length));
440
+ for (const r of sessionRoles) {
411
441
  const max = r.policy?.maxTokens;
412
442
  roleCeiling.set(r.id, max ?? r.budget_tokens ?? perRoleBudget);
413
443
  }
@@ -545,7 +575,18 @@ export const approvalsAction = async (ctx, name) => {
545
575
  }
546
576
  const shown = ctx.flags.all === true ? all : all.filter((a) => a.approved === null);
547
577
  if (orgJson(ctx))
548
- return printOrgJson({ v: 1, org: name, items: shown });
578
+ return printOrgJson({
579
+ v: 1,
580
+ org: name,
581
+ // M5: requestId / resolvedBy / input are always present (null for
582
+ // entries recorded before they existed).
583
+ items: shown.map((a) => ({
584
+ ...a,
585
+ requestId: a.requestId ?? null,
586
+ resolvedBy: a.resolvedBy ?? null,
587
+ input: a.input ?? null,
588
+ })),
589
+ });
549
590
  if (!shown.length) {
550
591
  log(output.info(all.length
551
592
  ? `No pending approvals for org ${name} (${all.length} resolved — use --all).`
@@ -555,10 +596,12 @@ export const approvalsAction = async (ctx, name) => {
555
596
  for (const a of shown) {
556
597
  const when = new Date(a.ts).toISOString().replace('T', ' ').slice(0, 16);
557
598
  const mark = a.approved === null ? '❓' : a.approved ? '✓' : '✗';
558
- log(output.info(`${mark} ${when} ${a.roleId}: ${a.action}`));
599
+ const ref = a.requestId ? ` [${a.requestId}]` : '';
600
+ const by = a.resolvedBy ? ` (by ${a.resolvedBy})` : '';
601
+ log(output.info(`${mark} ${when} ${a.roleId}: ${a.action}${ref}${by}`));
559
602
  }
560
603
  if (shown.some((a) => a.approved === null))
561
- log(output.info(`\nApprove with: monomind org approve ${name} <role> <action>\nDeny with: monomind org deny ${name} <role> <action>`));
604
+ 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>]`));
562
605
  return { success: true };
563
606
  };
564
607
  /** `org answer <name> <question-id> <answer...>` — answer a pending ask_human question.
@@ -587,6 +630,10 @@ export const answerAction = async (ctx, name) => {
587
630
  }
588
631
  if (q.answer !== null)
589
632
  return { success: false, message: `question "${questionId}" was already answered` };
633
+ const byFlag = await resolverFlag(ctx);
634
+ if (!byFlag.ok)
635
+ return { success: false, message: byFlag.message };
636
+ const resolvedBy = byFlag.by;
590
637
  // Live path: the hosting daemon updates questions.json and pushes into the role's mailbox.
591
638
  // SEC: answering a role's question is a human decision — authenticate with
592
639
  // the operator credential, not the agent-facing one in the broker entry.
@@ -601,7 +648,7 @@ export const answerAction = async (ctx, name) => {
601
648
  'Content-Type': 'application/json',
602
649
  ...(cred ? { 'x-monomind-cred': cred } : {}),
603
650
  },
604
- body: JSON.stringify({ org: name, role: q.role, questionId, answer }),
651
+ body: JSON.stringify({ org: name, role: q.role, questionId, answer, resolvedBy }),
605
652
  signal: AbortSignal.timeout(10_000),
606
653
  });
607
654
  const data = (await res.json().catch(() => ({})));
@@ -614,6 +661,7 @@ export const answerAction = async (ctx, name) => {
614
661
  role: q.role,
615
662
  delivery: 'live',
616
663
  answered: true,
664
+ resolvedBy,
617
665
  });
618
666
  log(output.success(`Answer delivered to ${name}:${q.role} (live).`));
619
667
  return { success: true };
@@ -666,8 +714,8 @@ export const answerAction = async (ctx, name) => {
666
714
  return { success: false, message: 'queueing failed — answer not recorded' };
667
715
  }
668
716
  const merged = fresh.some((x) => x.questionId === questionId)
669
- ? fresh.map((x) => (x.questionId === questionId ? { ...x, answer, answeredAt: Date.now() } : x))
670
- : [...fresh, { ...q, answer, answeredAt: Date.now() }];
717
+ ? fresh.map((x) => x.questionId === questionId ? { ...x, answer, answeredAt: Date.now(), resolvedBy } : x)
718
+ : [...fresh, { ...q, answer, answeredAt: Date.now(), resolvedBy }];
671
719
  const dest = join(ctx.cwd, ORG_DIR, name, 'questions.json');
672
720
  const tmp = `${dest}.${process.pid}.tmp`;
673
721
  writeFileSync(tmp, JSON.stringify({ questions: merged }, null, 2));
@@ -681,18 +729,42 @@ export const answerAction = async (ctx, name) => {
681
729
  role: q.role,
682
730
  delivery: 'queued',
683
731
  answered: true,
732
+ resolvedBy,
684
733
  });
685
734
  log(output.success(`Answer recorded — ${name}:${q.role} receives it when the org next runs.`));
686
735
  return { success: true };
687
736
  };
688
737
  /** `org inbox <name> --json '{"from":"orgA:role","subject":"...","body":"..."}' [--to role]`
738
+ * (or `--from/--subject/--body`) `[--format json]`.
689
739
  * Inbound entrypoint for cross-org/remote delivery — orgrt/remote.ts's deliverRemote()
690
- * shells out to exactly this command over SSH. Live path: POST to the hosting daemon's
691
- * /api/xdeliver when the org is registered with the broker (mirrors cross-org.ts's
692
- * deliverRemote). Offline path: spool into inbox.jsonl, which the daemon drains into
693
- * the target role's mailbox on the org's next start (daemon.ts drainInbox) — the same
694
- * semantics as a queued human answer. */
740
+ * shells out to exactly this command over SSH, and mono-agent replies through it.
741
+ *
742
+ * Live path (M3): POST to the hosting daemon's /api/xdeliver with the OPERATOR
743
+ * credential (readOperatorCredential), which lets the daemon trust `from` as
744
+ * given — the sender org need not be registered (`workflow`, an automation
745
+ * role). Without an operator credential it presents the sender org's own
746
+ * broker credential, if that org is registered. Offline path: spool into
747
+ * inbox.jsonl, drained into the role's mailbox on the org's next start.
748
+ *
749
+ * `--format json` prints `{"v":1,"org","to","from","delivery":"live"|"queued",
750
+ * "receipt","messageId"}`. Exit 0 for live and queued; non-zero only for
751
+ * invalid input or an unknown org. */
695
752
  export const inboxAction = async (ctx, name) => {
753
+ const json = orgJson(ctx);
754
+ // JSON mode keeps stdout to the single payload — diagnostics go to stderr.
755
+ const note = (text) => {
756
+ if (json)
757
+ process.stderr.write(`${text}\n`);
758
+ else
759
+ log(text);
760
+ };
761
+ const fail = (message) => {
762
+ if (json)
763
+ process.stdout.write(`${JSON.stringify({ v: 1, org: name, error: message })}\n`);
764
+ else
765
+ log(output.error(message));
766
+ return { success: false, message };
767
+ };
696
768
  let payload = {};
697
769
  const rawJson = ctx.flags.json;
698
770
  if (typeof rawJson === 'string') {
@@ -700,7 +772,7 @@ export const inboxAction = async (ctx, name) => {
700
772
  payload = JSON.parse(rawJson);
701
773
  }
702
774
  catch {
703
- return { success: false, message: 'org inbox: --json is not valid JSON' };
775
+ return fail('org inbox: --json is not valid JSON');
704
776
  }
705
777
  }
706
778
  else {
@@ -709,43 +781,50 @@ export const inboxAction = async (ctx, name) => {
709
781
  const from = typeof payload.from === 'string' ? payload.from.trim() : '';
710
782
  const subject = typeof payload.subject === 'string' ? payload.subject : '';
711
783
  const body = typeof payload.body === 'string' ? payload.body : '';
712
- if (!from || !body) {
713
- log(output.error('org inbox: payload requires "from" and "body" (via --json or --from/--body)'));
714
- return { success: false, message: 'inbox payload requires from and body' };
715
- }
784
+ if (!from || !body)
785
+ return fail('org inbox: payload requires "from" and "body" (via --json or --from/--body)');
786
+ if (!/^[^\s:]{1,128}(:[^\s:]{1,128})?$/.test(from))
787
+ return fail(`org inbox: invalid sender "${from}" — use "<org>:<role>" or "<role>"`);
788
+ const { lookupOrg, normalizeCredential, readOperatorCredential } = await import('../orgrt/broker.js');
789
+ const remote = lookupOrg(name);
790
+ const defPath = join(ctx.cwd, ORG_DIR, `${name}.json`);
791
+ if (!remote && !existsSync(defPath))
792
+ return fail(`Org not found: ${name}`);
716
793
  // Target role: explicit --to, else the org's coordinator (reports_to == null),
717
794
  // else the first role — matching where a role-less cross-org message should land.
718
795
  let toRole = typeof ctx.flags.to === 'string' ? ctx.flags.to : '';
719
- if (toRole && !/^[a-z0-9][a-z0-9_-]*$/i.test(toRole)) {
720
- log(output.error(`Invalid role id: ${toRole}`));
721
- return { success: false, message: 'invalid role id' };
722
- }
796
+ if (toRole && !/^[a-z0-9][a-z0-9_-]*$/i.test(toRole))
797
+ return fail(`Invalid role id: ${toRole}`);
723
798
  if (!toRole) {
724
- const defPath = join(ctx.cwd, ORG_DIR, `${name}.json`);
725
- if (!existsSync(defPath)) {
726
- log(output.error(`Org not found: ${name}`));
727
- return { success: false, message: 'org not found' };
728
- }
799
+ if (!existsSync(defPath))
800
+ return fail(`Org "${name}" config is not in this project — pass --to <role>.`);
729
801
  try {
730
802
  const def = JSON.parse(readFileSync(defPath, 'utf8'));
731
803
  const roles = Array.isArray(def.roles) ? def.roles : [];
732
804
  toRole = roles.find((r) => r.reports_to == null)?.id ?? roles[0]?.id ?? '';
733
805
  }
734
806
  catch (err) {
735
- log(output.error(`Could not read org config for ${name}: ${err instanceof Error ? err.message : String(err)}`));
736
- return { success: false, message: 'org config unreadable' };
807
+ return fail(`Could not read org config for ${name}: ${err instanceof Error ? err.message : String(err)}`);
737
808
  }
738
- if (!toRole) {
739
- log(output.error(`Org "${name}" has no roles to deliver to — pass --to <role>.`));
740
- return { success: false, message: 'no deliverable role' };
741
- }
742
- }
809
+ if (!toRole)
810
+ return fail(`Org "${name}" has no roles to deliver to — pass --to <role>.`);
811
+ }
812
+ const { newMessageId, queueMessage } = await import('../orgrt/inbox.js');
813
+ const messageId = newMessageId();
814
+ const to = `${name}:${toRole}`;
815
+ const done = (delivery, receipt) => {
816
+ if (json)
817
+ return printOrgJson({ v: 1, org: name, to, from, delivery, receipt, messageId });
818
+ log(output.success(receipt));
819
+ return { success: true, message: receipt };
820
+ };
743
821
  // Live path: a hosting daemon registered this org with the broker.
744
- const { lookupOrg, normalizeCredential } = await import('../orgrt/broker.js');
745
- const remote = lookupOrg(name);
746
822
  if (remote) {
747
823
  const [fromOrg, fromRole] = from.includes(':') ? from.split(':', 2) : ['external', from];
748
- const cred = normalizeCredential(remote.credential);
824
+ const operatorCred = readOperatorCredential(name);
825
+ const agentCred = normalizeCredential(remote.credential);
826
+ const cred = operatorCred ?? agentCred;
827
+ const fromCredential = operatorCred ? undefined : lookupOrg(fromOrg)?.credential;
749
828
  try {
750
829
  const res = await fetch(`${remote.url}/api/xdeliver`, {
751
830
  method: 'POST',
@@ -753,36 +832,48 @@ export const inboxAction = async (ctx, name) => {
753
832
  'Content-Type': 'application/json',
754
833
  ...(cred ? { 'x-monomind-cred': cred } : {}),
755
834
  },
756
- body: JSON.stringify({ fromOrg, fromRole, toOrg: name, toRole, subject, body }),
835
+ body: JSON.stringify({
836
+ fromOrg,
837
+ fromRole,
838
+ ...(fromCredential ? { fromCredential } : {}),
839
+ toOrg: name,
840
+ toRole,
841
+ subject,
842
+ body,
843
+ messageId,
844
+ }),
757
845
  signal: AbortSignal.timeout(10_000),
758
846
  });
759
847
  const data = (await res.json().catch(() => ({})));
760
848
  if (res.ok && data.ok) {
761
- log(output.success(data.receipt ?? `delivered to ${name}:${toRole}`));
762
- return { success: true, message: data.receipt ?? 'delivered' };
849
+ const receipt = data.receipt ?? `delivered to ${to}`;
850
+ // The daemon itself may have queued it (role starting, org waking).
851
+ return done(/^queued/.test(receipt) ? 'queued' : 'live', receipt);
763
852
  }
764
- log(output.warning(`Live delivery rejected (${data.error ?? res.status}) — falling back to offline queue.`));
853
+ note(output.warning(`Live delivery rejected (${data.error ?? res.status}) — falling back to offline queue.`));
765
854
  }
766
855
  catch (err) {
767
- log(output.warning(`Hosting daemon unreachable (${err instanceof Error ? err.message : 'error'}) — falling back to offline queue.`));
856
+ note(output.warning(`Hosting daemon unreachable (${err instanceof Error ? err.message : 'error'}) — falling back to offline queue.`));
768
857
  }
769
858
  }
770
859
  // Offline path: spool; drained into the role's mailbox when the org next starts.
771
- const { queueMessage } = await import('../orgrt/inbox.js');
772
860
  const queued = queueMessage(ctx.cwd, name, {
773
861
  fromQualified: from,
774
862
  toRole,
775
863
  subject,
776
864
  body,
777
865
  ts: Date.now(),
866
+ messageId,
778
867
  });
779
868
  if (!queued) {
780
- log(output.error(`Could not queue the message for ${name}:${toRole} (disk full or permissions).`));
869
+ const message = `Could not queue the message for ${to} (disk full or permissions).`;
870
+ if (json)
871
+ process.stdout.write(`${JSON.stringify({ v: 1, org: name, error: message })}\n`);
872
+ else
873
+ log(output.error(message));
781
874
  return { success: false, message: 'queueing failed' };
782
875
  }
783
- const receipt = `queued for ${name}:${toRole} (delivered when the org next runs)`;
784
- log(output.success(receipt));
785
- return { success: true, message: receipt };
876
+ return done('queued', `queued for ${to} (delivered when the org next runs)`);
786
877
  };
787
878
  /** `org create <name> --template <t> [--goal g] [--schedule s]` — scaffold a config from a template. */
788
879
  export const createAction = async (ctx, name) => {
@@ -875,9 +966,13 @@ export const costsAction = async (ctx, name) => {
875
966
  log(output.info(`Per-role cost breakdown for ${name} / ${run}:`));
876
967
  // Combine data from runtime.json (live metrics) and summary (historical)
877
968
  const roleData = new Map();
969
+ // M2: endpoint roles are automations — no row in the cost table.
970
+ const endpointIds = endpointRoleIds(ctx.cwd, name);
878
971
  // Add live metrics from runtime.json
879
972
  if (rt?.roleMetrics) {
880
973
  for (const [roleId, metrics] of Object.entries(rt.roleMetrics)) {
974
+ if (endpointIds.has(roleId))
975
+ continue;
881
976
  roleData.set(roleId, {
882
977
  tokens: metrics.tokens,
883
978
  costUsd: metrics.costUsd,
@@ -888,6 +983,8 @@ export const costsAction = async (ctx, name) => {
888
983
  // Add historical data from summary if available
889
984
  if (summary?.roles) {
890
985
  for (const [roleId, roleStats] of Object.entries(summary.roles)) {
986
+ if (endpointIds.has(roleId))
987
+ continue;
891
988
  const existing = roleData.get(roleId) ?? { tokens: 0, costUsd: 0, messages: 0 };
892
989
  roleData.set(roleId, {
893
990
  tokens: existing.tokens || roleStats.tokens,
@@ -963,7 +1060,11 @@ export const flowAction = async (ctx, name) => {
963
1060
  }
964
1061
  }
965
1062
  }
966
- const roles = Array.from(roleSet).sort();
1063
+ // M2: endpoint roles are automations — not listed as role nodes.
1064
+ const endpointIds = endpointRoleIds(ctx.cwd, name);
1065
+ const roles = Array.from(roleSet)
1066
+ .filter((r) => !endpointIds.has(r))
1067
+ .sort();
967
1068
  // Protocol JSON mode (§7.2): structured roles + edges instead of Mermaid.
968
1069
  if (orgJson(ctx))
969
1070
  return printOrgJson({ v: 1, org: name, run, roles, edges: edgeObjects });
@@ -992,6 +1093,14 @@ export const flowAction = async (ctx, name) => {
992
1093
  * the daemon is unreachable — mirrors answerAction's live-then-offline shape. */
993
1094
  async function resolveApproval(ctx, name, role, action, approved) {
994
1095
  const verb = approved ? 'approved' : 'denied';
1096
+ // M5: attribution and request scoping.
1097
+ const byFlag = await resolverFlag(ctx);
1098
+ if (!byFlag.ok)
1099
+ return { success: false, message: byFlag.message };
1100
+ const resolvedBy = byFlag.by;
1101
+ const requestId = typeof ctx.flags.request === 'string' && ctx.flags.request ? ctx.flags.request : undefined;
1102
+ if (ctx.flags.request !== undefined && !requestId)
1103
+ return { success: false, message: '--request needs an approval request id (apr-…)' };
995
1104
  // SEC: approvals carry human authority — the operator credential, never the
996
1105
  // broker entry's agent credential (which any agent subprocess can read).
997
1106
  const { lookupOrg, readOperatorCredential } = await import('../orgrt/broker.js');
@@ -1005,7 +1114,14 @@ async function resolveApproval(ctx, name, role, action, approved) {
1005
1114
  'Content-Type': 'application/json',
1006
1115
  ...(cred ? { 'x-monomind-cred': cred } : {}),
1007
1116
  },
1008
- body: JSON.stringify({ org: name, role, action, approved }),
1117
+ body: JSON.stringify({
1118
+ org: name,
1119
+ role,
1120
+ action,
1121
+ approved,
1122
+ resolvedBy,
1123
+ ...(requestId ? { requestId } : {}),
1124
+ }),
1009
1125
  signal: AbortSignal.timeout(10_000),
1010
1126
  });
1011
1127
  const data = (await res.json().catch(() => ({})));
@@ -1018,10 +1134,12 @@ async function resolveApproval(ctx, name, role, action, approved) {
1018
1134
  action,
1019
1135
  approved,
1020
1136
  delivery: 'live',
1137
+ resolvedBy,
1138
+ requestId: requestId ?? null,
1021
1139
  });
1022
1140
  log(approved
1023
- ? output.success(`Approved: ${role} may execute ${action} (live).`)
1024
- : output.info(`Denied: ${role} may NOT execute ${action} (live).`));
1141
+ ? output.success(`Approved: ${role} may execute ${action} (live, by ${resolvedBy}).`)
1142
+ : output.info(`Denied: ${role} may NOT execute ${action} (live, by ${resolvedBy}).`));
1025
1143
  return { success: true, message: `${verb} ${action} for ${role}` };
1026
1144
  }
1027
1145
  log(output.warning(`Live delivery rejected (${data.error ?? res.status}) — falling back to offline queue.`));
@@ -1037,24 +1155,45 @@ async function resolveApproval(ctx, name, role, action, approved) {
1037
1155
  }
1038
1156
  const data = JSON.parse(readFileSync(approvalsPath, 'utf8'));
1039
1157
  const pending = data.approvals ?? [];
1040
- const item = pending.find((a) => a.roleId === role && a.action === action);
1041
- if (!item) {
1158
+ // Same selection as the daemon's setApproval: the one request named by
1159
+ // --request, else every still-pending entry for the (role, action) pair.
1160
+ const items = pending.filter((a) => a.roleId === role &&
1161
+ a.action === action &&
1162
+ a.approved === null &&
1163
+ (requestId === undefined || a.requestId === requestId));
1164
+ if (items.length === 0) {
1042
1165
  return {
1043
1166
  success: false,
1044
- message: `no pending approval found for role ${role} action ${action}`,
1167
+ message: requestId
1168
+ ? `no pending approval ${requestId} found for role ${role} action ${action}`
1169
+ : `no pending approval found for role ${role} action ${action}`,
1045
1170
  };
1046
1171
  }
1047
- item.approved = approved;
1048
- item.ts = Date.now();
1172
+ const now = Date.now();
1173
+ for (const item of items) {
1174
+ item.approved = approved;
1175
+ item.ts = now;
1176
+ item.resolvedBy = resolvedBy;
1177
+ item.resolvedAt = now;
1178
+ }
1049
1179
  writeFileSync(approvalsPath, JSON.stringify({ approvals: pending }, null, 2));
1050
1180
  if (orgJson(ctx))
1051
- return printOrgJson({ v: 1, org: name, role, action, approved, delivery: 'recorded' });
1181
+ return printOrgJson({
1182
+ v: 1,
1183
+ org: name,
1184
+ role,
1185
+ action,
1186
+ approved,
1187
+ delivery: 'recorded',
1188
+ resolvedBy,
1189
+ requestId: requestId ?? null,
1190
+ });
1052
1191
  log(approved
1053
- ? output.success(`Approved: ${role} may execute ${action}`)
1054
- : output.info(`Denied: ${role} may NOT execute ${action}`));
1192
+ ? output.success(`Approved: ${role} may execute ${action} (by ${resolvedBy})`)
1193
+ : output.info(`Denied: ${role} may NOT execute ${action} (by ${resolvedBy})`));
1055
1194
  return { success: true, message: `${verb} ${action} for ${role}` };
1056
1195
  }
1057
- /** `org approve <org> <role> <action>` — approve a pending tool/action approval */
1196
+ /** `org approve <org> <role> <action> [--request <id>] [--by <resolver>]` — approve a pending tool/action approval */
1058
1197
  export const approveAction = async (ctx, name) => {
1059
1198
  const role = ctx.args[1];
1060
1199
  const action = ctx.args[2];
@@ -1063,7 +1202,7 @@ export const approveAction = async (ctx, name) => {
1063
1202
  }
1064
1203
  return resolveApproval(ctx, name, role, action, true);
1065
1204
  };
1066
- /** `org deny <org> <role> <action>` — deny a pending tool/action approval */
1205
+ /** `org deny <org> <role> <action> [--request <id>] [--by <resolver>]` — deny a pending tool/action approval */
1067
1206
  export const denyAction = async (ctx, name) => {
1068
1207
  const role = ctx.args[1];
1069
1208
  const action = ctx.args[2];
@@ -1315,6 +1454,10 @@ export const gateResolveAction = async (ctx, name, approved) => {
1315
1454
  return { success: false, message: `gate "${gateId}" not found for org "${name}"` };
1316
1455
  if (gate.status !== 'pending')
1317
1456
  return { success: false, message: `gate "${gateId}" already resolved (${gate.status})` };
1457
+ const byFlag = await resolverFlag(ctx);
1458
+ if (!byFlag.ok)
1459
+ return { success: false, message: byFlag.message };
1460
+ const resolvedBy = byFlag.by;
1318
1461
  // SEC: gate resolution is a human decision — operator credential only.
1319
1462
  const { lookupOrg, readOperatorCredential } = await import('../orgrt/broker.js');
1320
1463
  const remote = lookupOrg(name);
@@ -1327,7 +1470,7 @@ export const gateResolveAction = async (ctx, name, approved) => {
1327
1470
  'Content-Type': 'application/json',
1328
1471
  ...(cred ? { 'x-monomind-cred': cred } : {}),
1329
1472
  },
1330
- body: JSON.stringify({ org: name, gateId, approved, resolution }),
1473
+ body: JSON.stringify({ org: name, gateId, approved, resolution, resolvedBy }),
1331
1474
  signal: AbortSignal.timeout(10_000),
1332
1475
  });
1333
1476
  const d = (await res.json().catch(() => ({})));
@@ -1339,6 +1482,7 @@ export const gateResolveAction = async (ctx, name, approved) => {
1339
1482
  gate_id: gateId,
1340
1483
  status: approved ? 'approved' : 'rejected',
1341
1484
  delivery: 'live',
1485
+ resolvedBy,
1342
1486
  });
1343
1487
  log(output.success(`Gate ${gateId} ${approved ? 'approved' : 'rejected'} (live).`));
1344
1488
  return { success: true, message: `gate ${approved ? 'approved' : 'rejected'}` };
@@ -1377,7 +1521,7 @@ export const gateResolveAction = async (ctx, name, approved) => {
1377
1521
  ...fresh.gates[idx],
1378
1522
  status: approved ? 'approved' : 'rejected',
1379
1523
  resolvedAt: Date.now(),
1380
- resolvedBy: 'human',
1524
+ resolvedBy,
1381
1525
  resolution,
1382
1526
  };
1383
1527
  writeGates(ctx.cwd, name, fresh);
@@ -1388,6 +1532,7 @@ export const gateResolveAction = async (ctx, name, approved) => {
1388
1532
  gate_id: gateId,
1389
1533
  status: approved ? 'approved' : 'rejected',
1390
1534
  delivery: 'recorded',
1535
+ resolvedBy,
1391
1536
  });
1392
1537
  log(output.success(`Gate ${gateId} ${approved ? 'approved' : 'rejected'} — ${name} picks it up on its next cycle.`));
1393
1538
  return { success: true, message: `gate ${approved ? 'approved' : 'rejected'} (queued)` };