@event4u/agent-config 9.7.0 → 9.8.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 (185) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +144 -0
  3. package/README.md +5 -5
  4. package/dist/agent-src/commands/commit/in-chunks.md +8 -0
  5. package/dist/agent-src/commands/commit.md +17 -0
  6. package/dist/agent-src/commands/pr/create.md +4 -0
  7. package/dist/agent-src/contexts/override-system.md +75 -5
  8. package/dist/agent-src/rules/context-hygiene.md +2 -0
  9. package/dist/agent-src/rules/doc-screenshot-hygiene.md +97 -0
  10. package/dist/agent-src/rules/domain-safety-pii.md +1 -0
  11. package/dist/agent-src/rules/external-code-graph-interop.md +13 -3
  12. package/dist/agent-src/rules/framework-neutrality-in-generic-skills.md +2 -0
  13. package/dist/agent-src/rules/git-history-discipline.md +2 -0
  14. package/dist/agent-src/rules/language-and-tone.md +2 -0
  15. package/dist/agent-src/rules/lethal-trifecta-guard.md +3 -0
  16. package/dist/agent-src/rules/media-governance-routing.md +2 -0
  17. package/dist/agent-src/rules/minimal-safe-diff.md +2 -0
  18. package/dist/agent-src/rules/no-roadmap-references.md +3 -0
  19. package/dist/agent-src/rules/non-destructive-by-default.md +20 -0
  20. package/dist/agent-src/rules/onboarding-gate.md +2 -0
  21. package/dist/agent-src/rules/output-discipline.md +2 -0
  22. package/dist/agent-src/rules/persona-governance.md +2 -0
  23. package/dist/agent-src/rules/preservation-guard.md +3 -0
  24. package/dist/agent-src/rules/roadmap-progress-sync.md +2 -0
  25. package/dist/agent-src/rules/secret-vcs-guard.md +106 -0
  26. package/dist/agent-src/rules/security-sensitive-stop.md +1 -1
  27. package/dist/agent-src/rules/skill-quality.md +2 -0
  28. package/dist/agent-src/rules/source-confidentiality.md +2 -0
  29. package/dist/agent-src/rules/source-of-truth.md +2 -0
  30. package/dist/agent-src/rules/telegraph-speak.md +2 -0
  31. package/dist/agent-src/rules/token-optimizer-maintenance.md +2 -0
  32. package/dist/agent-src/rules/tool-safety.md +1 -0
  33. package/dist/agent-src/rules/verify-before-complete.md +2 -0
  34. package/dist/agent-src/skills/adr-create/SKILL.md +17 -1
  35. package/dist/agent-src/skills/code-intelligence/SKILL.md +94 -0
  36. package/dist/agent-src/skills/gated-reach/SKILL.md +185 -0
  37. package/dist/agent-src/skills/gated-reach/evals/triggers.json +72 -0
  38. package/dist/agent-src/skills/image-analyser/SKILL.md +14 -12
  39. package/dist/agent-src/skills/image-editing/SKILL.md +1 -0
  40. package/dist/agent-src/skills/readme-writing/SKILL.md +6 -1
  41. package/dist/agent-src/skills/screenshot-hygiene/SKILL.md +171 -0
  42. package/dist/agent-src/skills/secrets-management/SKILL.md +44 -1
  43. package/dist/agent-src/skills/subagent-orchestration/prompts/adversarial-verification-council.md +29 -0
  44. package/dist/agent-src/skills/subagent-orchestration/prompts/do-and-judge-two-stage.md +29 -0
  45. package/dist/agent-src/skills/subagent-orchestration/prompts/do-and-judge.md +29 -0
  46. package/dist/agent-src/skills/subagent-orchestration/prompts/do-competitively.md +29 -0
  47. package/dist/agent-src/skills/subagent-orchestration/prompts/do-in-parallel.md +29 -0
  48. package/dist/agent-src/skills/subagent-orchestration/prompts/do-in-steps.md +29 -0
  49. package/dist/agent-src/skills/subagent-orchestration/prompts/do-in-worktrees.md +29 -0
  50. package/dist/agent-src/skills/subagent-orchestration/prompts/judge-with-debate.md +29 -0
  51. package/dist/agent-src/skills/verify-repair-loop/SKILL.md +4 -0
  52. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  53. package/dist/agent-src/templates/github-workflows/secret-scan.yml +65 -0
  54. package/dist/cli/configRoot.js +112 -0
  55. package/dist/cli/configRoot.js.map +1 -0
  56. package/dist/cli/configRoot.test.js +97 -0
  57. package/dist/cli/configRoot.test.js.map +1 -0
  58. package/dist/cli/help.js +4 -1
  59. package/dist/cli/help.js.map +1 -1
  60. package/dist/cli/main.js +19 -2
  61. package/dist/cli/main.js.map +1 -1
  62. package/dist/cli/registry.js +1 -0
  63. package/dist/cli/registry.js.map +1 -1
  64. package/dist/discovery/deprecation-report.md +1 -1
  65. package/dist/discovery/discovery-manifest.json +161 -44
  66. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  67. package/dist/discovery/discovery-manifest.summary.md +4 -4
  68. package/dist/discovery/orphan-report.md +1 -1
  69. package/dist/discovery/packs.json +13 -8
  70. package/dist/discovery/trust-report.md +11 -11
  71. package/dist/discovery/workspaces.json +11 -6
  72. package/dist/install/install.mjs +16 -0
  73. package/dist/mcp/registry-manifest.json +3 -3
  74. package/dist/router.json +1 -1
  75. package/dist/server/app.js +10 -0
  76. package/dist/server/app.js.map +1 -1
  77. package/dist/server/routes/ping.js +18 -0
  78. package/dist/server/routes/ping.js.map +1 -1
  79. package/dist/server/schemas/settings.js +8 -0
  80. package/dist/server/schemas/settings.js.map +1 -1
  81. package/dist/server/serverInfo.js +6 -2
  82. package/dist/server/serverInfo.js.map +1 -1
  83. package/dist/server/token.js +11 -4
  84. package/dist/server/token.js.map +1 -1
  85. package/dist/server/writeRoot.js +15 -1
  86. package/dist/server/writeRoot.js.map +1 -1
  87. package/dist/shared/capabilities.js +33 -0
  88. package/dist/shared/capabilities.js.map +1 -0
  89. package/dist/shared/capabilities.test.js +39 -0
  90. package/dist/shared/capabilities.test.js.map +1 -0
  91. package/dist/ui/assets/{index-BiEZiZ31.js → index-CrmZ34Rg.js} +16 -16
  92. package/dist/ui/assets/index-CrmZ34Rg.js.map +1 -0
  93. package/dist/ui/index.html +14 -5
  94. package/docs/CLAIMS.md +62 -37
  95. package/docs/architecture.md +2 -2
  96. package/docs/benchmark.md +137 -0
  97. package/docs/catalog.md +8 -3
  98. package/docs/comparison.yaml +44 -2
  99. package/docs/contracts/local-server-ports.md +53 -0
  100. package/docs/contracts/no-runtime-boundary.md +1 -1
  101. package/docs/contracts/subagent-boundary.md +15 -0
  102. package/docs/customization.md +14 -3
  103. package/docs/decisions/ADR-016-installer-architecture.md +2 -0
  104. package/docs/decisions/ADR-088-no-external-runtime-federation.md +1 -1
  105. package/docs/decisions/ADR-094-agent-memory-layer-removal.md +1 -1
  106. package/docs/decisions/ADR-109-subagent-v1-contract.md +2 -0
  107. package/docs/decisions/ADR-110-discipline-profile-resolution-locus.md +2 -0
  108. package/docs/decisions/ADR-113-ci-native-release-label-trigger.md +11 -3
  109. package/docs/decisions/ADR-124-embedded-engine-doctrine.md +255 -0
  110. package/docs/decisions/ADR-125-doc-screenshot-anonymization.md +131 -0
  111. package/docs/decisions/ADR-126-internet-reach-operator-tooling.md +254 -0
  112. package/docs/decisions/ADR-127-enforcement-claims-must-resolve.md +150 -0
  113. package/docs/decisions/ADR-128-executable-evidence-and-measured-escalation.md +201 -0
  114. package/docs/decisions/INDEX.md +21 -0
  115. package/docs/decisions/engine-reclassification-2026-07.md +104 -0
  116. package/docs/distribution/awesome-list-submission.md +1 -1
  117. package/docs/distribution/registries.md +36 -27
  118. package/docs/enforcement-by-host.md +7 -0
  119. package/docs/featured-skills.md +2 -2
  120. package/docs/getting-started-by-role.md +1 -1
  121. package/docs/getting-started.md +1 -1
  122. package/docs/governance-advantage.md +1 -1
  123. package/docs/guides/gated-platform-reads.md +221 -0
  124. package/docs/positioning-evidence.md +1 -1
  125. package/docs/proof.md +92 -16
  126. package/docs/threat-model.md +1 -0
  127. package/package.json +5 -2
  128. package/src/config/agent-settings.template.yml +43 -0
  129. package/src/config/reach-channels.yml +217 -0
  130. package/src/config/reach-prescriptions-intake.yml +200 -0
  131. package/src/scripts/_dispatch.bash +15 -0
  132. package/src/scripts/_lib/exec_evidence.ts +251 -0
  133. package/src/scripts/_lib/fixtures/secret-detector/negatives.txt +10 -0
  134. package/src/scripts/_lib/fixtures/secret-detector/positives.txt +11 -0
  135. package/src/scripts/_lib/kernel_rules.ts +46 -0
  136. package/src/scripts/_lib/reddit_thread_parse.ts +598 -0
  137. package/src/scripts/_lib/risk_paths.ts +160 -0
  138. package/src/scripts/_lib/secret_detector.test.ts +103 -0
  139. package/src/scripts/_lib/secret_detector.ts +319 -0
  140. package/src/scripts/_lib/tool_probe.ts +359 -0
  141. package/src/scripts/adr/regenerate_index.ts +81 -6
  142. package/src/scripts/audit_adr_coverage.ts +33 -0
  143. package/src/scripts/build_proof.ts +162 -0
  144. package/src/scripts/check_adr_frontmatter.ts +195 -0
  145. package/src/scripts/check_artefact_count_messaging.ts +4 -0
  146. package/src/scripts/check_backstop_debt.ts +200 -0
  147. package/src/scripts/check_claims.ts +188 -8
  148. package/src/scripts/check_comparison.ts +12 -0
  149. package/src/scripts/check_enforcement_coverage.ts +592 -0
  150. package/src/scripts/check_kernel_rule_bundle.ts +16 -12
  151. package/src/scripts/check_reach_channels.ts +407 -0
  152. package/src/scripts/check_reach_staleness.ts +364 -0
  153. package/src/scripts/check_secret_leak.test.ts +47 -0
  154. package/src/scripts/check_secret_leak.ts +290 -0
  155. package/src/scripts/code_graph/build.ts +0 -0
  156. package/src/scripts/code_graph/cli.ts +207 -0
  157. package/src/scripts/code_graph/detect.ts +134 -0
  158. package/src/scripts/code_graph/extract.ts +386 -0
  159. package/src/scripts/code_graph/loader.ts +123 -0
  160. package/src/scripts/code_graph/query.ts +176 -0
  161. package/src/scripts/code_graph/sanitize.ts +30 -0
  162. package/src/scripts/code_graph/types.ts +83 -0
  163. package/src/scripts/code_graph/validate.ts +63 -0
  164. package/src/scripts/external_sources_denylist.json +3 -1
  165. package/src/scripts/generate_subagent_floor.ts +194 -0
  166. package/src/scripts/hook_manifest.yaml +13 -3
  167. package/src/scripts/hooks/code_graph_nudge_hook.ts +184 -0
  168. package/src/scripts/iron_law_sha.ts +3 -12
  169. package/src/scripts/lint_override_kernel_guard.ts +219 -0
  170. package/src/scripts/measure_rule_budget.ts +3 -12
  171. package/src/scripts/postinstall_gui.ts +129 -0
  172. package/src/scripts/reach_doctor.ts +1651 -0
  173. package/src/scripts/release.ts +24 -3
  174. package/src/scripts/risk_escalation_shadow.ts +160 -0
  175. package/src/scripts/schemas/agent-settings.schema.json +23 -0
  176. package/src/scripts/schemas/reach-channels.schema.json +134 -0
  177. package/src/scripts/schemas/reach-doctor-payload.schema.json +220 -0
  178. package/src/scripts/schemas/rule.schema.json +10 -0
  179. package/src/scripts/skill_linter.ts +55 -0
  180. package/src/scripts/validate_reach_prescriptions.ts +751 -0
  181. package/src/server/schemas/settings.ts +16 -0
  182. package/src/shared/capabilities.test.ts +43 -0
  183. package/src/shared/capabilities.ts +75 -0
  184. package/src/templates/minimal/agents-overrides-readme.md +20 -0
  185. package/dist/ui/assets/index-BiEZiZ31.js.map +0 -1
