@mmnto/cli 1.120.0 → 1.121.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dist/commands/config-drift.test.js +10 -2
  2. package/dist/commands/config-drift.test.js.map +1 -1
  3. package/dist/commands/doctor-parity.d.ts.map +1 -1
  4. package/dist/commands/doctor-parity.js +21 -6
  5. package/dist/commands/doctor-parity.js.map +1 -1
  6. package/dist/commands/doctor-parity.test.js +14 -7
  7. package/dist/commands/doctor-parity.test.js.map +1 -1
  8. package/dist/commands/doctor.d.ts +21 -1
  9. package/dist/commands/doctor.d.ts.map +1 -1
  10. package/dist/commands/doctor.js +167 -9
  11. package/dist/commands/doctor.js.map +1 -1
  12. package/dist/commands/doctor.test.js +234 -6
  13. package/dist/commands/doctor.test.js.map +1 -1
  14. package/dist/commands/ecl-gc.d.ts.map +1 -1
  15. package/dist/commands/ecl-gc.js +38 -5
  16. package/dist/commands/ecl-gc.js.map +1 -1
  17. package/dist/commands/ecl-gc.test.js +71 -0
  18. package/dist/commands/ecl-gc.test.js.map +1 -1
  19. package/dist/commands/eject-totemdir.test.d.ts +8 -0
  20. package/dist/commands/eject-totemdir.test.d.ts.map +1 -0
  21. package/dist/commands/eject-totemdir.test.js +101 -0
  22. package/dist/commands/eject-totemdir.test.js.map +1 -0
  23. package/dist/commands/eject.d.ts +30 -5
  24. package/dist/commands/eject.d.ts.map +1 -1
  25. package/dist/commands/eject.js +145 -31
  26. package/dist/commands/eject.js.map +1 -1
  27. package/dist/commands/eject.test.js +3 -3
  28. package/dist/commands/eject.test.js.map +1 -1
  29. package/dist/commands/hook-totemdir-render.test.d.ts +23 -0
  30. package/dist/commands/hook-totemdir-render.test.d.ts.map +1 -0
  31. package/dist/commands/hook-totemdir-render.test.js +288 -0
  32. package/dist/commands/hook-totemdir-render.test.js.map +1 -0
  33. package/dist/commands/init-templates.d.ts +4 -4
  34. package/dist/commands/init-templates.d.ts.map +1 -1
  35. package/dist/commands/init-templates.js +3 -3
  36. package/dist/commands/install-hooks-exit-contract.test.js +6 -6
  37. package/dist/commands/install-hooks-exit-contract.test.js.map +1 -1
  38. package/dist/commands/install-hooks.d.ts +162 -12
  39. package/dist/commands/install-hooks.d.ts.map +1 -1
  40. package/dist/commands/install-hooks.js +355 -88
  41. package/dist/commands/install-hooks.js.map +1 -1
  42. package/dist/commands/install-hooks.test.js +381 -147
  43. package/dist/commands/install-hooks.test.js.map +1 -1
  44. package/dist/commands/link.d.ts.map +1 -1
  45. package/dist/commands/link.js +71 -16
  46. package/dist/commands/link.js.map +1 -1
  47. package/dist/commands/link.test.d.ts +9 -0
  48. package/dist/commands/link.test.d.ts.map +1 -0
  49. package/dist/commands/link.test.js +92 -0
  50. package/dist/commands/link.test.js.map +1 -0
  51. package/dist/commands/mail.d.ts +48 -2
  52. package/dist/commands/mail.d.ts.map +1 -1
  53. package/dist/commands/mail.js +76 -2
  54. package/dist/commands/mail.js.map +1 -1
  55. package/dist/commands/mail.test.js +177 -0
  56. package/dist/commands/mail.test.js.map +1 -1
  57. package/dist/commands/pre-push-gate-matrix.test.js +9 -2
  58. package/dist/commands/pre-push-gate-matrix.test.js.map +1 -1
  59. package/dist/commands/shield.js +1 -1
  60. package/dist/commands/shield.js.map +1 -1
  61. package/dist/commands/tools-hook-parity.test.js +7 -3
  62. package/dist/commands/tools-hook-parity.test.js.map +1 -1
  63. package/dist/index.js +9 -0
  64. package/dist/index.js.map +1 -1
  65. package/package.json +2 -2
