@event4u/agent-config 9.2.0 → 9.3.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 (43) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +33 -0
  3. package/README.md +1 -1
  4. package/dist/agent-src/commands/fix/route.md +49 -0
  5. package/dist/agent-src/commands/fix.md +4 -2
  6. package/dist/agent-src/commands/tdd/green.md +32 -0
  7. package/dist/agent-src/commands/tdd/red.md +33 -0
  8. package/dist/agent-src/commands/tdd/refactor.md +31 -0
  9. package/dist/agent-src/commands/tdd.md +50 -0
  10. package/dist/agent-src/contexts/communication/rules-auto/guidelines-mechanics.md +1 -0
  11. package/dist/agent-src/contexts/execution/subagent-spawn-contract.md +81 -2
  12. package/dist/agent-src/rules/architecture.md +7 -0
  13. package/dist/agent-src/skills/fe-design/SKILL.md +1 -0
  14. package/dist/agent-src/skills/judge-code-quality/SKILL.md +1 -0
  15. package/dist/agent-src/skills/persona-improvement/SKILL.md +81 -0
  16. package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +10 -0
  17. package/dist/agent-src/skills/subagent-orchestration/SKILL.md +12 -23
  18. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  19. package/dist/discovery/deprecation-report.md +1 -1
  20. package/dist/discovery/discovery-manifest.json +169 -16
  21. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  22. package/dist/discovery/discovery-manifest.summary.md +3 -3
  23. package/dist/discovery/orphan-report.md +1 -1
  24. package/dist/discovery/packs.json +11 -5
  25. package/dist/discovery/trust-report.md +2 -2
  26. package/dist/discovery/workspaces.json +10 -4
  27. package/dist/mcp/registry-manifest.json +3 -3
  28. package/docs/CLAIMS.md +2 -2
  29. package/docs/architecture.md +3 -3
  30. package/docs/command-flows.md +9 -3
  31. package/docs/contracts/command-clusters.md +2 -1
  32. package/docs/decisions/ADR-122-adversarial-verification-council.md +1 -1
  33. package/docs/development.md +10 -1
  34. package/docs/featured-skills.md +2 -2
  35. package/docs/getting-started-by-role.md +1 -1
  36. package/docs/governance-advantage.md +1 -1
  37. package/docs/guidelines/component-oriented-and-oop-development.md +181 -0
  38. package/docs/proof.md +5 -5
  39. package/package.json +3 -2
  40. package/src/scripts/ai_council/prompts.ts +7 -0
  41. package/src/scripts/hook_manifest.yaml +17 -6
  42. package/src/scripts/install-hooks.sh +32 -17
  43. package/src/scripts/pr_url_reminder_hook.ts +177 -0
@@ -33,7 +33,7 @@ the host lacks (a static host gives it no runtime attention guarantee, but a
33
33
  runtime-only system gives it nothing at all here). A runtime-only system contributes *nothing* on these hosts.
34
34
 
35
35
  ### 2. Intent routing, not a 500-artefact dump