@@ -1451,9 +1451,30 @@ function execute(
1451
1451
  // publish that already happened.
1452
1452
  if (ci) {
1453
1453
  _step(9, total, 'Dispatch release-guard.yml + publish-npm.yml + cloud-release.yml for the tag');
1454
- run(['gh', 'workflow', 'run', 'release-guard.yml', '--ref', MAIN_BRANCH, '-f', `tag=${plan.target}`]);
1455
- run(['gh', 'workflow', 'run', 'publish-npm.yml', '--ref', MAIN_BRANCH, '-f', `tag=${plan.target}`]);
1456
- run(['gh', 'workflow', 'run', 'cloud-release.yml', '--ref', MAIN_BRANCH, '-f', `tag=${plan.target}`]);
1454
+ // NON-FATAL by design. By this point the release is already complete
1455
+ // — the tag is pushed and the GitHub Release is created above, and
1456
+ // npm publish runs asynchronously. These explicit dispatches are a
1457
+ // FALLBACK for a tag pushed with the default GITHUB_TOKEN, which does
1458
+ // NOT fire tag-triggered workflows (GitHub's recursion guard). When
1459
+ // the tag was pushed with a PAT (RELEASE_PR_TOKEN), those three
1460
+ // workflows already fired on the push and this dispatch is redundant.
1461
+ // Dispatching via the API additionally needs the token's
1462
+ // `actions:write` scope; a 403 here (scope missing) must NOT mark an
1463
+ // already-shipped release as failed — warn and continue.
1464
+ for (const wf of ['release-guard.yml', 'publish-npm.yml', 'cloud-release.yml']) {
1465
+ const r = run(['gh', 'workflow', 'run', wf, '--ref', MAIN_BRANCH, '-f', `tag=${plan.target}`], {
1466
+ check: false,
1467
+ });
1468
+ if (r.returncode !== 0) {
1469
+ process.stderr.write(
1470
+ `⚠️ Could not dispatch ${wf} (exit ${r.returncode}) — the release ${plan.target} is ` +
1471
+ `already complete (tag + GitHub Release created; npm publishes async). If the tag was ` +
1472
+ `pushed with a PAT, ${wf} already fired on the tag push. If you rely on the explicit ` +
1473
+ `dispatch, grant RELEASE_PR_TOKEN the "Actions: read and write" scope (fine-grained PAT) ` +
1474
+ `or the "workflow" scope (classic PAT).\n`,
1475
+ );
1476
+ }
1477
+ }
1457
1478
  }
1458
1479
  }
