@event4u/agent-config 9.24.0 → 9.26.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 (97) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +136 -0
  3. package/README.md +1 -1
  4. package/dist/agent-src/commands/analyze/conformance.md +182 -0
  5. package/dist/agent-src/commands/analyze/inbox.md +33 -2
  6. package/dist/agent-src/commands/review/changes.md +6 -5
  7. package/dist/agent-src/commands/roadmap/next.md +194 -0
  8. package/dist/agent-src/commands/roadmap.md +12 -4
  9. package/dist/agent-src/contexts/execution/subagent-routing.md +4 -1
  10. package/dist/agent-src/rules/decision-revisit-gate.md +2 -0
  11. package/dist/agent-src/rules/delegation-policy.md +3 -2
  12. package/dist/agent-src/rules/design-fidelity.md +17 -1
  13. package/dist/agent-src/rules/design-review-after-ui-write.md +116 -0
  14. package/dist/agent-src/rules/evaluator-independence.md +111 -0
  15. package/dist/agent-src/rules/session-canary.md +15 -1
  16. package/dist/agent-src/rules/settings-ask-protocol.md +176 -0
  17. package/dist/agent-src/rules/token-budget-discipline.md +6 -3
  18. package/dist/agent-src/rules/ui-audit-gate.md +3 -0
  19. package/dist/agent-src/rules/user-interaction.md +23 -0
  20. package/dist/agent-src/skills/finishing-a-development-branch/SKILL.md +1 -1
  21. package/dist/agent-src/skills/recursive-verification/SKILL.md +13 -0
  22. package/dist/agent-src/skills/rule-writing/SKILL.md +45 -0
  23. package/dist/agent-src/skills/skill-writing/SKILL.md +114 -0
  24. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  25. package/dist/cli/registry.js +1 -0
  26. package/dist/cli/registry.js.map +1 -1
  27. package/dist/cli-delegate/cmd_route_audit.js +4 -1
  28. package/dist/cli-delegate/cmd_route_explain.js +4 -1
  29. package/dist/discovery/deprecation-report.md +1 -1
  30. package/dist/discovery/discovery-manifest.json +146 -28
  31. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  32. package/dist/discovery/discovery-manifest.summary.md +5 -5
  33. package/dist/discovery/orphan-report.md +1 -1
  34. package/dist/discovery/packs.json +16 -11
  35. package/dist/discovery/trust-report.md +3 -3
  36. package/dist/discovery/workspaces.json +12 -7
  37. package/dist/hooks/dispatch.js +1501 -511
  38. package/dist/mcp/registry-manifest.json +3 -3
  39. package/dist/mcp/server.json +2 -2
  40. package/dist/router.json +1 -1
  41. package/dist/server/nicknameResolver.js +116 -0
  42. package/dist/server/nicknameResolver.js.map +1 -0
  43. package/dist/server/routes/ping.js +29 -11
  44. package/dist/server/routes/ping.js.map +1 -1
  45. package/dist/shared/interactiveContext.js +73 -0
  46. package/dist/shared/interactiveContext.js.map +1 -0
  47. package/dist/shared/nicknamePrefill.js +59 -0
  48. package/dist/shared/nicknamePrefill.js.map +1 -0
  49. package/dist/shared/settingsAsks.js +128 -0
  50. package/dist/shared/settingsAsks.js.map +1 -0
  51. package/dist/shared/settingsConsent.js +93 -0
  52. package/dist/shared/settingsConsent.js.map +1 -0
  53. package/package.json +1 -1
  54. package/src/config/agents-paths.yml +15 -0
  55. package/src/config/evaluator-budgets.json +68 -68
  56. package/src/config/gate-coverage.yml +131 -5
  57. package/src/config/gate-violation-baselines.json +11 -0
  58. package/src/scripts/_cli/cmd_route_audit.ts +4 -1
  59. package/src/scripts/_cli/cmd_route_explain.ts +9 -1
  60. package/src/scripts/_dispatch.bash +12 -0
  61. package/src/scripts/ai-video/stitch.sh +27 -9
  62. package/src/scripts/before_complete_hook.ts +146 -5
  63. package/src/scripts/bench_ab_v2_run.ts +272 -16
  64. package/src/scripts/bench_ab_v2_stats.ts +156 -3
  65. package/src/scripts/check_completion_review.ts +80 -3
  66. package/src/scripts/check_corpus_staleness.ts +692 -0
  67. package/src/scripts/check_depth_budget.ts +295 -0
  68. package/src/scripts/check_gate_coverage.ts +67 -0
  69. package/src/scripts/check_hook_bundle_freshness.ts +183 -0
  70. package/src/scripts/conformance_scan.ts +422 -0
  71. package/src/scripts/dispatch_r2_reviewer.ts +31 -2
  72. package/src/scripts/git_authorization_hook.ts +328 -0
  73. package/src/scripts/hook_manifest.yaml +69 -9
  74. package/src/scripts/hooks/block_no_verify.ts +3 -1
  75. package/src/scripts/hooks/block_unauthorized_git.ts +371 -0
  76. package/src/scripts/hooks/concern_registry.ts +8 -0
  77. package/src/scripts/hooks/design_slop_hook.ts +13 -1
  78. package/src/scripts/hooks/dispatch_hook.ts +97 -2
  79. package/src/scripts/hooks/evidence_independence.ts +368 -0
  80. package/src/scripts/language_mirror_hook.ts +456 -0
  81. package/src/scripts/lint_abstraction_thresholds.ts +41 -8
  82. package/src/scripts/lint_agents_layout.ts +2 -0
  83. package/src/scripts/lint_decision_impact.ts +246 -0
  84. package/src/scripts/lint_example_fences.ts +609 -0
  85. package/src/scripts/lint_hook_manifest.ts +29 -0
  86. package/src/scripts/lint_rule_interactions.ts +90 -0
  87. package/src/scripts/lint_rule_migration_ledger.ts +480 -0
  88. package/src/scripts/lint_skill_descriptions.ts +86 -2
  89. package/src/scripts/memory_learn_hook.ts +109 -6
  90. package/src/scripts/pr_url_reminder_hook.ts +17 -2
  91. package/src/scripts/release.ts +38 -1
  92. package/src/scripts/schemas/review-findings.schema.json +99 -0
  93. package/src/scripts/schemas/rule.schema.json +10 -0
  94. package/src/shared/interactiveContext.ts +98 -0
  95. package/src/shared/nicknamePrefill.ts +96 -0
  96. package/src/shared/settingsAsks.ts +176 -0
  97. package/src/shared/settingsConsent.ts +136 -0