@@ -716,6 +716,11 @@ describe('pollMail — lifecycle-aware broadcast denominators (mmnto-ai/totem#25
716
716
  // Additive field (fold): a markerless tree emits zero notices — the
717
717
  // channel's existence is the only delta vs the pre-#2511 result shape.
718
718
  notices: [],
719
+ // Additive REQUIRED field (mmnto-ai/totem#2685): the `to: cohort` exhibit
720
+ // above lives in a FOREIGN repo's outbox (this poll's repo root is
721
+ // `<workspace>/totem`), so it stays a roster WARNING and this channel is
722
+ // empty — the ownership test, asserted inside the whole-shape snapshot.
723
+ senderFaults: [],
719
724
  });
720
725
  });
721
726
  });
@@ -875,6 +880,157 @@ describe('pollMail — outbox roster-validation sensor (mmnto-ai/totem#2335)', (
875
880
  ]);
876
881
  expect(poll().warnings.filter((w) => w.startsWith(UNRESOLVABLE_PREFIX))).toHaveLength(2);
877
882
  });
883
+ // ─── Sender-fault floor (mmnto-ai/totem#2685) ─────────
884
+ //
885
+ // The same detector, split by OWNERSHIP: a dispatch in an outbox THIS repo
886
+ // hosts for a seat in the poll's self-set is the polling seat's own problem
887
+ // (structured fault + `Error:` line + exit 4 + a red ecl-gc gate), while a
888
+ // foreign repo's undeliverable dispatch keeps the exit-neutral warning above.
889
+ // Live specimen: the 2026-08-26 blind round, `to: lc-agy, lc-codex,
890
+ // lc-gemini, lc-kimi` — a comma list parses as ONE scalar recipient matching
891
+ // no seat, so the sender's poll exited 0 with a clean inbox verdict beside a
892
+ // round that never left.
893
+ describe('own-hosted sender faults (mmnto-ai/totem#2685)', () => {
894
+ const COMMA_TO = 'lc-agy, lc-codex, lc-gemini, lc-kimi';
895
+ const COMMA_HINT = 'a comma list is not a recipient — ADR-098 `to:` is single-valued; send one directed dispatch per recipient';
896
+ const FAULT_FILE = '2026-08-26T0900Z-lc-agy-blind-round.md';
897
+ /** Write a dispatch into THIS repo's own outbox (`<workspace>/totem/…`). */
898
+ function writeOwnOutbox(seat, files) {
899
+ writeOutbox('totem', seat, files);
900
+ }
901
+ it('an own-hosted unresolvable `to:` yields ONE structured fault and NO roster warning for that file', () => {
902
+ writeOwnOutbox('totem-claude', [{ name: FAULT_FILE, to: COMMA_TO, subject: 'r2 blind' }]);
903
+ const result = poll({ env: SELF_CLAUDE });
904
+ expect(result.senderFaults).toEqual([
905
+ {
906
+ kind: 'undeliverable-to',
907
+ repo: 'totem',
908
+ agent: 'totem-claude',
909
+ file: FAULT_FILE,
910
+ to: COMMA_TO,
911
+ hint: COMMA_HINT,
912
+ },
913
+ ]);
914
+ // NON-VACUITY: the channels are disjoint per file — routing the fault to
915
+ // `warnings` too would pin the verdict to INCOMPLETE on a complete scan.
916
+ expect(result.warnings.filter((w) => w.startsWith(UNRESOLVABLE_PREFIX))).toEqual([]);
917
+ expect(result.warnings).toEqual([]);
918
+ expect(resolveMailExitCode(result)).toBe(4);
919
+ });
920
+ it('`hint` is non-null iff the `to:` carries a comma', () => {
921
+ writeOwnOutbox('totem-claude', [
922
+ { name: '2026-08-26T0901Z-typo.md', to: 'totem-claud', subject: 'fat-fingered' },
923
+ ]);
924
+ const result = poll({ env: SELF_CLAUDE });
925
+ expect(result.senderFaults).toHaveLength(1);
926
+ expect(result.senderFaults[0].to).toBe('totem-claud');
927
+ expect(result.senderFaults[0].hint).toBeNull();
928
+ });
929
+ it('renders an `Error:` line that never masks the inbox verdict', () => {
930
+ writeOwnOutbox('totem-claude', [{ name: FAULT_FILE, to: COMMA_TO, subject: 'r2 blind' }]);
931
+ const text = formatTextResult(poll({ env: SELF_CLAUDE })).split('\n');
932
+ const errorIdx = text.findIndex((l) => l.startsWith('Error: '));
933
+ const verdictIdx = text.findIndex((l) => l.startsWith('No unread mail'));
934
+ expect(text[errorIdx]).toBe(`Error: undeliverable dispatch in YOUR outbox: totem/totem-claude/${FAULT_FILE} — to: "${COMMA_TO}" matches no roster agent; invisible to every seat-scoped poll. Fix the to: (one recipient per dispatch, or broadcast) and re-poll. ${COMMA_HINT}`);
935
+ // The verdict STILL renders, after the fault line and unqualified: a
936
+ // sender fault is not a scan-integrity anomaly, so the #2516 INCOMPLETE
937
+ // discriminator (keyed on `warnings`) must stay silent.
938
+ expect(verdictIdx).toBeGreaterThan(errorIdx);
939
+ expect(text.join('\n')).not.toContain('INCOMPLETE');
940
+ });
941
+ it('--json carries the structured fault object (additive key)', async () => {
942
+ writeOwnOutbox('totem-claude', [{ name: FAULT_FILE, to: COMMA_TO, subject: 'r2 blind' }]);
943
+ const writes = [];
944
+ const spy = vi
945
+ .spyOn(process.stdout, 'write')
946
+ .mockImplementation((chunk) => {
947
+ writes.push(typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf-8'));
948
+ return true;
949
+ });
950
+ let exitCode;
951
+ try {
952
+ ({ exitCode } = await mailCommand({
953
+ json: true,
954
+ repoRoot: selfRepoRoot(),
955
+ workspace,
956
+ env: SELF_CLAUDE,
957
+ }));
958
+ }
959
+ finally {
960
+ spy.mockRestore();
961
+ }
962
+ const parsed = JSON.parse(writes[0]);
963
+ expect(parsed.senderFaults).toEqual([
964
+ {
965
+ kind: 'undeliverable-to',
966
+ repo: 'totem',
967
+ agent: 'totem-claude',
968
+ file: FAULT_FILE,
969
+ to: COMMA_TO,
970
+ hint: COMMA_HINT,
971
+ },
972
+ ]);
973
+ expect(exitCode).toBe(4);
974
+ });
975
+ it('a same-named seat dir in a FOREIGN repo stays a warning naming that repo — never a fault', () => {
976
+ // PATH-keyed ownership, not label-keyed: `totem-claude` is in this poll's
977
+ // self-set, but the outbox lives in another workspace repo (residue dir,
978
+ // second checkout, fan-out copy). NON-VACUITY: a label-only test would
979
+ // fault here and hand this seat an exit-4 it cannot fix.
980
+ writeOutbox('liquid-city', 'totem-claude', [
981
+ { name: FAULT_FILE, to: COMMA_TO, subject: 'r2 blind' },
982
+ ]);
983
+ const result = poll({ env: SELF_CLAUDE });
984
+ expect(result.senderFaults).toEqual([]);
985
+ const rosterWarnings = result.warnings.filter((w) => w.startsWith(UNRESOLVABLE_PREFIX));
986
+ expect(rosterWarnings).toHaveLength(1);
987
+ expect(rosterWarnings[0]).toContain(`liquid-city/totem-claude/${FAULT_FILE}`);
988
+ expect(resolveMailExitCode(result)).toBe(0);
989
+ });
990
+ it('an identity-gated poll says "an outbox this repo hosts" and stays exit 2 (NOT-DERIVED precedence)', () => {
991
+ // No explicit identity: `totem` resolves two seats via dirs∪map, so the
992
+ // self-set is a UNION — the poll cannot claim the outbox is YOURS.
993
+ writeOwnOutbox('totem-claude', [{ name: FAULT_FILE, to: COMMA_TO, subject: 'r2 blind' }]);
994
+ const result = poll();
995
+ expect(result.seatGate).toBeDefined();
996
+ expect(result.senderFaults).toHaveLength(1);
997
+ const text = formatTextResult(result);
998
+ expect(text).toContain('Error: undeliverable dispatch in an outbox this repo hosts:');
999
+ expect(text).not.toContain('YOUR outbox');
1000
+ // Exit 2: nothing in the outbox is worth telling this caller to fix until
1001
+ // identity resolves — and exit 4 here would be read as "fix the to:".
1002
+ expect(resolveMailExitCode(result)).toBe(2);
1003
+ });
1004
+ it('--all-seats + a fault in a PEER host seat outbox is exit 4 (dashboard semantics)', () => {
1005
+ // The union view is asked for BY NAME, so every seat this repo hosts is
1006
+ // "own" — the dashboard is the surface that should go red.
1007
+ writeOwnOutbox('totem-gemini', [{ name: FAULT_FILE, to: COMMA_TO, subject: 'r2 blind' }]);
1008
+ const result = poll({ allSeats: true });
1009
+ expect(result.seatGate).toBeUndefined();
1010
+ expect(result.senderFaults.map((f) => f.agent)).toEqual(['totem-gemini']);
1011
+ expect(formatTextResult(result)).toContain('an outbox this repo hosts');
1012
+ expect(resolveMailExitCode(result)).toBe(4);
1013
+ });
1014
+ it('`to: broadcast` and an env self-id outside the cohort map never fault', () => {
1015
+ writeOwnOutbox('totem-claude', [
1016
+ { name: '2026-08-26T0902Z-broadcast-round.md', to: 'broadcast', subject: 'cohort-wide' },
1017
+ { name: '2026-08-26T0903Z-custom-id-note.md', to: 'custom-id', subject: 'env self' },
1018
+ ]);
1019
+ const result = poll({ env: { TOTEM_SELF_AGENT: 'custom-id' } });
1020
+ expect(result.senderFaults).toEqual([]);
1021
+ expect(result.warnings.filter((w) => w.startsWith(UNRESOLVABLE_PREFIX))).toEqual([]);
1022
+ expect(resolveMailExitCode(result)).toBe(0);
1023
+ });
1024
+ it('a clean poll carries an empty channel and stays exit 0', () => {
1025
+ writeOwnOutbox('totem-claude', [
1026
+ { name: '2026-08-26T0904Z-lc-claude-ok.md', to: 'lc-claude', subject: 'deliverable' },
1027
+ ]);
1028
+ const result = poll({ env: SELF_CLAUDE });
1029
+ expect(result.senderFaults).toEqual([]);
1030
+ expect(resolveMailExitCode(result)).toBe(0);
1031
+ expect(formatTextResult(result)).not.toContain('Error:');
1032
+ });
1033
+ });
878
1034
  });
