@event4u/agent-config 8.10.0 → 8.12.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 (226) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/AGENTS.md +1 -0
  3. package/CHANGELOG.md +67 -149
  4. package/README.md +6 -6
  5. package/dist/agent-src/commands/challenge-me/vision.md +121 -32
  6. package/dist/agent-src/commands/challenge-me/with-docs.md +21 -8
  7. package/dist/agent-src/commands/commit/in-chunks.md +1 -1
  8. package/dist/agent-src/commands/commit.md +2 -1
  9. package/dist/agent-src/commands/cost/report.md +2 -1
  10. package/dist/agent-src/commands/feature/dev.md +4 -4
  11. package/dist/agent-src/commands/humanize.md +101 -0
  12. package/dist/agent-src/commands/jira-ticket.md +2 -2
  13. package/dist/agent-src/commands/memory/load.md +15 -0
  14. package/dist/agent-src/commands/optimize/project.md +12 -0
  15. package/dist/agent-src/commands/roadmap/create.md +12 -11
  16. package/dist/agent-src/commands/tests/create.md +1 -1
  17. package/dist/agent-src/commands/worktree/cleanup.md +22 -17
  18. package/dist/agent-src/commands/worktree/create.md +3 -1
  19. package/dist/agent-src/commands/worktree/status.md +12 -0
  20. package/dist/agent-src/contexts/execution/autonomy-mechanics.md +66 -0
  21. package/dist/agent-src/contexts/execution/orchestration-benchmark-gate.md +1 -0
  22. package/dist/agent-src/contexts/execution/orchestration-telemetry.md +47 -3
  23. package/dist/agent-src/contexts/execution/roadmap-ci-steps-mechanics.md +117 -0
  24. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +111 -85
  25. package/dist/agent-src/contexts/execution/roadmap-writing-source-derived.md +65 -0
  26. package/dist/agent-src/contexts/execution/subagent-modes-detail.md +84 -0
  27. package/dist/agent-src/rules/architecture.md +6 -51
  28. package/dist/agent-src/rules/autonomous-execution.md +7 -37
  29. package/dist/agent-src/rules/broken-access-control.md +7 -45
  30. package/dist/agent-src/rules/code-comment-discipline.md +64 -0
  31. package/dist/agent-src/rules/context-hygiene.md +14 -0
  32. package/dist/agent-src/rules/decision-revisit-gate.md +9 -77
  33. package/dist/agent-src/rules/delegation-policy.md +10 -8
  34. package/dist/agent-src/rules/design-fidelity.md +4 -0
  35. package/dist/agent-src/rules/git-history-discipline.md +4 -79
  36. package/dist/agent-src/rules/improve-before-implement.md +7 -66
  37. package/dist/agent-src/rules/legal-safety-floor.md +11 -55
  38. package/dist/agent-src/rules/no-roadmap-references.md +13 -77
  39. package/dist/agent-src/rules/persona-governance.md +5 -47
  40. package/dist/agent-src/rules/provider-lifecycle-discipline.md +6 -32
  41. package/dist/agent-src/rules/roadmap-ci-steps-policy.md +21 -104
  42. package/dist/agent-src/rules/roadmap-progress-sync.md +5 -56
  43. package/dist/agent-src/rules/think-before-action.md +10 -0
  44. package/dist/agent-src/rules/untrusted-input-defense.md +3 -75
  45. package/dist/agent-src/rules/verify-before-complete.md +12 -0
  46. package/dist/agent-src/skills/agent-docs-writing/SKILL.md +96 -12
  47. package/dist/agent-src/skills/ai-council/SKILL.md +19 -9
  48. package/dist/agent-src/skills/api-design/SKILL.md +7 -5
  49. package/dist/agent-src/skills/api-testing/SKILL.md +1 -1
  50. package/dist/agent-src/skills/authz-review/SKILL.md +59 -2
  51. package/dist/agent-src/skills/blast-radius-analyzer/SKILL.md +1 -1
  52. package/dist/agent-src/skills/bug-analyzer/SKILL.md +19 -16
  53. package/dist/agent-src/skills/code-refactoring/SKILL.md +9 -9
  54. package/dist/agent-src/skills/content-funnel-design/SKILL.md +2 -0
  55. package/dist/agent-src/skills/data-flow-mapper/SKILL.md +1 -1
  56. package/dist/agent-src/skills/decision-review/SKILL.md +86 -0
  57. package/dist/agent-src/skills/defense-in-depth/SKILL.md +2 -2
  58. package/dist/agent-src/skills/dependency-upgrade/SKILL.md +7 -7
  59. package/dist/agent-src/skills/design-intelligence/SKILL.md +8 -8
  60. package/dist/agent-src/skills/design-review/SKILL.md +23 -2
  61. package/dist/agent-src/skills/doc-coauthoring/SKILL.md +2 -0
  62. package/dist/agent-src/skills/editorial-calendar/SKILL.md +2 -0
  63. package/dist/agent-src/skills/fe-design/SKILL.md +7 -9
  64. package/dist/agent-src/skills/git-workflow/SKILL.md +86 -0
  65. package/dist/agent-src/skills/humanizer/SKILL.md +147 -0
  66. package/dist/agent-src/skills/humanizer/data/patterns.md +190 -0
  67. package/dist/agent-src/skills/humanizer/evals/triggers.json +17 -0
  68. package/dist/agent-src/skills/laravel/SKILL.md +14 -1
  69. package/dist/agent-src/skills/launch-readiness/SKILL.md +20 -19
  70. package/dist/agent-src/skills/legal-practice-profile/SKILL.md +126 -0
  71. package/dist/agent-src/skills/logging-monitoring/SKILL.md +1 -1
  72. package/dist/agent-src/skills/mcp/SKILL.md +1 -1
  73. package/dist/agent-src/skills/memory-consolidation/SKILL.md +14 -1
  74. package/dist/agent-src/skills/migration-architect/SKILL.md +2 -1
  75. package/dist/agent-src/skills/module-detect-on-the-fly/SKILL.md +58 -1
  76. package/dist/agent-src/skills/performance/SKILL.md +1 -1
  77. package/dist/agent-src/skills/php-debugging/SKILL.md +17 -0
  78. package/dist/agent-src/skills/readme-writing/SKILL.md +2 -0
  79. package/dist/agent-src/skills/readme-writing-package/SKILL.md +2 -0
  80. package/dist/agent-src/skills/release-comms/SKILL.md +2 -0
  81. package/dist/agent-src/skills/roadmap-writing/SKILL.md +22 -51
  82. package/dist/agent-src/skills/security-audit/SKILL.md +6 -9
  83. package/dist/agent-src/skills/sentry-integration/SKILL.md +2 -2
  84. package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +8 -0
  85. package/dist/agent-src/skills/source-discovery/SKILL.md +26 -24
  86. package/dist/agent-src/skills/subagent-orchestration/SKILL.md +55 -46
  87. package/dist/agent-src/skills/subagent-orchestration/evals/evals.json +42 -0
  88. package/dist/agent-src/skills/systematic-debugging/SKILL.md +10 -0
  89. package/dist/agent-src/skills/using-git-worktrees/SKILL.md +10 -0
  90. package/dist/agent-src/skills/worktree-lifecycle/SKILL.md +27 -20
  91. package/dist/agent-src/skills/worktree-lifecycle/evals/evals.json +38 -0
  92. package/dist/agent-src/templates/agent-settings.md +3 -2
  93. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +15 -1
  94. package/dist/agent-src/templates/contexts/knowledge-card.md +12 -0
  95. package/dist/agent-src/templates/roadmaps.md +14 -0
  96. package/dist/cli/commands/recordTriggerEval.js +4 -14
  97. package/dist/cli/commands/recordTriggerEval.js.map +1 -1
  98. package/dist/discovery/deprecation-report.md +1 -1
  99. package/dist/discovery/discovery-manifest.json +157 -88
  100. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  101. package/dist/discovery/discovery-manifest.summary.md +6 -6
  102. package/dist/discovery/orphan-report.md +1 -1
  103. package/dist/discovery/packs.json +13 -10
  104. package/dist/discovery/trust-report.md +3 -3
  105. package/dist/discovery/workspaces.json +14 -9
  106. package/dist/install/install.mjs +12 -7
  107. package/dist/mcp/registry-manifest.json +4 -4
  108. package/dist/router.json +1 -1
  109. package/dist/scripts/_lib/claude_settings_hooks.js +8 -0
  110. package/dist/scripts/_lib/claude_settings_hooks.js.map +1 -1
  111. package/dist/scripts/_lib/trigger_eval_floors.js +24 -0
  112. package/dist/scripts/_lib/trigger_eval_floors.js.map +1 -0
  113. package/dist/scripts/condense.js +12 -2
  114. package/dist/scripts/condense.js.map +1 -1
  115. package/dist/server/schemas/settings.js +5 -2
  116. package/dist/server/schemas/settings.js.map +1 -1
  117. package/docs/CLAIMS.md +54 -5
  118. package/docs/architecture.md +4 -4
  119. package/docs/archive/CHANGELOG-pre-8.12.0.md +304 -0
  120. package/docs/benchmark.md +90 -0
  121. package/docs/catalog.md +8 -5
  122. package/docs/command-flows.md +2 -1
  123. package/docs/contracts/ai-council-config.md +97 -0
  124. package/docs/contracts/command-clusters.md +1 -0
  125. package/docs/contracts/evidence-based-pruning.md +1 -0
  126. package/docs/contracts/hook-architecture-v1.md +25 -0
  127. package/docs/contracts/kernel-membership.md +35 -0
  128. package/docs/contracts/measurement-baseline.md +1 -0
  129. package/docs/contracts/persona-schema.md +49 -0
  130. package/docs/contracts/provider-lifecycle.md +29 -4
  131. package/docs/contracts/release-sizing.md +22 -0
  132. package/docs/contracts/write-engine.md +38 -1
  133. package/docs/customization.md +12 -0
  134. package/docs/decisions/ADR-002-kernel-bucket-overrides.md +11 -0
  135. package/docs/decisions/ADR-003-flat-cluster-subs-and-colon-syntax.md +11 -0
  136. package/docs/decisions/ADR-006-skill-tools-python-pilot.md +1 -1
  137. package/docs/decisions/ADR-028-root-layout.md +1 -1
  138. package/docs/decisions/ADR-038-canonical-settings-path.md +10 -0
  139. package/docs/decisions/ADR-092-defer-command-tier-alias-removal.md +29 -5
  140. package/docs/decisions/ADR-093-ai-council-config-user-global.md +1 -1
  141. package/docs/decisions/ADR-100-global-knowledge-card-sharing.md +7 -0
  142. package/docs/decisions/ADR-103-global-knowledge-default-off-until-measured.md +2 -2
  143. package/docs/decisions/ADR-105-automatic-subagent-orchestration.md +1 -1
  144. package/docs/decisions/ADR-114-commit-policy-iron-law-override.md +11 -0
  145. package/docs/decisions/ADR-116-memory-tripwire-activation-path.md +11 -1
  146. package/docs/decisions/ADR-117-subagents-auto-default-on.md +2 -1
  147. package/docs/decisions/ADR-118-loop-engineering-boundaries.md +162 -0
  148. package/docs/decisions/ADR-119-global-knowledge-default-on.md +104 -0
  149. package/docs/decisions/ADR-120-council-chairman-mode.md +70 -0
  150. package/docs/decisions/ADR-121-knowledge-sensitivity-classes.md +90 -0
  151. package/docs/decisions/ADR-200-python-to-typescript-migration.md +11 -1
  152. package/docs/decisions/ADR-rule-kernel-and-router.md +11 -0
  153. package/docs/decisions/INDEX.md +13 -9
  154. package/docs/design/council-vs-solo-baseline.md +92 -0
  155. package/docs/design/cross-model-parity-eval.md +82 -0
  156. package/docs/design/length-neutral-judge-rerun.md +92 -0
  157. package/docs/design/utilization-window-criteria.md +51 -0
  158. package/docs/featured-skills.md +2 -2
  159. package/docs/getting-started-by-role.md +1 -1
  160. package/docs/getting-started.md +2 -2
  161. package/docs/governance-advantage.md +1 -1
  162. package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +68 -0
  163. package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +80 -5
  164. package/docs/guidelines/agent-infra/rule-body-migration-inventory.md +241 -0
  165. package/docs/guidelines/agent-infra/untrusted-input-spotlighting.md +80 -3
  166. package/docs/guidelines/code-clarity.md +191 -1
  167. package/docs/guidelines/design-antipatterns.md +20 -0
  168. package/docs/guidelines/php/php-coding-patterns.md +12 -2
  169. package/docs/maintainers/branch-protection.md +87 -0
  170. package/docs/maintainers/system-map.md +85 -0
  171. package/docs/proof.md +32 -15
  172. package/docs/wedge/production-validator/production-validator.md +27 -0
  173. package/package.json +1 -1
  174. package/src/config/agent-settings.template.yml +38 -9
  175. package/src/scripts/_cli/cmd_doctor.ts +129 -1
  176. package/src/scripts/_lib/auto_dispatch.corpus.test.ts +87 -0
  177. package/src/scripts/_lib/claude_settings_hooks.ts +8 -0
  178. package/src/scripts/_lib/knowledge_global.ts +50 -2
  179. package/src/scripts/_lib/knowledge_global_promote.ts +150 -0
  180. package/src/scripts/_lib/knowledge_global_redaction.ts +42 -4
  181. package/src/scripts/_lib/lexical_index.ts +15 -2
  182. package/src/scripts/_lib/orchestration_record.ts +14 -0
  183. package/src/scripts/_lib/trigger_eval_floors.ts +31 -0
  184. package/src/scripts/ai_council/chairman.ts +90 -0
  185. package/src/scripts/ai_council/clients.ts +18 -3
  186. package/src/scripts/ai_council/config.ts +157 -0
  187. package/src/scripts/ai_council/debate_gates.ts +89 -0
  188. package/src/scripts/ai_council/orchestrator.ts +252 -2
  189. package/src/scripts/ai_council/prompts.ts +121 -11
  190. package/src/scripts/ai_council/replay.ts +17 -0
  191. package/src/scripts/ai_council/stance_tally.ts +184 -0
  192. package/src/scripts/ai_tells_rules.ts +433 -0
  193. package/src/scripts/bench_humanizer_eval.ts +240 -0
  194. package/src/scripts/bench_persona_placebo.ts +308 -0
  195. package/src/scripts/bench_quality_rerun.ts +357 -0
  196. package/src/scripts/build_discovery_manifest.ts +6 -0
  197. package/src/scripts/build_proof.ts +30 -9
  198. package/src/scripts/check_artefact_count_messaging.ts +56 -0
  199. package/src/scripts/check_claims.ts +40 -0
  200. package/src/scripts/check_knowledge_cards.ts +33 -4
  201. package/src/scripts/check_portability.ts +1 -0
  202. package/src/scripts/check_rule_invariants.ts +10 -3
  203. package/src/scripts/complexity_report.ts +776 -0
  204. package/src/scripts/condense.ts +16 -2
  205. package/src/scripts/consumer_matrix.ts +292 -14
  206. package/src/scripts/council_cli.ts +203 -1
  207. package/src/scripts/detect_ai_tells.ts +312 -0
  208. package/src/scripts/explain_run.ts +593 -0
  209. package/src/scripts/install.ts +12 -0
  210. package/src/scripts/knowledge_global_cli.ts +235 -46
  211. package/src/scripts/lint_framework_leakage.ts +88 -7
  212. package/src/scripts/lint_framework_leakage_allowlist.json +59 -572
  213. package/src/scripts/lint_knowledge_scale.ts +30 -0
  214. package/src/scripts/lint_originality_shingles.ts +210 -0
  215. package/src/scripts/orchestration_record.ts +1 -0
  216. package/src/scripts/rule_backlinks.ts +0 -0
  217. package/src/scripts/schemas/agent-settings.schema.json +11 -0
  218. package/src/scripts/self_review_gate.ts +41 -15
  219. package/src/scripts/skill_eval_coverage.ts +71 -3
  220. package/src/scripts/skill_linter.ts +1 -0
  221. package/src/scripts/trigger_eval_rotation.ts +344 -0
  222. package/src/scripts/update_counts.ts +5 -17
  223. package/src/scripts/worktree_cleanup_check.ts +263 -0
  224. package/src/server/schemas/settings.ts +8 -3
  225. package/src/scripts/analysis_freshness.ts +0 -362
  226. package/src/scripts/export_replay_corpus.ts +0 -208