@@ -0,0 +1,328 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Git-authorization ledger — `user_prompt_submit` concern.
4
+ *
5
+ * Records which git operations the user's OWN WORDS authorize this turn, so
6
+ * `hooks/block_unauthorized_git.ts` can check an operation against a fact
7
+ * instead of against the model's recollection of the conversation.
8
+ *
9
+ * WHY — the measured defect (30-session conformance audit, 2026-08-06):
10
+ *
11
+ * - A full release chain (prod-trunk merge, tag push, GitHub release, npm
12
+ * publish) executed after the agent itself wrote "das ist ein Hard-Floor-
13
+ * Schritt, dafür brauche ich Dein explizites Go" and never received one —
14
+ * the user's next turn was a pasted `git push … rejected` stack trace,
15
+ * which was read as implicit continuation.
16
+ * - Two complete PRs opened on turns carrying no git authorization at all.
17
+ * - Force-push over a bot's commits off a bare "fixe auch diese 4 pr's".
18
+ * - Twice the constraint was named in writing and contradicted minutes later.
19
+ *
20
+ * `commit-policy` and `scope-control` already forbid all of this in prose, and
21
+ * the prose was in context every time. The ledger is the missing fact, not a
22
+ * new obligation.
23
+ *
24
+ * State: `agents/state/git-authorization.json`
25
+ * { "session_id": str, "detected_at": iso8601, "authorized": [op, …],
26
+ * "evidence": { op: "phrase" }, "prompt_chars": int }
27
+ *
28
+ * Each user turn REPLACES the ledger — that is the point. `commit-policy`'s
29
+ * "one-shot authorization is not a standing license" says an authorization is
30
+ * spent on the operation it named; carrying it forward is the exact inference
31
+ * the rule forbids, so a new prompt with no authorization phrase yields an
32
+ * empty ledger rather than an inherited one.
33
+ *
34
+ * Never blocks. Exit 0 always. The blocking half lives in the pre_tool_use
35
+ * concern, which is where the operation is actually observable.
36
+ */
37
+ import fs from "node:fs";
38
+ import path from "node:path";
39
+ import { fileURLToPath, pathToFileURL } from "node:url";
40
+
41
+ import { atomic_write_json } from "./hooks/state_io.js";
42
+ import { readHookStdin } from "./hooks/hook_stdin.js";
43
+
44
+ const EXIT_ALLOW = 0;
45
+
46
+ export const STATE_FILE = path.join("agents", "state", "git-authorization.json");
47
+
48
+ type JsonValue = string | number | boolean | null | JsonValue[] | { [k: string]: JsonValue };
49
+ type JsonObject = { [k: string]: JsonValue };
50
+
51
+ function _isObject(v: unknown): v is JsonObject {
52
+ return typeof v === "object" && v !== null && !Array.isArray(v);
53
+ }
54
+
55
+ /** The operations the ledger can authorize. */
56
+ export type GitOp =
57
+ | "commit"
58
+ | "push"
59
+ | "branch"
60
+ | "pr-create"
61
+ | "pr-merge"
62
+ | "tag"
63
+ | "release"
64
+ | "publish";
65
+
66
+ export const ALL_OPS: readonly GitOp[] = [
67
+ "commit",
68
+ "push",
69
+ "branch",
70
+ "pr-create",
71
+ "pr-merge",
72
+ "tag",
73
+ "release",
74
+ "publish",
75
+ ];
76
+
77
+ /**
78
+ * Prose phrases, German and English, that authorize an operation.
79
+ *
80
+ * Deliberately a keyword list rather than an LLM judge: the classifier runs on
81
+ * every prompt and must be cheap, deterministic and reviewable. A miss costs a
82
+ * warn (or, on the irreversible subset, one confirmation); an LLM judge would
83
+ * cost latency on every turn and be unauditable.
84
+ */
85
+ const PHRASES: ReadonlyArray<{ op: GitOp; re: RegExp }> = [
86
+ { op: "commit", re: /\b(commit(e|et|te|ten)?|committe|einchecken)\b/i },
87
+ { op: "push", re: /\b(push(e|en|t)?|hochladen|hochschieben|raufschieben)\b/i },
88
+ { op: "branch", re: /\b(branch|feature-branch|zweig)\b/i },
89
+ // A creation verb is required — "schau dir den PR an" is not "open a PR".
90
+ {
91
+ op: "pr-create",
92
+ re: /\b(erstell(e|en)?|mach(e|en)?|leg(e|en)?\s+an|(er)?(ö|oe)ffne|open|create|raise|aufmachen)\b[^.\n]{0,30}\b(pr|pull[- ]request)\b|\b(pr|pull[- ]request)\b[^.\n]{0,20}\b(erstellen|aufmachen|anlegen|(er)?(ö|oe)ffnen)\b/i,
93
+ },
94
+ // `merge` as an ACTION, never as the noun in "merge conflict" / "merge commit".
95
+ { op: "pr-merge", re: /\b(merge|merg(e|en|st|t)|zusammenf(ü|ue)hren|reinmergen)\b(?!\s*[- ]?(conflict|konflikt|commit|base|queue|state|status))/i },
96
+ // A tag is an ACTION here — bare "Tag" is the German word for day, and
97
+ // "Version" is an ordinary noun. Both authorized a BLOCK op before this.
98
+ { op: "tag", re: /\b(tagge(n|st)?|tag\s+(setzen|anlegen|erstellen)|git\s+tag|--tags|--follow-tags)\b/i },
99
+ // "die release notes sind falsch" is a noun phrase, not an authorization.
100
+ {
101
+ op: "release",
102
+ re: /\b(releasen?|ver(ö|oe)ffentlich(e|en))\b(?!\s*[- ]?(notes?|branch|candidate|pr\b|datum|date|zweig))/i,
103
+ },
104
+ { op: "publish", re: /\b(publish(e|en)?|publiziere[n]?)\b/i },
105
+ ];
106
+
107
+ /**
108
+ * Lines that are pasted TOOL OUTPUT rather than the user instructing.
109
+ *
110
+ * Round-2 adversarial review: an UNFENCED paste of a `git push … rejected`
111
+ * trace authorized `push` through the prose matcher — i.e. the gate
112
+ * pre-authorized the exact scenario it was built to stop. Fenced pastes were
113
+ * already handled; prose was not.
114
+ */
115
+ const OUTPUT_LINE =
116
+ /^\s*(To\s+\S+|remote:|error:|fatal:|hint:|warning:|!\s|\?\?\s|\s*\^|[-+]{3}\s|@@\s|\$\s|>\s|\d+\s+(pass|fail)|npm ERR!|Error:|Traceback|at\s+\S+:\d+)/i;
117
+
118
+ /**
119
+ * A prompt that ASKS about an operation does not authorize it.
120
+ *
121
+ * `question-not-instruction` states this for the agent; the ledger needs it
122
+ * too. "was macht npm publish eigentlich genau?" authorized a real publish
123
+ * before this check.
124
+ */
125
+ export function isInterrogative(prose: string): boolean {
126
+ const t = prose.trim();
127
+ if (!t) {
128
+ return false;
129
+ }
130
+ const hasImperative =
131
+ /\b(mach|mache|bitte|leg|lege|erstell|erstelle|f(ü|ue)hr|f(ü|ue)hre|setz|setze|starte|los|jetzt|go ahead|do it|ja[,.]?\s|ok[,.]?\s)\b/i.test(
132
+ t,
133
+ );
134
+ if (hasImperative) {
135
+ return false;
136
+ }
137
+ return (
138
+ /\?\s*$/.test(t) ||
139
+ /^(was|wie|warum|wieso|weshalb|wann|wer|welche[rs]?|wo|ist|sind|kann|kannst|k(ö|oe)nnen|soll|sollen|darf|d(ü|ue)rfen|why|what|how|when|which|who|is|are|can|could|should|does|do)\b/i.test(
140
+ t,
141
+ )
142
+ );
143
+ }
144
+
145
+ /** Executable commands a user may paste, mapped to the op they authorize. */
146
+ const PASTED_COMMANDS: ReadonlyArray<{ op: GitOp; re: RegExp }> = [
147
+ { op: "publish", re: /\bnpm\s+publish\b/i },
148
+ { op: "tag", re: /\bgit\s+push\s+[^\n]*--tags\b|\bgit\s+tag\s+-a\b/i },
149
+ { op: "release", re: /\bgh\s+release\s+create\b/i },
150
+ { op: "pr-merge", re: /\bgh\s+pr\s+merge\b/i },
151
+ { op: "pr-create", re: /\bgh\s+pr\s+create\b/i },
152
+ { op: "push", re: /\bgit\s+push\b/i },
153
+ { op: "commit", re: /\bgit\s+commit\b/i },
154
+ { op: "branch", re: /\bgit\s+(checkout\s+-b|switch\s+-c|branch)\b/i },
155
+ ];
156
+
157
+ /**
158
+ * Split a prompt into prose and fenced-code regions.
159
+ *
160
+ * The council's point: a user who PASTES an executable `git push origin main`
161
+ * is authorizing that command; the same literal appearing inside a log line or
162
+ * an error trace is not authorization. The discriminator used here is whether
163
+ * the line reads as a command invocation (starts at the command, optionally
164
+ * after a shell prompt marker) rather than being embedded in prose or in a
165
+ * `remote:` / `error:` / `hint:` diagnostic line.
166
+ */
167
+ export function splitFences(prompt: string): { prose: string; fences: string[] } {
168
+ const fences: string[] = [];
169
+ const prose = prompt.replace(/```[^\n]*\n([\s\S]*?)```/g, (_m, body: string) => {
170
+ fences.push(body);
171
+ return " ";
172
+ });
173
+ return { prose, fences };
174
+ }
175
+
176
+ /** A fenced line that is an actual command invocation, not diagnostic output. */
177
+ function _commandLines(fence: string): string[] {
178
+ return fence
179
+ .split("\n")
180
+ .map((l) => l.trim())
181
+ .filter((l) => l.length > 0)
182
+ // Drop git/npm diagnostic output — this is what "pasted an error trace" looks like.
183
+ .filter((l) => !/^(remote:|error:|hint:|fatal:|warning:|To |!\s|\s*\^)/i.test(l))
184
+ // Strip a leading shell prompt marker.
185
+ .map((l) => l.replace(/^[$>#]\s*/, ""))
186
+ // Keep only lines that START with the command.
187
+ .filter((l) => /^(git|gh|npm|pnpm|yarn|task)\b/.test(l));
188
+ }
189
+
190
+ export interface Ledger extends JsonObject {
191
+ session_id: string;
192
+ detected_at: string;
193
+ authorized: GitOp[];
194
+ evidence: { [op: string]: string };
195
+ prompt_chars: number;
196
+ }
197
+
198
+ /** Classify which ops a prompt authorizes. Exported for direct testing. */
199
+ export function classifyAuthorization(prompt: string): {
200
+ authorized: GitOp[];
201
+ evidence: Record<string, string>;
202
+ } {
203
+ const { prose, fences } = splitFences(prompt);
204
+ const authorized = new Set<GitOp>();
205
+ const evidence: Record<string, string> = {};
206
+
207
+ // Drop pasted tool output from the prose before matching (C4), then refuse
208
+ // to read a question as an instruction (C3).
209
+ const instruction = prose
210
+ .split("\n")
211
+ .filter((l) => !OUTPUT_LINE.test(l))
212
+ .join("\n");
213
+
214
+ for (const { op, re } of isInterrogative(instruction) ? [] : PHRASES) {
215
+ const m = re.exec(instruction);
216
+ if (m) {
217
+ authorized.add(op);
218
+ evidence[op] = `prose: "${m[0]}"`;
219
+ }
220
+ }
221
+
222
+ for (const fence of fences) {
223
+ // A fence that also carries tool OUTPUT is a transcript of something that
224
+ // already happened, not a command the user is handing over. Pasting
225
+ // $ git push origin main
226
+ // ! [rejected] …
227
+ // is showing a failure, and reading it as "authorized push" is exactly the
228
+ // implicit-continuation misread this gate exists to stop.
229
+ if (fence.split("\n").some((l) => OUTPUT_LINE.test(l))) {
230
+ continue;
231
+ }
232
+ for (const line of _commandLines(fence)) {
233
+ for (const { op, re } of PASTED_COMMANDS) {
234
+ if (re.test(line)) {
235
+ authorized.add(op);
236
+ evidence[op] = `pasted command: "${line.slice(0, 80)}"`;
237
+ }
238
+ }
239
+ }
240
+ }
241
+
242
+ return { authorized: [...authorized], evidence };
243
+ }
244
+
245
+ export function run(stdin_text: string, options: { consumer_root: string }): number {
246
+ let envelope: JsonObject = {};
247
+ if (stdin_text.trim()) {
248
+ try {
249
+ const decoded = JSON.parse(stdin_text) as unknown;
250
+ if (_isObject(decoded)) {
251
+ envelope = decoded;
252
+ }
253
+ } catch {
254
+ return EXIT_ALLOW;
255
+ }
256
+ }
257
+
258
+ const payload = _isObject(envelope["payload"]) ? (envelope["payload"] as JsonObject) : envelope;
259
+ let prompt = "";
260
+ for (const key of ["prompt", "userPrompt", "user_prompt", "message", "text"]) {
261
+ const v = payload[key];
262
+ if (typeof v === "string" && v.trim()) {
263
+ prompt = v;
264
+ break;
265
+ }
266
+ }
267
+ if (!prompt) {
268
+ return EXIT_ALLOW;
269
+ }
270
+
271
+ const { authorized, evidence } = classifyAuthorization(prompt);
272
+ const ledger: Ledger = {
273
+ session_id: typeof envelope["session_id"] === "string" ? envelope["session_id"] : "",
274
+ detected_at: new Date().toISOString(),
275
+ authorized,
276
+ evidence,
277
+ prompt_chars: prompt.length,
278
+ };
279
+
280
+ try {
281
+ atomic_write_json(path.join(options.consumer_root, STATE_FILE), ledger);
282
+ } catch {
283
+ // Observability only — a failed write degrades the gate to "no ledger",
284
+ // which the pre_tool_use concern treats as "not authorized" for the
285
+ // irreversible subset and as a warn for the rest.
286
+ }
287
+ return EXIT_ALLOW;
288
+ }
289
+
290
+ export function main(argv?: string[]): number {
291
+ const args = argv ?? process.argv.slice(2);
292
+ let consumer_root = process.cwd();
293
+ for (let i = 0; i < args.length; i += 1) {
294
+ const a = args[i];
295
+ if (a === "--project-dir" && args[i + 1] !== undefined) {
296
+ consumer_root = args[i + 1] as string;
297
+ i += 1;
298
+ } else if (a !== undefined && a.startsWith("--project-dir=")) {
299
+ consumer_root = a.slice("--project-dir=".length);
300
+ }
301
+ }
302
+ return run(readHookStdin(), { consumer_root });
303
+ }
304
+
305
+ declare const __AGENT_CONFIG_BUNDLE__: boolean | undefined;
306
+ function _isCliEntry(): boolean {
307
+ if (typeof __AGENT_CONFIG_BUNDLE__ !== "undefined" && __AGENT_CONFIG_BUNDLE__) {
308
+ return false;
309
+ }
310
+ if (process.argv[1] === undefined) {
311
+ return false;
312
+ }
313
+ const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
314
+ if (import.meta.url === argvUrl) {
315
+ return true;
316
+ }
317
+ try {
318
+ const here = fs.realpathSync(fileURLToPath(import.meta.url));
319
+ const argv = fs.realpathSync(path.resolve(process.argv[1]));
320
+ return here === argv;
321
+ } catch {
322
+ return false;
323
+ }
324
+ }
325
+
326
+ if (_isCliEntry()) {
327
+ process.exit(main());
328
+ }
@@ -163,6 +163,25 @@ concerns:
163
163
  args: []
164
164
  fail_closed: false
165
165
  severity: advisory
166
+ # Per-concern tool filter — the dispatcher skips this concern in-process on
167
+ # any other tool. See `_concern_matches_tool` in hooks/dispatch_hook.ts for
168
+ # the semantics (absent = every event, "*" = the same, non-tool events are
169
+ # never filtered) and for why the filter is NOT a host `matcher`.
170
+ #
171
+ # This set is not a guess: the hook's entire branch surface is
172
+ # `name === 'Grep' || name === 'Glob'` and `name === 'Read'`
173
+ # (code_graph_nudge_hook.ts), and a test pins the manifest list against it.
174
+ #
175
+ # Deliberately NOT declared on the three blocking PreToolUse guards
176
+ # (block-no-verify, block-kernel-rule-writes, block-config-weakening): their
177
+ # tool sets span host-specific naming variants (`Bash` / `BashTool` /
178
+ # `launch-process` / `launch_process`, `str-replace-editor` /
179
+ # `str_replace_editor` / `save-file` / …), so a list that misses one variant
180
+ # silently disables a security guard on that host. The concerns already
181
+ # re-read `tool_name` and return early; paying one dispatch to keep a guard
182
+ # correct is the right trade, and the cheap filter is reserved for advisory
183
+ # concerns whose set is provable from their own source.
184
+ tools: [Grep, Glob, Read]
166
185
  # road-to-reachable-code-memory Phase 4 — session_end learning-sidecar
167
186
  # aggregation. Joins the EXISTING session_end concern set (no new platform
168
187
  # wiring). Aggregates agents/memory/intake/*.jsonl through the learning
@@ -240,6 +259,45 @@ concerns:
240
259
  args: []
241
260
  fail_closed: false
242
261
  severity: advisory
262
+ # Language-mirror pin — carries the ONE fact the transcript cannot express:
263
+ # which language the human actually typed. 47 skill/slash-command bodies were
264
+ # measured arriving in the `user` role across 21 of 30 audited sessions, and
265
+ # in the worst case a 4,196-char English skill body was the last user-role
266
+ # content before 136 consecutive English replies to a German prompt.
267
+ # `user_prompt_submit` is the only event a skill body never reaches.
268
+ # Advisory: it supplies a fact, it never blocks.
269
+ language-mirror:
270
+ script: src/scripts/language_mirror_hook.ts
271
+ args: []
272
+ fail_closed: false
273
+ severity: advisory
274
+ # Git-authorization ledger — records which git ops the user's OWN WORDS
275
+ # authorize this turn. Writes state only; never blocks.
276
+ git-authorization:
277
+ script: src/scripts/git_authorization_hook.ts
278
+ args: []
279
+ fail_closed: false
280
+ severity: advisory
281
+ # The blocking half. BLOCKS only the irreversible subset that
282
+ # non-destructive-by-default already declares never-autonomous (npm publish,
283
+ # tag push, gh release create, gh pr merge); WARNS on commit / push /
284
+ # pr-create / branch. Severity, not reach — see the header of the script.
285
+ block-unauthorized-git:
286
+ script: src/scripts/hooks/block_unauthorized_git.ts
287
+ args: []
288
+ fail_closed: true
289
+ severity: blocking
290
+ # Evaluator independence. Gates EVALUATION dispatches only — a verdict
291
+ # pre-loaded into the prompt, or a second evaluation of the same work in one
292
+ # turn. Ordinary parallel fan-out is untouched: the council's "second dispatch
293
+ # of any kind" criterion would have blocked 6 of the 7 analysis subagents in
294
+ # the very session that produced this guard. fail_closed: false — a malformed
295
+ # envelope must never block a dispatch.
296
+ evidence-independence:
297
+ script: src/scripts/hooks/evidence_independence.ts
298
+ args: []
299
+ fail_closed: false
300
+ severity: blocking
243
301
  # session_start canary injection — deterministic carrier for the
244
302
  # session-canary rule (greet-by-name at task start + reply-close markers
245
303
  # as a user-observable context-degradation signal). Gated on
@@ -258,15 +316,15 @@ platforms:
258
316
  session_start: [chat-history, hot-context, handoff-context, session-canary, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe]
259
317
  session_end: [chat-history, memory-learn]
260
318
  stop: [chat-history, hot-context, verify-before-complete]
261
- pre_tool_use: [block-no-verify, block-kernel-rule-writes, block-config-weakening, rtk-wrap, design-slop, code-graph-nudge]
319
+ pre_tool_use: [block-no-verify, block-unauthorized-git, evidence-independence, block-kernel-rule-writes, block-config-weakening, rtk-wrap, design-slop, code-graph-nudge]
262
320
  post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
263
321
 
264
322
  claude:
265
323
  session_start: [chat-history, hot-context, handoff-context, session-canary, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe]
266
324
  session_end: [chat-history, memory-learn]
267
325
  stop: [chat-history, hot-context, verify-before-complete, team-review-gate]
268
- user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff]
269
- pre_tool_use: [block-no-verify, block-kernel-rule-writes, block-config-weakening, rtk-wrap, design-slop, code-graph-nudge]
326
+ user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization]
327
+ pre_tool_use: [block-no-verify, block-unauthorized-git, evidence-independence, block-kernel-rule-writes, block-config-weakening, rtk-wrap, design-slop, code-graph-nudge]
270
328
  post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
271
329
 
272
330
  # Cowork — the Claude desktop app's local-agent-mode runtime, built
@@ -287,8 +345,8 @@ platforms:
287
345
  session_start: [chat-history, hot-context, handoff-context, session-canary, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe]
288
346
  session_end: [chat-history, memory-learn]
289
347
  stop: [chat-history, hot-context, verify-before-complete]
290
- user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff]
291
- pre_tool_use: [block-no-verify, block-kernel-rule-writes, block-config-weakening, rtk-wrap, design-slop, code-graph-nudge]
348
+ user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization]
349
+ pre_tool_use: [block-no-verify, block-unauthorized-git, evidence-independence, block-kernel-rule-writes, block-config-weakening, rtk-wrap, design-slop, code-graph-nudge]
292
350
  post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
