@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/AGENTS.md +1 -0
- package/CHANGELOG.md +67 -149
- package/README.md +6 -6
- package/dist/agent-src/commands/challenge-me/vision.md +121 -32
- package/dist/agent-src/commands/challenge-me/with-docs.md +21 -8
- package/dist/agent-src/commands/commit/in-chunks.md +1 -1
- package/dist/agent-src/commands/commit.md +2 -1
- package/dist/agent-src/commands/cost/report.md +2 -1
- package/dist/agent-src/commands/feature/dev.md +4 -4
- package/dist/agent-src/commands/humanize.md +101 -0
- package/dist/agent-src/commands/jira-ticket.md +2 -2
- package/dist/agent-src/commands/memory/load.md +15 -0
- package/dist/agent-src/commands/optimize/project.md +12 -0
- package/dist/agent-src/commands/roadmap/create.md +12 -11
- package/dist/agent-src/commands/tests/create.md +1 -1
- package/dist/agent-src/commands/worktree/cleanup.md +22 -17
- package/dist/agent-src/commands/worktree/create.md +3 -1
- package/dist/agent-src/commands/worktree/status.md +12 -0
- package/dist/agent-src/contexts/execution/autonomy-mechanics.md +66 -0
- package/dist/agent-src/contexts/execution/orchestration-benchmark-gate.md +1 -0
- package/dist/agent-src/contexts/execution/orchestration-telemetry.md +47 -3
- package/dist/agent-src/contexts/execution/roadmap-ci-steps-mechanics.md +117 -0
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +111 -85
- package/dist/agent-src/contexts/execution/roadmap-writing-source-derived.md +65 -0
- package/dist/agent-src/contexts/execution/subagent-modes-detail.md +84 -0
- package/dist/agent-src/rules/architecture.md +6 -51
- package/dist/agent-src/rules/autonomous-execution.md +7 -37
- package/dist/agent-src/rules/broken-access-control.md +7 -45
- package/dist/agent-src/rules/code-comment-discipline.md +64 -0
- package/dist/agent-src/rules/context-hygiene.md +14 -0
- package/dist/agent-src/rules/decision-revisit-gate.md +9 -77
- package/dist/agent-src/rules/delegation-policy.md +10 -8
- package/dist/agent-src/rules/design-fidelity.md +4 -0
- package/dist/agent-src/rules/git-history-discipline.md +4 -79
- package/dist/agent-src/rules/improve-before-implement.md +7 -66
- package/dist/agent-src/rules/legal-safety-floor.md +11 -55
- package/dist/agent-src/rules/no-roadmap-references.md +13 -77
- package/dist/agent-src/rules/persona-governance.md +5 -47
- package/dist/agent-src/rules/provider-lifecycle-discipline.md +6 -32
- package/dist/agent-src/rules/roadmap-ci-steps-policy.md +21 -104
- package/dist/agent-src/rules/roadmap-progress-sync.md +5 -56
- package/dist/agent-src/rules/think-before-action.md +10 -0
- package/dist/agent-src/rules/untrusted-input-defense.md +3 -75
- package/dist/agent-src/rules/verify-before-complete.md +12 -0
- package/dist/agent-src/skills/agent-docs-writing/SKILL.md +96 -12
- package/dist/agent-src/skills/ai-council/SKILL.md +19 -9
- package/dist/agent-src/skills/api-design/SKILL.md +7 -5
- package/dist/agent-src/skills/api-testing/SKILL.md +1 -1
- package/dist/agent-src/skills/authz-review/SKILL.md +59 -2
- package/dist/agent-src/skills/blast-radius-analyzer/SKILL.md +1 -1
- package/dist/agent-src/skills/bug-analyzer/SKILL.md +19 -16
- package/dist/agent-src/skills/code-refactoring/SKILL.md +9 -9
- package/dist/agent-src/skills/content-funnel-design/SKILL.md +2 -0
- package/dist/agent-src/skills/data-flow-mapper/SKILL.md +1 -1
- package/dist/agent-src/skills/decision-review/SKILL.md +86 -0
- package/dist/agent-src/skills/defense-in-depth/SKILL.md +2 -2
- package/dist/agent-src/skills/dependency-upgrade/SKILL.md +7 -7
- package/dist/agent-src/skills/design-intelligence/SKILL.md +8 -8
- package/dist/agent-src/skills/design-review/SKILL.md +23 -2
- package/dist/agent-src/skills/doc-coauthoring/SKILL.md +2 -0
- package/dist/agent-src/skills/editorial-calendar/SKILL.md +2 -0
- package/dist/agent-src/skills/fe-design/SKILL.md +7 -9
- package/dist/agent-src/skills/git-workflow/SKILL.md +86 -0
- package/dist/agent-src/skills/humanizer/SKILL.md +147 -0
- package/dist/agent-src/skills/humanizer/data/patterns.md +190 -0
- package/dist/agent-src/skills/humanizer/evals/triggers.json +17 -0
- package/dist/agent-src/skills/laravel/SKILL.md +14 -1
- package/dist/agent-src/skills/launch-readiness/SKILL.md +20 -19
- package/dist/agent-src/skills/legal-practice-profile/SKILL.md +126 -0
- package/dist/agent-src/skills/logging-monitoring/SKILL.md +1 -1
- package/dist/agent-src/skills/mcp/SKILL.md +1 -1
- package/dist/agent-src/skills/memory-consolidation/SKILL.md +14 -1
- package/dist/agent-src/skills/migration-architect/SKILL.md +2 -1
- package/dist/agent-src/skills/module-detect-on-the-fly/SKILL.md +58 -1
- package/dist/agent-src/skills/performance/SKILL.md +1 -1
- package/dist/agent-src/skills/php-debugging/SKILL.md +17 -0
- package/dist/agent-src/skills/readme-writing/SKILL.md +2 -0
- package/dist/agent-src/skills/readme-writing-package/SKILL.md +2 -0
- package/dist/agent-src/skills/release-comms/SKILL.md +2 -0
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +22 -51
- package/dist/agent-src/skills/security-audit/SKILL.md +6 -9
- package/dist/agent-src/skills/sentry-integration/SKILL.md +2 -2
- package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +8 -0
- package/dist/agent-src/skills/source-discovery/SKILL.md +26 -24
- package/dist/agent-src/skills/subagent-orchestration/SKILL.md +55 -46
- package/dist/agent-src/skills/subagent-orchestration/evals/evals.json +42 -0
- package/dist/agent-src/skills/systematic-debugging/SKILL.md +10 -0
- package/dist/agent-src/skills/using-git-worktrees/SKILL.md +10 -0
- package/dist/agent-src/skills/worktree-lifecycle/SKILL.md +27 -20
- package/dist/agent-src/skills/worktree-lifecycle/evals/evals.json +38 -0
- package/dist/agent-src/templates/agent-settings.md +3 -2
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +15 -1
- package/dist/agent-src/templates/contexts/knowledge-card.md +12 -0
- package/dist/agent-src/templates/roadmaps.md +14 -0
- package/dist/cli/commands/recordTriggerEval.js +4 -14
- package/dist/cli/commands/recordTriggerEval.js.map +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +157 -88
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +6 -6
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +13 -10
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +14 -9
- package/dist/install/install.mjs +12 -7
- package/dist/mcp/registry-manifest.json +4 -4
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/claude_settings_hooks.js +8 -0
- package/dist/scripts/_lib/claude_settings_hooks.js.map +1 -1
- package/dist/scripts/_lib/trigger_eval_floors.js +24 -0
- package/dist/scripts/_lib/trigger_eval_floors.js.map +1 -0
- package/dist/scripts/condense.js +12 -2
- package/dist/scripts/condense.js.map +1 -1
- package/dist/server/schemas/settings.js +5 -2
- package/dist/server/schemas/settings.js.map +1 -1
- package/docs/CLAIMS.md +54 -5
- package/docs/architecture.md +4 -4
- package/docs/archive/CHANGELOG-pre-8.12.0.md +304 -0
- package/docs/benchmark.md +90 -0
- package/docs/catalog.md +8 -5
- package/docs/command-flows.md +2 -1
- package/docs/contracts/ai-council-config.md +97 -0
- package/docs/contracts/command-clusters.md +1 -0
- package/docs/contracts/evidence-based-pruning.md +1 -0
- package/docs/contracts/hook-architecture-v1.md +25 -0
- package/docs/contracts/kernel-membership.md +35 -0
- package/docs/contracts/measurement-baseline.md +1 -0
- package/docs/contracts/persona-schema.md +49 -0
- package/docs/contracts/provider-lifecycle.md +29 -4
- package/docs/contracts/release-sizing.md +22 -0
- package/docs/contracts/write-engine.md +38 -1
- package/docs/customization.md +12 -0
- package/docs/decisions/ADR-002-kernel-bucket-overrides.md +11 -0
- package/docs/decisions/ADR-003-flat-cluster-subs-and-colon-syntax.md +11 -0
- package/docs/decisions/ADR-006-skill-tools-python-pilot.md +1 -1
- package/docs/decisions/ADR-028-root-layout.md +1 -1
- package/docs/decisions/ADR-038-canonical-settings-path.md +10 -0
- package/docs/decisions/ADR-092-defer-command-tier-alias-removal.md +29 -5
- package/docs/decisions/ADR-093-ai-council-config-user-global.md +1 -1
- package/docs/decisions/ADR-100-global-knowledge-card-sharing.md +7 -0
- package/docs/decisions/ADR-103-global-knowledge-default-off-until-measured.md +2 -2
- package/docs/decisions/ADR-105-automatic-subagent-orchestration.md +1 -1
- package/docs/decisions/ADR-114-commit-policy-iron-law-override.md +11 -0
- package/docs/decisions/ADR-116-memory-tripwire-activation-path.md +11 -1
- package/docs/decisions/ADR-117-subagents-auto-default-on.md +2 -1
- package/docs/decisions/ADR-118-loop-engineering-boundaries.md +162 -0
- package/docs/decisions/ADR-119-global-knowledge-default-on.md +104 -0
- package/docs/decisions/ADR-120-council-chairman-mode.md +70 -0
- package/docs/decisions/ADR-121-knowledge-sensitivity-classes.md +90 -0
- package/docs/decisions/ADR-200-python-to-typescript-migration.md +11 -1
- package/docs/decisions/ADR-rule-kernel-and-router.md +11 -0
- package/docs/decisions/INDEX.md +13 -9
- package/docs/design/council-vs-solo-baseline.md +92 -0
- package/docs/design/cross-model-parity-eval.md +82 -0
- package/docs/design/length-neutral-judge-rerun.md +92 -0
- package/docs/design/utilization-window-criteria.md +51 -0
- package/docs/featured-skills.md +2 -2
- package/docs/getting-started-by-role.md +1 -1
- package/docs/getting-started.md +2 -2
- package/docs/governance-advantage.md +1 -1
- package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +68 -0
- package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +80 -5
- package/docs/guidelines/agent-infra/rule-body-migration-inventory.md +241 -0
- package/docs/guidelines/agent-infra/untrusted-input-spotlighting.md +80 -3
- package/docs/guidelines/code-clarity.md +191 -1
- package/docs/guidelines/design-antipatterns.md +20 -0
- package/docs/guidelines/php/php-coding-patterns.md +12 -2
- package/docs/maintainers/branch-protection.md +87 -0
- package/docs/maintainers/system-map.md +85 -0
- package/docs/proof.md +32 -15
- package/docs/wedge/production-validator/production-validator.md +27 -0
- package/package.json +1 -1
- package/src/config/agent-settings.template.yml +38 -9
- package/src/scripts/_cli/cmd_doctor.ts +129 -1
- package/src/scripts/_lib/auto_dispatch.corpus.test.ts +87 -0
- package/src/scripts/_lib/claude_settings_hooks.ts +8 -0
- package/src/scripts/_lib/knowledge_global.ts +50 -2
- package/src/scripts/_lib/knowledge_global_promote.ts +150 -0
- package/src/scripts/_lib/knowledge_global_redaction.ts +42 -4
- package/src/scripts/_lib/lexical_index.ts +15 -2
- package/src/scripts/_lib/orchestration_record.ts +14 -0
- package/src/scripts/_lib/trigger_eval_floors.ts +31 -0
- package/src/scripts/ai_council/chairman.ts +90 -0
- package/src/scripts/ai_council/clients.ts +18 -3
- package/src/scripts/ai_council/config.ts +157 -0
- package/src/scripts/ai_council/debate_gates.ts +89 -0
- package/src/scripts/ai_council/orchestrator.ts +252 -2
- package/src/scripts/ai_council/prompts.ts +121 -11
- package/src/scripts/ai_council/replay.ts +17 -0
- package/src/scripts/ai_council/stance_tally.ts +184 -0
- package/src/scripts/ai_tells_rules.ts +433 -0
- package/src/scripts/bench_humanizer_eval.ts +240 -0
- package/src/scripts/bench_persona_placebo.ts +308 -0
- package/src/scripts/bench_quality_rerun.ts +357 -0
- package/src/scripts/build_discovery_manifest.ts +6 -0
- package/src/scripts/build_proof.ts +30 -9
- package/src/scripts/check_artefact_count_messaging.ts +56 -0
- package/src/scripts/check_claims.ts +40 -0
- package/src/scripts/check_knowledge_cards.ts +33 -4
- package/src/scripts/check_portability.ts +1 -0
- package/src/scripts/check_rule_invariants.ts +10 -3
- package/src/scripts/complexity_report.ts +776 -0
- package/src/scripts/condense.ts +16 -2
- package/src/scripts/consumer_matrix.ts +292 -14
- package/src/scripts/council_cli.ts +203 -1
- package/src/scripts/detect_ai_tells.ts +312 -0
- package/src/scripts/explain_run.ts +593 -0
- package/src/scripts/install.ts +12 -0
- package/src/scripts/knowledge_global_cli.ts +235 -46
- package/src/scripts/lint_framework_leakage.ts +88 -7
- package/src/scripts/lint_framework_leakage_allowlist.json +59 -572
- package/src/scripts/lint_knowledge_scale.ts +30 -0
- package/src/scripts/lint_originality_shingles.ts +210 -0
- package/src/scripts/orchestration_record.ts +1 -0
- package/src/scripts/rule_backlinks.ts +0 -0
- package/src/scripts/schemas/agent-settings.schema.json +11 -0
- package/src/scripts/self_review_gate.ts +41 -15
- package/src/scripts/skill_eval_coverage.ts +71 -3
- package/src/scripts/skill_linter.ts +1 -0
- package/src/scripts/trigger_eval_rotation.ts +344 -0
- package/src/scripts/update_counts.ts +5 -17
- package/src/scripts/worktree_cleanup_check.ts +263 -0
- package/src/server/schemas/settings.ts +8 -3
- package/src/scripts/analysis_freshness.ts +0 -362
- package/src/scripts/export_replay_corpus.ts +0 -208
package/src/scripts/condense.ts
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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 →
|
|
8
|
-
* uninstall. Optional upgrade leg: install the last
|
|
9
|
-
* registry, then upgrade to the packed tarball and
|
|
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({
|
|
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
|
-
|
|
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
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
+
}
|