36
- A prompt does not load all 275 skills. `dist/router.json` (101 routed rules, intent →
36
+ A prompt does not load all 276 skills. `dist/router.json` (101 routed rules, intent →
37
37
  skill, keyword/phrase/path triggers) carries the right skill on intent. The
38
38
  governance value is *selection discipline* — the host gets the relevant rule
39
39
  set, not the whole catalogue as context ballast.
@@ -0,0 +1,181 @@
1
+ # Component-Oriented & Object-Oriented Development
2
+
3
+ > Cross-cutting development standard: **think in reusable units, and — in an
4
+ > object-oriented / class-based codebase — treat object-oriented design as a
5
+ > high-priority default where it reduces complexity.** Split by surface below
6
+ > (frontend / backend), each pointing to the stack-specific carve-outs rather
7
+ > than restating them. Framework-neutral by construction — React / Storybook /
8
+ > Laravel / Symfony specifics live in their own skills, never here.
9
+
10
+ ## Paradigm-appropriate first — detect, then apply
11
+
12
+ **Reuse is universal; OOP is paradigm-scoped.** Detect the codebase's paradigm
13
+ before applying the object-oriented half: class-based / OO code (PHP, Java, C#,
14
+ typical TS/React service layers) → this standard's OOP guidance is the
15
+ high-priority default. A **functional / pipeline / actor-model** codebase
16
+ (Elixir/Phoenix, Clojure, Haskell, Go-idiomatic, heavy FP-in-TS) → apply *that*
17
+ paradigm's principles (immutability, pure functions, pattern-matching,
18
+ composition of functions), and read the OOP bullets as "the same rigor, its
19
+ paradigm's shape." **Never impose classes on functional code to satisfy this
20
+ standard** — consistency with the codebase's chosen paradigm wins over OOP
21
+ advocacy, always.
22
+
23
+ ## The standard, in one breath
24
+
25
+ - **Reuse before you build** (every paradigm). Compose from units that already
26
+ exist — audit first, reuse the design system / shared abstractions, extract a
27
+ new unit only when the same shape repeats.
28
+ - **Object-oriented where it fits** (in an OO codebase). Encapsulation, single
29
+ responsibility, composition over inheritance, and behavior-rich objects are
30
+ the high-priority default *when they reduce complexity* — not a quota to hit,
31
+ and not imposed on a codebase that has chosen a different paradigm.
32
+
33
+ ## The load-bearing caveat — reuse/OOP is a tool, never a mandate to abstract
34
+
35
+ This standard **never** authorizes speculative abstraction. It is bounded by the
36
+ package's restraint posture and defers to it on every conflict:
37
+
38
+ - [`minimal-safe-diff`](../../src/rules/minimal-safe-diff.md) (+ its
39
+ anti-over-engineering mechanics) — smallest change; no premature abstraction,
40
+ no speculative feature, no rewrite for taste.
41
+ - [`senior-engineering-discipline`](../../src/rules/senior-engineering-discipline.md)
42
+ — reuse the *existing* abstraction; generalize, don't overfit; never invent an
43
+ API to look object-oriented.
44
+ - [`improve-before-implement`](../../src/rules/improve-before-implement.md) —
45
+ "does it fit the existing architecture?" before adding structure.
46
+ - **When NOT to use a pattern** ([`php/patterns.md`](php/patterns.md) §) — KISS &
47
+ YAGNI trump pattern purity: no interface for a single implementation, no
48
+ pattern for a five-line function, no class where a function is clearer.
49
+
50
+ Rule of thumb: **two real repetitions (or a genuine second axis of change)
51
+ before you extract.** One occurrence is not a component; one branch is not a
52
+ Strategy. A reviewer should be able to name the concrete duplication or the
53
+ concrete change-axis the abstraction removes.
54
+
55
+ **Precedence (explicit): `minimal-safe-diff` / YAGNI always trump OOP
56
+ advocacy.** When "prefer OO shape" and "smallest change" conflict, the smallest
57
+ change wins — this standard adds a *consideration*, never a competing objective
58
+ that pressures preemptive abstraction. The review lens (below) may flag OO shape
59
+ only where the duplication or the growing branch is **already present in the
60
+ diff**, never as "this could grow later."
61
+
62
+ ## Frontend — think in reusable components
63
+
64
+ Component-first is the default mindset, not an afterthought: decompose a screen
65
+ into composable units, reuse the project's primitives, and extract shared shells
66
+ when they repeat. Depth and mechanics already live in dedicated skills — consult
67
+ them, don't duplicate:
68
+
69
+ - [`fe-design`](../../src/skills/fe-design/SKILL.md) § Component Architecture —
70
+ the stack-agnostic "kind, not framework" taxonomy, granularity, extract-on-3+.
71
+ - [`ui-component-architect`](../../src/skills/ui-component-architect/SKILL.md) —
72
+ how to *shape* a reusable component: composition over inheritance, slot
73
+ contracts, prop-API caps, controlled/uncontrolled, the componentization
74
+ threshold (repeat **and** real state).
75
+ - [`existing-ui-audit`](../../src/skills/existing-ui-audit/SKILL.md) — the
76
+ reuse-before-invent gate: inventory existing components/tokens/primitives
77
+ first (enforced by [`ui-audit-gate`](../../src/rules/ui-audit-gate.md)).
78
+ - [`design-tokens`](../../src/skills/design-tokens/SKILL.md) /
79
+ [`design-system-capture`](../../src/skills/design-system-capture/SKILL.md) —
80
+ the shared token/design-system layer components draw from.
81
+
82
+ **Component workshop (Storybook et al.) — when the project is large enough.**
83
+ For a project with a real, growing shared-component library, isolating and
84
+ documenting each reusable component in a component workshop (Storybook or the
85
+ ecosystem equivalent) pays for itself — it makes components discoverable,
86
+ reviewable in isolation, and reused instead of re-invented. It is **not** worth
87
+ the setup for a small surface with a handful of one-off components. The
88
+ framework-specific setup is a carve-out, not a generic mandate — React /
89
+ Storybook specifics: [`react-shadcn-ui`](../../src/skills/react-shadcn-ui/SKILL.md)
90
+ § Component workshop.
91
+
92
+ Framework carve-outs (never restated here): React →
93
+ [`react-shadcn-ui`](../../src/skills/react-shadcn-ui/SKILL.md); Next.js
94
+ server/client component boundary →
95
+ [`nextjs-patterns`](../../src/skills/nextjs-patterns/SKILL.md); Laravel FE →
96
+ [`blade-ui`](../../src/skills/blade-ui/SKILL.md) /
97
+ [`livewire`](../../src/skills/livewire/SKILL.md) /
98
+ [`flux`](../../src/skills/flux/SKILL.md); styling →
99
+ [`tailwind-engineer`](../../src/skills/tailwind-engineer/SKILL.md).
100
+
101
+ ## Backend — object-oriented where it reduces complexity
102
+
103
+ The structural half — thin handlers, business logic in services / use-cases,
104
+ behavior-rich domain models, validation at the boundary — is owned by
105
+ [`architecture`](../../src/rules/architecture.md) and holds across paradigms. In
106
+ an **object-oriented / class-based** service layer, layer the OO principles below
107
+ on top of it, each applied only where it earns its place. In a **functional /
108
+ pipeline** codebase, apply that paradigm's equivalent (pure functions +
109
+ composition instead of behavior-rich objects; a dispatch map / pattern-match
110
+ instead of a Strategy class) — the goal (encapsulate change, one responsibility,
111
+ compose don't duplicate) is paradigm-independent; only its *shape* is OO here:
112
+
113
+ - **Encapsulation** — objects own their data + the behavior over it; no anemic
114
+ bags mutated from the outside.
115
+ - **Single responsibility** — one class, one reason to change; no god services.
116
+ - **Composition over inheritance** — inject collaborators; reach for a base
117
+ class only for genuine is-a substitutability, not code sharing.
118
+ - **Behavior-rich domain objects** — push rules into the model, keep services
119
+ orchestration-thin.
120
+ - **Patterns where they earn their place** — a Strategy when a branch on a
121
+ discriminator will grow (`if-else` chain → polymorphism); a Factory when
122
+ construction is non-trivial; DI on an abstraction when there is a *real*
123
+ second implementation. Not before.
124
+
125
+ Language / framework carve-outs (never restated here): PHP →
126
+ [`php-coding-patterns`](php/php-coding-patterns.md) +
127
+ [`patterns.md`](php/patterns.md) (+ `patterns/strategy.md`,
128
+ `patterns/dependency-injection.md`, `patterns/factory.md`, `patterns/service-layer.md`)
129
+ + skills [`php-coder`](../../src/skills/php-coder/SKILL.md) /
130
+ [`php-service`](../../src/skills/php-service/SKILL.md); Laravel →
131
+ [`laravel`](../../src/skills/laravel/SKILL.md); Symfony →
132
+ [`symfony-workflow`](../../src/skills/symfony-workflow/SKILL.md); Next.js / TS →
133
+ [`nextjs-patterns`](../../src/skills/nextjs-patterns/SKILL.md); cross-language
134
+ clean-code baseline → [`code-clarity`](code-clarity.md).
135
+
136
+ ## Enums over scattered literals
137
+
138
+ A field with two or more non-boolean states is an enum, not a bare string /
139
+ magic number — the small OO move that keeps a codebase greppable and safe to
140
+ change. Full rule + the defer-and-ask flow for pre-existing literals:
141
+ [`prefer-enums-over-literals`](../../src/rules/prefer-enums-over-literals.md).
142
+
143
+ ## How this is reviewed
144
+
145
+ "Reuse existing components + object-oriented shape (where it reduces
146
+ complexity)" is a check on the code-quality review lens
147
+ ([`judge-code-quality`](../../src/skills/judge-code-quality/SKILL.md)), so it
148
+ reaches `/review-changes`, `/judge`, the subagent judge modes, and team-mode's
149
+ fallback review frame. It fires **only where the duplication or the growing
150
+ branch is already present in the diff** — never "this could grow later" — and
151
+ in the **codebase's own paradigm** (it will not tell a functional service to
152
+ grow a class). It stays a human-judgment call that defers to the codebase's own
153
+ conventions — never a generic SOLID dogma imposed over a project that has chosen
154
+ differently.
155
+
156
+ ## Council review (2026-07-15)
157
+
158
+ Deep debate, 3 rounds, cross-vendor (`anthropic/claude-sonnet-4-5` +
159
+ `openai/gpt-4o`), $0.16. Split verdict; the load-bearing (anthropic) critique —
160
+ that an "OOP-supreme default" over-indexes on OOP for a multi-paradigm consumer
161
+ base and can conflict with `minimal-safe-diff` — was applied, bounded by this
162
+ standard's stated purpose (OOP *is* a deliberate high priority for the operator):
163
+
164
+ - **Paradigm-appropriate framing** — OOP is the high-priority default *within an
165
+ OO / class-based codebase*; detect and defer to a functional / pipeline /
166
+ actor paradigm; never impose classes on functional code (§ Paradigm-appropriate).
167
+ - **Explicit precedence** — `minimal-safe-diff` / YAGNI win on conflict; the
168
+ review lens flags only *already-present* duplication / complexity, never
169
+ preemptive (§ caveat, § How this is reviewed).
170
+
171
+ Divergence held: the frontend section stays (an intentional FE / backend
172
+ entry-point + carve-out index, per the operator's explicit "split by FE /
173
+ backend" ask) — against the anthropic "delete as redundant"; it is a cohesive
174
+ index of the existing FE surfaces, not a claim of novel guidance. Trace:
175
+ `agents/runtime/council/responses/component-oop-standard.json/` (gitignored, local-only).
176
+
177
+ ## See also
178
+
179
+ - [`architecture`](../../src/rules/architecture.md) — the structural home this layers onto.
180
+ - [`code-clarity`](code-clarity.md) — the cross-language clean-code baseline.
181
+ - [`minimal-safe-diff`](../../src/rules/minimal-safe-diff.md) · [`senior-engineering-discipline`](../../src/rules/senior-engineering-discipline.md) — the restraint anchors this standard is bounded by.
package/docs/proof.md CHANGED
@@ -27,7 +27,7 @@ evidence pointer, or `task check-claims` fails the build.
27
27
  | Claim | Kind | Evidence | Resolves |
28
28
  |---|---|---|---|
29
29
  | The release process is documented as an inheritable runbook + succession doc, and the project's bus-factor (trailing-90-day distinct human reviewers) is tracked and reported truthfully — currently 1, not implied to be more. | qual | `docs/succession.md#trailing 90 days` | ✅ |
30
- | 184 commands. | quant | `src/scripts/check_artefact_count_messaging.ts#Artefact-count messaging gate` | ✅ |
30
+ | 189 commands. | quant | `src/scripts/check_artefact_count_messaging.ts#Artefact-count messaging gate` | ✅ |
31
31
  | A MEASURED-BUT-NOT-SHIPPED experiment — the thin rule projection reduced eager rule load 78,513 → 13,881 GPT-tokens (whole always-loaded projection 98,529 → 33,897, ~65.6%), but FAILED the quality gate (thin win-rate 36.2% vs required 48%) and does not ship; it un-defers only behind `discipline_profile: essential`. Shipped behavior does NOT include this reduction. Method: `agent-config benchmark` over the pinned token baseline; the baseline is the honest "what the user pays if everything loads eagerly", NOT a synthetic full-corpus strawman (council Q4); quality gate per the Phase-0 paired judge run. | quant | `internal/bench/reports/token-baseline.json#eager_rule_load` | ✅ |
32
32
  | The first cross-vendor parity pass (5 orchestration-corpus tasks × 2 vendors × 3 repeats, identical prompts via the council transport, $0.16) measured real per-host finding-count differences — claude-sonnet-4-5 surfaced ~2× the findings of gpt-4o on the multi-file analysis task (median 11 vs 5) while both vendors were identical on the planted hollow-implementation task (2 vs 2) and perfectly silent on the clean-code negative control (0 vs 0, no spurious findings). The per-task `finding_floor` values are calibrated from the cross-host lower envelope and the gate is armed. | quant | `internal/bench/reports/parity-count.json#min over hosts of median` | ✅ |
33
33
  | On a weak host (claude-haiku-4-5) the package produces a significant, placebo-controlled discipline lift on scope/downstream traps; on a strong host the same measurement is a published null — the package transplants discipline a weak model lacks, not model intelligence. | quant | `docs/benchmark.md#weak-host-specific` | ✅ |
@@ -45,7 +45,7 @@ evidence pointer, or `task check-claims` fails the build.
45
45
  | On a deterministic multi-session recall corpus, the memory substrate produces a measured, placebo-controlled recall lift — memory-on 27/27 vs no-memory 10/27 and vs equal-byte placebo 9/27 (claude-haiku-4-5, n=9 tasks x 3 seeds, sign test p=0.031 for BOTH pairings). Scoped honestly: this is the context-value upper bound (perfect retrieval on a one-fact-per-task corpus), not retrieval precision under a large store. | quant | `internal/bench/reports/second-brain-delta.json` | ✅ |
46
46
  | Removing the perfect-retrieval assumption, the substrate's REAL keyword retrieval recalls the needed decision into the top-5 under keyword-overlapping confusers (precision@5 9/9) and the model disambiguates it from the co-injected confusers — retrieval-on 27/27 vs no-memory 5/27 and vs equal-count placebo 5/27 (claude-haiku-4-5, 9 tasks x 3 seeds, sign test p=0.008 both). Named limit: retrieval RECALLS but does not RANK (mean tie-set 3.3, ties broken by store order, not relevance) — the discrimination gap that motivates the SQLite-FTS5 activation path (ADR-116) at larger scale. | quant | `internal/bench/reports/second-brain-retrieval.json` | ✅ |
47
47
  | Every artifact the package ships — source AND the condensed projection that reaches consumers — is machine-scanned in CI for hidden-Unicode, mixed-script-confusable, and instruction-smuggling payloads (the rules-file-backdoor class); a finding blocks the release before `npm publish`, not just the merge. | qual | `.github/workflows/publish-npm.yml#lint_agent_security` | ✅ |
48
- | 275 skills. | quant | `src/scripts/check_artefact_count_messaging.ts#Artefact-count messaging gate` | ✅ |
48
+ | 276 skills. | quant | `src/scripts/check_artefact_count_messaging.ts#Artefact-count messaging gate` | ✅ |
49
49
  | Removes only its own keys from a shared host config (matched by JSON-pointer + SHA-256), never a neighbour tool's entries. | qual | `docs/contracts/install-layout.md#JSON-pointer` | ✅ |
50
50
 
51
51
  **21 backed claim(s)** — all evidence pointers resolve in CI.
@@ -77,11 +77,11 @@ shipping theater; the full per-cell data is committed at
77
77
  `internal/bench/reports/persona-placebo.json`.
78
78
 
79
79
  **Behavioural-eval coverage — the honest baseline.** Skill *quality* is only
80
- as good as its measurement. Today **42 of 275** skills carry a behavioural
80
+ as good as its measurement. Today **42 of 276** skills carry a behavioural
81
81
  `evals.json`; the highest-traffic / highest-cost tiers (default-surface +
82
82
  `rich` + routers) are **fully covered (35 of 35)**, the long tail
83
- (7 of 240) is not. We publish that gap rather than imply
84
- "275 evaluated skills": coverage is measured per tier
83
+ (7 of 241) is not. We publish that gap rather than imply
84
+ "276 evaluated skills": coverage is measured per tier
85
85
  (`./scripts-run src/scripts/skill_eval_coverage`), **CI-ratcheted so it can
86
86
  only rise**, and the priority tiers carry a hard **tier floor**: every
87
87
  rich / default-surface / router skill MUST have a behavioural eval or an
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@event4u/agent-config",
3
- "version": "9.2.0",
3
+ "version": "9.3.0",
4
4
  "description": "Universal AI Agent OS — audited skills, governance rules, commands, and templates for AI coding tools (Claude Code, Cursor, Windsurf, Copilot).",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -70,7 +70,8 @@
70
70
  "lint:ts": "eslint 'src/**/*.ts'",
71
71
  "test:ts": "vitest run",
72
72
  "prepack": "npm run build && npm run build:discovery && npm run build:mcp-manifest && node src/scripts/prepack-check.mjs",
73
- "prepublishOnly": "./scripts-run src/scripts/check_release_includes_discovery"
73
+ "prepublishOnly": "./scripts-run src/scripts/check_release_includes_discovery",
74
+ "prepare": "[ -d .git ] && bash src/scripts/install-hooks.sh || true"
74
75
  },