293
351
 
294
352
  # Phase 7.5 — Cursor. `.cursor/hooks.json` (project) is read by the
@@ -302,7 +360,7 @@ platforms:
302
360
  session_start: [chat-history, hot-context, handoff-context, session-canary, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe]
303
361
  session_end: [chat-history, memory-learn]
304
362
  stop: [chat-history, hot-context, verify-before-complete]
305
- user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff]
363
+ user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization]
306
364
  post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
307
365
 
308
366
  # Phase 7.6 — Cline. Hooks live under `.clinerules/hooks/<HookName>`
@@ -317,7 +375,7 @@ platforms:
317
375
  session_start: [chat-history, hot-context, handoff-context, session-canary, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe]
318
376
  session_end: [chat-history, memory-learn]
319
377
  stop: [chat-history, hot-context, verify-before-complete]
320
- user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff]
378
+ user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization]
321
379
  post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
322
380
 
323
381
  # Phase 7.7 — Windsurf (Cascade). Hooks live at `.windsurf/hooks.json`
@@ -335,7 +393,9 @@ platforms:
335
393
  windsurf:
336
394
  session_start: [chat-history, hot-context, handoff-context, session-canary, first-run-gate, onboarding-gate, verify-before-complete, profile-staleness, wrapper-freshness, surface-probe]