1459
1480
 
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Risk-escalation shadow report — measure before adding teeth.
3
+ *
4
+ * Walks the last N first-parent merges, classifies each one's touched paths by
5
+ * trust-boundary risk, and writes what *would* have escalated. Nothing is
6
+ * gated. Nothing fails. The output is a number whose only job is to decide
7
+ * whether the gate is worth building.
8
+ *
9
+ * This is the same sequencing ADR-127 used on `exec:` evidence: pre-register a
10
+ * threshold, measure, then decide. The threshold here — >= 5 of ~100 merges
11
+ * landing in a trust-boundary class — was written into
12
+ * `agents/roadmaps/road-to-executable-evidence.md` Phase 2 before this script
13
+ * was first run, so a disappointing number cannot be re-framed into a
14
+ * justification afterwards.
15
+ *
16
+ * Why it must not gate yet: the escalation the reference pairs this with is
17
+ * *non-declinable* — a user's "no" stops counting. That trades away something
18
+ * this package's authority model holds (`agent-authority`, `ask-when-uncertain`),
19
+ * and a trade that size needs evidence that the class it protects is real here,
20
+ * not merely real somewhere.
21
+ *
22
+ * Usage:
23
+ * ./scripts-run src/scripts/risk_escalation_shadow
24
+ * ./scripts-run src/scripts/risk_escalation_shadow --limit 200
25
+ * ./scripts-run src/scripts/risk_escalation_shadow --json
26
+ */
27
+
28
+ import { spawnSync } from 'node:child_process';
29
+ import * as fs from 'node:fs';
30
+ import * as path from 'node:path';
31
+ import { fileURLToPath, pathToFileURL } from 'node:url';
32
+
33
+ import { classify_diff, type RiskClass } from './_lib/risk_paths.js';
34
+
35
+ const _FILE = fileURLToPath(import.meta.url);
36
+ const REPO = path.resolve(path.dirname(_FILE), '..', '..');
37
+ const OUT_REL = 'internal/reports/risk-escalation-shadow.json';
38
+
39
+ /** Pre-registered in the roadmap BEFORE this report was first read. */
40
+ const TEETH_THRESHOLD = 5;
41
+
42
+ interface MergeRow {
43
+ sha: string;
44
+ subject: string;
45
+ files: number;
46
+ risk: RiskClass;
47
+ reasons: string[];
48
+ }
49
+
50
+ function git(args: string[]): string {
51
+ const r = spawnSync('git', args, { cwd: REPO, encoding: 'utf-8', maxBuffer: 64 * 1024 * 1024 });
52
+ if (r.status !== 0) return '';
53
+ return r.stdout ?? '';
54
+ }
55
+
56
+ function parse_limit(argv: string[]): number {
57
+ const i = argv.indexOf('--limit');
58
+ if (i === -1) return 100;
59
+ const n = Number(argv[i + 1]);
60
+ return Number.isFinite(n) && n > 0 ? n : 100;
61
+ }
62
+
63
+ export function main(argv: string[] = process.argv.slice(2)): number {
64
+ const limit = parse_limit(argv);
65
+ const asJson = argv.includes('--json');
66
+
67
+ const log = git(['log', '--first-parent', `-${limit}`, '--format=%H%x00%s']).trim();
68
+ if (!log) {
69
+ process.stderr.write('❌ risk_escalation_shadow: no git history readable\n');
70
+ return 2;
71
+ }
72
+
73
+ const rows: MergeRow[] = [];
74
+ for (const line of log.split('\n')) {
75
+ const [sha, subject] = line.split('\0');
76
+ if (!sha) continue;
77
+ // First-parent diff: what this merge brought to the trunk.
78
+ const files = git(['show', '--first-parent', '--name-only', '--format=', sha])
79
+ .split('\n')
80
+ .map((s) => s.trim())
81
+ .filter((s) => s.length > 0);
82
+ if (files.length === 0) continue;
83
+ const verdict = classify_diff(files);
84
+ rows.push({
85
+ sha: sha.slice(0, 9),
86
+ subject: (subject ?? '').slice(0, 90),
87
+ files: files.length,
88
+ risk: verdict.risk,
89
+ reasons: [...new Set(verdict.reasons.map((r) => r.because))],
90
+ });
91
+ }
92
+
93
+ const counts: Record<RiskClass, number> = {
94
+ none: 0,
95
+ governance: 0,
96
+ auth: 0,
97
+ 'trust-boundary': 0,
98
+ };
99
+ for (const r of rows) counts[r.risk] += 1;
100
+
101
+ const trustBoundary = counts['trust-boundary'];
102
+ const justified = trustBoundary >= TEETH_THRESHOLD;
103
+
104
+ const report = {
105
+ _doc:
106
+ 'Shadow report for trust-boundary risk escalation. NOTHING IS GATED BY THIS FILE. It answers one ' +
107
+ 'question: over recent history, how often did a merge land in a class the reference would have ' +
108
+ 'escalated? The threshold below was pre-registered in the roadmap before this was first read.',
109
+ measured_at_sha: rows[0]?.sha ?? null,
110
+ merges_examined: rows.length,
111
+ pre_registered_teeth_threshold: TEETH_THRESHOLD,
112
+ counts,
113
+ trust_boundary_merges: trustBoundary,
114
+ decision: justified
115
+ ? `JUSTIFIED — ${trustBoundary} of ${rows.length} first-parent merges touched a trust-boundary path, ` +
116
+ `at or above the pre-registered threshold of ${TEETH_THRESHOLD}. A non-declinable escalation for ` +
117
+ 'that class gets its own ADR and its own PR; it is deliberately NOT enabled by this report.'
118
+ : `NOT JUSTIFIED — ${trustBoundary} of ${rows.length} first-parent merges touched a trust-boundary ` +
119
+ `path, below the pre-registered threshold of ${TEETH_THRESHOLD}. The escalation stays unbuilt and ` +
120
+ 'the authority model stays intact. This is the honest-null outcome, published as the answer.',
121
+ note:
122
+ 'Classification is path-based and reproducible from the diff alone — no model judgement. Docs-only ' +
123
+ 'changes are exempt by extension, except on kernel-rule paths, where a markdown file IS the ' +
124
+ 'governed surface.',
125
+ escalating_merges: rows.filter((r) => r.risk !== 'none').slice(0, 40),
126
+ };
127
+
128
+ const abs = path.join(REPO, OUT_REL);
129
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
130
+ fs.writeFileSync(abs, JSON.stringify(report, null, 2) + '\n');
131
+
132
+ if (asJson) {
133
+ process.stdout.write(JSON.stringify(report, null, 2) + '\n');
134
+ return 0;
135
+ }
136
+
137
+ process.stdout.write(
138
+ `risk-escalation shadow · ${rows.length} first-parent merges examined\n` +
139
+ ` trust-boundary ${counts['trust-boundary']} · auth ${counts.auth} · ` +
140
+ `governance ${counts.governance} · none ${counts.none}\n` +
141
+ ` pre-registered threshold: ${TEETH_THRESHOLD} → ${justified ? 'JUSTIFIED' : 'NOT JUSTIFIED'}\n` +
142
+ `✅ wrote ${OUT_REL}\n`,
143
+ );
144
+ return 0;
145
+ }
146
+
147
+ function _isCliEntry(): boolean {
148
+ const a = process.argv[1];
149
+ if (!a) return false;
150
+ if (a === _FILE || pathToFileURL(path.resolve(a)).href === import.meta.url) return true;
151
+ try {
152
+ return fs.realpathSync(a) === fs.realpathSync(_FILE);
153
+ } catch {
154
+ return false;
155
+ }
156
+ }
157
+
158
+ if (_isCliEntry()) process.exit(main());
159
+
160
+ export { REPO, OUT_REL, TEETH_THRESHOLD };
@@ -95,6 +95,29 @@
95
95
  }
