@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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +3 -1
- package/.cursor-plugin/plugin.json +3 -1
- package/.github/plugin/marketplace.json +1 -1
- package/README.md +11 -11
- package/agents/AGENTS.md +1 -1
- package/agents/README.md +1 -1
- package/agents/cross-functional/agentic-security-review-agent/AGENT.md +168 -0
- package/agents/cross-functional/agentic-security-review-agent/harnesses/claude-code.agent.md +158 -0
- package/agents/cross-functional/agentic-security-review-agent/harnesses/codex.toml +165 -0
- package/agents/cross-functional/agentic-security-review-agent/harnesses/copilot.agent.md +163 -0
- package/agents/cross-functional/agentic-security-review-agent/harnesses/cursor.agent.md +158 -0
- package/agents/cross-functional/agentic-security-review-agent/harnesses/gemini.agent.md +164 -0
- package/agents/cross-functional/agentic-security-review-agent/harnesses/kiro-cli.agent.json +5 -0
- package/agents/cross-functional/agentic-security-review-agent/harnesses/kiro-ide.agent.md +157 -0
- package/agents/cross-functional/agentic-security-review-agent/metadata.json +45 -0
- package/agents/cross-functional/agentic-system-architecture-review-agent/AGENT.md +157 -0
- package/agents/cross-functional/agentic-system-architecture-review-agent/harnesses/claude-code.agent.md +147 -0
- package/agents/cross-functional/agentic-system-architecture-review-agent/harnesses/codex.toml +154 -0
- package/agents/cross-functional/agentic-system-architecture-review-agent/harnesses/copilot.agent.md +152 -0
- package/agents/cross-functional/agentic-system-architecture-review-agent/harnesses/cursor.agent.md +147 -0
- package/agents/cross-functional/agentic-system-architecture-review-agent/harnesses/gemini.agent.md +153 -0
- package/agents/cross-functional/agentic-system-architecture-review-agent/harnesses/kiro-cli.agent.json +5 -0
- package/agents/cross-functional/agentic-system-architecture-review-agent/harnesses/kiro-ide.agent.md +146 -0
- package/agents/cross-functional/agentic-system-architecture-review-agent/metadata.json +46 -0
- package/agents/typescript/typescript-maestro-agent/AGENT.md +1 -1
- package/agents/typescript/typescript-maestro-agent/harnesses/claude-code.agent.md +1 -1
- package/agents/typescript/typescript-maestro-agent/harnesses/codex.toml +1 -1
- package/agents/typescript/typescript-maestro-agent/harnesses/copilot.agent.md +1 -1
- package/agents/typescript/typescript-maestro-agent/harnesses/cursor.agent.md +1 -1
- package/agents/typescript/typescript-maestro-agent/harnesses/gemini.agent.md +1 -1
- package/agents/typescript/typescript-maestro-agent/harnesses/kiro-cli.agent.json +1 -1
- package/agents/typescript/typescript-maestro-agent/harnesses/kiro-ide.agent.md +1 -1
- package/agents/typescript/typescript-mcp-tool-contract-agent/AGENT.md +1 -1
- package/agents/typescript/typescript-mcp-tool-contract-agent/harnesses/claude-code.agent.md +1 -1
- package/agents/typescript/typescript-mcp-tool-contract-agent/harnesses/copilot.agent.md +1 -1
- package/agents/typescript/typescript-mcp-tool-contract-agent/harnesses/cursor.agent.md +1 -1
- package/agents/typescript/typescript-mcp-tool-contract-agent/harnesses/gemini.agent.md +1 -1
- package/agents/typescript/typescript-mcp-tool-contract-agent/harnesses/kiro-cli.agent.json +1 -1
- package/agents/typescript/typescript-mcp-tool-contract-agent/harnesses/kiro-ide.agent.md +1 -1
- package/catalog/AGENTS.md +2 -1
- package/catalog/agents.json +71 -0
- package/catalog/asset-integrity.json +1639 -92
- package/catalog/index.json +3 -2
- package/catalog/install-roles.json +24 -3
- package/catalog/model-assignments.json +66 -0
- package/catalog/mods.json +131 -0
- package/catalog/skill-manifest.json +134 -8
- package/catalog/skills.json +89 -0
- package/mods/README.md +220 -0
- package/mods/audit-trail/README.md +70 -0
- package/mods/audit-trail/bin/hook.mjs +65 -0
- package/mods/audit-trail/claude-code/UNSUPPORTED.md +11 -0
- package/mods/audit-trail/claude-code/settings.hooks.json +14 -0
- package/mods/audit-trail/codex/hooks.json +14 -0
- package/mods/audit-trail/kiro/UNSUPPORTED.md +7 -0
- package/mods/audit-trail/kiro/audit-trail-post-tool-use-hook.json +14 -0
- package/mods/audit-trail/lib/common.mjs +213 -0
- package/mods/audit-trail/lib/core.mjs +50 -0
- package/mods/audit-trail/lib/runtime.mjs +140 -0
- package/mods/audit-trail/mod.toml +37 -0
- package/mods/blast-radius-guard/.claude-plugin/plugin.json +5 -0
- package/mods/blast-radius-guard/README.md +53 -0
- package/mods/blast-radius-guard/bin/hook.mjs +18 -0
- package/mods/blast-radius-guard/claude-code/settings.hooks.json +14 -0
- package/mods/blast-radius-guard/codex/UNSUPPORTED.md +7 -0
- package/mods/blast-radius-guard/codex/hooks.json +14 -0
- package/mods/blast-radius-guard/hooks/hooks.json +5 -0
- package/mods/blast-radius-guard/hooks/register.test.ts +97 -0
- package/mods/blast-radius-guard/hooks/register.ts +13 -0
- package/mods/blast-radius-guard/kiro/UNSUPPORTED.md +7 -0
- package/mods/blast-radius-guard/kiro/blast-radius-guard-pre-tool-use-hook.json +14 -0
- package/mods/blast-radius-guard/lib/common.mjs +213 -0
- package/mods/blast-radius-guard/lib/core.mjs +66 -0
- package/mods/blast-radius-guard/lib/patterns.mjs +102 -0
- package/mods/blast-radius-guard/lib/runtime.mjs +140 -0
- package/mods/blast-radius-guard/mod.toml +34 -0
- package/mods/blast-radius-guard/patterns.json +101 -0
- package/mods/context-burn/.claude-plugin/plugin.json +5 -0
- package/mods/context-burn/README.md +66 -0
- package/mods/context-burn/bin/hook.mjs +38 -0
- package/mods/context-burn/claude-code/UNSUPPORTED.md +7 -0
- package/mods/context-burn/claude-code/settings.hooks.json +24 -0
- package/mods/context-burn/codex/UNSUPPORTED.md +7 -0
- package/mods/context-burn/codex/hooks.json +24 -0
- package/mods/context-burn/hooks/hooks.json +5 -0
- package/mods/context-burn/hooks/register.test.ts +151 -0
- package/mods/context-burn/hooks/register.ts +35 -0
- package/mods/context-burn/kiro/UNSUPPORTED.md +7 -0
- package/mods/context-burn/kiro/context-burn-post-tool-use-hook.json +14 -0
- package/mods/context-burn/kiro/context-burn-session-start-hook.json +14 -0
- package/mods/context-burn/lib/common.mjs +213 -0
- package/mods/context-burn/lib/core.mjs +79 -0
- package/mods/context-burn/lib/runtime.mjs +140 -0
- package/mods/context-burn/mod.toml +37 -0
- package/mods/live-guard-gate/.claude-plugin/plugin.json +5 -0
- package/mods/live-guard-gate/README.md +43 -0
- package/mods/live-guard-gate/bin/hook.mjs +12 -0
- package/mods/live-guard-gate/claude-code/settings.hooks.json +14 -0
- package/mods/live-guard-gate/codex/UNSUPPORTED.md +7 -0
- package/mods/live-guard-gate/codex/hooks.json +14 -0
- package/mods/live-guard-gate/hooks/hooks.json +5 -0
- package/mods/live-guard-gate/hooks/register.test.ts +59 -0
- package/mods/live-guard-gate/hooks/register.ts +11 -0
- package/mods/live-guard-gate/kiro/UNSUPPORTED.md +7 -0
- package/mods/live-guard-gate/kiro/live-guard-gate-pre-tool-use-hook.json +14 -0
- package/mods/live-guard-gate/lib/common.mjs +213 -0
- package/mods/live-guard-gate/lib/core.mjs +24 -0
- package/mods/live-guard-gate/lib/live-agents.mjs +65 -0
- package/mods/live-guard-gate/lib/patterns.mjs +523 -0
- package/mods/live-guard-gate/lib/runtime.mjs +140 -0
- package/mods/live-guard-gate/mod.toml +34 -0
- package/mods/live-guard-gate/patterns.json +522 -0
- package/mods/secret-redactor/.claude-plugin/plugin.json +5 -0
- package/mods/secret-redactor/README.md +86 -0
- package/mods/secret-redactor/bin/hook.mjs +35 -0
- package/mods/secret-redactor/claude-code/UNSUPPORTED.md +7 -0
- package/mods/secret-redactor/claude-code/settings.hooks.json +24 -0
- package/mods/secret-redactor/codex/UNSUPPORTED.md +11 -0
- package/mods/secret-redactor/codex/hooks.json +14 -0
- package/mods/secret-redactor/hooks/hooks.json +5 -0
- package/mods/secret-redactor/hooks/register.test.ts +150 -0
- package/mods/secret-redactor/hooks/register.ts +19 -0
- package/mods/secret-redactor/kiro/UNSUPPORTED.md +11 -0
- package/mods/secret-redactor/kiro/secret-redactor-pre-tool-use-hook.json +14 -0
- package/mods/secret-redactor/lib/common.mjs +213 -0
- package/mods/secret-redactor/lib/core.mjs +61 -0
- package/mods/secret-redactor/lib/patterns.mjs +75 -0
- package/mods/secret-redactor/lib/runtime.mjs +140 -0
- package/mods/secret-redactor/mod.toml +43 -0
- package/mods/secret-redactor/patterns.json +74 -0
- package/package.json +9 -4
- package/plugins/vanguard-frontier-agentic/.codex-plugin/plugin.json +1 -1
- package/powers/vanguard-generic/POWER.md +6 -6
- package/powers/vanguard-kubernetes/POWER.md +1 -1
- package/powers/vanguard-multi-cloud/POWER.md +1 -1
- package/schemas/agentic-handoff.schema.json +443 -0
- package/scripts/generate-kiro-powers.mjs +17 -6
- package/scripts/generate-mods.mjs +784 -0
- package/scripts/mods/common.mjs +213 -0
- package/scripts/mods/runtime.mjs +140 -0
- package/scripts/typescript_data/agents/00-typescript-maestro-agent.json +2 -2
- package/scripts/typescript_data/agents/11-typescript-mcp-tool-contract-agent.json +4 -4
- package/skills/cross-functional/agentic-board-protocol/SKILL.md +85 -0
- package/skills/cross-functional/agentic-board-protocol/metadata.json +28 -0
- package/skills/cross-functional/agentic-board-protocol/references/decision-record.md +134 -0
- package/skills/cross-functional/agentic-board-protocol/references/handoff-envelope.md +208 -0
- package/skills/cross-functional/agentic-board-protocol/references/risk-tier-crosswalk.md +88 -0
- package/skills/cross-functional/agentic-board-protocol/references/stop-conditions.md +122 -0
- package/skills/cross-functional/agentic-board-protocol/references/value-case.md +159 -0
- package/skills/cross-functional/agentic-security-review/SKILL.md +78 -0
- package/skills/cross-functional/agentic-security-review/metadata.json +30 -0
- package/skills/cross-functional/agentic-security-review/references/identity-and-delegated-authorization.md +144 -0
- package/skills/cross-functional/agentic-security-review/references/supply-chain-and-provenance.md +111 -0
- package/skills/cross-functional/agentic-security-review/references/threat-model-owasp-asi.md +55 -0
- package/skills/cross-functional/agentic-security-review/references/tools-memory-and-data-reach.md +125 -0
- package/skills/cross-functional/agentic-security-review/references/workflow-and-output.md +210 -0
- package/skills/cross-functional/agentic-system-architecture-review/SKILL.md +74 -0
- package/skills/cross-functional/agentic-system-architecture-review/metadata.json +31 -0
- package/skills/cross-functional/agentic-system-architecture-review/references/delegation-and-handoff-review.md +107 -0
- package/skills/cross-functional/agentic-system-architecture-review/references/pattern-selection.md +81 -0
- package/skills/cross-functional/agentic-system-architecture-review/references/reliability-and-containment.md +86 -0
- package/skills/cross-functional/agentic-system-architecture-review/references/state-memory-and-durability.md +102 -0
- package/skills/cross-functional/agentic-system-architecture-review/references/workflow-and-output.md +135 -0
- package/skills/typescript/typescript-maestro/SKILL.md +1 -1
- package/skills/typescript/typescript-maestro/references/routing-taxonomy.md +1 -1
- package/skills/typescript/typescript-mcp-tool-contract/SKILL.md +1 -1
- package/tests/agentic-board-suite/_build_fixtures.py +1751 -0
- package/tests/agentic-board-suite/_build_harnesses.py +123 -0
- package/tests/agentic-board-suite/agentic_board_rules.py +857 -0
- package/tests/agentic-board-suite/fixtures/invalid/CH-PROPOSAL-LINK--challenge-names-a-later-envelope.json +244 -0
- package/tests/agentic-board-suite/fixtures/invalid/CH-PROPOSAL-LINK--dangling-challenge-beside-a-request.json +410 -0
- package/tests/agentic-board-suite/fixtures/invalid/CH-PROPOSAL-LINK--dangling-proposal.json +187 -0
- package/tests/agentic-board-suite/fixtures/invalid/CH-ROUND-COUNTED--round-two-declared-as-one.json +481 -0
- package/tests/agentic-board-suite/fixtures/invalid/CH-ROUND-COUNTED--self-declared-round.json +187 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-ALTERNATIVES--no-alternatives.json +340 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-CHALLENGE-COMPLETE--hard-gate-left-behind-by-proposer-swap.json +532 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-CHALLENGE-COMPLETE--hard-gate-left-unattached.json +480 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-CHALLENGE-COMPLETE--hard-gate-on-root-left-unattached.json +410 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-CHALLENGE-COMPLETE--late-challenge-before-record.json +410 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-CHALLENGE-COMPLETE--superseded-request-missed-earlier-challenge.json +565 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-CHALLENGE-PRECEDES--withdrawal-attached-before-it-was-sent.json +483 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-CHALLENGE-PRECEDES--withdrawal-descends-from-the-record.json +547 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-CHALLENGE-REQUIRED--r1-flag-without-challenge.json +349 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-CHALLENGE-REQUIRED--r2-without-challenge.json +339 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-CHALLENGE-REQUIRED--r3-no-flags-without-challenge.json +339 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-CHALLENGE-RESOLVES--dangling-challenge.json +347 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-CHALLENGE-TARGETS-PROPOSAL--challenge-of-requester-message.json +415 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-CLASS-FROM-ROOT--root-declares-no-class.json +345 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-CLASS-FROM-ROOT--root-declares-other-class.json +346 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-DESIGNATED-CHALLENGER--designated-challenger-only-on-the-framing.json +346 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-DESIGNATED-CHALLENGER--investment-without-finance.json +436 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-DESIGNATED-CHALLENGER--stopped-designated-challenge.json +348 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-HARD-GATE--conditional-over-hard-gate.json +357 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-HARD-GATE--unresolved-hard-gate.json +357 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-NO-APPROVE-OVER-DISSENT--approve-over-a-stopped-challenge.json +413 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-NO-APPROVE-OVER-DISSENT--approve-over-dissent.json +346 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-ONE-REQUEST--re-ask-without-supersedes.json +426 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-ONE-REQUEST--request-superseded-twice.json +581 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-ONE-REQUEST--supersedes-a-challenge.json +427 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-ONE-REQUEST--supersedes-a-dangling-id.json +427 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-ONE-REQUEST--supersedes-a-later-request.json +500 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-ONE-REQUEST--superseding-request-from-another-proposer.json +621 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-ONE-REQUEST--supersession-cycle.json +501 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-OWNER-FROM-ROOT--agent-opens-decision-path.json +480 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-OWNER-FROM-ROOT--owner-is-the-requester.json +273 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-OWNER-FROM-ROOT--request-to-later-sorting-non-owner.json +273 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-OWNER-FROM-ROOT--request-to-non-owner.json +273 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-OWNER-FROM-ROOT--root-names-no-owner.json +341 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-PROPOSER-IS-SENDER--challenger-assembles-request.json +337 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-PROPOSER-NOT-CHALLENGER--self-challenge.json +411 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-RISK-CARRIED--concurrent-higher-tier-envelope.json +403 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-RISK-CARRIED--r5-decided-through-lower-tier-request.json +433 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-RISK-CARRIED--request-drops-materiality-flags.json +479 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-RISK-CARRIED--risk-raised-after-request-before-decision.json +403 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-TO-PRINCIPAL--request-to-agent.json +272 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-UNRESOLVED-DISSENT--dissent-dropped.json +341 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-UNRESOLVED-DISSENT--stopped-challenge-recorded-withdrawn.json +408 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-UNRESOLVED-DISSENT--unresolved-without-dissent.json +341 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-VALUE-CASE-REQUIRED--investment-without-value-case.json +353 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-AFTER-REQUEST--decided-before-request.json +408 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-AFTER-REQUEST--request-descends-from-its-record.json +408 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-APPROVAL-STATE--state-contradicts-outcome.json +346 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-CO-SIGN-BINDING--co-sign-approves-a-redesign.json +433 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-CO-SIGN-BINDING--co-sign-contradicts-outcome.json +433 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-CO-SIGN-BINDING--co-sign-does-not-descend-from-its-record.json +499 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-CO-SIGN-BINDING--co-sign-is-the-parent-of-its-record.json +500 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-CO-SIGN-BINDING--co-sign-names-another-approver.json +438 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-CO-SIGN-BINDING--co-sign-nobody-asked-for.json +421 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-CO-SIGN-BINDING--co-sign-sent-before-the-record.json +499 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-CO-SIGNED--decision-in-force-not-co-signed.json +591 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-CO-SIGNED--later-sorting-second-approver-never-signs.json +359 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-CO-SIGNED--named-second-approver-never-signs.json +359 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-COMPLETED--record-left-dispatched.json +346 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-DECIDER-IS-ADDRESSEE--decided-by-non-addressee.json +408 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-DECIDER-IS-ADDRESSEE--earlier-sorting-non-addressee-decides.json +408 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-DECIDER-IS-SENDER--relayed-decision.json +408 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-HARD-GATE-ACCEPTANCE--override-without-acceptance.json +356 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-HARD-GATE-ACCEPTANCE--same-round-withdrawal.json +423 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-HARD-GATE-ACCEPTANCE--stopped-over-cap-withdrawal.json +623 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-HARD-GATE-ACCEPTANCE--unrelated-withdrawal-does-not-clear-gate.json +482 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-HARD-GATE-ACCEPTANCE--withdrawal-names-an-undelivered-envelope.json +482 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-HARD-GATE-ACCEPTANCE--withdrawal-on-another-proposers-revision.json +600 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-NOT-REQUESTER--second-approver-is-requester.json +433 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-ONE-RECORD--contradicting-records.json +416 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-ONE-RECORD--duplicate-co-signature.json +507 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-PRINCIPAL-SOURCE--agent-writes-decision.json +345 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-REQUEST-BINDING--points-at-challenge.json +346 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-REQUEST-BINDING--record-names-a-missing-request.json +346 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-REQUEST-BINDING--request-edited-after-decision.json +346 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-SECOND-APPROVER--r5-request-decided-on-lower-tier-branch.json +418 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-SECOND-APPROVER--r5-single-approver.json +354 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-SECOND-APPROVER--second-approver-is-decider.json +359 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-AGENT-NO-APPROVAL--agent-claims-approval.json +132 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-APPROVAL-ONLY-ON-RECORD--principal-delegate-claims-approval.json +132 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-ATTEN-ACTIONS--child-widens-actions.json +345 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-ATTEN-BUDGET--child-raises-limit.json +132 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-ATTEN-BUDGET--currency-changed.json +132 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-BLOCK-AS-CHALLENGE--block-sent-as-return.json +184 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-CHAIN-CUSTODY--parent-sender-continues-unstopped.json +187 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-CHAIN-CUSTODY--reply-from-outside-chain.json +132 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-CHAIN-PARENT--hop-skips.json +132 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-CHAIN-TIME--sent-before-parent.json +132 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-DESTINATION-TIER--delegate-to-live-guard.json +132 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-DESTINATION-TIER--delegate-to-untiered-agent.json +132 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-ESCALATE-TO-PRINCIPAL--escalate-to-agent.json +131 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-ESCALATE-TO-PRINCIPAL--escalated-delegate-to-agent.json +132 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-MONO-CLASS--classification-lowered.json +346 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-MONO-DEADLINE--deadline-extended.json +132 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-MONO-MATERIALITY--flag-dropped.json +434 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-MONO-MAX-HOPS--max-hops-raised.json +132 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-MONO-PROHIBITIONS--prohibition-dropped.json +131 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-MONO-SPENT--one-counter-decreases.json +346 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-MONO-SPENT--spent-counter-reset.json +346 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-MONO-TIER--tier-lowered.json +346 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-NO-EXECUTION--deploy-authorized.json +133 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-NO-SELF-ADDRESS--challenge-to-self.json +187 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-ONE-CORRELATION--two-correlations.json +132 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-ONE-ROOT--second-root-beside-a-decision.json +410 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-ONE-ROOT--two-roots.json +190 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-PAYLOAD-CHALLENGE--challenge-without-payload.json +180 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-PAYLOAD-DECISION-RECORD--record-without-payload.json +331 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-PAYLOAD-DECISION-REQUEST--request-without-payload.json +324 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-PAYLOAD-ESCALATE--escalate-without-payload.json +128 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-PAYLOAD-EXCLUSIVE--return-with-escalation.json +136 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-PAYLOAD-RETURN--return-without-verdict.json +124 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-REASON-HAS-STATUS--reason-on-completed.json +134 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-RETURN-EVIDENCE--pass-on-inference-only.json +132 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-RETURN-EVIDENCE--pass-over-blocking-gap.json +137 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-RETURN-EVIDENCE--pass-with-no-evidence.json +127 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-RETURN-TO-SENDER--return-skips-the-delegator.json +185 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-ROOT-PARENT--root-with-parent.json +132 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-SCHEMA--unknown-credential-field.json +133 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-STOP-NOTE--conflict-without-note.json +133 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-STOPPED-HAS-REASON--stopped-without-reason.json +132 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-UNIQUE-TASK-ID--duplicate-task-id.json +198 -0
- package/tests/agentic-board-suite/fixtures/invalid/STOP-BUDGET--budget-undeclared.json +123 -0
- package/tests/agentic-board-suite/fixtures/invalid/STOP-BUDGET--return-overspent.json +132 -0
- package/tests/agentic-board-suite/fixtures/invalid/STOP-CHALLENGE-ROUNDS--revisions-carried-in-rebuttal-challenges.json +461 -0
- package/tests/agentic-board-suite/fixtures/invalid/STOP-CHALLENGE-ROUNDS--third-round.json +454 -0
- package/tests/agentic-board-suite/fixtures/invalid/STOP-CHALLENGE-ROUNDS--third-version-by-return-naming-the-original.json +461 -0
- package/tests/agentic-board-suite/fixtures/invalid/STOP-DEADLINE--sent-after-deadline.json +132 -0
- package/tests/agentic-board-suite/fixtures/invalid/STOP-DEPTH--depth-undeclared.json +182 -0
- package/tests/agentic-board-suite/fixtures/invalid/STOP-INSTRUCTION-BAIT--claimed-approval-dispatched.json +132 -0
- package/tests/agentic-board-suite/fixtures/invalid/STOP-LIVE-GUARD--mutation-dispatched.json +130 -0
- package/tests/agentic-board-suite/fixtures/invalid/STOP-MISSING-EVIDENCE--blocking-gap-dispatched.json +137 -0
- package/tests/agentic-board-suite/fixtures/invalid/STOP-MISSING-EVIDENCE--decision-request-blocking-gap.json +351 -0
- package/tests/agentic-board-suite/fixtures/invalid/STOP-ROUTING-LOOP--exchanges-on-the-original-envelope.json +381 -0
- package/tests/agentic-board-suite/fixtures/invalid/STOP-ROUTING-LOOP--loop-undeclared.json +182 -0
- package/tests/agentic-board-suite/fixtures/invalid/STOP-ROUTING-LOOP--revisit-to-challenger-of-another-party.json +256 -0
- package/tests/agentic-board-suite/fixtures/invalid/STOP-ROUTING-LOOP--second-revisit-to-challenger.json +540 -0
- package/tests/agentic-board-suite/fixtures/invalid/STOP-TERMINAL--challenge-answers-stopped-delegate.json +247 -0
- package/tests/agentic-board-suite/fixtures/invalid/STOP-TERMINAL--decision-on-escalated-request.json +353 -0
- package/tests/agentic-board-suite/fixtures/invalid/STOP-TERMINAL--decision-on-stopped-request.json +422 -0
- package/tests/agentic-board-suite/fixtures/invalid/STOP-TERMINAL--reply-to-stopped-delegate.json +244 -0
- package/tests/agentic-board-suite/fixtures/invalid/VC-BENEFIT-CEILING--benefit-above-baseline.json +436 -0
- package/tests/agentic-board-suite/fixtures/invalid/VC-COST-SIGN--negative-risk-loss.json +436 -0
- package/tests/agentic-board-suite/fixtures/invalid/VC-COST-SIGN--risk-loss-low-minus-one.json +436 -0
- package/tests/agentic-board-suite/fixtures/invalid/VC-COUNTERFACTUALS--single-agent-counterfactual-missing.json +427 -0
- package/tests/agentic-board-suite/fixtures/invalid/VC-COUNTERFACTUALS--workflow-counterfactual-missing.json +427 -0
- package/tests/agentic-board-suite/fixtures/invalid/VC-NAV-ARITHMETIC--nav-off-by-one.json +436 -0
- package/tests/agentic-board-suite/fixtures/invalid/VC-ORDER--inverted-range.json +436 -0
- package/tests/agentic-board-suite/fixtures/invalid/VC-OVERSIGHT--free-oversight.json +436 -0
- package/tests/agentic-board-suite/fixtures/invalid/VC-PROMOTION--measured-but-not-incremental.json +436 -0
- package/tests/agentic-board-suite/fixtures/invalid/VC-PROMOTION--promoted-on-estimate.json +436 -0
- package/tests/agentic-board-suite/fixtures/invalid/VC-RECOMMEND-INCREMENTAL--recommend-non-incremental.json +436 -0
- package/tests/agentic-board-suite/fixtures/invalid/VC-RUNS-BOUND--more-successes-than-runs.json +436 -0
- package/tests/agentic-board-suite/fixtures/invalid/VC-SYNTHETIC--synthetic-claims-measured.json +436 -0
- package/tests/agentic-board-suite/fixtures/invalid/VC-UNIT-COST--no-successes-nonzero-cost.json +436 -0
- package/tests/agentic-board-suite/fixtures/invalid/VC-UNIT-COST--unit-cost-mismatch.json +436 -0
- package/tests/agentic-board-suite/fixtures/valid/v01-r0-single-review.json +129 -0
- package/tests/agentic-board-suite/fixtures/valid/v01x-block-return-on-inference.json +125 -0
- package/tests/agentic-board-suite/fixtures/valid/v01x-block-return-to-principal.json +125 -0
- package/tests/agentic-board-suite/fixtures/valid/v01x-pass-return-documentation-evidence.json +129 -0
- package/tests/agentic-board-suite/fixtures/valid/v01x-pass-return-mixed-evidence.json +133 -0
- package/tests/agentic-board-suite/fixtures/valid/v01x-pass-return-nonblocking-gap.json +134 -0
- package/tests/agentic-board-suite/fixtures/valid/v02-r2-architecture-dissent.json +343 -0
- package/tests/agentic-board-suite/fixtures/valid/v02x-challenge-after-decision.json +407 -0
- package/tests/agentic-board-suite/fixtures/valid/v02x-challenge-after-record-same-second.json +464 -0
- package/tests/agentic-board-suite/fixtures/valid/v02x-redispatch-after-cured-stop.json +534 -0
- package/tests/agentic-board-suite/fixtures/valid/v02x-security-proposal-returned-to-its-delegator.json +407 -0
- package/tests/agentic-board-suite/fixtures/valid/v03-investment-loses-to-workflow.json +433 -0
- package/tests/agentic-board-suite/fixtures/valid/v04-r3-hard-gate-risk-acceptance.json +354 -0
- package/tests/agentic-board-suite/fixtures/valid/v04x-live-hard-gate-recommend-escalate.json +354 -0
- package/tests/agentic-board-suite/fixtures/valid/v04x-live-hard-gate-recommend-reject.json +354 -0
- package/tests/agentic-board-suite/fixtures/valid/v05-live-guard-escalation.json +129 -0
- package/tests/agentic-board-suite/fixtures/valid/v06-r5-dual-approval.json +430 -0
- package/tests/agentic-board-suite/fixtures/valid/v06x-co-sign-same-second.json +430 -0
- package/tests/agentic-board-suite/fixtures/valid/v06x-re-asked-r5-decision-co-signed-again.json +662 -0
- package/tests/agentic-board-suite/fixtures/valid/v06x-unsigned-approve-superseded-by-co-signed-reject.json +588 -0
- package/tests/agentic-board-suite/fixtures/valid/v07-depth-and-routing-loop.json +180 -0
- package/tests/agentic-board-suite/fixtures/valid/v08-budget-exhausted.json +186 -0
- package/tests/agentic-board-suite/fixtures/valid/v09-instruction-bait.json +131 -0
- package/tests/agentic-board-suite/fixtures/valid/v09x-requester-restates-after-bait.json +251 -0
- package/tests/agentic-board-suite/fixtures/valid/v10-r1-optional-challenge.json +184 -0
- package/tests/agentic-board-suite/fixtures/valid/v10x-report-up-after-challenge.json +246 -0
- package/tests/agentic-board-suite/fixtures/valid/v11-two-rounds-hard-gate-withdrawn.json +478 -0
- package/tests/agentic-board-suite/fixtures/valid/v11x-designated-challenge-first-answer-over-cap.json +598 -0
- package/tests/agentic-board-suite/fixtures/valid/v11x-interleaved-challenger-round-two.json +604 -0
- package/tests/agentic-board-suite/fixtures/valid/v11x-proposal-handed-to-the-challenger-capped.json +860 -0
- package/tests/agentic-board-suite/fixtures/valid/v11x-round-three-escalation-path.json +747 -0
- package/tests/agentic-board-suite/fixtures/valid/v11x-same-second-round-two.json +478 -0
- package/tests/agentic-board-suite/fixtures/valid/v11x-two-findings-one-round.json +549 -0
- package/tests/agentic-board-suite/fixtures/valid/v12-superseded-request.json +497 -0
- package/tests/agentic-board-suite/fixtures/valid/v12x-challenge-after-the-first-decision.json +566 -0
- package/tests/agentic-board-suite/fixtures/valid/v12x-late-challenge-cured-by-supersede.json +566 -0
- package/tests/agentic-board-suite/fixtures/valid/v12x-supersede-in-the-same-second.json +497 -0
- package/tests/agentic-board-suite/test_agentic_board.py +502 -0
- package/tests/fixtures/README.md +1 -1
- package/tests/fuzz-properties.test.mjs +254 -0
- package/tests/mods.test.mjs +768 -0
- package/tests/validate-agent-tool-tiers.py +7 -5
- package/tests/validate-asset-integrity.py +1 -0
- 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()
|