337
395
  stop: [chat-history, hot-context, verify-before-complete]
338
- user_prompt_submit: [chat-history, verify-before-complete]
396
+ # language-mirror IS bound here: unlike minimal-safe-diff it needs only the
397
+ # user_prompt_submit slot, which Windsurf has.
398
+ user_prompt_submit: [chat-history, verify-before-complete, language-mirror, git-authorization]
339
399
 
340
400
  # Phase 7.8 — Gemini CLI. Hooks live at `.gemini/settings.json`
341
401
  # (project) or `~/.gemini/settings.json` (user). Per Gemini docs
@@ -351,7 +411,7 @@ platforms:
351
411
  session_start: [chat-history, hot-context, handoff-context, session-canary, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe]
352
412
  session_end: [chat-history, memory-learn]
353
413
  stop: [chat-history, hot-context, verify-before-complete]
354
- user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff]
414
+ user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization]
355
415
  post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
356
416
 
357
417
  # Phase 7.9 — Copilot has no hook surface. Concerns route through
@@ -339,7 +339,9 @@ function _check_command(cmd: string): [boolean, string] {
339
339
  if (/\bgit\b/.test(cmd)) {
340
340
  return [
341
341
  true,
342
- 'command parse failed (shlex) on a git-containing command — fail-closed (git-history-discipline)',
342
+ 'command parse failed (shlex) on a git-containing command — fail-closed (git-history-discipline). \n'
343
+ + 'Most common cause: an apostrophe or backtick inside a heredoc commit message in the SAME compound command as `git`. \n'
344
+ + 'Fix: write the message to a file and use `git commit -F <file>` — do not retry the heredoc.',
343
345
  ];
344
346
  }
345
347
  return [false, ''];