75
76
  "publishConfig": {
76
77
  "access": "public",
@@ -88,6 +88,13 @@ correctness lens AND a shipping-risk lens:
88
88
  Is the blast radius bigger than the title implies?
89
89
  5. Reviewer fatigue — is anything in the diff that a tired reviewer
90
90
  would rubber-stamp but should not?
91
+ 6. Reuse & object-oriented shape — does the diff reinvent a component
92
+ or abstraction the codebase likely already provides (should compose
93
+ / reuse instead), and where it adds structure, is the OO shape
94
+ (encapsulation, composition over inheritance) justified by
95
+ complexity it removes rather than speculative abstraction? Judge in
96
+ the codebase's own paradigm — do not push classes onto functional /
97
+ pipeline code; smallest-change still wins over OO advocacy.
91
98
  End with: APPROVE / REQUEST_CHANGES / REJECT, one sentence why, and
92
99
  the single highest-leverage change the PR author should make before
93
100
  merge.`;
@@ -145,6 +145,17 @@ concerns:
145
145
  script: src/scripts/team_review_gate_hook.ts
146
146
  args: []
147
147
  fail_closed: false
148
+ # Reply-shape backstop for the direct-answers rule (reply-close): a PR
149
+ # created THIS turn must end the reply as the literal last raw URL. This
150
+ # PostToolUse hook fires right after a PR *create* (gh pr create, or a
151
+ # github create-pull-request tool) that produced a `…/pull/<n>` URL, and
152
+ # WARNS in context (exit 2) so the reminder lands before the reply closes.
153
+ # Never blocks; unconditional (reinforces an always-on rule, fires only on
154
+ # the rare create event, so no settings gate). fail_closed: false.
155
+ pr-url-reminder:
156
+ script: src/scripts/pr_url_reminder_hook.ts
157
+ args: []
158
+ fail_closed: false
148
159
 
149
160
  platforms:
150
161
  augment:
@@ -152,7 +163,7 @@ platforms:
152
163
  session_end: [chat-history]
153
164
  stop: [chat-history, hot-context, verify-before-complete]
154
165
  pre_tool_use: [block-no-verify, rtk-wrap, design-slop]
155
- post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan]
166
+ post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
156
167
 
157
168
  claude:
158
169
  session_start: [chat-history, hot-context, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe]
@@ -160,7 +171,7 @@ platforms:
160
171
  stop: [chat-history, hot-context, verify-before-complete, team-review-gate]
161
172
  user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff]
162
173
  pre_tool_use: [block-no-verify, rtk-wrap, design-slop]
163
- post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan]
174
+ post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
164
175
 
165
176
  # Cowork — the Claude desktop app's local-agent-mode runtime, built
166
177
  # on top of the Claude Code CLI. Same lifecycle vocabulary, same
@@ -182,7 +193,7 @@ platforms:
182
193
  stop: [chat-history, hot-context, verify-before-complete]
183
194
  user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff]
184
195
  pre_tool_use: [block-no-verify, rtk-wrap, design-slop]
185
- post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan]
196
+ post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
186
197
 
187
198
  # Phase 7.5 — Cursor. `.cursor/hooks.json` (project) is read by the
188
199
  # IDE and CLI; `~/.cursor/hooks.json` (user) is opt-in via
@@ -196,7 +207,7 @@ platforms:
196
207
  session_end: [chat-history]
197
208
  stop: [chat-history, hot-context, verify-before-complete]
198
209
  user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff]
199
- post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan]
210
+ post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
200
211
 
201
212
  # Phase 7.6 — Cline. Hooks live under `.clinerules/hooks/<HookName>`
202
213
  # (project, no file extension, must be executable per Cline docs) or
@@ -211,7 +222,7 @@ platforms:
211
222
  session_end: [chat-history]
212
223
  stop: [chat-history, hot-context, verify-before-complete]
213
224
  user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff]
214
- post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan]
225
+ post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
215
226
 
216
227
  # Phase 7.7 — Windsurf (Cascade). Hooks live at `.windsurf/hooks.json`
217
228
  # (project) or `~/.codeium/windsurf/hooks.json` (user). Cascade has
@@ -245,7 +256,7 @@ platforms:
245
256
  session_end: [chat-history]
246
257
  stop: [chat-history, hot-context, verify-before-complete]
247
258
  user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff]
248
- post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan]
259
+ post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
249
260
 
250
261
  # Phase 7.9 — Copilot has no hook surface. Concerns route through
251
262
  # rule-only fallback; the dispatcher silently no-ops on --platform copilot.
@@ -8,32 +8,47 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8
8
  # This installer lives at src/scripts/install-hooks.sh, so the repo root
9
9
  # (which owns .git/) is two levels up.
10
10
  PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
11
- HOOKS_DIR="$PROJECT_ROOT/.git/hooks"
11
+ # Resolve the SHARED hooks dir via git so this also works from a linked worktree
12
+ # (where $PROJECT_ROOT/.git is a file, not a dir, and a literal .git/hooks path
13
+ # would break under `set -e`). --git-common-dir points at the main .git even from
14
+ # a worktree; fall back to the literal path only outside a git repo.
15
+ if COMMON_GIT_DIR="$(git -C "$PROJECT_ROOT" rev-parse --git-common-dir 2>/dev/null)"; then
16
+ case "$COMMON_GIT_DIR" in /*) : ;; *) COMMON_GIT_DIR="$PROJECT_ROOT/$COMMON_GIT_DIR" ;; esac
17
+ HOOKS_DIR="$COMMON_GIT_DIR/hooks"
18
+ else
19
+ HOOKS_DIR="$PROJECT_ROOT/.git/hooks"
20
+ fi
12
21
 
13
22
  mkdir -p "$HOOKS_DIR"
14
23
 
15
24
  cat > "$HOOKS_DIR/pre-push" << 'EOF'
16
25
  #!/usr/bin/env bash
17
- # Pre-push hook: verify dist/agent-src/ is in sync with .agent-src.uncondensed/
18
- # and that the canonical command count matches README + getting-started docs.
26
+ # Pre-push hook: mirror the CI "Sync + Generate Tools Consistency" gate LOCALLY
27
+ # so derived-output drift is caught BEFORE push, not in remote CI.
19
28
  #
20
- # The command-count gate exists because three consecutive PRs landed
21
- # post-CI count-drift fixes (e.g. f2fb0026 "bump command count
22
- # 101→103"). Catching the drift pre-push stops it from flooding remote
23
- # CI. Runtime ~0.1s.
29
+ # History: the remote "Consistency" check failed in a large share of PRs because
30
+ # the old hook only verified dist sync + the COMMAND count — it missed
31
+ # guideline/skill/rule count drift and the generate-tools / router / corpus
32
+ # outputs (e.g. a new guideline bumping the count in README + docs/architecture).
33
+ # `task consistency` is the EXACT CI mirror (sync-check + sync-check-hashes +
34
+ # sync + generate-tools + compile-router + compile-corpus + `git diff --quiet`),
35
+ # so any derived-output drift blocks the push here. Runtime ~15-40s (it
36
+ # regenerates the tool trees) — cheaper than a red CI run and a fixup re-push.
24
37
 
25
38
  fail=0
26
39
 
27
- echo "🔍 Checking dist/agent-src/ sync..."
28
- if ! ./scripts-run src/scripts/condense --check; then
29
- echo "❌ dist/agent-src/ is out of sync. Run 'task sync' and condense changed .md files, then commit."
30
- fail=1
31
- fi
32
-
33
- echo "🔍 Checking command count messaging..."
34
- if ! ./scripts-run src/scripts/check_command_count_messaging; then
35
- echo "❌ Command-count drift in README / AGENTS.md / getting-started. Run 'task counts-update', stage the changes, then re-commit."
36
- fail=1
40
+ if ! command -v task >/dev/null 2>&1; then
41
+ echo "⚠️ 'task' (go-task) not found — cannot run the consistency gate locally."
42
+ echo " Install it (https://taskfile.dev) so pre-push can mirror the CI check;"
43
+ echo " skipping the consistency check for this push."
44
+ else
45
+ echo "🔍 Consistency — mirroring the CI 'Sync + Generate Tools Consistency' gate..."
46
+ if ! task consistency; then
47
+ echo "❌ Derived outputs drifted (counts / dist / generated tool trees / router / corpus)."
48
+ echo " 'task consistency' just regenerated them into your working tree —"
49
+ echo " review + 'git add' the changes (or run 'task consistency-fix'), commit, then re-push."
50
+ fail=1
51
+ fi
37
52
  fi
38
53
 
39
54
  echo "🔍 Checking for leftover conflict markers / unmerged paths..."
@@ -0,0 +1,177 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * PostToolUse PR-URL reminder — reply-shape backstop for `direct-answers`.
4
+ *
5
+ * The `direct-answers` rule (reply-close) requires: a PR created THIS turn puts
6
+ * its raw URL as the **literal last line** of the reply. That is a behavioural
7
+ * rule with no deterministic backstop, so it can be silently missed — the URL
8
+ * ends up inline mid-reply instead of last. This hook fires right after a
9
+ * PR-creating tool call, extracts the created PR URL from the tool output, and
10
+ * WARNS in context (exit 2) so the reminder lands exactly when the PR is born,
11
+ * before the reply closes.
12
+ *
13
+ * Fires ONLY on an actual PR *create*:
14
+ * - a shell command matching `gh pr create`, OR
15
+ * - a GitHub tool whose name means "create a pull request"
16
+ * (e.g. create_pull_request / createPullRequest / pulls.create),
17
+ * AND only when the tool output actually contains a `…/pull/<n>` URL — so a
18
+ * failed / dry-run create (no URL) does not nag, and `gh pr view/checks/list/
19
+ * comment` (which also print PR URLs) never fire it.
20
+ *
21
+ * Warn only — never blocks (fail_closed: false). Unconditional: it reinforces
22
+ * an always-on rule and only fires on the rare PR-create event, so it carries
23
+ * no settings gate (mirrors verify-before-complete / roadmap-progress).
24
+ *
25
+ * Exit codes (dispatcher contract): 0 allow · 2 warn (+ `{decision,reason}`
26
+ * JSON on stdout).
27
+ */
28
+ import fs from "node:fs";
29
+ import path from "node:path";
30
+ import { fileURLToPath, pathToFileURL } from "node:url";
31
+
32
+ const EXIT_ALLOW = 0;
33
+ const EXIT_WARN = 2;
34
+
35
+ type JsonValue = string | number | boolean | null | JsonValue[] | { [k: string]: JsonValue };
36
+ type JsonObject = { [k: string]: JsonValue };
37
+
38
+ // Tool names across platforms whose command field carries a shell command
39
+ // (kept in lock-step with verify_before_complete_hook.COMMAND_TOOLS).
40
+ const COMMAND_TOOLS: ReadonlySet<string> = new Set([
41
+ "launch-process",
42
+ "launch_process",
43
+ "Bash",
44
+ "BashTool",
45
+ "run-process",
46
+ "runProcess",
47
+ "shell",
48
+ "execute_shell",
49
+ "RunShellCommand",
50
+ ]);
51
+
52
+ // A shell command that CREATES a pull request. Anchored on a shell separator so
53
+ // it matches inside a chained command (`git push && gh pr create …`) but not a
54
+ // substring of another word. `gh pr view/checks/list/comment` do not match.
55
+ const _GH_PR_CREATE = /(?:^|[\s;&|`(])gh\s+pr\s+create\b/i;
56
+
57
+ // A non-shell tool whose NAME means "create a pull request" (github-api / MCP).
58
+ const _CREATE_PR_TOOL = /(?:create[_-]?pull_?request|createpullrequest|pulls[._-]create|create_pr\b)/i;
59
+
60
+ // A GitHub pull-request URL as printed by `gh pr create` / returned as html_url.
61
+ const _PR_URL = /https?:\/\/github\.com\/[^\s"'`<>]+\/pull\/\d+/i;
62
+
63
+ function _isObject(v: unknown): v is JsonObject {
64
+ return typeof v === "object" && v !== null && !Array.isArray(v);
65
+ }
66
+
67
+ /** Return [tool_name, command_text] from a tool-event payload. */
68
+ function _extractCommand(payload: JsonObject): [string | null, string | null] {
69
+ const toolRaw = payload["tool_name"] ?? payload["toolName"] ?? payload["tool"];
70
+ const tool = typeof toolRaw === "string" ? toolRaw : null;
71
+ if (!(tool !== null && COMMAND_TOOLS.has(tool))) {
72
+ return [tool, null];
73
+ }
74
+ const ti = payload["tool_input"];
75
+ if (_isObject(ti)) {
76
+ for (const key of ["command", "cmd", "shell_command"]) {
77
+ const v = ti[key];
78
+ if (typeof v === "string" && v) {
79
+ return [tool, v];
80
+ }
81
+ }
82
+ }
83
+ for (const key of ["command", "cmd"]) {
84
+ const v = payload[key];
85
+ if (typeof v === "string" && v) {
86
+ return [tool, v];
87
+ }
88
+ }
89
+ return [tool, null];
90
+ }
91
+
92
+ /** Best-effort extraction of the tool-output text from the envelope. */
93
+ function _toolOutput(envelope: JsonObject): string {
94
+ for (const key of ["tool_response", "tool_result", "toolResponse", "output", "result", "stdout"]) {
95
+ const v = envelope[key];
96
+ if (typeof v === "string") {
97
+ return v;
98
+ }
99
+ if (_isObject(v) || Array.isArray(v)) {
100
+ return JSON.stringify(v);
101
+ }
102
+ }
103
+ return JSON.stringify(envelope);
104
+ }
105
+
106
+ /** True when this tool event created a pull request. */
107
+ function _isPrCreate(tool: string | null, command: string | null): boolean {
108
+ if (command !== null && _GH_PR_CREATE.test(command)) {
109
+ return true;
110
+ }
111
+ return tool !== null && _CREATE_PR_TOOL.test(tool);
112
+ }
113
+
114
+ export function main(): number {
115
+ let envelope: JsonValue;
116
+ try {
117
+ const raw = _readStdin();
118
+ envelope = raw.trim() ? (JSON.parse(raw) as JsonValue) : {};
119
+ } catch {
120
+ return EXIT_ALLOW; // never block on a malformed envelope
121
+ }
122
+ if (!_isObject(envelope)) {
123
+ return EXIT_ALLOW;
124
+ }
125
+
126
+ const [tool, command] = _extractCommand(envelope);
127
+ if (!_isPrCreate(tool, command)) {
128
+ return EXIT_ALLOW;
129
+ }
130
+
131
+ const match = _PR_URL.exec(_toolOutput(envelope));
132
+ if (match === null) {
133
+ return EXIT_ALLOW; // create attempted but no URL produced (failed / dry-run) — do not nag
134
+ }
135
+ const url = match[0];
136
+
137
+ // `reason` (≤ 200 chars) goes to stderr; `additional_context` is what
138
+ // surfaces back to the model on platforms that support it — so the
139
+ // actionable instruction lives there (a stderr-only reason would not
140
+ // change the reply that this hook exists to shape).
141
+ const reason = `PR created this turn (${url}) — end your reply with this raw URL as the literal last line (direct-answers/reply-close).`;
142
+ const additional_context =
143
+ `You created a pull request this turn: ${url}\n\n` +
144
+ `Per the direct-answers rule (reply-close): a PR created this turn must appear ` +
145
+ `as the raw URL on the LITERAL LAST LINE of your reply — not inline mid-message. ` +
146
+ `Before ending the turn, make your final line exactly:\n${url}\n` +
147
+ `If it already is, no action needed.`;
148
+ process.stdout.write(`${JSON.stringify({ decision: "warn", reason, additional_context })}\n`);
149
+ return EXIT_WARN;
150
+ }
151
+
152
+ function _readStdin(): string {
153
+ return fs.readFileSync(0, "utf-8");
154
+ }
155
+
156
+ function _isCliEntry(): boolean {
157
+ if (process.argv[1] === undefined) {
158
+ return false;
159
+ }
160
+ const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
161
+ if (import.meta.url === argvUrl) {
162
+ return true;
163
+ }
164
+ // Symlinked invocation (installed projection, or macOS /var → /private/var
165
+ // temp dirs) makes the raw URLs differ; compare realpaths so the guard fires.
166
+ try {
167
+ const here = fs.realpathSync(fileURLToPath(import.meta.url));
168
+ const argv = fs.realpathSync(path.resolve(process.argv[1]));
169
+ return here === argv;
170
+ } catch {
171
+ return false;
172
+ }
173
+ }
174
+
175
+ if (_isCliEntry()) {
176
+ process.exit(main());
177
+ }