@raishin/vanguard-frontier-agentic 3.15.0 → 3.17.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 (374) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +3 -1
  3. package/.cursor-plugin/plugin.json +3 -1
  4. package/.github/plugin/marketplace.json +1 -1
  5. package/README.md +11 -11
  6. package/agents/AGENTS.md +1 -1
  7. package/agents/README.md +1 -1
  8. package/agents/cross-functional/agentic-security-review-agent/AGENT.md +168 -0
  9. package/agents/cross-functional/agentic-security-review-agent/harnesses/claude-code.agent.md +158 -0
  10. package/agents/cross-functional/agentic-security-review-agent/harnesses/codex.toml +165 -0
  11. package/agents/cross-functional/agentic-security-review-agent/harnesses/copilot.agent.md +163 -0
  12. package/agents/cross-functional/agentic-security-review-agent/harnesses/cursor.agent.md +158 -0
  13. package/agents/cross-functional/agentic-security-review-agent/harnesses/gemini.agent.md +164 -0
  14. package/agents/cross-functional/agentic-security-review-agent/harnesses/kiro-cli.agent.json +5 -0
  15. package/agents/cross-functional/agentic-security-review-agent/harnesses/kiro-ide.agent.md +157 -0
  16. package/agents/cross-functional/agentic-security-review-agent/metadata.json +45 -0
  17. package/agents/cross-functional/agentic-system-architecture-review-agent/AGENT.md +157 -0
  18. package/agents/cross-functional/agentic-system-architecture-review-agent/harnesses/claude-code.agent.md +147 -0
  19. package/agents/cross-functional/agentic-system-architecture-review-agent/harnesses/codex.toml +154 -0
  20. package/agents/cross-functional/agentic-system-architecture-review-agent/harnesses/copilot.agent.md +152 -0
  21. package/agents/cross-functional/agentic-system-architecture-review-agent/harnesses/cursor.agent.md +147 -0
  22. package/agents/cross-functional/agentic-system-architecture-review-agent/harnesses/gemini.agent.md +153 -0
  23. package/agents/cross-functional/agentic-system-architecture-review-agent/harnesses/kiro-cli.agent.json +5 -0
  24. package/agents/cross-functional/agentic-system-architecture-review-agent/harnesses/kiro-ide.agent.md +146 -0
  25. package/agents/cross-functional/agentic-system-architecture-review-agent/metadata.json +46 -0
  26. package/agents/typescript/typescript-maestro-agent/AGENT.md +1 -1
  27. package/agents/typescript/typescript-maestro-agent/harnesses/claude-code.agent.md +1 -1
  28. package/agents/typescript/typescript-maestro-agent/harnesses/codex.toml +1 -1
  29. package/agents/typescript/typescript-maestro-agent/harnesses/copilot.agent.md +1 -1
  30. package/agents/typescript/typescript-maestro-agent/harnesses/cursor.agent.md +1 -1
  31. package/agents/typescript/typescript-maestro-agent/harnesses/gemini.agent.md +1 -1
  32. package/agents/typescript/typescript-maestro-agent/harnesses/kiro-cli.agent.json +1 -1
  33. package/agents/typescript/typescript-maestro-agent/harnesses/kiro-ide.agent.md +1 -1
  34. package/agents/typescript/typescript-mcp-tool-contract-agent/AGENT.md +1 -1
  35. package/agents/typescript/typescript-mcp-tool-contract-agent/harnesses/claude-code.agent.md +1 -1
  36. package/agents/typescript/typescript-mcp-tool-contract-agent/harnesses/copilot.agent.md +1 -1
  37. package/agents/typescript/typescript-mcp-tool-contract-agent/harnesses/cursor.agent.md +1 -1
  38. package/agents/typescript/typescript-mcp-tool-contract-agent/harnesses/gemini.agent.md +1 -1
  39. package/agents/typescript/typescript-mcp-tool-contract-agent/harnesses/kiro-cli.agent.json +1 -1
  40. package/agents/typescript/typescript-mcp-tool-contract-agent/harnesses/kiro-ide.agent.md +1 -1
  41. package/catalog/AGENTS.md +2 -1
  42. package/catalog/agents.json +71 -0
  43. package/catalog/asset-integrity.json +1639 -92
  44. package/catalog/index.json +3 -2
  45. package/catalog/install-roles.json +24 -3
  46. package/catalog/model-assignments.json +66 -0
  47. package/catalog/mods.json +131 -0
  48. package/catalog/skill-manifest.json +134 -8
  49. package/catalog/skills.json +89 -0
  50. package/mods/README.md +220 -0
  51. package/mods/audit-trail/README.md +70 -0
  52. package/mods/audit-trail/bin/hook.mjs +65 -0
  53. package/mods/audit-trail/claude-code/UNSUPPORTED.md +11 -0
  54. package/mods/audit-trail/claude-code/settings.hooks.json +14 -0
  55. package/mods/audit-trail/codex/hooks.json +14 -0
  56. package/mods/audit-trail/kiro/UNSUPPORTED.md +7 -0
  57. package/mods/audit-trail/kiro/audit-trail-post-tool-use-hook.json +14 -0
  58. package/mods/audit-trail/lib/common.mjs +213 -0
  59. package/mods/audit-trail/lib/core.mjs +50 -0
  60. package/mods/audit-trail/lib/runtime.mjs +140 -0
  61. package/mods/audit-trail/mod.toml +37 -0
  62. package/mods/blast-radius-guard/.claude-plugin/plugin.json +5 -0
  63. package/mods/blast-radius-guard/README.md +53 -0
  64. package/mods/blast-radius-guard/bin/hook.mjs +18 -0
  65. package/mods/blast-radius-guard/claude-code/settings.hooks.json +14 -0
  66. package/mods/blast-radius-guard/codex/UNSUPPORTED.md +7 -0
  67. package/mods/blast-radius-guard/codex/hooks.json +14 -0
  68. package/mods/blast-radius-guard/hooks/hooks.json +5 -0
  69. package/mods/blast-radius-guard/hooks/register.test.ts +97 -0
  70. package/mods/blast-radius-guard/hooks/register.ts +13 -0
  71. package/mods/blast-radius-guard/kiro/UNSUPPORTED.md +7 -0
  72. package/mods/blast-radius-guard/kiro/blast-radius-guard-pre-tool-use-hook.json +14 -0
  73. package/mods/blast-radius-guard/lib/common.mjs +213 -0
  74. package/mods/blast-radius-guard/lib/core.mjs +66 -0
  75. package/mods/blast-radius-guard/lib/patterns.mjs +102 -0
  76. package/mods/blast-radius-guard/lib/runtime.mjs +140 -0
  77. package/mods/blast-radius-guard/mod.toml +34 -0
  78. package/mods/blast-radius-guard/patterns.json +101 -0
  79. package/mods/context-burn/.claude-plugin/plugin.json +5 -0
  80. package/mods/context-burn/README.md +66 -0
  81. package/mods/context-burn/bin/hook.mjs +38 -0
  82. package/mods/context-burn/claude-code/UNSUPPORTED.md +7 -0
  83. package/mods/context-burn/claude-code/settings.hooks.json +24 -0
  84. package/mods/context-burn/codex/UNSUPPORTED.md +7 -0
  85. package/mods/context-burn/codex/hooks.json +24 -0
  86. package/mods/context-burn/hooks/hooks.json +5 -0
  87. package/mods/context-burn/hooks/register.test.ts +151 -0
  88. package/mods/context-burn/hooks/register.ts +35 -0
  89. package/mods/context-burn/kiro/UNSUPPORTED.md +7 -0
  90. package/mods/context-burn/kiro/context-burn-post-tool-use-hook.json +14 -0
  91. package/mods/context-burn/kiro/context-burn-session-start-hook.json +14 -0
  92. package/mods/context-burn/lib/common.mjs +213 -0
  93. package/mods/context-burn/lib/core.mjs +79 -0
  94. package/mods/context-burn/lib/runtime.mjs +140 -0
  95. package/mods/context-burn/mod.toml +37 -0
  96. package/mods/live-guard-gate/.claude-plugin/plugin.json +5 -0
  97. package/mods/live-guard-gate/README.md +43 -0
  98. package/mods/live-guard-gate/bin/hook.mjs +12 -0
  99. package/mods/live-guard-gate/claude-code/settings.hooks.json +14 -0
  100. package/mods/live-guard-gate/codex/UNSUPPORTED.md +7 -0
  101. package/mods/live-guard-gate/codex/hooks.json +14 -0
  102. package/mods/live-guard-gate/hooks/hooks.json +5 -0
  103. package/mods/live-guard-gate/hooks/register.test.ts +59 -0
  104. package/mods/live-guard-gate/hooks/register.ts +11 -0
  105. package/mods/live-guard-gate/kiro/UNSUPPORTED.md +7 -0
  106. package/mods/live-guard-gate/kiro/live-guard-gate-pre-tool-use-hook.json +14 -0
  107. package/mods/live-guard-gate/lib/common.mjs +213 -0
  108. package/mods/live-guard-gate/lib/core.mjs +24 -0
  109. package/mods/live-guard-gate/lib/live-agents.mjs +65 -0
  110. package/mods/live-guard-gate/lib/patterns.mjs +523 -0
  111. package/mods/live-guard-gate/lib/runtime.mjs +140 -0
  112. package/mods/live-guard-gate/mod.toml +34 -0
  113. package/mods/live-guard-gate/patterns.json +522 -0
  114. package/mods/secret-redactor/.claude-plugin/plugin.json +5 -0
  115. package/mods/secret-redactor/README.md +86 -0
  116. package/mods/secret-redactor/bin/hook.mjs +35 -0
  117. package/mods/secret-redactor/claude-code/UNSUPPORTED.md +7 -0
  118. package/mods/secret-redactor/claude-code/settings.hooks.json +24 -0
  119. package/mods/secret-redactor/codex/UNSUPPORTED.md +11 -0
  120. package/mods/secret-redactor/codex/hooks.json +14 -0
  121. package/mods/secret-redactor/hooks/hooks.json +5 -0
  122. package/mods/secret-redactor/hooks/register.test.ts +150 -0
  123. package/mods/secret-redactor/hooks/register.ts +19 -0
  124. package/mods/secret-redactor/kiro/UNSUPPORTED.md +11 -0
  125. package/mods/secret-redactor/kiro/secret-redactor-pre-tool-use-hook.json +14 -0
  126. package/mods/secret-redactor/lib/common.mjs +213 -0
  127. package/mods/secret-redactor/lib/core.mjs +61 -0
  128. package/mods/secret-redactor/lib/patterns.mjs +75 -0
  129. package/mods/secret-redactor/lib/runtime.mjs +140 -0
  130. package/mods/secret-redactor/mod.toml +43 -0
  131. package/mods/secret-redactor/patterns.json +74 -0
  132. package/package.json +9 -4
  133. package/plugins/vanguard-frontier-agentic/.codex-plugin/plugin.json +1 -1
  134. package/powers/vanguard-generic/POWER.md +6 -6
  135. package/powers/vanguard-kubernetes/POWER.md +1 -1
  136. package/powers/vanguard-multi-cloud/POWER.md +1 -1
  137. package/schemas/agentic-handoff.schema.json +443 -0
  138. package/scripts/generate-kiro-powers.mjs +17 -6
  139. package/scripts/generate-mods.mjs +784 -0
  140. package/scripts/mods/common.mjs +213 -0
  141. package/scripts/mods/runtime.mjs +140 -0
  142. package/scripts/typescript_data/agents/00-typescript-maestro-agent.json +2 -2
  143. package/scripts/typescript_data/agents/11-typescript-mcp-tool-contract-agent.json +4 -4
  144. package/skills/cross-functional/agentic-board-protocol/SKILL.md +85 -0
  145. package/skills/cross-functional/agentic-board-protocol/metadata.json +28 -0
  146. package/skills/cross-functional/agentic-board-protocol/references/decision-record.md +134 -0
  147. package/skills/cross-functional/agentic-board-protocol/references/handoff-envelope.md +208 -0
  148. package/skills/cross-functional/agentic-board-protocol/references/risk-tier-crosswalk.md +88 -0
  149. package/skills/cross-functional/agentic-board-protocol/references/stop-conditions.md +122 -0
  150. package/skills/cross-functional/agentic-board-protocol/references/value-case.md +159 -0
  151. package/skills/cross-functional/agentic-security-review/SKILL.md +78 -0
  152. package/skills/cross-functional/agentic-security-review/metadata.json +30 -0
  153. package/skills/cross-functional/agentic-security-review/references/identity-and-delegated-authorization.md +144 -0
  154. package/skills/cross-functional/agentic-security-review/references/supply-chain-and-provenance.md +111 -0
  155. package/skills/cross-functional/agentic-security-review/references/threat-model-owasp-asi.md +55 -0
  156. package/skills/cross-functional/agentic-security-review/references/tools-memory-and-data-reach.md +125 -0
  157. package/skills/cross-functional/agentic-security-review/references/workflow-and-output.md +210 -0
  158. package/skills/cross-functional/agentic-system-architecture-review/SKILL.md +74 -0
  159. package/skills/cross-functional/agentic-system-architecture-review/metadata.json +31 -0
  160. package/skills/cross-functional/agentic-system-architecture-review/references/delegation-and-handoff-review.md +107 -0
  161. package/skills/cross-functional/agentic-system-architecture-review/references/pattern-selection.md +81 -0
  162. package/skills/cross-functional/agentic-system-architecture-review/references/reliability-and-containment.md +86 -0
  163. package/skills/cross-functional/agentic-system-architecture-review/references/state-memory-and-durability.md +102 -0
  164. package/skills/cross-functional/agentic-system-architecture-review/references/workflow-and-output.md +135 -0
  165. package/skills/typescript/typescript-maestro/SKILL.md +1 -1
  166. package/skills/typescript/typescript-maestro/references/routing-taxonomy.md +1 -1
  167. package/skills/typescript/typescript-mcp-tool-contract/SKILL.md +1 -1
  168. package/tests/agentic-board-suite/_build_fixtures.py +1751 -0
  169. package/tests/agentic-board-suite/_build_harnesses.py +123 -0
  170. package/tests/agentic-board-suite/agentic_board_rules.py +857 -0
  171. package/tests/agentic-board-suite/fixtures/invalid/CH-PROPOSAL-LINK--challenge-names-a-later-envelope.json +244 -0
  172. package/tests/agentic-board-suite/fixtures/invalid/CH-PROPOSAL-LINK--dangling-challenge-beside-a-request.json +410 -0
  173. package/tests/agentic-board-suite/fixtures/invalid/CH-PROPOSAL-LINK--dangling-proposal.json +187 -0
  174. package/tests/agentic-board-suite/fixtures/invalid/CH-ROUND-COUNTED--round-two-declared-as-one.json +481 -0
  175. package/tests/agentic-board-suite/fixtures/invalid/CH-ROUND-COUNTED--self-declared-round.json +187 -0
  176. package/tests/agentic-board-suite/fixtures/invalid/DR-ALTERNATIVES--no-alternatives.json +340 -0
  177. package/tests/agentic-board-suite/fixtures/invalid/DR-CHALLENGE-COMPLETE--hard-gate-left-behind-by-proposer-swap.json +532 -0
  178. package/tests/agentic-board-suite/fixtures/invalid/DR-CHALLENGE-COMPLETE--hard-gate-left-unattached.json +480 -0
  179. package/tests/agentic-board-suite/fixtures/invalid/DR-CHALLENGE-COMPLETE--hard-gate-on-root-left-unattached.json +410 -0
  180. package/tests/agentic-board-suite/fixtures/invalid/DR-CHALLENGE-COMPLETE--late-challenge-before-record.json +410 -0
  181. package/tests/agentic-board-suite/fixtures/invalid/DR-CHALLENGE-COMPLETE--superseded-request-missed-earlier-challenge.json +565 -0
  182. package/tests/agentic-board-suite/fixtures/invalid/DR-CHALLENGE-PRECEDES--withdrawal-attached-before-it-was-sent.json +483 -0
  183. package/tests/agentic-board-suite/fixtures/invalid/DR-CHALLENGE-PRECEDES--withdrawal-descends-from-the-record.json +547 -0
  184. package/tests/agentic-board-suite/fixtures/invalid/DR-CHALLENGE-REQUIRED--r1-flag-without-challenge.json +349 -0
  185. package/tests/agentic-board-suite/fixtures/invalid/DR-CHALLENGE-REQUIRED--r2-without-challenge.json +339 -0
  186. package/tests/agentic-board-suite/fixtures/invalid/DR-CHALLENGE-REQUIRED--r3-no-flags-without-challenge.json +339 -0
  187. package/tests/agentic-board-suite/fixtures/invalid/DR-CHALLENGE-RESOLVES--dangling-challenge.json +347 -0
  188. package/tests/agentic-board-suite/fixtures/invalid/DR-CHALLENGE-TARGETS-PROPOSAL--challenge-of-requester-message.json +415 -0
  189. package/tests/agentic-board-suite/fixtures/invalid/DR-CLASS-FROM-ROOT--root-declares-no-class.json +345 -0
  190. package/tests/agentic-board-suite/fixtures/invalid/DR-CLASS-FROM-ROOT--root-declares-other-class.json +346 -0
  191. package/tests/agentic-board-suite/fixtures/invalid/DR-DESIGNATED-CHALLENGER--designated-challenger-only-on-the-framing.json +346 -0
  192. package/tests/agentic-board-suite/fixtures/invalid/DR-DESIGNATED-CHALLENGER--investment-without-finance.json +436 -0
  193. package/tests/agentic-board-suite/fixtures/invalid/DR-DESIGNATED-CHALLENGER--stopped-designated-challenge.json +348 -0
  194. package/tests/agentic-board-suite/fixtures/invalid/DR-HARD-GATE--conditional-over-hard-gate.json +357 -0
  195. package/tests/agentic-board-suite/fixtures/invalid/DR-HARD-GATE--unresolved-hard-gate.json +357 -0
  196. package/tests/agentic-board-suite/fixtures/invalid/DR-NO-APPROVE-OVER-DISSENT--approve-over-a-stopped-challenge.json +413 -0
  197. package/tests/agentic-board-suite/fixtures/invalid/DR-NO-APPROVE-OVER-DISSENT--approve-over-dissent.json +346 -0
  198. package/tests/agentic-board-suite/fixtures/invalid/DR-ONE-REQUEST--re-ask-without-supersedes.json +426 -0
  199. package/tests/agentic-board-suite/fixtures/invalid/DR-ONE-REQUEST--request-superseded-twice.json +581 -0
  200. package/tests/agentic-board-suite/fixtures/invalid/DR-ONE-REQUEST--supersedes-a-challenge.json +427 -0
  201. package/tests/agentic-board-suite/fixtures/invalid/DR-ONE-REQUEST--supersedes-a-dangling-id.json +427 -0
  202. package/tests/agentic-board-suite/fixtures/invalid/DR-ONE-REQUEST--supersedes-a-later-request.json +500 -0
  203. package/tests/agentic-board-suite/fixtures/invalid/DR-ONE-REQUEST--superseding-request-from-another-proposer.json +621 -0
  204. package/tests/agentic-board-suite/fixtures/invalid/DR-ONE-REQUEST--supersession-cycle.json +501 -0
  205. package/tests/agentic-board-suite/fixtures/invalid/DR-OWNER-FROM-ROOT--agent-opens-decision-path.json +480 -0
  206. package/tests/agentic-board-suite/fixtures/invalid/DR-OWNER-FROM-ROOT--owner-is-the-requester.json +273 -0
  207. package/tests/agentic-board-suite/fixtures/invalid/DR-OWNER-FROM-ROOT--request-to-later-sorting-non-owner.json +273 -0
  208. package/tests/agentic-board-suite/fixtures/invalid/DR-OWNER-FROM-ROOT--request-to-non-owner.json +273 -0
  209. package/tests/agentic-board-suite/fixtures/invalid/DR-OWNER-FROM-ROOT--root-names-no-owner.json +341 -0
  210. package/tests/agentic-board-suite/fixtures/invalid/DR-PROPOSER-IS-SENDER--challenger-assembles-request.json +337 -0
  211. package/tests/agentic-board-suite/fixtures/invalid/DR-PROPOSER-NOT-CHALLENGER--self-challenge.json +411 -0
  212. package/tests/agentic-board-suite/fixtures/invalid/DR-RISK-CARRIED--concurrent-higher-tier-envelope.json +403 -0
  213. package/tests/agentic-board-suite/fixtures/invalid/DR-RISK-CARRIED--r5-decided-through-lower-tier-request.json +433 -0
  214. package/tests/agentic-board-suite/fixtures/invalid/DR-RISK-CARRIED--request-drops-materiality-flags.json +479 -0
  215. package/tests/agentic-board-suite/fixtures/invalid/DR-RISK-CARRIED--risk-raised-after-request-before-decision.json +403 -0
  216. package/tests/agentic-board-suite/fixtures/invalid/DR-TO-PRINCIPAL--request-to-agent.json +272 -0
  217. package/tests/agentic-board-suite/fixtures/invalid/DR-UNRESOLVED-DISSENT--dissent-dropped.json +341 -0
  218. package/tests/agentic-board-suite/fixtures/invalid/DR-UNRESOLVED-DISSENT--stopped-challenge-recorded-withdrawn.json +408 -0
  219. package/tests/agentic-board-suite/fixtures/invalid/DR-UNRESOLVED-DISSENT--unresolved-without-dissent.json +341 -0
  220. package/tests/agentic-board-suite/fixtures/invalid/DR-VALUE-CASE-REQUIRED--investment-without-value-case.json +353 -0
  221. package/tests/agentic-board-suite/fixtures/invalid/DRC-AFTER-REQUEST--decided-before-request.json +408 -0
  222. package/tests/agentic-board-suite/fixtures/invalid/DRC-AFTER-REQUEST--request-descends-from-its-record.json +408 -0
  223. package/tests/agentic-board-suite/fixtures/invalid/DRC-APPROVAL-STATE--state-contradicts-outcome.json +346 -0
  224. package/tests/agentic-board-suite/fixtures/invalid/DRC-CO-SIGN-BINDING--co-sign-approves-a-redesign.json +433 -0
  225. package/tests/agentic-board-suite/fixtures/invalid/DRC-CO-SIGN-BINDING--co-sign-contradicts-outcome.json +433 -0
  226. package/tests/agentic-board-suite/fixtures/invalid/DRC-CO-SIGN-BINDING--co-sign-does-not-descend-from-its-record.json +499 -0
  227. package/tests/agentic-board-suite/fixtures/invalid/DRC-CO-SIGN-BINDING--co-sign-is-the-parent-of-its-record.json +500 -0
  228. package/tests/agentic-board-suite/fixtures/invalid/DRC-CO-SIGN-BINDING--co-sign-names-another-approver.json +438 -0
  229. package/tests/agentic-board-suite/fixtures/invalid/DRC-CO-SIGN-BINDING--co-sign-nobody-asked-for.json +421 -0
  230. package/tests/agentic-board-suite/fixtures/invalid/DRC-CO-SIGN-BINDING--co-sign-sent-before-the-record.json +499 -0
  231. package/tests/agentic-board-suite/fixtures/invalid/DRC-CO-SIGNED--decision-in-force-not-co-signed.json +591 -0
  232. package/tests/agentic-board-suite/fixtures/invalid/DRC-CO-SIGNED--later-sorting-second-approver-never-signs.json +359 -0
  233. package/tests/agentic-board-suite/fixtures/invalid/DRC-CO-SIGNED--named-second-approver-never-signs.json +359 -0
  234. package/tests/agentic-board-suite/fixtures/invalid/DRC-COMPLETED--record-left-dispatched.json +346 -0
  235. package/tests/agentic-board-suite/fixtures/invalid/DRC-DECIDER-IS-ADDRESSEE--decided-by-non-addressee.json +408 -0
  236. package/tests/agentic-board-suite/fixtures/invalid/DRC-DECIDER-IS-ADDRESSEE--earlier-sorting-non-addressee-decides.json +408 -0
  237. package/tests/agentic-board-suite/fixtures/invalid/DRC-DECIDER-IS-SENDER--relayed-decision.json +408 -0
  238. package/tests/agentic-board-suite/fixtures/invalid/DRC-HARD-GATE-ACCEPTANCE--override-without-acceptance.json +356 -0
  239. package/tests/agentic-board-suite/fixtures/invalid/DRC-HARD-GATE-ACCEPTANCE--same-round-withdrawal.json +423 -0
  240. package/tests/agentic-board-suite/fixtures/invalid/DRC-HARD-GATE-ACCEPTANCE--stopped-over-cap-withdrawal.json +623 -0
  241. package/tests/agentic-board-suite/fixtures/invalid/DRC-HARD-GATE-ACCEPTANCE--unrelated-withdrawal-does-not-clear-gate.json +482 -0
  242. package/tests/agentic-board-suite/fixtures/invalid/DRC-HARD-GATE-ACCEPTANCE--withdrawal-names-an-undelivered-envelope.json +482 -0
  243. package/tests/agentic-board-suite/fixtures/invalid/DRC-HARD-GATE-ACCEPTANCE--withdrawal-on-another-proposers-revision.json +600 -0
  244. package/tests/agentic-board-suite/fixtures/invalid/DRC-NOT-REQUESTER--second-approver-is-requester.json +433 -0
  245. package/tests/agentic-board-suite/fixtures/invalid/DRC-ONE-RECORD--contradicting-records.json +416 -0
  246. package/tests/agentic-board-suite/fixtures/invalid/DRC-ONE-RECORD--duplicate-co-signature.json +507 -0
  247. package/tests/agentic-board-suite/fixtures/invalid/DRC-PRINCIPAL-SOURCE--agent-writes-decision.json +345 -0
  248. package/tests/agentic-board-suite/fixtures/invalid/DRC-REQUEST-BINDING--points-at-challenge.json +346 -0
  249. package/tests/agentic-board-suite/fixtures/invalid/DRC-REQUEST-BINDING--record-names-a-missing-request.json +346 -0
  250. package/tests/agentic-board-suite/fixtures/invalid/DRC-REQUEST-BINDING--request-edited-after-decision.json +346 -0
  251. package/tests/agentic-board-suite/fixtures/invalid/DRC-SECOND-APPROVER--r5-request-decided-on-lower-tier-branch.json +418 -0
  252. package/tests/agentic-board-suite/fixtures/invalid/DRC-SECOND-APPROVER--r5-single-approver.json +354 -0
  253. package/tests/agentic-board-suite/fixtures/invalid/DRC-SECOND-APPROVER--second-approver-is-decider.json +359 -0
  254. package/tests/agentic-board-suite/fixtures/invalid/HO-AGENT-NO-APPROVAL--agent-claims-approval.json +132 -0
  255. package/tests/agentic-board-suite/fixtures/invalid/HO-APPROVAL-ONLY-ON-RECORD--principal-delegate-claims-approval.json +132 -0
  256. package/tests/agentic-board-suite/fixtures/invalid/HO-ATTEN-ACTIONS--child-widens-actions.json +345 -0
  257. package/tests/agentic-board-suite/fixtures/invalid/HO-ATTEN-BUDGET--child-raises-limit.json +132 -0
  258. package/tests/agentic-board-suite/fixtures/invalid/HO-ATTEN-BUDGET--currency-changed.json +132 -0
  259. package/tests/agentic-board-suite/fixtures/invalid/HO-BLOCK-AS-CHALLENGE--block-sent-as-return.json +184 -0
  260. package/tests/agentic-board-suite/fixtures/invalid/HO-CHAIN-CUSTODY--parent-sender-continues-unstopped.json +187 -0
  261. package/tests/agentic-board-suite/fixtures/invalid/HO-CHAIN-CUSTODY--reply-from-outside-chain.json +132 -0
  262. package/tests/agentic-board-suite/fixtures/invalid/HO-CHAIN-PARENT--hop-skips.json +132 -0
  263. package/tests/agentic-board-suite/fixtures/invalid/HO-CHAIN-TIME--sent-before-parent.json +132 -0
  264. package/tests/agentic-board-suite/fixtures/invalid/HO-DESTINATION-TIER--delegate-to-live-guard.json +132 -0
  265. package/tests/agentic-board-suite/fixtures/invalid/HO-DESTINATION-TIER--delegate-to-untiered-agent.json +132 -0
  266. package/tests/agentic-board-suite/fixtures/invalid/HO-ESCALATE-TO-PRINCIPAL--escalate-to-agent.json +131 -0
  267. package/tests/agentic-board-suite/fixtures/invalid/HO-ESCALATE-TO-PRINCIPAL--escalated-delegate-to-agent.json +132 -0
  268. package/tests/agentic-board-suite/fixtures/invalid/HO-MONO-CLASS--classification-lowered.json +346 -0
  269. package/tests/agentic-board-suite/fixtures/invalid/HO-MONO-DEADLINE--deadline-extended.json +132 -0
  270. package/tests/agentic-board-suite/fixtures/invalid/HO-MONO-MATERIALITY--flag-dropped.json +434 -0
  271. package/tests/agentic-board-suite/fixtures/invalid/HO-MONO-MAX-HOPS--max-hops-raised.json +132 -0
  272. package/tests/agentic-board-suite/fixtures/invalid/HO-MONO-PROHIBITIONS--prohibition-dropped.json +131 -0
  273. package/tests/agentic-board-suite/fixtures/invalid/HO-MONO-SPENT--one-counter-decreases.json +346 -0
  274. package/tests/agentic-board-suite/fixtures/invalid/HO-MONO-SPENT--spent-counter-reset.json +346 -0
  275. package/tests/agentic-board-suite/fixtures/invalid/HO-MONO-TIER--tier-lowered.json +346 -0
  276. package/tests/agentic-board-suite/fixtures/invalid/HO-NO-EXECUTION--deploy-authorized.json +133 -0
  277. package/tests/agentic-board-suite/fixtures/invalid/HO-NO-SELF-ADDRESS--challenge-to-self.json +187 -0
  278. package/tests/agentic-board-suite/fixtures/invalid/HO-ONE-CORRELATION--two-correlations.json +132 -0
  279. package/tests/agentic-board-suite/fixtures/invalid/HO-ONE-ROOT--second-root-beside-a-decision.json +410 -0
  280. package/tests/agentic-board-suite/fixtures/invalid/HO-ONE-ROOT--two-roots.json +190 -0
  281. package/tests/agentic-board-suite/fixtures/invalid/HO-PAYLOAD-CHALLENGE--challenge-without-payload.json +180 -0
  282. package/tests/agentic-board-suite/fixtures/invalid/HO-PAYLOAD-DECISION-RECORD--record-without-payload.json +331 -0
  283. package/tests/agentic-board-suite/fixtures/invalid/HO-PAYLOAD-DECISION-REQUEST--request-without-payload.json +324 -0
  284. package/tests/agentic-board-suite/fixtures/invalid/HO-PAYLOAD-ESCALATE--escalate-without-payload.json +128 -0
  285. package/tests/agentic-board-suite/fixtures/invalid/HO-PAYLOAD-EXCLUSIVE--return-with-escalation.json +136 -0
  286. package/tests/agentic-board-suite/fixtures/invalid/HO-PAYLOAD-RETURN--return-without-verdict.json +124 -0
  287. package/tests/agentic-board-suite/fixtures/invalid/HO-REASON-HAS-STATUS--reason-on-completed.json +134 -0
  288. package/tests/agentic-board-suite/fixtures/invalid/HO-RETURN-EVIDENCE--pass-on-inference-only.json +132 -0
  289. package/tests/agentic-board-suite/fixtures/invalid/HO-RETURN-EVIDENCE--pass-over-blocking-gap.json +137 -0
  290. package/tests/agentic-board-suite/fixtures/invalid/HO-RETURN-EVIDENCE--pass-with-no-evidence.json +127 -0
  291. package/tests/agentic-board-suite/fixtures/invalid/HO-RETURN-TO-SENDER--return-skips-the-delegator.json +185 -0
  292. package/tests/agentic-board-suite/fixtures/invalid/HO-ROOT-PARENT--root-with-parent.json +132 -0
  293. package/tests/agentic-board-suite/fixtures/invalid/HO-SCHEMA--unknown-credential-field.json +133 -0
  294. package/tests/agentic-board-suite/fixtures/invalid/HO-STOP-NOTE--conflict-without-note.json +133 -0
  295. package/tests/agentic-board-suite/fixtures/invalid/HO-STOPPED-HAS-REASON--stopped-without-reason.json +132 -0
  296. package/tests/agentic-board-suite/fixtures/invalid/HO-UNIQUE-TASK-ID--duplicate-task-id.json +198 -0
  297. package/tests/agentic-board-suite/fixtures/invalid/STOP-BUDGET--budget-undeclared.json +123 -0
  298. package/tests/agentic-board-suite/fixtures/invalid/STOP-BUDGET--return-overspent.json +132 -0
  299. package/tests/agentic-board-suite/fixtures/invalid/STOP-CHALLENGE-ROUNDS--revisions-carried-in-rebuttal-challenges.json +461 -0
  300. package/tests/agentic-board-suite/fixtures/invalid/STOP-CHALLENGE-ROUNDS--third-round.json +454 -0
  301. package/tests/agentic-board-suite/fixtures/invalid/STOP-CHALLENGE-ROUNDS--third-version-by-return-naming-the-original.json +461 -0
  302. package/tests/agentic-board-suite/fixtures/invalid/STOP-DEADLINE--sent-after-deadline.json +132 -0
  303. package/tests/agentic-board-suite/fixtures/invalid/STOP-DEPTH--depth-undeclared.json +182 -0
  304. package/tests/agentic-board-suite/fixtures/invalid/STOP-INSTRUCTION-BAIT--claimed-approval-dispatched.json +132 -0
  305. package/tests/agentic-board-suite/fixtures/invalid/STOP-LIVE-GUARD--mutation-dispatched.json +130 -0
  306. package/tests/agentic-board-suite/fixtures/invalid/STOP-MISSING-EVIDENCE--blocking-gap-dispatched.json +137 -0
  307. package/tests/agentic-board-suite/fixtures/invalid/STOP-MISSING-EVIDENCE--decision-request-blocking-gap.json +351 -0
  308. package/tests/agentic-board-suite/fixtures/invalid/STOP-ROUTING-LOOP--exchanges-on-the-original-envelope.json +381 -0
  309. package/tests/agentic-board-suite/fixtures/invalid/STOP-ROUTING-LOOP--loop-undeclared.json +182 -0
  310. package/tests/agentic-board-suite/fixtures/invalid/STOP-ROUTING-LOOP--revisit-to-challenger-of-another-party.json +256 -0
  311. package/tests/agentic-board-suite/fixtures/invalid/STOP-ROUTING-LOOP--second-revisit-to-challenger.json +540 -0
  312. package/tests/agentic-board-suite/fixtures/invalid/STOP-TERMINAL--challenge-answers-stopped-delegate.json +247 -0
  313. package/tests/agentic-board-suite/fixtures/invalid/STOP-TERMINAL--decision-on-escalated-request.json +353 -0
  314. package/tests/agentic-board-suite/fixtures/invalid/STOP-TERMINAL--decision-on-stopped-request.json +422 -0
  315. package/tests/agentic-board-suite/fixtures/invalid/STOP-TERMINAL--reply-to-stopped-delegate.json +244 -0
  316. package/tests/agentic-board-suite/fixtures/invalid/VC-BENEFIT-CEILING--benefit-above-baseline.json +436 -0
  317. package/tests/agentic-board-suite/fixtures/invalid/VC-COST-SIGN--negative-risk-loss.json +436 -0
  318. package/tests/agentic-board-suite/fixtures/invalid/VC-COST-SIGN--risk-loss-low-minus-one.json +436 -0
  319. package/tests/agentic-board-suite/fixtures/invalid/VC-COUNTERFACTUALS--single-agent-counterfactual-missing.json +427 -0
  320. package/tests/agentic-board-suite/fixtures/invalid/VC-COUNTERFACTUALS--workflow-counterfactual-missing.json +427 -0
  321. package/tests/agentic-board-suite/fixtures/invalid/VC-NAV-ARITHMETIC--nav-off-by-one.json +436 -0
  322. package/tests/agentic-board-suite/fixtures/invalid/VC-ORDER--inverted-range.json +436 -0
  323. package/tests/agentic-board-suite/fixtures/invalid/VC-OVERSIGHT--free-oversight.json +436 -0
  324. package/tests/agentic-board-suite/fixtures/invalid/VC-PROMOTION--measured-but-not-incremental.json +436 -0
  325. package/tests/agentic-board-suite/fixtures/invalid/VC-PROMOTION--promoted-on-estimate.json +436 -0
  326. package/tests/agentic-board-suite/fixtures/invalid/VC-RECOMMEND-INCREMENTAL--recommend-non-incremental.json +436 -0
  327. package/tests/agentic-board-suite/fixtures/invalid/VC-RUNS-BOUND--more-successes-than-runs.json +436 -0
  328. package/tests/agentic-board-suite/fixtures/invalid/VC-SYNTHETIC--synthetic-claims-measured.json +436 -0
  329. package/tests/agentic-board-suite/fixtures/invalid/VC-UNIT-COST--no-successes-nonzero-cost.json +436 -0
  330. package/tests/agentic-board-suite/fixtures/invalid/VC-UNIT-COST--unit-cost-mismatch.json +436 -0
  331. package/tests/agentic-board-suite/fixtures/valid/v01-r0-single-review.json +129 -0
  332. package/tests/agentic-board-suite/fixtures/valid/v01x-block-return-on-inference.json +125 -0
  333. package/tests/agentic-board-suite/fixtures/valid/v01x-block-return-to-principal.json +125 -0
  334. package/tests/agentic-board-suite/fixtures/valid/v01x-pass-return-documentation-evidence.json +129 -0
  335. package/tests/agentic-board-suite/fixtures/valid/v01x-pass-return-mixed-evidence.json +133 -0
  336. package/tests/agentic-board-suite/fixtures/valid/v01x-pass-return-nonblocking-gap.json +134 -0
  337. package/tests/agentic-board-suite/fixtures/valid/v02-r2-architecture-dissent.json +343 -0
  338. package/tests/agentic-board-suite/fixtures/valid/v02x-challenge-after-decision.json +407 -0
  339. package/tests/agentic-board-suite/fixtures/valid/v02x-challenge-after-record-same-second.json +464 -0
  340. package/tests/agentic-board-suite/fixtures/valid/v02x-redispatch-after-cured-stop.json +534 -0
  341. package/tests/agentic-board-suite/fixtures/valid/v02x-security-proposal-returned-to-its-delegator.json +407 -0
  342. package/tests/agentic-board-suite/fixtures/valid/v03-investment-loses-to-workflow.json +433 -0
  343. package/tests/agentic-board-suite/fixtures/valid/v04-r3-hard-gate-risk-acceptance.json +354 -0
  344. package/tests/agentic-board-suite/fixtures/valid/v04x-live-hard-gate-recommend-escalate.json +354 -0
  345. package/tests/agentic-board-suite/fixtures/valid/v04x-live-hard-gate-recommend-reject.json +354 -0
  346. package/tests/agentic-board-suite/fixtures/valid/v05-live-guard-escalation.json +129 -0
  347. package/tests/agentic-board-suite/fixtures/valid/v06-r5-dual-approval.json +430 -0
  348. package/tests/agentic-board-suite/fixtures/valid/v06x-co-sign-same-second.json +430 -0
  349. package/tests/agentic-board-suite/fixtures/valid/v06x-re-asked-r5-decision-co-signed-again.json +662 -0
  350. package/tests/agentic-board-suite/fixtures/valid/v06x-unsigned-approve-superseded-by-co-signed-reject.json +588 -0
  351. package/tests/agentic-board-suite/fixtures/valid/v07-depth-and-routing-loop.json +180 -0
  352. package/tests/agentic-board-suite/fixtures/valid/v08-budget-exhausted.json +186 -0
  353. package/tests/agentic-board-suite/fixtures/valid/v09-instruction-bait.json +131 -0
  354. package/tests/agentic-board-suite/fixtures/valid/v09x-requester-restates-after-bait.json +251 -0
  355. package/tests/agentic-board-suite/fixtures/valid/v10-r1-optional-challenge.json +184 -0
  356. package/tests/agentic-board-suite/fixtures/valid/v10x-report-up-after-challenge.json +246 -0
  357. package/tests/agentic-board-suite/fixtures/valid/v11-two-rounds-hard-gate-withdrawn.json +478 -0
  358. package/tests/agentic-board-suite/fixtures/valid/v11x-designated-challenge-first-answer-over-cap.json +598 -0
  359. package/tests/agentic-board-suite/fixtures/valid/v11x-interleaved-challenger-round-two.json +604 -0
  360. package/tests/agentic-board-suite/fixtures/valid/v11x-proposal-handed-to-the-challenger-capped.json +860 -0
  361. package/tests/agentic-board-suite/fixtures/valid/v11x-round-three-escalation-path.json +747 -0
  362. package/tests/agentic-board-suite/fixtures/valid/v11x-same-second-round-two.json +478 -0
  363. package/tests/agentic-board-suite/fixtures/valid/v11x-two-findings-one-round.json +549 -0
  364. package/tests/agentic-board-suite/fixtures/valid/v12-superseded-request.json +497 -0
  365. package/tests/agentic-board-suite/fixtures/valid/v12x-challenge-after-the-first-decision.json +566 -0
  366. package/tests/agentic-board-suite/fixtures/valid/v12x-late-challenge-cured-by-supersede.json +566 -0
  367. package/tests/agentic-board-suite/fixtures/valid/v12x-supersede-in-the-same-second.json +497 -0
  368. package/tests/agentic-board-suite/test_agentic_board.py +502 -0
  369. package/tests/fixtures/README.md +1 -1
  370. package/tests/fuzz-properties.test.mjs +254 -0
  371. package/tests/mods.test.mjs +768 -0
  372. package/tests/validate-agent-tool-tiers.py +7 -5
  373. package/tests/validate-asset-integrity.py +1 -0
  374. package/tests/validate-mods.py +821 -0