96
96
  }
97
97
  },
98
+ "screenshots": {
99
+ "type": "object",
100
+ "additionalProperties": true,
101
+ "properties": {
102
+ "identity_allowlist": {
103
+ "type": "array",
104
+ "items": { "type": "string" },
105
+ "default": [],
106
+ "description": "Consumed by the doc-screenshot-hygiene rule and screenshot-hygiene skill. Public identity tokens SAFE to show unredacted in a documentation screenshot — the maintainer's own public handles plus well-known fake-data tokens. Not a general fake-data dictionary and not identity-resolution: everything not listed is treated as sensitive by default, and a public handle co-located with a real name does not whitelist the real name. Default [] = nothing auto-allowed."
107
+ },
108
+ "forbid_terminal_capture": {
109
+ "type": "boolean",
110
+ "default": true,
111
+ "description": "Consumed by the doc-screenshot-hygiene rule. true (default) = terminal/CLI/IDE screenshots are forbidden (highest leak vector: absolute local paths, env tokens); use text code blocks with text redaction instead. false = allowed, still subject to the data-bearing human gate."
112
+ },
113
+ "data_bearing_gate": {
114
+ "type": "string",
115
+ "enum": ["on", "off"],
116
+ "default": "on",
117
+ "description": "Consumed by the doc-screenshot-hygiene rule. on (default) = a data-bearing screenshot embed is gated behind this-turn human confirmation; uncertain/unresolved regions redact-or-refuse, never ship-and-hope; illustrative/no-data screenshots may embed with a stated justification. off = no data-bearing gate (the anonymization taxonomy still applies)."
118
+ }
119
+ }
120
+ },
98
121
  "code_style": {
99
122
  "type": "object",
100
123
  "additionalProperties": true,
@@ -0,0 +1,134 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://github.com/event4u-app/agent-config/scripts/schemas/reach-channels.schema.json",
4
+ "x-schemaVersion": "1.0.0",
5
+ "title": "Reach channel registry (src/config/reach-channels.yml)",
6
+ "$comment": "road-to-internet-reach Phase 1. Validated by src/scripts/check_reach_channels.ts (task check-reach-channels). SCOPE: standalone operator tooling — the registry answers 'which external backend does a reach recipe need, is it healthy, is its install command pinned'. The Phase 0 benchmark returned band: stop, so no channel here is routed, recommended, or agent-facing; nothing in this schema may be read as a routing table. Lifecycle vocabulary is REUSED from docs/contracts/provider-lifecycle.md — never fork a second enum. NO $ref / patternProperties / if-then anywhere: the validator is the Draft-07 subset exported by src/scripts/validate_frontmatter.ts (type, required, properties, additionalProperties:false, items, minItems, pattern, minLength, enum), so the install-string schema is inlined per platform key on purpose. DELIBERATELY ABSENT — a per-backend readiness field (road-to-gated-reach Phase 2 step 3): a backend can pass its `--version` probe and still be unable to retrieve (`yt-dlp` needs an external JavaScript runtime it must be configured to use), and that config-semantic check is implemented DOCTOR-SIDE, keyed on the backend id, in src/scripts/reach_doctor.ts § READINESS_REQUIREMENTS. Rationale: (1) it keeps this schema honest for the backends that have no such requirement — they declare nothing, instead of each omitting an optional key a reader must interpret as an absence; (2) the requirement is a property of the upstream tool, not of an operator's registry entry, so a registry that could omit it would hand back the very false green the check closes; (3) this file is UNTRUSTED input at runtime (reach_doctor.collect() validates it before spawning anything), and a field of the form 'check this config file' would widen that input's influence into which local path the doctor reads; (4) same precedent as the doctor's DEEP_PROBES table, which is code-side for the same reason. The rejected trade-off, stated: a field here would be visible to an operator reading the registry and would let a future backend with the same need be added in YAML rather than in code — that cost is accepted, because its failure mode is only extra maintainer work, whereas the alternative's failure mode is a wrong health verdict.",
7
+ "type": "object",
8
+ "additionalProperties": false,
9
+ "required": ["schema_version", "channels"],
10
+ "properties": {
11
+ "schema_version": {
12
+ "type": "string",
13
+ "enum": ["reach-channels-v1"],
14
+ "description": "Registry format version. Bumped only on a breaking shape change."
15
+ },
16
+ "channels": {
17
+ "type": "array",
18
+ "minItems": 1,
19
+ "description": "Channel list. A channel is a retrieval need (repository metadata, a feed, a page read); its `backends` are the ORDERED candidate tools that can satisfy it — first healthy candidate wins, so switching a backend is reordering this list, never editing code. Order carries meaning; channel order does not.",
20
+ "items": {
21
+ "type": "object",
22
+ "additionalProperties": false,
23
+ "required": ["id", "description", "tier", "lifecycle", "override_key", "last_verified", "backends"],
24
+ "properties": {
25
+ "id": {
26
+ "type": "string",
27
+ "pattern": "^[a-z][a-z0-9-]*$",
28
+ "description": "Channel slug — unique within the registry, lowercase."
29
+ },
30
+ "description": {
31
+ "type": "string",
32
+ "minLength": 10,
33
+ "description": "What the channel retrieves, in operator terms. Never a recommendation to use it."
34
+ },
35
+ "tier": {
36
+ "type": "string",
37
+ "enum": ["zero-config", "free-key", "login"],
38
+ "description": "Access cost to the operator: `zero-config` = no credential; `free-key` = a free token/API key the operator provisions; `login` = an authenticated account session. Documents setup effort only — it does not authorize credential harvesting of any kind."
39
+ },
40
+ "lifecycle": {
41
+ "type": "string",
42
+ "enum": ["experimental", "stable", "deprecated", "community"],
43
+ "description": "Reused verbatim from docs/contracts/provider-lifecycle.md § tiers. `experimental` = shape is right, no maintainer-verified real run; `stable` = maintainer-verified against a real run at least once; `deprecated` = successor declared, kept for one migration window (see removal_after / replacement); `community` = externally contributed, best-effort."
44
+ },
45
+ "override_key": {
46
+ "type": "string",
47
+ "pattern": "^reach\\.channels\\.[a-z0-9-]+\\.backend$",
48
+ "description": "Settings key by which an operator pins one backend for this channel, bypassing candidate order."
49
+ },
50
+ "last_verified": {
51
+ "type": "string",
52
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$",
53
+ "description": "ISO date the channel's backends and pins were last checked by a human. MUST be quoted in YAML — an unquoted date parses as a timestamp, not a string, and fails this type check loudly. Replaces a scheduled network CI job: staleness is checked offline against this field."
54
+ },
55
+ "removal_after": {
56
+ "type": "string",
57
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$",
58
+ "description": "Deprecation path, optional. Only meaningful with lifecycle `deprecated`: until this date the channel still probes with a warning; past it the doctor reports the channel as removed and the staleness lint flags its continued presence in this file."
59
+ },
60
+ "replacement": {
61
+ "type": "string",
62
+ "pattern": "^[a-z][a-z0-9-]*$",
63
+ "description": "Deprecation path, optional. Channel id that supersedes this one."
64
+ },
65
+ "credential_path": {
66
+ "type": "string",
67
+ "minLength": 1,
68
+ "pattern": "^[^\\n]+$",
69
+ "description": "Optional path to the credential file this channel's backend reads (`~` is expanded). The doctor ONLY stats it, to warn when it is group- or world-readable — it never opens it, never hashes it, and never reports its contents. Declared here because reach_doctor.ts enforces this schema at runtime and additionalProperties is false, so a field the code supports must be declared or no registry could use it. No shipped channel declares one."
70
+ },
71
+ "backends": {
72
+ "type": "array",
73
+ "minItems": 1,
74
+ "description": "Ordered candidate tools. Each is an alternative that satisfies the channel on its own — not a pipeline stage.",
75
+ "items": {
76
+ "type": "object",
77
+ "additionalProperties": false,
78
+ "required": ["id", "probe_cmd", "probe_args", "install"],
79
+ "properties": {
80
+ "id": {
81
+ "type": "string",
82
+ "pattern": "^[a-z][a-z0-9_.-]*$",
83
+ "description": "Backend slug — unique within its channel."
84
+ },
85
+ "probe_cmd": {
86
+ "type": "string",
87
+ "pattern": "^[a-z][a-z0-9_.-]*$",
88
+ "description": "Bare executable name resolved on PATH. No path separators, no shell — the probe layer spawns it directly through hardenedSpawnEnv(). MUST equal this backend's `id`: a backend probes its own binary and nothing else, so a report row labelled `gh` can never be the health of `/bin/sh`. The equality is a cross-field rule this Draft-07 subset cannot express, so it is asserted by check_probe_cmd_binding() in src/scripts/check_reach_channels.ts and reported as rule `probe_cmd-binding`."
89
+ },
90
+ "probe_args": {
91
+ "type": "array",
92
+ "minItems": 1,
93
+ "description": "Side-effect-free probe arguments only — a version / help / status query. The item pattern is an ALLOWLIST, not a denylist: an argument must be flag-shaped (`--version`, `-V`, `version`, `rate_limit`) and nothing else. A denylist was tried first and leaked — it admitted `cp`, `cat`, `-c`, `--data-binary` and every URL, so `probe_cmd: sh` + `probe_args: [\"-c\", \"<payload>\"]` passed the gate. What is actually true after the allowlist plus the probe_cmd===id binding plus runtime enforcement of this schema in reach_doctor.collect(): no path, no URL, no shell metacharacter, and no `-c`-style inline payload can reach the spawned child, and the child is the backend's own binary. This bounds the argument shape; it is not a proof that every upstream binary is side-effect-free under every flag — that judgement stays with the human who adds the backend.",
94
+ "items": {
95
+ "type": "string",
96
+ "pattern": "^-{0,2}[A-Za-z][A-Za-z0-9._-]*$"
97
+ }
98
+ },
99
+ "install": {
100
+ "type": "object",
101
+ "additionalProperties": false,
102
+ "required": ["default"],
103
+ "description": "Human-performed install prescription per platform, selected by process.platform with `default` as the fallback. The package NEVER runs these: prescription-first, no auto-install, no piped remote installer. Platform keys are optional; `default` is not.",
104
+ "properties": {
105
+ "darwin": {
106
+ "type": "string",
107
+ "pattern": "^(?!.*(?:\\blatest\\b|\\bmain\\b|\\bmaster\\b|\\bHEAD\\b|archive/|\\.zip|\\.tgz|\\.tar\\.gz|\\.tar\\.xz))(?:os-baseline: [^\\n]{10,}|(?=.*(?:(?:==|@|=)v?[0-9]+(?:\\.[0-9]+)*|--version[ =]v?[0-9]+(?:\\.[0-9]+)*|\\sv?[0-9]+\\.[0-9]+\\.[0-9]+))[^\\n]+)$",
108
+ "description": "See `default` for the pinning contract."
109
+ },
110
+ "linux": {
111
+ "type": "string",
112
+ "pattern": "^(?!.*(?:\\blatest\\b|\\bmain\\b|\\bmaster\\b|\\bHEAD\\b|archive/|\\.zip|\\.tgz|\\.tar\\.gz|\\.tar\\.xz))(?:os-baseline: [^\\n]{10,}|(?=.*(?:(?:==|@|=)v?[0-9]+(?:\\.[0-9]+)*|--version[ =]v?[0-9]+(?:\\.[0-9]+)*|\\sv?[0-9]+\\.[0-9]+\\.[0-9]+))[^\\n]+)$",
113
+ "description": "See `default` for the pinning contract."
114
+ },
115
+ "win32": {
116
+ "type": "string",
117
+ "pattern": "^(?!.*(?:\\blatest\\b|\\bmain\\b|\\bmaster\\b|\\bHEAD\\b|archive/|\\.zip|\\.tgz|\\.tar\\.gz|\\.tar\\.xz))(?:os-baseline: [^\\n]{10,}|(?=.*(?:(?:==|@|=)v?[0-9]+(?:\\.[0-9]+)*|--version[ =]v?[0-9]+(?:\\.[0-9]+)*|\\sv?[0-9]+\\.[0-9]+\\.[0-9]+))[^\\n]+)$",
118
+ "description": "See `default` for the pinning contract."
119
+ },
120
+ "default": {
121
+ "type": "string",
122
+ "pattern": "^(?!.*(?:\\blatest\\b|\\bmain\\b|\\bmaster\\b|\\bHEAD\\b|archive/|\\.zip|\\.tgz|\\.tar\\.gz|\\.tar\\.xz))(?:os-baseline: [^\\n]{10,}|(?=.*(?:(?:==|@|=)v?[0-9]+(?:\\.[0-9]+)*|--version[ =]v?[0-9]+(?:\\.[0-9]+)*|\\sv?[0-9]+\\.[0-9]+\\.[0-9]+))[^\\n]+)$",
123
+ "description": "PINNING CONTRACT — an install prescription is EITHER (a) a command carrying an explicit version specifier (`name==1.2.3`, `name@22`, `--version 2.96.0`, or a whitespace-separated exact `1.2.3` triple), OR (b) an `os-baseline: <explanation>` sentinel for a tool the platform ships and no package install is prescribed for. Everything else FAILS: an unpinned command, and any moving-target source — `latest`, `main`, `master`, `HEAD`, `archive/…`, `.zip`, `.tgz`, `.tar.gz`, `.tar.xz`. This mechanizes the supply-chain intake floor (verify, pin, lock, never pipe a remote script to a shell) instead of asserting it."
124
+ }
125
+ }
126
+ }
127
+ }
128
+ }
129
+ }
130
+ }
131
+ }
132
+ }
133
+ }
134
+ }
@@ -0,0 +1,220 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://github.com/event4u-app/agent-config/scripts/schemas/reach-doctor-payload.schema.json",
4
+ "x-schemaVersion": "1.0.0",
5
+ "title": "reach:doctor --format json payload",
6
+ "$comment": "road-to-internet-reach Phase 2 step 5 — the falsifier for 'the JSON is clean'. Emitted by src/scripts/reach_doctor.ts; real output is validated against this file by tests/scripts/reach_doctor.test.ts (Ajv, the same validator tests/scripts/evals_schema.test.ts uses — full Draft-07, so nullable unions are expressed honestly here rather than being weakened for the frontmatter validator's Draft-07 subset). SCOPE: a health report over operator-chosen backends. It is NOT a routing table and carries no field that ranks, recommends, or routes a channel to an agent — the Phase 0 benchmark returned band: stop. Any future field implying agent-facing preference is a scope violation, not a feature.",
7
+ "type": "object",
8
+ "additionalProperties": false,
9
+ "required": [
10
+ "schema_version",
11
+ "platform",
12
+ "registry",
13
+ "deep",
14
+ "channels",
15
+ "last_verified_suggestions"
16
+ ],
17
+ "properties": {
18
+ "schema_version": {
19
+ "type": "integer",
20
+ "enum": [1],
21
+ "description": "Payload format version, mirroring the hooks:doctor convention. Bumped only on a breaking payload shape change."
22
+ },
23
+ "platform": {
24
+ "type": "string",
25
+ "minLength": 1,
26
+ "description": "process.platform of the reporting machine — the key by which each install prescription was selected (with `default` as the fallback)."
27
+ },
28
+ "registry": {
29
+ "type": "string",
30
+ "minLength": 1,
31
+ "description": "Registry file actually read, repo-relative when it lives inside the repo. Present so a report is never ambiguous about its own input."
32
+ },
33
+ "deep": {
34
+ "type": "boolean",
35
+ "description": "True only when --deep ran, i.e. when real network requests were made. False means the run touched no network at all."
36
+ },
37
+ "channels": {
38
+ "type": "array",
39
+ "description": "One row per admitted channel, in registry order. A malformed channel appears as its own `error` row rather than removing the report — per-channel isolation is part of the contract.",
40
+ "items": {
41
+ "type": "object",
42
+ "additionalProperties": false,
43
+ "required": [
44
+ "id",
45
+ "status",
46
+ "active_backend",
47
+ "tier",
48
+ "lifecycle",
49
+ "last_verified",
50
+ "fix",
51
+ "warnings",
52
+ "credential",
53
+ "backends"
54
+ ],
55
+ "properties": {
56
+ "id": {
57
+ "type": "string",
58
+ "minLength": 1,
59
+ "description": "Channel id from the registry, or `(channel #N)` when the entry was too malformed to carry one."
60
+ },
61
+ "status": {
62
+ "type": "string",
63
+ "enum": ["ok", "missing", "broken", "timeout", "error", "removed", "not-ready"],
64
+ "description": "The five probe states plus `removed` and `not-ready`. `ok` = some backend is USABLE (probes ok AND, if it declares a readiness requirement, that requirement is confirmed satisfied); `not-ready` = a candidate is installed and answering its probe while its readiness requirement is unsatisfied or unconfirmed — the 'installed, unverified' ceiling, deliberately NOT reported as ok because that was the false green the readiness layer exists to close; otherwise the FIRST candidate's probe verdict (the one whose install fix the operator runs); `removed` = past removal_after, never probed. Which of unsatisfied-vs-unconfirmed applies is carried precisely by backends[].readiness.status."
65
+ },
66
+ "active_backend": {
67
+ "type": ["string", "null"],
68
+ "description": "First backend in the ordered candidate list that probes ok; null when none does. Candidate order is the switch — this field reports it, it does not choose."
69
+ },
70
+ "tier": {
71
+ "type": "string",
72
+ "description": "Access cost to the operator (zero-config | free-key | login), echoed from the registry. Documents setup effort only."
73
+ },
74
+ "lifecycle": {
75
+ "type": "string",
76
+ "description": "Registry lifecycle value, reused verbatim from docs/contracts/provider-lifecycle.md."
77
+ },
78
+ "last_verified": {
79
+ "type": "string",
80
+ "description": "ISO date a human last checked this channel, echoed from the registry."
81
+ },
82
+ "fix": {
83
+ "type": ["string", "null"],
84
+ "description": "The exact PINNED install command for this platform, echoed verbatim from the registry, and only when the first candidate is missing or broken. Never composed, never invented, never executed by the package — a human reads and runs it."
85
+ },
86
+ "warnings": {
87
+ "type": "array",
88
+ "description": "Operator-facing warnings: deprecation, removal, credential permissions. Never carries a credential's contents or any fingerprint of it.",
89
+ "items": { "type": "string", "minLength": 1 }
90
+ },
91
+ "credential": {
92
+ "type": ["object", "null"],
93
+ "additionalProperties": false,
94
+ "required": ["path", "present", "mode", "group_or_world_readable", "checked"],
95
+ "description": "Permission metadata for a declared credential file, or null when the channel declares none (the shipped registry declares none — see reach_doctor.ts § inspectCredential). Path + mode ONLY: contents and secret fingerprints are deliberately absent, because a fingerprint is a confirmation oracle rather than a redaction.",
96
+ "properties": {
97
+ "path": { "type": "string", "minLength": 1, "description": "Resolved credential path (~ expanded) — the path a chmod would target." },
98
+ "present": { "type": "boolean", "description": "Whether the declared file exists." },
99
+ "mode": {
100
+ "type": ["string", "null"],
101
+ "pattern": "^0[0-7]{3}$",
102
+ "description": "Octal permission bits, e.g. `0600`. Null when absent or unchecked."
103
+ },
104
+ "group_or_world_readable": {
105
+ "type": "boolean",
106
+ "description": "True when the mode grants group- or other-read (0o044) — the silent exposure this check exists to surface."
107
+ },
108
+ "checked": {
109
+ "type": "boolean",
110
+ "description": "False on win32, where POSIX mode bits are not meaningful; the check is skipped rather than asserted with a different meaning."
111
+ }
112
+ }
113
+ },
114
+ "backends": {
115
+ "type": "array",
116
+ "description": "Per-candidate probe results in registry order. Empty for a `removed` channel (not probed) and for an `error` row (not readable).",
117
+ "items": {
118
+ "type": "object",
119
+ "additionalProperties": false,
120
+ "required": ["id", "probe_cmd", "status", "path", "exit_code", "diagnostic", "fix", "deep"],
121
+ "properties": {
122
+ "id": { "type": "string", "minLength": 1, "description": "Backend slug within its channel." },
123
+ "probe_cmd": { "type": "string", "minLength": 1, "description": "Executable probed on PATH." },
124
+ "status": {
125
+ "type": "string",
126
+ "enum": ["ok", "missing", "broken", "timeout", "error"],
127
+ "description": "tool_probe's five-state taxonomy, unchanged."
128
+ },
129
+ "path": { "type": ["string", "null"], "description": "Resolved binary path, or null when resolution failed." },
130
+ "exit_code": { "type": ["integer", "null"], "description": "Probe exit code, or null when the child never reported one." },
131
+ "diagnostic": { "type": "string", "minLength": 1, "description": "Short, OUTPUT-FREE explanation. Probe stdio is ignored, so fetched content and credentials can never reach a report." },
132
+ "fix": { "type": ["string", "null"], "description": "Platform-selected pinned prescription for this backend; populated for missing/broken only." },
133
+ "readiness": {
134
+ "type": "object",
135
+ "additionalProperties": false,
136
+ "required": [
137
+ "kind",
138
+ "status",
139
+ "config_path",
140
+ "config_present",
141
+ "config_flag",
142
+ "runtimes",
143
+ "version",
144
+ "fix",
145
+ "detail"
146
+ ],
147
+ "description": "Config-semantic readiness verdict — the answer to 'installed, or actually able to retrieve?'. PRESENT ONLY for a backend that declares a readiness requirement (declared doctor-side in reach_doctor.ts § READINESS_REQUIREMENTS, not in the registry — see the $comment of reach-channels.schema.json for why). Its ABSENCE, rather than a null value, is how a backend with no such requirement is expressed: the other channels' backend rows gain no field at all. Read-only and local: a config file is read bounded and symlink-refusing, PATH is consulted, and no network request is made in either mode.",
148
+ "properties": {
149
+ "kind": {
150
+ "type": "string",
151
+ "enum": ["js-runtime"],
152
+ "description": "Which readiness requirement this row settles. One kind today: an external JavaScript runtime the extractor must be configured to use."
153
+ },
154
+ "status": {
155
+ "type": "string",
156
+ "enum": ["ready", "not-ready", "unknown"],
157
+ "description": "`ready` = confirmed usable; `not-ready` = confirmed unusable as configured; `unknown` = deliberately not settled (the backend is not installed, or its version could not be parsed so flag support cannot be confirmed). `unknown` never reads as ok — an 'installed, unverified' ceiling is preferred to a false green."
158
+ },
159
+ "config_path": {
160
+ "type": "string",
161
+ "minLength": 1,
162
+ "description": "The config file path resolved THE WAY THE UPSTREAM TOOL RESOLVES IT ($XDG_CONFIG_HOME/yt-dlp/config, else ~/.config/yt-dlp/config). Reported so the doctor and the operator can never be reading different files — the specific confusion this layer exists to prevent. The emitted fix targets this same path."
163
+ },
164
+ "config_present": {
165
+ "type": "boolean",
166
+ "description": "True only when that path is a real, readable regular file. False also covers a REFUSED read (a symlink, a directory) — the reason is named in `detail`, so a refusal is never silent."
167
+ },
168
+ "config_flag": {
169
+ "type": "boolean",
170
+ "description": "True when the literal flag was found in the config (any runtime value — presence of the flag is the signal). The file's contents never appear anywhere in this payload; only this boolean is extracted."
171
+ },
172
+ "runtimes": {
173
+ "type": "array",
174
+ "description": "External JavaScript runtimes found on PATH, in the order checked (deno first — the extractor enables it by default, so finding it settles the question with no config).",
175
+ "items": { "type": "string", "minLength": 1 }
176
+ },
177
+ "version": {
178
+ "type": ["string", "null"],
179
+ "description": "The backend version PARSED to a YYYY.M.D release, or null when it could not be parsed (which is reported as `unknown`, never as new-enough). Raw `--version` output never reaches this payload — only this re-rendered numeric form, so child output cannot travel into a report."
180
+ },
181
+ "fix": {
182
+ "type": ["string", "null"],
183
+ "description": "An IDEMPOTENT, OS-specific remedy for the operator to run: create the config dir if needed and append the flag only when it is not already present. Printed, never executed. Null when there is nothing to fix, when the remedy is an upgrade rather than a config edit (a build predating the flag would reject it), or when readiness could not be settled."
184
+ },
185
+ "detail": {
186
+ "type": "string",
187
+ "minLength": 1,
188
+ "description": "One-line explanation of the verdict, including any refusal reason. Carries no config contents and no raw child output."
189
+ }
190
+ }
191
+ },
192
+ "deep": {
193
+ "type": ["object", "null"],
194
+ "additionalProperties": false,
195
+ "required": ["status", "endpoint", "exit_code", "detail"],
196
+ "description": "Result of the one real read-only request made under --deep; null in the default shallow mode (no network).",
197
+ "properties": {
198
+ "status": {
199
+ "type": "string",
200
+ "enum": ["ok", "failed", "skipped", "not-declared"],
201
+ "description": "`not-declared` = no deep endpoint is declared for this backend, and none is invented for it."
202
+ },
203
+ "endpoint": { "type": ["string", "null"], "description": "The endpoint actually contacted; null when no deep probe is declared." },
204
+ "exit_code": { "type": ["integer", "null"], "description": "Exit code of the deep request." },
205
+ "detail": { "type": "string", "minLength": 1, "description": "One-line explanation of the deep verdict." }
206
+ }
207
+ }
208
+ }
209
+ }
210
+ }
211
+ }
212
+ }
213
+ },
214
+ "last_verified_suggestions": {
215
+ "type": "array",
216
+ "description": "--deep only (empty otherwise): the `last_verified` lines an operator MAY choose to commit. The command prints them and writes nothing — staleness stays a human-attested field, not a machine-refreshed one.",
217
+ "items": { "type": "string", "minLength": 1 }
218
+ }
219
+ }
220
+ }
@@ -121,6 +121,16 @@
121
121
  "default": "active",
122
122
  "description": "ADR-013 lifecycle state."
123
123
  },
124
+ "enforced_by": {
125
+ "type": "array",
126
+ "minItems": 1,
127
+ "uniqueItems": true,
128
+ "items": {
129
+ "type": "string",
130
+ "pattern": "^(hook:[a-z0-9_-]+|validator:[A-Za-z0-9_./-]+|test:[A-Za-z0-9_./-]+|observer:[a-z0-9 _-]+|none)$"
131
+ },
132
+ "description": "What mechanically enforces this rule. `validator:<script>` and `test:<path>` must resolve on disk AND be wired into a taskfile, workflow, or the hook manifest — a script that exists but runs nowhere resolves to unwired, not covered. `hook:<name>` must be registered in hook_manifest.yaml with fail_closed: true; a hook that never blocks is `observer:`, not `validator:`. `none` is legal and counted — an honest gap beats a false claim. Resolved, not trusted, by check_enforcement_coverage.ts."
133
+ },
124
134
  "trust": {
125
135
  "type": "object",
126
136
  "additionalProperties": false,