@@ -1634,10 +1634,20 @@ export function generate_claude_subagents(): number {
1634
1634
  }
1635
1635
  _mkdirp(MODULE_STATE.CLAUDE_AGENTS_DIR);
1636
1636
 
1637
- const sources = _rglobSorted(MODULE_STATE.SUBAGENTS_SOURCE, '*.md').filter((p) => _isFile(p));
1637
+ const sources = _rglobSorted(MODULE_STATE.SUBAGENTS_SOURCE, '*.md').filter(
1638
+ (p) => _isFile(p) && !path.basename(p).startsWith('_'),
1639
+ );
1638
1640
  const current = new Set<string>();
1639
1641
  let count = 0;
1640
1642
 
1643
+ // Prompt-defense preamble partial (road-to-opt-subagent-harvest P1.1):
1644
+ // `_`-prefixed files in src/subagents/ are partials, never agents. The
1645
+ // preamble is injected at the top of every projected body so the whole
1646
+ // fleet carries a uniform injection-defense baseline — each dispatched
1647
+ // subagent is an untrusted-content ingestion point.
1648
+ const defensePath = path.join(MODULE_STATE.SUBAGENTS_SOURCE, '_prompt-defense.md');
1649
+ const defense = _isFile(defensePath) ? _readText(defensePath).trim() : '';
1650
+
1641
1651
  for (const src of sources) {
1642
1652
  const stem = path.basename(src, '.md');
1643
1653
  const text = _readText(src);
@@ -1661,6 +1671,10 @@ export function generate_claude_subagents(): number {
1661
1671
  const tier = typeof fm['model_tier'] === 'string' ? (fm['model_tier'] as string) : 'inherit';
1662
1672
  const model = tier === 'inherit' ? 'inherit' : (_TIER_TO_CLAUDE_MODEL[tier] ?? 'inherit');
1663
1673
 
1674
+ const guardedBody =
1675
+ defense !== '' && !body.includes('prompt-defense-preamble')
1676
+ ? `\n${defense}\n${body}`
1677
+ : body;
1664
1678
  const out =
1665
1679
  '---\n' +
1666
1680
  `name: ${name}\n` +
@@ -1668,7 +1682,7 @@ export function generate_claude_subagents(): number {
1668
1682
  `tools: ${tools}\n` +
1669
1683
  `model: ${model}\n` +
1670
1684
  '---\n' +
1671
- body;
1685
+ guardedBody;
1672
1686
 
1673
1687
  const target = path.join(MODULE_STATE.CLAUDE_AGENTS_DIR, `${name}.md`);
1674
1688
  _writeText(target, out);
@@ -4,15 +4,25 @@
4
4
  *
5
5
  * Exercises the PUBLISHED TARBALL the way a consumer does: pack → fresh
6
6
  * global install (isolated prefix) → `init` into a fresh project → doctor →
7
- * conformance → MCP stdio handshake → hook health → projection presence →
8
- * uninstall. Optional upgrade leg: install the last published minor from the
9
- * registry, then upgrade to the packed tarball and assert doctor stays green.
7
+ * conformance → MCP stdio handshake → hook health → hook lifecycle →
8
+ * projection presence → uninstall. Optional upgrade leg: install the last
9
+ * published minor from the registry, then upgrade to the packed tarball and
10
+ * assert doctor stays green.
10
11
  *
11
12
  * Why this exists: the release-PR gate skips source-level matrices (soundly —
12
13
  * a version-bump diff cannot regress source behaviour), but nothing between
13
14
  * merge and tag exercised the tarball shape. Two published minors shipped
14
15
  * without `src/install/` while published code imported from it; `tsx` was
15
16
  * missing from the published package; each manifested only at publish time.
17
+ * A third bug (missing `dist/install/rule_scope.js`) shipped past every leg
18
+ * because none of them fired a REAL installed hook command — `conformance`'s
19
+ * dispatcher smoke calls `dispatch_hook.ts` directly via `tsx`, bypassing the
20
+ * `agent-config` binary (and its ESM import graph) entirely. The `hooks`
21
+ * (static doctor) and `hook-lifecycle` (this file's live leg, below) legs
22
+ * close that gap: the latter runs the EXACT shell command Claude installed in
23
+ * `~/.claude/settings.json`, so a crash anywhere in the binary's load path —
24
+ * including a missing compiled sibling module — fails the leg with the
25
+ * culprit's stderr, not silently at publish time.
16
26
  * Every leg here asserts exit code + an expected artifact, no LLM calls.
17
27
  *
18
28
  * Usage:
@@ -30,7 +40,9 @@ import { spawnSync, spawn } from 'node:child_process';
30
40
  import * as fs from 'node:fs';
31
41
  import * as os from 'node:os';
32
42
  import * as path from 'node:path';
33
- import { fileURLToPath } from 'node:url';
43
+ import { fileURLToPath, pathToFileURL } from 'node:url';
44
+
45
+ import { MANAGED_SIGNATURE } from './_lib/claude_settings_hooks.js';
34
46
 
35
47
  const _HERE = fileURLToPath(import.meta.url);
36
48
  const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
@@ -52,6 +64,8 @@ interface LegResult {
52
64
  ok: boolean;
53
65
  detail: string;
54
66
  durationMs: number;
67
+ /** fn ran to completion (a declared skip or an after-failure skip is NOT executed). */
68
+ executed: boolean;
55
69
  }
56
70
 
57
71
  function log(msg: string): void {
@@ -61,7 +75,7 @@ function log(msg: string): void {
61
75
  function run(
62
76
  cmd: string,
63
77
  args: string[],
64
- opts: { cwd?: string; env?: NodeJS.ProcessEnv; timeoutMs?: number } = {},
78
+ opts: { cwd?: string; env?: NodeJS.ProcessEnv; timeoutMs?: number; input?: string } = {},
65
79
  ): { status: number; stdout: string; stderr: string } {
66
80
  const r = spawnSync(cmd, args, {
67
81
  cwd: opts.cwd ?? REPO_ROOT,
@@ -69,6 +83,7 @@ function run(
69
83
  encoding: 'utf8',
70
84
  timeout: opts.timeoutMs ?? 600_000,
71
85
  maxBuffer: 32 * 1024 * 1024,
86
+ input: opts.input,
72
87
  });
73
88
  return { status: r.status ?? 1, stdout: r.stdout ?? '', stderr: r.stderr ?? '' };
74
89
  }
@@ -214,6 +229,210 @@ function legHooks(ctx: Ctx): string {
214
229
  return 'hooks:doctor green';
215
230
  }
216
231
 
232
+ // ── hook-lifecycle leg ─────────────────────────────────────────────
233
+ //
234
+ // `hooks:doctor` (above) is a read-only static report — it never runs
235
+ // anything. `conformance`'s dispatcher smoke calls `dispatch_hook.ts`
236
+ // directly via `tsx`, bypassing the installed `agent-config` binary (and
237
+ // its ESM import graph) entirely. Neither exercises what a real Claude
238
+ // Code SessionStart/PreToolUse/PostToolUse/Stop event actually invokes:
239
+ // the exact shell command the installer registered in
240
+ // `~/.claude/settings.json`. This leg does — plus a deterministic
241
+ // completeness check on the compiled entrypoint's module graph, so a
242
+ // missing sibling `.js` (the historical `dist/install/rule_scope.js` bug)
243
+ // fails with the exact culprit instead of a live crash.
244
+
245
+ interface HookCommandEntry {
246
+ /** Native platform event name, e.g. `SessionStart`, `PreToolUse`. */
247
+ nativeEvent: string;
248
+ /** The exact managed shell command string installed for this event. */
249
+ command: string;
250
+ }
251
+
252
+ /**
253
+ * Read every agent-config-managed hook command out of an installed Claude
254
+ * settings file, keyed by native event. Mirrors the shape
255
+ * `ensure_managed_hooks` (src/scripts/_lib/claude_settings_hooks.ts) writes;
256
+ * a group is "managed" iff one of its commands contains `MANAGED_SIGNATURE`
257
+ * — the same identity check the installer itself uses, so this can never
258
+ * drift from what actually got registered.
259
+ */
260
+ export function readInstalledClaudeHookCommands(settingsPath: string): HookCommandEntry[] {
261
+ if (!fs.existsSync(settingsPath)) return [];
262
+ const raw = JSON.parse(fs.readFileSync(settingsPath, 'utf8')) as {
263
+ hooks?: Record<string, Array<{ hooks?: Array<{ command?: unknown }> }>>;
264
+ };
265
+ const hooks = raw.hooks ?? {};
266
+ const out: HookCommandEntry[] = [];
267
+ for (const [nativeEvent, groups] of Object.entries(hooks)) {
268
+ if (!Array.isArray(groups)) continue;
269
+ for (const group of groups) {
270
+ for (const h of group.hooks ?? []) {
271
+ if (typeof h.command === 'string' && h.command.includes(MANAGED_SIGNATURE)) {
272
+ out.push({ nativeEvent, command: h.command });
273
+ }
274
+ }
275
+ }
276
+ }
277
+ return out;
278
+ }
279
+
280
+ /** A representative Claude Code event envelope, shaped per native event. */
281
+ export function buildHookEnvelope(nativeEvent: string, projectDir: string): string {
282
+ const base = {
283
+ session_id: 'consumer-matrix-fixture',
284
+ transcript_path: '',
285
+ cwd: projectDir,
286
+ hook_event_name: nativeEvent,
287
+ };
288
+ switch (nativeEvent) {
289
+ case 'PreToolUse':
290
+ return JSON.stringify({
291
+ ...base,
292
+ tool_name: 'Read',
293
+ tool_input: { file_path: path.join(projectDir, 'README.md') },
294
+ });
295
+ case 'PostToolUse':
296
+ return JSON.stringify({
297
+ ...base,
298
+ tool_name: 'Read',
299
+ tool_input: { file_path: path.join(projectDir, 'README.md') },
300
+ tool_response: { success: true },
301
+ });
302
+ case 'Stop':
303
+ return JSON.stringify({ ...base, stop_hook_active: false });
304
+ case 'UserPromptSubmit':
305
+ return JSON.stringify({ ...base, prompt: 'consumer-matrix smoke prompt' });
306
+ case 'SessionEnd':
307
+ return JSON.stringify({ ...base, reason: 'exit' });
308
+ case 'SessionStart':
309
+ default:
310
+ return JSON.stringify({ ...base, source: 'startup' });
311
+ }
312
+ }
313
+
314
+ /**
315
+ * Run one installed hook command exactly as the platform would — a shell
316
+ * invocation with the event envelope piped to stdin — and report whether it
317
+ * exited clean.
318
+ */
319
+ export function invokeHookCommand(
320
+ entry: HookCommandEntry,
321
+ opts: { cwd: string; env: NodeJS.ProcessEnv; projectDir: string; timeoutMs?: number },
322
+ ): { ok: boolean; status: number; stderrTail: string } {
323
+ const payload = buildHookEnvelope(entry.nativeEvent, opts.projectDir);
324
+ const r = run('bash', ['-c', entry.command], {
325
+ cwd: opts.cwd,
326
+ env: opts.env,
327
+ timeoutMs: opts.timeoutMs ?? 30_000,
328
+ input: payload,
329
+ });
330
+ const stderrTail = r.stderr.trim().split('\n').slice(-5).join(' | ');
331
+ return { ok: r.status === 0, status: r.status, stderrTail };
332
+ }
333
+
334
+ interface MissingModule {
335
+ from: string;
336
+ spec: string;
337
+ resolved: string;
338
+ }
339
+
340
+ /**
341
+ * Extract relative (`./`, `../`) ESM specifiers from a compiled `.js` file —
342
+ * static `from "..."`, bare `import "...";`, and dynamic `import("...")`.
343
+ * Regex, not a full parser: good enough for tsc's fixed output shape, and
344
+ * deliberately ignores bare package specifiers (`from 'commander'`) so the
345
+ * walk never leaves the package's own `dist/` tree.
346
+ */
347
+ export function parseRelativeSpecifiers(source: string): string[] {
348
+ const out = new Set<string>();
349
+ const patterns = [
350
+ /\bfrom\s*["']([^"']+)["']/g,
351
+ /^\s*import\s*["']([^"']+)["']\s*;/gm,
352
+ /\bimport\(\s*["']([^"']+)["']\s*\)/g,
353
+ ];
354
+ for (const re of patterns) {
355
+ let m: RegExpExecArray | null;
356
+ while ((m = re.exec(source)) !== null) {
357
+ const spec = m[1];
358
+ if (spec && (spec.startsWith('./') || spec.startsWith('../'))) out.add(spec);
359
+ }
360
+ }
361
+ return [...out];
362
+ }
363
+
364
+ /**
365
+ * Walk the compiled ESM module graph reachable from `entryFile`, following
366
+ * every relative import/export/dynamic-import specifier, and report any
367
+ * resolved target that does not exist on disk — the deterministic twin of
368
+ * "does this crash at require time", without spawning a process.
369
+ */
370
+ export function walkEsmModuleGraph(entryFile: string): { visited: string[]; missing: MissingModule[] } {
371
+ const visited = new Set<string>();
372
+ const missing: MissingModule[] = [];
373
+ const stack: string[] = [entryFile];
374
+ while (stack.length > 0) {
375
+ const file = stack.pop() as string;
376
+ if (visited.has(file)) continue;
377
+ visited.add(file);
378
+ let source: string;
379
+ try {
380
+ source = fs.readFileSync(file, 'utf8');
381
+ } catch {
382
+ missing.push({ from: '(entry)', spec: file, resolved: file });
383
+ continue;
384
+ }
385
+ for (const spec of parseRelativeSpecifiers(source)) {
386
+ const resolved = path.resolve(path.dirname(file), spec);
387
+ if (!fs.existsSync(resolved)) {
388
+ missing.push({ from: file, spec, resolved });
389
+ continue;
390
+ }
391
+ if (!visited.has(resolved)) stack.push(resolved);
392
+ }
393
+ }
394
+ return { visited: [...visited], missing };
395
+ }
396
+
397
+ /** Dist-manifest completeness gate — every module the entrypoint can reach exists. */
398
+ export function checkDistManifestCompleteness(entryFile: string): { ok: boolean; message: string } {
399
+ const { visited, missing } = walkEsmModuleGraph(entryFile);
400
+ if (missing.length > 0) {
401
+ const detail = missing
402
+ .slice(0, 5)
403
+ .map((m) => `${path.relative(path.dirname(entryFile), m.from)} → missing ${m.spec}`)
404
+ .join('; ');
405
+ return { ok: false, message: `dist-manifest incomplete: ${missing.length} missing module(s): ${detail}` };
406
+ }
407
+ return {
408
+ ok: true,
409
+ message: `dist-manifest complete — ${visited.length} module(s) reachable from ${path.basename(entryFile)}`,
410
+ };
411
+ }
412
+
413
+ function legHookLifecycle(ctx: Ctx): string {
414
+ const settingsPath = path.join(ctx.tmpRoot, 'home', '.claude', 'settings.json');
415
+ const entries = readInstalledClaudeHookCommands(settingsPath);
416
+ if (entries.length === 0) {
417
+ throw new Error(`no managed Claude hook commands found in ${settingsPath} (did init register hooks?)`);
418
+ }
419
+
420
+ const entryScript = fs.realpathSync(ctx.bin);
421
+ const manifestCheck = checkDistManifestCompleteness(entryScript);
422
+ if (!manifestCheck.ok) throw new Error(manifestCheck.message);
423
+
424
+ const env = { ...binEnv(ctx), CLAUDE_PROJECT_DIR: ctx.projectDir };
425
+ const fired: string[] = [];
426
+ for (const entry of entries) {
427
+ const result = invokeHookCommand(entry, { cwd: ctx.projectDir, env, projectDir: ctx.projectDir });
428
+ if (!result.ok) {
429
+ throw new Error(`hook '${entry.nativeEvent}' exited ${result.status}: ${result.stderrTail}`);
430
+ }
431
+ fired.push(`${entry.nativeEvent}=${result.status}`);
432
+ }
433
+ return `${manifestCheck.message}; fired ${fired.length} live event(s): ${fired.join(', ')}`;
434
+ }
435
+
217
436
  function legProjections(ctx: Ctx): string {
218
437
  const home = path.join(ctx.tmpRoot, 'home');
219
438
  const missing = ['.cursor/rules', '.codeium/windsurf', '.claude/skills']
@@ -276,6 +495,7 @@ const LEGS: Array<{ name: string; fn: (ctx: Ctx) => string | Promise<string> }>
276
495
  { name: 'conformance', fn: legConformance },
277
496
  { name: 'mcp-handshake', fn: legMcpHandshake },
278
497
  { name: 'hooks', fn: legHooks },
498
+ { name: 'hook-lifecycle', fn: legHookLifecycle },
279
499
  { name: 'projections', fn: legProjections },
280
500
  { name: 'uninstall', fn: legUninstall },
281
501
  { name: 'upgrade', fn: legUpgrade },
@@ -313,13 +533,20 @@ async function main(): Promise<number> {
313
533
  if (only && !only.includes(leg.name)) continue;
314
534
  // Legs after a hard dependency failure cannot run meaningfully.
315
535
  if (failed) {
316
- ctx.results.push({ leg: leg.name, ok: false, detail: 'skipped (earlier leg failed)', durationMs: 0 });
536
+ ctx.results.push({
537
+ leg: leg.name,
538
+ ok: false,
539
+ detail: 'skipped (earlier leg failed)',
540
+ durationMs: 0,
541
+ executed: false,
542
+ });
317
543
  continue;
318
544
  }
319
545
  const t0 = Date.now();
320
546
  try {
321
547
  const detail = await leg.fn(ctx);
322
- ctx.results.push({ leg: leg.name, ok: true, detail, durationMs: Date.now() - t0 });
548
+ const executed = !detail.startsWith('skipped');
549
+ ctx.results.push({ leg: leg.name, ok: true, detail, durationMs: Date.now() - t0, executed });
323
550
  log(` ✅ ${leg.name} — ${detail}`);
324
551
  } catch (err) {
325
552
  ctx.results.push({
@@ -327,12 +554,42 @@ async function main(): Promise<number> {
327
554
  ok: false,
328
555
  detail: (err as Error).message,
329
556
  durationMs: Date.now() - t0,
557
+ executed: true,
330
558
  });
331
559
  log(` ❌ ${leg.name} — ${(err as Error).message}`);
332
560
  failed = true;
333
561
  }
334
562
  }
335
563
 
564
+ // Leg-completeness gate (road-to-fable-feedback-5 Phase 6a): a full run
565
+ // (no --only) must have EXECUTED every expected leg — a silently-skipped
566
+ // leg can no longer ride a green workflow. The only declared skip is
567
+ // `upgrade` under an explicit --skip-registry flag.
568
+ if (!only && !failed) {
569
+ const declaredSkips = new Set(ctx.skipRegistry ? ['upgrade'] : []);
570
+ const missing = LEGS.filter((l) => {
571
+ const r = ctx.results.find((x) => x.leg === l.name);
572
+ return r === undefined || (!r.executed && !declaredSkips.has(l.name));
573
+ }).map((l) => l.name);
574
+ if (missing.length > 0) {
575
+ log(`❌ leg-completeness gate: expected leg(s) did not execute: ${missing.join(', ')}`);
576
+ failed = true;
577
+ }
578
+ }
579
+
580
+ const manifestPath = getOpt('--manifest');
581
+ if (manifestPath) {
582
+ const manifest = {
583
+ ok: !failed,
584
+ node: process.version,
585
+ skipRegistry: ctx.skipRegistry,
586
+ expected: LEGS.map((l) => l.name),
587
+ results: ctx.results,
588
+ };
589
+ fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
590
+ log(`leg manifest written: ${manifestPath}`);
591
+ }
592
+
336
593
  if (argv.includes('--json')) {
337
594
  process.stdout.write(`${JSON.stringify({ ok: !failed, results: ctx.results }, null, 2)}\n`);
338
595
  }
@@ -344,10 +601,31 @@ async function main(): Promise<number> {
344
601
  return failed ? 1 : 0;
345
602
  }
346
603
 
347
- main().then(
348
- (code) => process.exit(code),
349
- (err) => {
350
- log(`❌ consumer-matrix crashed: ${(err as Error).stack ?? err}`);
351
- process.exit(2);
352
- },
353
- );
604
+ // Entry guard (mirrors src/scripts/hooks/dispatch_hook.ts): a test file
605
+ // imports the pure helpers above (readInstalledClaudeHookCommands,
606
+ // checkDistManifestCompleteness, invokeHookCommand, …) without wanting a
607
+ // full `npm pack` + global-install run to fire as a side effect of the
608
+ // import. `main()` only runs when this file is the process's actual entry
609
+ // point (`npx tsx src/scripts/consumer_matrix.ts …`).
610
+ function _isCliEntry(): boolean {
611
+ if (process.argv[1] === undefined) return false;
612
+ const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
613
+ if (import.meta.url === argvUrl) return true;
614
+ try {
615
+ const here = fs.realpathSync(fileURLToPath(import.meta.url));
616
+ const argv = fs.realpathSync(path.resolve(process.argv[1]));
617
+ return here === argv;
618
+ } catch {
619
+ return false;
620
+ }
621
+ }
622
+
623
+ if (_isCliEntry()) {
624
+ main().then(
625
+ (code) => process.exit(code),
626
+ (err) => {
627
+ log(`❌ consumer-matrix crashed: ${(err as Error).stack ?? err}`);
628
+ process.exit(2);
629
+ },
630
+ );
631
+ }