@@ -0,0 +1,784 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Generator: the files marked "G" under mods/<id>/ plus catalog/mods.json.
4
+ *
5
+ * Source of truth is mods/<id>/mod.toml (plus patterns.json for the pattern mods) and the
6
+ * two shared files scripts/mods/common.mjs and scripts/mods/runtime.mjs. Everything else
7
+ * this script writes is derived and must never be hand-edited. Hand-written files in a mod
8
+ * (README.md, bin/, hooks/register.ts, lib/core.mjs, ...) are never touched.
9
+ *
10
+ * mod.toml is read with a strict TOML-subset parser (comments, bare-key tables, basic
11
+ * strings, integers, booleans, arrays of strings). Anything else is an error, so a manifest
12
+ * can never mean one thing to this script and another to Python's tomllib.
13
+ *
14
+ * Deterministic by construction: no clock, no env, no network, no shell, nothing outside
15
+ * the repo. Output is sorted; JSON is 2-space with a trailing newline.
16
+ *
17
+ * Usage:
18
+ * node scripts/generate-mods.mjs # write generated files, delete stale ones
19
+ * node scripts/generate-mods.mjs --check # verify in sync (CI); writes nothing
20
+ * node scripts/generate-mods.mjs --print-manifests # parsed mod.toml files as JSON
21
+ *
22
+ * Exports: parseToml, validateManifest, loadMods, renderMod, renderAll.
23
+ * loadMods(root) -> { mods, ctx, errors } (the only function that reads the disk)
24
+ * renderAll(mods, ctx) -> Map<repo-relative path, content>, sorted by path
25
+ */
26
+ import {
27
+ lstatSync,
28
+ mkdirSync,
29
+ readdirSync,
30
+ readFileSync,
31
+ realpathSync,
32
+ rmdirSync,
33
+ unlinkSync,
34
+ writeFileSync,
35
+ } from 'node:fs'
36
+ import path from 'node:path'
37
+ import { fileURLToPath, pathToFileURL } from 'node:url'
38
+
39
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
40
+
41
+ const HARNESSES = ['claude-code', 'kiro', 'codex']
42
+ const HARNESS_NAMES = { 'claude-code': 'Claude Code', kiro: 'Kiro', codex: 'Codex' }
43
+ const EVENTS = ['PreToolUse', 'PostToolUse', 'SessionStart'] // canonical order
44
+ const EV_KEBAB = { PreToolUse: 'pre-tool-use', PostToolUse: 'post-tool-use', SessionStart: 'session-start' }
45
+ const SURFACES = ['native', 'classic']
46
+ const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype'])
47
+ const ID_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/
48
+ const VERSION_RE = /^\d+\.\d+\.\d+$/
49
+ const CTRL_RE = /[\u0000-\u001f\u007f]/
50
+ const LIVE_AGENT_RE = /-live-.*-guard-agent$/
51
+ const GEN_HEADER = '// Generated by scripts/generate-mods.mjs'
52
+
53
+ // Paths the generator owns inside a mod. Everything else in a mod is hand-written.
54
+ const OWNED_DIRS = ['.claude-plugin', 'claude-code', 'kiro', 'codex'] // every file inside is generated
55
+ const OWNED_FILES = {
56
+ lib: ['common.mjs', 'runtime.mjs', 'patterns.mjs', 'live-agents.mjs'],
57
+ hooks: ['hooks.json'],
58
+ }
59
+
60
+ const cmp = (a, b) => (a < b ? -1 : a > b ? 1 : 0)
61
+ const json = (v) => JSON.stringify(v, null, 2) + '\n'
62
+ const isTable = (v) => v !== null && typeof v === 'object' && !Array.isArray(v)
63
+
64
+ function aggregate(errors) {
65
+ const e = new Error(errors.join('\n'))
66
+ e.errors = errors
67
+ return e
68
+ }
69
+
70
+ // ---------------------------------------------------------------------------------------
71
+ // TOML subset parser
72
+ // ---------------------------------------------------------------------------------------
73
+
74
+ // table object -> Map(key -> line); key '' is the table's own header line. Lets schema
75
+ // errors point at file:line without changing the plain-object shape parseToml returns.
76
+ const LINES = new WeakMap()
77
+ const ESCAPES = { '"': '"', '\\': '\\', n: '\n', t: '\t', r: '\r' }
78
+
79
+ export function parseToml(text, fileLabel = 'mod.toml') {
80
+ const n = text.length
81
+ const root = {}
82
+ LINES.set(root, new Map())
83
+ const defined = new Set() // explicitly declared [tables], by dotted path
84
+ let cur = root
85
+ let pos = 0
86
+ let line = 1
87
+
88
+ const fail = (msg = 'unsupported TOML', at = line) => {
89
+ throw new Error(`${fileLabel}:${at}: ${msg}`)
90
+ }
91
+ const isWs = (c) => c === ' ' || c === '\t'
92
+ const atNewline = () => text[pos] === '\n' || (text[pos] === '\r' && text[pos + 1] === '\n')
93
+ const eatNewline = () => {
94
+ pos += text[pos] === '\r' ? 2 : 1
95
+ line++
96
+ }
97
+ const skipWs = () => {
98
+ while (pos < n && isWs(text[pos])) pos++
99
+ }
100
+ const skipComment = () => {
101
+ if (text[pos] === '#') while (pos < n && !atNewline()) pos++
102
+ }
103
+ const skipWsNl = () => {
104
+ for (;;) {
105
+ skipWs()
106
+ if (pos < n && atNewline()) eatNewline()
107
+ else if (text[pos] === '#') skipComment()
108
+ else return
109
+ }
110
+ }
111
+ const endOfLine = () => {
112
+ skipWs()
113
+ skipComment()
114
+ if (pos >= n) return
115
+ if (atNewline()) eatNewline()
116
+ else fail()
117
+ }
118
+
119
+ const BARE = /[A-Za-z0-9_-]+/y
120
+ const bareKey = () => {
121
+ BARE.lastIndex = pos
122
+ const m = BARE.exec(text)
123
+ if (!m) return ''
124
+ pos += m[0].length
125
+ if (FORBIDDEN_KEYS.has(m[0])) fail(`forbidden key ${m[0]}`)
126
+ return m[0]
127
+ }
128
+
129
+ const readString = () => {
130
+ if (text.startsWith('"""', pos)) fail() // multiline basic string
131
+ pos++
132
+ let out = ''
133
+ for (;;) {
134
+ if (pos >= n) fail()
135
+ const c = text[pos]
136
+ if (c === '"') {
137
+ pos++
138
+ return out
139
+ }
140
+ if (c === '\\') {
141
+ const e = text[pos + 1]
142
+ if (e === 'u') {
143
+ const hex = text.slice(pos + 2, pos + 6)
144
+ if (!/^[0-9A-Fa-f]{4}$/.test(hex)) fail()
145
+ const cp = parseInt(hex, 16)
146
+ if (cp >= 0xd800 && cp <= 0xdfff) fail() // lone surrogate is not a scalar value
147
+ out += String.fromCharCode(cp)
148
+ pos += 6
149
+ } else if (e !== undefined && Object.hasOwn(ESCAPES, e)) {
150
+ out += ESCAPES[e]
151
+ pos += 2
152
+ } else fail()
153
+ continue
154
+ }
155
+ const code = c.charCodeAt(0)
156
+ if (code === 0x0a || code === 0x0d || (code < 0x20 && c !== '\t') || code === 0x7f) fail()
157
+ out += c
158
+ pos++
159
+ }
160
+ }
161
+
162
+ const readArray = () => {
163
+ const startLine = line
164
+ pos++
165
+ const items = []
166
+ for (;;) {
167
+ skipWsNl()
168
+ if (pos >= n) fail('unsupported TOML', startLine)
169
+ if (text[pos] === ']') {
170
+ pos++
171
+ return items
172
+ }
173
+ if (text[pos] !== '"') fail() // only strings: no nesting, no mixed arrays, no inline tables
174
+ items.push(readString())
175
+ skipWsNl()
176
+ if (text[pos] === ',') pos++
177
+ else if (text[pos] === ']') {
178
+ pos++
179
+ return items
180
+ } else fail()
181
+ }
182
+ }
183
+
184
+ const SCALAR = /(?:-?(?:0|[1-9][0-9]*)|true|false)(?=[ \t\r\n#,\]]|$)/y
185
+ const readValue = () => {
186
+ const c = text[pos]
187
+ if (c === '"') return readString()
188
+ if (c === '[') return readArray()
189
+ SCALAR.lastIndex = pos
190
+ const m = SCALAR.exec(text)
191
+ if (!m) fail() // literal strings, floats, dates, inline tables, ...
192
+ pos += m[0].length
193
+ if (m[0] === 'true') return true
194
+ if (m[0] === 'false') return false
195
+ const v = Number(m[0])
196
+ if (!Number.isSafeInteger(v)) fail()
197
+ return v || 0 // folds -0 into 0
198
+ }
199
+
200
+ while (pos < n) {
201
+ skipWs()
202
+ if (pos >= n) break
203
+ if (atNewline()) {
204
+ eatNewline()
205
+ continue
206
+ }
207
+ if (text[pos] === '#') {
208
+ skipComment()
209
+ continue
210
+ }
211
+ if (text[pos] === '[') {
212
+ if (text[pos + 1] === '[') fail() // array of tables
213
+ const headerLine = line
214
+ pos++
215
+ const segs = []
216
+ for (;;) {
217
+ skipWs()
218
+ const seg = bareKey()
219
+ if (!seg) fail()
220
+ segs.push(seg)
221
+ skipWs()
222
+ if (text[pos] === '.') pos++
223
+ else if (text[pos] === ']') {
224
+ pos++
225
+ break
226
+ } else fail()
227
+ }
228
+ endOfLine()
229
+ cur = root
230
+ for (const seg of segs) {
231
+ if (!Object.hasOwn(cur, seg)) {
232
+ const t = {}
233
+ LINES.set(t, new Map())
234
+ cur[seg] = t
235
+ LINES.get(cur).set(seg, headerLine)
236
+ } else if (!isTable(cur[seg])) fail(`duplicate key ${seg}`, headerLine)
237
+ cur = cur[seg]
238
+ }
239
+ const dotted = segs.join('.')
240
+ if (defined.has(dotted)) fail(`duplicate table [${dotted}]`, headerLine)
241
+ defined.add(dotted)
242
+ LINES.get(cur).set('', headerLine)
243
+ continue
244
+ }
245
+ const keyLine = line
246
+ const key = bareKey() // quoted keys fall through to fail() below
247
+ if (!key) fail()
248
+ skipWs()
249
+ if (text[pos] !== '=') fail() // also rejects dotted keys
250
+ pos++
251
+ skipWs()
252
+ const value = readValue()
253
+ endOfLine()
254
+ if (Object.hasOwn(cur, key)) fail(`duplicate key ${key}`, keyLine)
255
+ cur[key] = value
256
+ LINES.get(cur).set(key, keyLine)
257
+ }
258
+ return root
259
+ }
260
+
261
+ // ---------------------------------------------------------------------------------------
262
+ // mod.toml schema
263
+ // ---------------------------------------------------------------------------------------
264
+
265
+ /** Returns an array of `<label>[:<line>]: <message>` strings; empty when the manifest is valid. */
266
+ export function validateManifest(m, dirName, label) {
267
+ const errs = []
268
+ const err = (table, key, msg) => {
269
+ const l = LINES.get(table)?.get(key)
270
+ errs.push(`${label}${l ? `:${l}` : ''}: ${msg}`)
271
+ }
272
+ const q = (name, k) => (name ? `${name}.${k}` : k)
273
+ const keys = (t, name, required, optional = []) => {
274
+ for (const k of required) if (!Object.hasOwn(t, k)) err(t, '', `missing key ${q(name, k)}`)
275
+ for (const k of Object.keys(t)) {
276
+ if (!required.includes(k) && !optional.includes(k)) err(t, k, `unknown key ${q(name, k)}`)
277
+ }
278
+ }
279
+ const sub = (t, k, name) => {
280
+ if (!Object.hasOwn(t, k)) {
281
+ err(t, '', `missing table [${q(name, k)}]`)
282
+ return null
283
+ }
284
+ if (!isTable(t[k])) {
285
+ err(t, k, `${q(name, k)} must be a table`)
286
+ return null
287
+ }
288
+ return t[k]
289
+ }
290
+ const text = (t, k, name) => {
291
+ const v = t[k]
292
+ if (v === undefined) return // keys() already reported it missing
293
+ if (typeof v !== 'string' || !v.trim() || CTRL_RE.test(v)) {
294
+ err(t, k, `${q(name, k)} must be a non-empty single-line string`)
295
+ }
296
+ }
297
+ const strArray = (t, k, name, { oneOf, unique = false, nonEmpty = false } = {}) => {
298
+ const v = t[k]
299
+ if (v === undefined) return
300
+ if (!Array.isArray(v) || v.some((s) => typeof s !== 'string' || !s || CTRL_RE.test(s))) {
301
+ err(t, k, `${q(name, k)} must be an array of non-empty single-line strings`)
302
+ return
303
+ }
304
+ if (nonEmpty && v.length === 0) err(t, k, `${q(name, k)} must not be empty`)
305
+ if (unique && new Set(v).size !== v.length) err(t, k, `${q(name, k)} must not repeat a value`)
306
+ if (oneOf) for (const s of v) if (!oneOf.includes(s)) err(t, k, `${q(name, k)}: unknown value "${s}"`)
307
+ }
308
+
309
+ if (!isTable(m)) return [`${label}: manifest must be a table`]
310
+ keys(m, '', ['mod', 'footprint', 'harnesses'])
311
+
312
+ const mod = Object.hasOwn(m, 'mod') ? sub(m, 'mod', '') : null
313
+ if (mod) {
314
+ keys(mod, 'mod', ['id', 'name', 'description', 'version', 'reach_level', 'events'])
315
+ if (typeof mod.id === 'string') {
316
+ if (!ID_RE.test(mod.id)) err(mod, 'id', 'mod.id must be kebab-case (^[a-z0-9]+(-[a-z0-9]+)*$)')
317
+ else if (mod.id !== dirName) err(mod, 'id', `mod.id "${mod.id}" must equal the directory name "${dirName}"`)
318
+ } else if (mod.id !== undefined) err(mod, 'id', 'mod.id must be a string')
319
+ text(mod, 'name', 'mod')
320
+ text(mod, 'description', 'mod')
321
+ if (mod.version !== undefined && !(typeof mod.version === 'string' && VERSION_RE.test(mod.version))) {
322
+ err(mod, 'version', 'mod.version must match ^\\d+\\.\\d+\\.\\d+$')
323
+ }
324
+ if (mod.reach_level !== undefined && !(Number.isInteger(mod.reach_level) && mod.reach_level >= 0 && mod.reach_level <= 3)) {
325
+ err(mod, 'reach_level', 'mod.reach_level must be an integer 0..3')
326
+ }
327
+ strArray(mod, 'events', 'mod', { oneOf: EVENTS, unique: true, nonEmpty: true })
328
+ }
329
+
330
+ const fp = Object.hasOwn(m, 'footprint') ? sub(m, 'footprint', '') : null
331
+ if (fp) {
332
+ const names = ['env_reads', 'file_writes', 'spawns', 'egress_hosts']
333
+ keys(fp, 'footprint', names)
334
+ for (const k of names) strArray(fp, k, 'footprint')
335
+ }
336
+
337
+ const hs = Object.hasOwn(m, 'harnesses') ? sub(m, 'harnesses', '') : null
338
+ if (hs) {
339
+ keys(hs, 'harnesses', HARNESSES)
340
+ for (const h of HARNESSES) {
341
+ if (!Object.hasOwn(hs, h)) continue
342
+ const t = sub(hs, h, 'harnesses')
343
+ if (!t) continue
344
+ const name = `harnesses.${h}`
345
+ if (typeof t.supported !== 'boolean') {
346
+ err(t, Object.hasOwn(t, 'supported') ? 'supported' : '', `${name}.supported must be a boolean`)
347
+ continue
348
+ }
349
+ if (!t.supported) {
350
+ keys(t, name, ['supported', 'reason'])
351
+ text(t, 'reason', name)
352
+ continue
353
+ }
354
+ const claude = h === 'claude-code'
355
+ keys(t, name, claude ? ['supported', 'events', 'surfaces'] : ['supported', 'events'], claude ? ['unsupported', 'degraded'] : ['requires', 'unsupported', 'degraded'])
356
+ strArray(t, 'events', name, { oneOf: EVENTS, unique: true })
357
+ if (claude) strArray(t, 'surfaces', name, { oneOf: SURFACES, unique: true })
358
+ else strArray(t, 'requires', name)
359
+ const allowed = claude ? [...EVENTS, ...SURFACES] : EVENTS
360
+ for (const k of ['unsupported', 'degraded']) {
361
+ if (!Object.hasOwn(t, k)) continue
362
+ const st = sub(t, k, name)
363
+ if (!st) continue
364
+ for (const key of Object.keys(st)) {
365
+ if (!allowed.includes(key)) err(st, key, `${name}.${k}.${key}: key must be one of ${allowed.join(', ')}`)
366
+ else text(st, key, `${name}.${k}`)
367
+ }
368
+ }
369
+ }
370
+ }
371
+ return errs
372
+ }
373
+
374
+ // ---------------------------------------------------------------------------------------
375
+ // Loading (the only disk reads)
376
+ // ---------------------------------------------------------------------------------------
377
+
378
+ // Check components without following symlinks, before any read, mkdir or removal.
379
+ function safePath(root, rel) {
380
+ const base = path.resolve(root)
381
+ const abs = path.resolve(base, rel)
382
+ const relative = path.relative(base, abs)
383
+ if (relative === '..' || relative.startsWith('..' + path.sep) || path.isAbsolute(relative)) {
384
+ throw new Error(`${rel}: escapes the repo`)
385
+ }
386
+ let current = base
387
+ for (const part of relative.split(path.sep).filter(Boolean)) {
388
+ current = path.join(current, part)
389
+ try {
390
+ if (lstatSync(current).isSymbolicLink()) throw new Error(`${rel}: symlink in repo path`)
391
+ } catch (e) {
392
+ if (e.code === 'ENOENT') break
393
+ throw e
394
+ }
395
+ }
396
+ return abs
397
+ }
398
+
399
+ function readText(root, rel) {
400
+ const buf = readFileSync(safePath(root, rel))
401
+ const s = buf.toString('utf8')
402
+ if (!Buffer.from(s, 'utf8').equals(buf)) throw new Error('not valid UTF-8, so it cannot be copied byte for byte')
403
+ return s
404
+ }
405
+
406
+ function modDirs(root, errors) {
407
+ try {
408
+ return readdirSync(safePath(root, 'mods'), { withFileTypes: true })
409
+ .filter((e) => e.isDirectory() && !e.name.startsWith('.'))
410
+ .map((e) => e.name)
411
+ .sort(cmp)
412
+ } catch (e) {
413
+ errors.push(e.code === 'ENOENT' ? 'mods/: directory is missing' : e.message)
414
+ return []
415
+ }
416
+ }
417
+
418
+ /** Parse (no schema check) every mods/<dir>/mod.toml. */
419
+ function readManifests(root) {
420
+ const errors = []
421
+ const entries = []
422
+ for (const dir of modDirs(root, errors)) {
423
+ const label = `mods/${dir}/mod.toml`
424
+ try {
425
+ entries.push({ dir, label, manifest: parseToml(readText(root, label), label) })
426
+ } catch (e) {
427
+ errors.push(e.code === 'ENOENT' ? `${label}: missing` : e.message.startsWith(label) ? e.message : `${label}: ${e.message}`)
428
+ }
429
+ }
430
+ return { entries, errors }
431
+ }
432
+
433
+ const usesLiveAgents = (patterns) => Array.isArray(patterns) && patterns.some((p) => Array.isArray(p?.agents))
434
+
435
+ /**
436
+ * Read and validate every mod. Mods with errors are left out of `mods`; every error is
437
+ * collected rather than thrown. `ctx` carries the shared inputs renderMod needs:
438
+ * { common, runtime, agentIds } (agentIds is null unless a mod needs the live-agent lists).
439
+ */
440
+ export function loadMods(root = ROOT) {
441
+ const { entries, errors } = readManifests(root)
442
+ const mods = []
443
+ for (const { dir, label, manifest } of entries) {
444
+ let ok = true
445
+ const report = (msgs) => {
446
+ errors.push(...msgs)
447
+ if (msgs.length) ok = false
448
+ }
449
+ report(validateManifest(manifest, dir, label))
450
+ let patterns = null
451
+ const pLabel = `mods/${dir}/patterns.json`
452
+ try {
453
+ const p = JSON.parse(readText(root, pLabel))
454
+ if (!Array.isArray(p) || p.some((e) => !isTable(e))) throw new Error('must be an array of objects')
455
+ for (const e of p) {
456
+ if (Object.hasOwn(e, 'agents') && !(Array.isArray(e.agents) && e.agents.every((a) => typeof a === 'string'))) {
457
+ throw new Error(`pattern ${e.id}: agents must be an array of strings`)
458
+ }
459
+ }
460
+ patterns = p
461
+ } catch (e) {
462
+ if (e.code !== 'ENOENT') report([`${pLabel}: ${e.message}`])
463
+ }
464
+ if (ok) mods.push({ id: dir, dir: `mods/${dir}`, manifest, patterns })
465
+ }
466
+
467
+ const ctx = { common: null, runtime: null, agentIds: null }
468
+ for (const k of ['common', 'runtime']) {
469
+ const rel = `scripts/mods/${k}.mjs`
470
+ try {
471
+ ctx[k] = readText(root, rel)
472
+ } catch (e) {
473
+ errors.push(`${rel}: ${e.code === 'ENOENT' ? 'missing' : e.message}`)
474
+ }
475
+ }
476
+ if (mods.some((m) => usesLiveAgents(m.patterns))) {
477
+ try {
478
+ const agents = JSON.parse(readText(root, 'catalog/agents.json'))
479
+ if (!Array.isArray(agents)) throw new Error('must be an array')
480
+ ctx.agentIds = agents.map((a) => a?.id).filter((id) => typeof id === 'string')
481
+ } catch (e) {
482
+ errors.push(`catalog/agents.json: ${e.code === 'ENOENT' ? 'missing' : e.message}`)
483
+ }
484
+ }
485
+ return { mods, ctx, errors }
486
+ }
487
+
488
+ // ---------------------------------------------------------------------------------------
489
+ // Rendering (pure)
490
+ // ---------------------------------------------------------------------------------------
491
+
492
+ const inCanonicalOrder = (events) => EVENTS.filter((e) => events.includes(e))
493
+
494
+ function hookEntry(command) {
495
+ return [{ hooks: [{ type: 'command', command }] }]
496
+ }
497
+
498
+ function hooksByEvent(events, command) {
499
+ return { hooks: Object.fromEntries(inCanonicalOrder(events).map((e) => [e, hookEntry(command(e))])) }
500
+ }
501
+
502
+ function renderUnsupported(manifest, h) {
503
+ const t = manifest.harnesses[h]
504
+ const unsupported = t.unsupported ?? {}
505
+ const degraded = t.degraded ?? {}
506
+ if (t.supported && !Object.keys(unsupported).length && !Object.keys(degraded).length) return null
507
+ const blocks = [
508
+ `# ${manifest.mod.name} on ${HARNESS_NAMES[h]}`,
509
+ '<!-- Generated by scripts/generate-mods.mjs from mod.toml. Do not edit. -->',
510
+ ]
511
+ if (!t.supported) blocks.push(`## Not supported\n\n${t.reason}`)
512
+ for (const [title, table] of [['Unsupported', unsupported], ['Degraded', degraded]]) {
513
+ const ks = Object.keys(table).sort(cmp)
514
+ if (ks.length) blocks.push(`## ${title}\n\n${ks.map((k) => `- \`${k}\`: ${table[k]}`).join('\n')}`)
515
+ }
516
+ return blocks.join('\n\n') + '\n'
517
+ }
518
+
519
+ /** Every generated file of one mod: Map<repo-relative path, content>. */
520
+ export function renderMod(mod, ctx) {
521
+ if (typeof ctx?.common !== 'string' || typeof ctx?.runtime !== 'string') {
522
+ throw new TypeError('renderMod: ctx.common and ctx.runtime must be strings (see loadMods)')
523
+ }
524
+ const { id, manifest: m, patterns } = mod
525
+ const out = new Map()
526
+ const put = (rel, content) => out.set(`mods/${id}/${rel}`, content)
527
+
528
+ put('lib/common.mjs', ctx.common)
529
+ put('lib/runtime.mjs', ctx.runtime)
530
+
531
+ if (patterns) {
532
+ put('lib/patterns.mjs', `${GEN_HEADER} from patterns.json. Do not edit.\nexport default ${JSON.stringify(patterns, null, 2)};\n`)
533
+ }
534
+ if (usesLiveAgents(patterns)) {
535
+ if (!Array.isArray(ctx.agentIds)) throw new TypeError('renderMod: ctx.agentIds is required for a mod with agent-bearing patterns')
536
+ const selected = [...new Set(ctx.agentIds.filter((a) => LIVE_AGENT_RE.test(a)))].sort(cmp)
537
+ const known = new Set(selected)
538
+ const covered = new Set()
539
+ const errors = []
540
+ for (const p of patterns) {
541
+ for (const a of Array.isArray(p.agents) ? p.agents : []) {
542
+ if (known.has(a)) covered.add(a)
543
+ else errors.push(`mods/${id}/patterns.json: pattern ${p.id} references "${a}", which is not a live guard agent in catalog/agents.json`)
544
+ }
545
+ }
546
+ if (errors.length) throw aggregate(errors)
547
+ const coveredList = [...covered].sort(cmp)
548
+ const uncovered = selected.filter((a) => !covered.has(a))
549
+ put(
550
+ 'lib/live-agents.mjs',
551
+ `${GEN_HEADER} from patterns.json and catalog/agents.json. Do not edit.\n` +
552
+ `export const covered = ${JSON.stringify(coveredList, null, 2)};\n` +
553
+ `export const uncovered = ${JSON.stringify(uncovered, null, 2)};\n`,
554
+ )
555
+ }
556
+
557
+ const cc = m.harnesses['claude-code']
558
+ if (cc.supported && cc.surfaces.includes('native')) {
559
+ put('.claude-plugin/plugin.json', json({ name: id, version: m.mod.version, description: m.mod.description }))
560
+ put('hooks/hooks.json', json({ modules: ['./register.ts'] }))
561
+ }
562
+ if (cc.supported && cc.surfaces.includes('classic')) {
563
+ put(
564
+ 'claude-code/settings.hooks.json',
565
+ json(hooksByEvent(cc.events, (e) => `node "$CLAUDE_PROJECT_DIR/.claude/vanguard-mods/${id}/bin/hook.mjs" --harness claude-code --event ${e}`)),
566
+ )
567
+ }
568
+ if (m.harnesses.kiro.supported) {
569
+ for (const e of inCanonicalOrder(m.harnesses.kiro.events)) {
570
+ put(
571
+ `kiro/${id}-${EV_KEBAB[e]}-hook.json`,
572
+ json({
573
+ version: 'v1',
574
+ hooks: [
575
+ {
576
+ name: `${id}:${e}`,
577
+ description: m.mod.description,
578
+ trigger: e,
579
+ action: { type: 'command', command: `node .kiro/vanguard-mods/${id}/bin/hook.mjs --harness kiro --event ${e}` },
580
+ },
581
+ ],
582
+ }),
583
+ )
584
+ }
585
+ }
586
+ if (m.harnesses.codex.supported) {
587
+ put('codex/hooks.json', json(hooksByEvent(m.harnesses.codex.events, (e) => `node .codex/vanguard-mods/${id}/bin/hook.mjs --harness codex --event ${e}`)))
588
+ }
589
+ for (const h of HARNESSES) {
590
+ const md = renderUnsupported(m, h)
591
+ if (md) put(`${h}/UNSUPPORTED.md`, md)
592
+ }
593
+ return new Map([...out].sort((a, b) => cmp(a[0], b[0])))
594
+ }
595
+
596
+ /** Every generated file for all mods plus catalog/mods.json, sorted by path; input order is irrelevant. */
597
+ export function renderAll(mods, ctx) {
598
+ const sorted = [...mods].sort((a, b) => cmp(a.id, b.id))
599
+ const files = new Map()
600
+ const errors = []
601
+ for (const mod of sorted) {
602
+ try {
603
+ for (const [k, v] of renderMod(mod, ctx)) files.set(k, v)
604
+ } catch (e) {
605
+ errors.push(...(e.errors ?? [e.message]))
606
+ }
607
+ }
608
+ if (errors.length) throw aggregate(errors)
609
+ files.set(
610
+ 'catalog/mods.json',
611
+ json(
612
+ sorted.map(({ id, manifest: m }) => ({
613
+ id,
614
+ name: m.mod.name,
615
+ version: m.mod.version,
616
+ description: m.mod.description,
617
+ reach_level: m.mod.reach_level,
618
+ events: [...m.mod.events],
619
+ harnesses: HARNESSES.filter((h) => m.harnesses[h].supported).sort(cmp),
620
+ path: `mods/${id}`,
621
+ footprint: {
622
+ env_reads: [...m.footprint.env_reads],
623
+ file_writes: [...m.footprint.file_writes],
624
+ spawns: [...m.footprint.spawns],
625
+ egress_hosts: [...m.footprint.egress_hosts],
626
+ },
627
+ })),
628
+ ),
629
+ )
630
+ return new Map([...files].sort((a, b) => cmp(a[0], b[0])))
631
+ }
632
+
633
+ // ---------------------------------------------------------------------------------------
634
+ // Disk sync
635
+ // ---------------------------------------------------------------------------------------
636
+
637
+ const exists = (abs) => {
638
+ try {
639
+ lstatSync(abs)
640
+ return true
641
+ } catch {
642
+ return false
643
+ }
644
+ }
645
+
646
+ function walkFiles(root, rel, found = []) {
647
+ const abs = safePath(root, rel)
648
+ let entries
649
+ try {
650
+ entries = readdirSync(abs, { withFileTypes: true })
651
+ } catch {
652
+ return found
653
+ }
654
+ for (const e of entries) {
655
+ const child = `${rel}/${e.name}`
656
+ safePath(root, child)
657
+ if (e.isDirectory()) walkFiles(root, child, found)
658
+ else found.push(child) // files, symlinks and anything else count as files
659
+ }
660
+ return found
661
+ }
662
+
663
+ /** Generated paths on disk that the current render no longer produces. */
664
+ function findStale(root, ids, expected) {
665
+ const stale = []
666
+ for (const id of ids) {
667
+ const base = `mods/${id}`
668
+ for (const d of OWNED_DIRS) stale.push(...walkFiles(root, `${base}/${d}`))
669
+ for (const [d, names] of Object.entries(OWNED_FILES)) {
670
+ for (const name of names) if (exists(safePath(root, `${base}/${d}/${name}`))) stale.push(`${base}/${d}/${name}`)
671
+ }
672
+ }
673
+ return stale.filter((rel) => !expected.has(rel)).sort(cmp)
674
+ }
675
+
676
+ function findDrift(root, expected) {
677
+ const drift = []
678
+ for (const [rel, content] of expected) {
679
+ const abs = safePath(root, rel)
680
+ let actual = null
681
+ try {
682
+ actual = readFileSync(abs)
683
+ } catch {
684
+ // missing or unreadable counts as drift
685
+ }
686
+ if (!actual || !actual.equals(Buffer.from(content, 'utf8'))) drift.push(rel)
687
+ }
688
+ return drift
689
+ }
690
+
691
+ function safeWrite(root, realRoot, rel, content) {
692
+ const abs = safePath(root, rel)
693
+ mkdirSync(path.dirname(abs), { recursive: true })
694
+ const dir = realpathSync(path.dirname(abs))
695
+ if (dir !== realRoot && !dir.startsWith(realRoot + path.sep)) throw new Error(`${rel}: resolves outside the repo`)
696
+ if (exists(abs) && lstatSync(abs).isSymbolicLink()) throw new Error(`${rel}: is a symlink; refusing to write through it`)
697
+ writeFileSync(abs, content)
698
+ }
699
+
700
+ function pruneEmpty(abs) {
701
+ let entries
702
+ try {
703
+ entries = readdirSync(abs, { withFileTypes: true })
704
+ } catch {
705
+ return
706
+ }
707
+ for (const e of entries) if (e.isDirectory()) pruneEmpty(path.join(abs, e.name))
708
+ if (readdirSync(abs).length === 0) rmdirSync(abs)
709
+ }
710
+
711
+ // ---------------------------------------------------------------------------------------
712
+ // CLI
713
+ // ---------------------------------------------------------------------------------------
714
+
715
+ export function main(argv = process.argv.slice(2), root = ROOT) {
716
+ const modes = ['--check', '--print-manifests']
717
+ if (argv.length > 1 || (argv.length === 1 && !modes.includes(argv[0]))) {
718
+ console.error('Usage: node scripts/generate-mods.mjs [--check | --print-manifests]')
719
+ return 2
720
+ }
721
+ const fail = (errors) => {
722
+ for (const e of errors) console.error(e)
723
+ return 1
724
+ }
725
+
726
+ if (argv[0] === '--print-manifests') {
727
+ const { entries, errors } = readManifests(root)
728
+ if (errors.length) return fail(errors)
729
+ console.log(JSON.stringify(Object.fromEntries(entries.map((e) => [e.dir, e.manifest])), null, 2))
730
+ return 0
731
+ }
732
+
733
+ const { mods, ctx, errors } = loadMods(root)
734
+ let expected = new Map()
735
+ if (ctx.common !== null && ctx.runtime !== null) {
736
+ try {
737
+ expected = renderAll(mods, ctx)
738
+ } catch (e) {
739
+ errors.push(...(e.errors ?? [e.message]))
740
+ }
741
+ }
742
+ if (errors.length) return fail(errors)
743
+
744
+ let drift, stale
745
+ try {
746
+ drift = findDrift(root, expected)
747
+ stale = findStale(root, mods.map((m) => m.id), expected)
748
+ } catch (e) {
749
+ return fail([e.message])
750
+ }
751
+
752
+ if (argv[0] === '--check') {
753
+ const paths = [...new Set([...drift, ...stale])].sort(cmp)
754
+ if (paths.length === 0) {
755
+ console.log('OK: mods in sync')
756
+ return 0
757
+ }
758
+ console.error(`FAIL: mods out of sync (${paths.length} paths). Run: npm run mods:write`)
759
+ for (const p of paths) console.error(` ${p}`)
760
+ return 1
761
+ }
762
+
763
+ try {
764
+ const realRoot = realpathSync(root)
765
+ for (const rel of drift) safeWrite(root, realRoot, rel, expected.get(rel))
766
+ for (const rel of stale) unlinkSync(path.join(root, rel))
767
+ for (const m of mods) for (const d of OWNED_DIRS) pruneEmpty(path.join(root, 'mods', m.id, d))
768
+ } catch (e) {
769
+ return fail([e.message])
770
+ }
771
+ console.log(`Generated ${mods.length} mods for ${HARNESSES.length} harnesses`)
772
+ return 0
773
+ }
774
+
775
+ function isEntryPoint() {
776
+ if (!process.argv[1]) return false
777
+ try {
778
+ return import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href
779
+ } catch {
780
+ return false
781
+ }
782
+ }
783
+
784
+ if (isEntryPoint()) process.exitCode = main()