879
1035
  // ─── Sort + metadata ────────────────────────────────────
880
1036
  describe('pollMail — sort + metadata', () => {
@@ -1607,12 +1763,33 @@ describe('resolveMailExitCode (unit)', () => {
1607
1763
  workspace: '/w',
1608
1764
  warnings: [],
1609
1765
  notices: [],
1766
+ senderFaults: [],
1610
1767
  };
1611
1768
  }
1612
1769
  it('is 2 when self is unresolved (source none), 0 when resolved', () => {
1613
1770
  expect(resolveMailExitCode(result({ agents: [], source: 'none' }))).toBe(2);
1614
1771
  expect(resolveMailExitCode(result({ agents: ['totem-claude'], source: 'map' }))).toBe(0);
1615
1772
  });
1773
+ it('is 4 on a sender fault, and NOT-DERIVED keeps precedence over it (mmnto-ai/totem#2685)', () => {
1774
+ const fault = {
1775
+ kind: 'undeliverable-to',
1776
+ repo: 'totem',
1777
+ agent: 'totem-claude',
1778
+ file: '2026-08-26T0900Z-blind-round.md',
1779
+ to: 'lc-agy, lc-codex',
1780
+ hint: 'a comma list is not a recipient — ADR-098 `to:` is single-valued; send one directed dispatch per recipient',
1781
+ };
1782
+ const resolved = {
1783
+ ...result({ agents: ['totem-claude'], source: 'env' }),
1784
+ senderFaults: [fault],
1785
+ };
1786
+ expect(resolveMailExitCode(resolved)).toBe(4);
1787
+ // NOT-DERIVED (unresolved self) outranks the fault arm: until identity
1788
+ // resolves there is nothing in the outbox the caller can be told to fix.
1789
+ expect(resolveMailExitCode({ ...resolved, selfAgents: { agents: [], source: 'none' } })).toBe(2);
1790
+ // …and so does the identity gate.
1791
+ expect(resolveMailExitCode({ ...resolved, seatGate: { withheldDirected: 1 } })).toBe(2);
1792
+ });
1616
1793
  });
1617
1794
  describe('mailCommand — exit contract + NOT-DERIVED verdict (mmnto-ai/totem#2312)', () => {
1618
1795
  function unknownRepo() {