@event4u/agent-config 9.25.0 → 9.27.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 (222) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +58 -143
  3. package/README.md +1 -1
  4. package/dist/agent-src/commands/analyze/conformance.md +21 -2
  5. package/dist/agent-src/commands/review/changes.md +6 -5
  6. package/dist/agent-src/commands/roadmap/next.md +256 -0
  7. package/dist/agent-src/commands/roadmap.md +12 -4
  8. package/dist/agent-src/contexts/execution/subagent-routing.md +4 -1
  9. package/dist/agent-src/rules/active-remediation.md +2 -0
  10. package/dist/agent-src/rules/analysis-skill-routing.md +2 -0
  11. package/dist/agent-src/rules/architecture.md +2 -0
  12. package/dist/agent-src/rules/artifact-drafting-protocol.md +2 -0
  13. package/dist/agent-src/rules/artifact-engagement-recording.md +2 -0
  14. package/dist/agent-src/rules/augment-edit-discipline.md +2 -0
  15. package/dist/agent-src/rules/autonomous-execution.md +2 -0
  16. package/dist/agent-src/rules/brand-consistency.md +2 -0
  17. package/dist/agent-src/rules/brand-source-of-truth.md +2 -0
  18. package/dist/agent-src/rules/broken-access-control.md +2 -0
  19. package/dist/agent-src/rules/cli-output-handling.md +2 -0
  20. package/dist/agent-src/rules/code-comment-discipline.md +2 -0
  21. package/dist/agent-src/rules/code-provenance.md +2 -0
  22. package/dist/agent-src/rules/command-suggestion-policy.md +2 -0
  23. package/dist/agent-src/rules/commit-conventions.md +2 -0
  24. package/dist/agent-src/rules/communication-through-line.md +2 -0
  25. package/dist/agent-src/rules/content-quoting-floor.md +2 -0
  26. package/dist/agent-src/rules/context-hygiene.md +10 -0
  27. package/dist/agent-src/rules/copilot-routing.md +2 -0
  28. package/dist/agent-src/rules/cross-source-consistency.md +2 -0
  29. package/dist/agent-src/rules/decision-revisit-gate.md +2 -0
  30. package/dist/agent-src/rules/delegation-policy.md +5 -2
  31. package/dist/agent-src/rules/design-fidelity.md +19 -1
  32. package/dist/agent-src/rules/design-review-after-ui-write.md +120 -0
  33. package/dist/agent-src/rules/devcontainer-routing.md +2 -0
  34. package/dist/agent-src/rules/doc-screenshot-hygiene.md +2 -0
  35. package/dist/agent-src/rules/docker-commands.md +2 -0
  36. package/dist/agent-src/rules/domain-adoption-policy.md +2 -0
  37. package/dist/agent-src/rules/domain-safety-disclaimer.md +2 -0
  38. package/dist/agent-src/rules/domain-safety-pii.md +2 -0
  39. package/dist/agent-src/rules/domain-safety-retention.md +2 -0
  40. package/dist/agent-src/rules/downstream-changes.md +2 -0
  41. package/dist/agent-src/rules/engineering-safety-floor.md +2 -0
  42. package/dist/agent-src/rules/evaluator-independence.md +19 -3
  43. package/dist/agent-src/rules/external-code-graph-interop.md +2 -0
  44. package/dist/agent-src/rules/external-reference-deep-dive.md +2 -0
  45. package/dist/agent-src/rules/fast-path-marker-visibility.md +2 -0
  46. package/dist/agent-src/rules/finance-safety-floor.md +2 -0
  47. package/dist/agent-src/rules/framework-neutrality-in-generic-skills.md +2 -0
  48. package/dist/agent-src/rules/git-history-discipline.md +3 -1
  49. package/dist/agent-src/rules/guidelines.md +2 -0
  50. package/dist/agent-src/rules/history-discipline.md +2 -0
  51. package/dist/agent-src/rules/icon-consistency.md +2 -0
  52. package/dist/agent-src/rules/image-likeness-and-rights.md +2 -0
  53. package/dist/agent-src/rules/improve-before-implement.md +2 -0
  54. package/dist/agent-src/rules/invite-challenge.md +2 -0
  55. package/dist/agent-src/rules/laravel-routing.md +2 -0
  56. package/dist/agent-src/rules/laravel-translations.md +2 -0
  57. package/dist/agent-src/rules/legal-safety-floor.md +2 -0
  58. package/dist/agent-src/rules/lethal-trifecta-guard.md +2 -0
  59. package/dist/agent-src/rules/linked-projects-onboarding-gate.md +2 -0
  60. package/dist/agent-src/rules/low-impact-corpus-privacy-floor.md +2 -0
  61. package/dist/agent-src/rules/markdown-safe-codeblocks.md +2 -0
  62. package/dist/agent-src/rules/media-governance-routing.md +2 -0
  63. package/dist/agent-src/rules/media-sync-ground-truth.md +2 -0
  64. package/dist/agent-src/rules/minimal-safe-diff.md +2 -0
  65. package/dist/agent-src/rules/missing-tool-handling.md +2 -0
  66. package/dist/agent-src/rules/model-recommendation.md +2 -0
  67. package/dist/agent-src/rules/no-attribution-footers.md +2 -0
  68. package/dist/agent-src/rules/no-decorative-emojis-in-git-surfaces.md +2 -0
  69. package/dist/agent-src/rules/no-pr-progress-comments.md +2 -0
  70. package/dist/agent-src/rules/no-roadmap-references.md +2 -0
  71. package/dist/agent-src/rules/notes-first-reasoning.md +2 -0
  72. package/dist/agent-src/rules/onboarding-gate.md +2 -0
  73. package/dist/agent-src/rules/output-discipline.md +2 -0
  74. package/dist/agent-src/rules/package-ci-checks.md +2 -0
  75. package/dist/agent-src/rules/persona-governance.md +2 -0
  76. package/dist/agent-src/rules/php-coding.md +2 -0
  77. package/dist/agent-src/rules/prefer-enums-over-literals.md +2 -0
  78. package/dist/agent-src/rules/preservation-guard.md +2 -0
  79. package/dist/agent-src/rules/provider-lifecycle-discipline.md +2 -0
  80. package/dist/agent-src/rules/question-not-instruction.md +2 -0
  81. package/dist/agent-src/rules/reviewer-awareness.md +2 -0
  82. package/dist/agent-src/rules/roadmap-ci-steps-policy.md +2 -0
  83. package/dist/agent-src/rules/roadmap-progress-sync.md +2 -0
  84. package/dist/agent-src/rules/role-mode-adherence.md +2 -0
  85. package/dist/agent-src/rules/rule-type-governance.md +2 -0
  86. package/dist/agent-src/rules/runtime-safety.md +2 -0
  87. package/dist/agent-src/rules/scale-discipline.md +2 -0
  88. package/dist/agent-src/rules/secret-vcs-guard.md +2 -0
  89. package/dist/agent-src/rules/security-sensitive-stop.md +2 -0
  90. package/dist/agent-src/rules/senior-engineering-discipline.md +2 -0
  91. package/dist/agent-src/rules/session-canary.md +63 -19
  92. package/dist/agent-src/rules/settings-ask-protocol.md +180 -0
  93. package/dist/agent-src/rules/size-enforcement.md +2 -0
  94. package/dist/agent-src/rules/skill-improvement-trigger.md +2 -0
  95. package/dist/agent-src/rules/skill-quality.md +2 -0
  96. package/dist/agent-src/rules/slash-command-routing-policy.md +2 -0
  97. package/dist/agent-src/rules/source-confidentiality.md +2 -0
  98. package/dist/agent-src/rules/source-discovery-gate.md +2 -0
  99. package/dist/agent-src/rules/source-of-truth.md +2 -0
  100. package/dist/agent-src/rules/spreadsheet-source-quality.md +2 -0
  101. package/dist/agent-src/rules/strategy-safety-floor.md +2 -0
  102. package/dist/agent-src/rules/symfony-routing.md +2 -0
  103. package/dist/agent-src/rules/think-before-action.md +2 -0
  104. package/dist/agent-src/rules/token-budget-discipline.md +8 -3
  105. package/dist/agent-src/rules/token-efficiency.md +2 -0
  106. package/dist/agent-src/rules/token-optimizer-maintenance.md +2 -0
  107. package/dist/agent-src/rules/tool-safety.md +2 -0
  108. package/dist/agent-src/rules/ui-audit-gate.md +7 -0
  109. package/dist/agent-src/rules/untrusted-input-defense.md +2 -0
  110. package/dist/agent-src/rules/upstream-proposal.md +2 -0
  111. package/dist/agent-src/rules/user-interaction.md +5 -2
  112. package/dist/agent-src/rules/user-interrupt-priority.md +2 -0
  113. package/dist/agent-src/skills/recursive-verification/SKILL.md +13 -0
  114. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  115. package/dist/cli/registry.js +2 -0
  116. package/dist/cli/registry.js.map +1 -1
  117. package/dist/cli-delegate/{chunk-SJQMJI7M.js → chunk-3KJIMFTB.js} +1 -1
  118. package/dist/cli-delegate/{chunk-BSLUHV2F.js → chunk-BCPIOAGV.js} +148 -133
  119. package/dist/cli-delegate/cmd_conformance.js +1 -1
  120. package/dist/cli-delegate/cmd_doctor.js +1 -1
  121. package/dist/cli-delegate/cmd_export.js +1 -1
  122. package/dist/cli-delegate/cmd_prune.js +1 -1
  123. package/dist/cli-delegate/cmd_route_audit.js +4 -1
  124. package/dist/cli-delegate/cmd_route_explain.js +4 -1
  125. package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
  126. package/dist/cli-delegate/cmd_sync.js +1 -1
  127. package/dist/cli-delegate/cmd_uninstall.js +1 -1
  128. package/dist/cli-delegate/cmd_validate.js +1 -1
  129. package/dist/discovery/deprecation-report.md +1 -1
  130. package/dist/discovery/discovery-manifest.json +191 -120
  131. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  132. package/dist/discovery/discovery-manifest.summary.md +4 -4
  133. package/dist/discovery/orphan-report.md +1 -1
  134. package/dist/discovery/packs.json +11 -8
  135. package/dist/discovery/trust-report.md +3 -3
  136. package/dist/discovery/workspaces.json +9 -6
  137. package/dist/hooks/dispatch.js +1148 -349
  138. package/dist/install/install.mjs +1 -1
  139. package/dist/mcp/registry-manifest.json +3 -3
  140. package/dist/mcp/server.json +2 -2
  141. package/dist/mcp/server.mjs +571 -417
  142. package/dist/router.json +1 -1
  143. package/dist/scripts/condense.js +33 -2
  144. package/dist/scripts/condense.js.map +1 -1
  145. package/dist/server/io/yamlIO.js +85 -0
  146. package/dist/server/io/yamlIO.js.map +1 -1
  147. package/dist/server/nicknameResolver.js +116 -0
  148. package/dist/server/nicknameResolver.js.map +1 -0
  149. package/dist/server/routes/ping.js +29 -11
  150. package/dist/server/routes/ping.js.map +1 -1
  151. package/dist/server/routes/settings.js +1 -12
  152. package/dist/server/routes/settings.js.map +1 -1
  153. package/dist/server/routes/wizard.js +52 -2
  154. package/dist/server/routes/wizard.js.map +1 -1
  155. package/dist/shared/interactiveContext.js +73 -0
  156. package/dist/shared/interactiveContext.js.map +1 -0
  157. package/dist/shared/nicknamePrefill.js +59 -0
  158. package/dist/shared/nicknamePrefill.js.map +1 -0
  159. package/dist/shared/settingsAsks.js +128 -0
  160. package/dist/shared/settingsAsks.js.map +1 -0
  161. package/dist/shared/settingsCarveOut.js +118 -0
  162. package/dist/shared/settingsCarveOut.js.map +1 -0
  163. package/dist/shared/settingsConsent.js +93 -0
  164. package/dist/shared/settingsConsent.js.map +1 -0
  165. package/dist/shared/settingsSurface.js +7 -1
  166. package/dist/shared/settingsSurface.js.map +1 -1
  167. package/dist/ui/assets/index-DmR0mbk4.js.map +1 -1
  168. package/package.json +1 -1
  169. package/src/config/agents-paths.yml +15 -0
  170. package/src/config/evaluator-budgets.json +4 -4
  171. package/src/config/gate-coverage.yml +29 -1
  172. package/src/config/gate-violation-baselines.json +13 -7
  173. package/src/scripts/_cli/cmd_doctor.ts +6 -21
  174. package/src/scripts/_cli/cmd_route_audit.ts +4 -1
  175. package/src/scripts/_cli/cmd_route_explain.ts +9 -1
  176. package/src/scripts/_dispatch.bash +18 -0
  177. package/src/scripts/_lib/git_common_dir.ts +128 -0
  178. package/src/scripts/_lib/obligation_frequency.ts +391 -0
  179. package/src/scripts/_lib/session_register.ts +361 -0
  180. package/src/scripts/ai-video/stitch.sh +27 -9
  181. package/src/scripts/before_complete_hook.ts +23 -4
  182. package/src/scripts/bench_ab_v2_run.ts +272 -16
  183. package/src/scripts/bench_ab_v2_stats.ts +156 -3
  184. package/src/scripts/check_completion_review.ts +80 -3
  185. package/src/scripts/check_corpus_staleness.ts +692 -0
  186. package/src/scripts/check_enforcement_coverage.ts +192 -7
  187. package/src/scripts/check_gate_coverage.ts +67 -0
  188. package/src/scripts/check_hook_bundle_freshness.ts +183 -0
  189. package/src/scripts/check_rule_projection_integrity.ts +364 -0
  190. package/src/scripts/condense.ts +36 -2
  191. package/src/scripts/conformance_scan.ts +82 -17
  192. package/src/scripts/dispatch_r2_reviewer.ts +31 -2
  193. package/src/scripts/generate_settings_reference.ts +232 -0
  194. package/src/scripts/git_authorization_hook.ts +75 -10
  195. package/src/scripts/hook_manifest.yaml +87 -27
  196. package/src/scripts/hooks/block_no_verify.ts +59 -2
  197. package/src/scripts/hooks/block_unauthorized_git.ts +199 -16
  198. package/src/scripts/hooks/concern_registry.ts +2 -0
  199. package/src/scripts/hooks/design_slop_hook.ts +13 -1
  200. package/src/scripts/hooks/dispatch_hook.ts +97 -2
  201. package/src/scripts/hooks/evidence_independence.ts +40 -6
  202. package/src/scripts/language_mirror_hook.ts +256 -17
  203. package/src/scripts/lint_abstraction_thresholds.ts +41 -8
  204. package/src/scripts/lint_agents_layout.ts +2 -0
  205. package/src/scripts/lint_hook_manifest.ts +29 -0
  206. package/src/scripts/memory_learn_hook.ts +109 -6
  207. package/src/scripts/orchestration_backfill.ts +354 -0
  208. package/src/scripts/pr_url_reminder_hook.ts +17 -2
  209. package/src/scripts/release.ts +180 -37
  210. package/src/scripts/schemas/review-findings.schema.json +99 -0
  211. package/src/scripts/schemas/rule.schema.json +5 -0
  212. package/src/scripts/session_canary_hook.ts +35 -3
  213. package/src/scripts/session_register_hook.ts +288 -0
  214. package/src/scripts/sessions_cli.ts +140 -0
  215. package/src/scripts/validate_frontmatter.ts +68 -0
  216. package/src/server/io/yamlIO.ts +87 -0
  217. package/src/shared/interactiveContext.ts +98 -0
  218. package/src/shared/nicknamePrefill.ts +96 -0
  219. package/src/shared/settingsAsks.ts +176 -0
  220. package/src/shared/settingsCarveOut.ts +143 -0
  221. package/src/shared/settingsConsent.ts +136 -0
  222. package/src/shared/settingsSurface.ts +7 -1
@@ -0,0 +1,232 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Generate the human-readable settings reference page.
4
+ *
5
+ * `road-to-zero-ceremony-settings` Phase 3 step 3. The user's global settings
6
+ * file is becoming sparse, so the long-form explanation that used to ride along
7
+ * as template comments has to survive somewhere the user can still read it.
8
+ *
9
+ * The page is derived from TWO sources that already exist, and from nothing
10
+ * else — the point is that there is no third surface to keep in sync:
11
+ *
12
+ * 1. the zod settings schema (`src/server/schemas/settings.ts`), flattened
13
+ * through the same `flattenSurface` the installer uses for its upgrade
14
+ * delta, so type / default / enum / description come from one place;
15
+ * 2. the A/B/C class table (`docs/contracts/settings-classes.md`), parsed by
16
+ * the same `parseSettingsClassRows` the lint and the writer use, so the
17
+ * page cannot claim a class the fence does not enforce.
18
+ *
19
+ * The package VERSION is deliberately absent from the output. Rendering it
20
+ * would make this page drift on every release bump and red `--check` for a
21
+ * change nobody made — the page describes the schema's shape, not the release
22
+ * that shipped it.
23
+ *
24
+ * Produces: docs/settings-reference.md
25
+ *
26
+ * Modes:
27
+ * --check Regenerate to memory and diff against the committed page.
28
+ * (default) Regenerate in place; exit 0 on success.
29
+ *
30
+ * Exit codes: 0 = ok, 1 = drift (--check), 2 = bad usage, 3 = internal.
31
+ */
32
+ import * as fs from 'node:fs';
33
+ import * as path from 'node:path';
34
+ import { fileURLToPath, pathToFileURL } from 'node:url';
35
+
36
+ import { zodToJsonSchema } from 'zod-to-json-schema';
37
+
38
+ import { settingsSchema } from '../server/schemas/settings.js';
39
+ import {
40
+ buildSettingsClassIndex,
41
+ parseSettingsClassRows,
42
+ type SettingsClass,
43
+ } from '../shared/settingsClasses.js';
44
+ import { flattenSurface, type SurfaceEntry } from '../shared/settingsSurface.js';
45
+
46
+ const _HERE = path.dirname(fileURLToPath(import.meta.url));
47
+ export const ROOT = path.resolve(_HERE, '..', '..');
48
+
49
+ export const CONTRACT_REL = path.join('docs', 'contracts', 'settings-classes.md');
50
+ export const OUTPUT_REL = path.join('docs', 'settings-reference.md');
51
+
52
+ /** What each class means for the reader. Mirrors the contract's own table. */
53
+ const CLASS_BLURB: Record<SettingsClass, string> = {
54
+ A: 'preference — the agent may set it; you are never asked',
55
+ B: 'consent — the agent asks once, then persists your answer',
56
+ C: 'guarded — only you (GUI or hand-edit) may change it',
57
+ };
58
+
59
+ /** A key with no row in the contract. The lint fails on these separately. */
60
+ const UNCLASSIFIED = '—';
61
+
62
+ export interface ReferenceRow {
63
+ key: string;
64
+ cls: SettingsClass | null;
65
+ entry: SurfaceEntry;
66
+ }
67
+
68
+ /**
69
+ * Group by top-level segment so the page reads like the template did.
70
+ * Leaves with no dot (e.g. `discipline_profile`) group under `(root)`.
71
+ */
72
+ export function groupRows(rows: readonly ReferenceRow[]): Map<string, ReferenceRow[]> {
73
+ const groups = new Map<string, ReferenceRow[]>();
74
+ for (const row of rows) {
75
+ const dot = row.key.indexOf('.');
76
+ const section = dot === -1 ? '(root)' : row.key.slice(0, dot);
77
+ const bucket = groups.get(section);
78
+ if (bucket === undefined) groups.set(section, [row]);
79
+ else bucket.push(row);
80
+ }
81
+ return groups;
82
+ }
83
+
84
+ /** Render a value for a markdown table cell — never let it break the row. */
85
+ export function renderCell(value: unknown): string {
86
+ if (value === undefined) return '';
87
+ const json = JSON.stringify(value);
88
+ if (json === undefined) return '';
89
+ return `\`${json.replace(/\|/g, '\\|')}\``;
90
+ }
91
+
92
+ /** Descriptions are free prose; pipes and newlines would break the table. */
93
+ export function renderProse(value: string | undefined): string {
94
+ if (value === undefined || value === '') return '';
95
+ return value.replace(/\s+/g, ' ').replace(/\|/g, '\\|').trim();
96
+ }
97
+
98
+ export function buildRows(contractMarkdown: string): ReferenceRow[] {
99
+ const jsonSchema = zodToJsonSchema(settingsSchema, {
100
+ name: 'AgentSettings',
101
+ $refStrategy: 'none',
102
+ target: 'jsonSchema7',
103
+ }) as Parameters<typeof flattenSurface>[0];
104
+ // The version is a required parameter of the shared flattener and is
105
+ // discarded here on purpose — see the module header.
106
+ const surface = flattenSurface(jsonSchema, 'unversioned');
107
+ const classIndex = buildSettingsClassIndex(parseSettingsClassRows(contractMarkdown));
108
+
109
+ return Object.keys(surface.entries)
110
+ .sort()
111
+ .map((key) => ({
112
+ key,
113
+ cls: classIndex.get(key) ?? null,
114
+ entry: surface.entries[key] as SurfaceEntry,
115
+ }));
116
+ }
117
+
118
+ export function renderPage(rows: readonly ReferenceRow[]): string {
119
+ const out: string[] = [];
120
+ out.push('# Settings reference — every key, its class, and its default');
121
+ out.push('');
122
+ out.push('> **Generated** by `src/scripts/generate_settings_reference.ts` — do NOT');
123
+ out.push('> hand-edit. Derived from the zod settings schema plus the A/B/C class');
124
+ out.push('> table in [`settings-classes`](contracts/settings-classes.md).');
125
+ out.push('> Drift-checked in CI (`--check`).');
126
+ out.push('');
127
+ out.push('Your global settings file is **sparse**: it records the decisions you');
128
+ out.push('actually made, not every key that exists. A key absent from the file');
129
+ out.push('resolves to the default listed here. This page is where the long-form');
130
+ out.push('explanation lives now that the file no longer carries it as comments.');
131
+ out.push('');
132
+ out.push('**Class** answers one question — *who may write this key.*');
133
+ out.push('');
134
+ for (const cls of ['A', 'B', 'C'] as const) {
135
+ out.push(`- **${cls}** — ${CLASS_BLURB[cls]}`);
136
+ }
137
+ out.push('');
138
+
139
+ const groups = groupRows(rows);
140
+ for (const section of [...groups.keys()].sort()) {
141
+ const bucket = groups.get(section) as ReferenceRow[];
142
+ out.push(`## ${section}`);
143
+ out.push('');
144
+ out.push('| Key | Class | Type | Default | Allowed values | What it does |');
145
+ out.push('|---|---|---|---|---|---|');
146
+ for (const row of bucket) {
147
+ const allowed = row.entry.enum === undefined
148
+ ? ''
149
+ : row.entry.enum.map((v) => `\`${String(v)}\``).join(' · ');
150
+ out.push(
151
+ `| \`${row.key}\` | ${row.cls ?? UNCLASSIFIED} | ${row.entry.type} | `
152
+ + `${renderCell(row.entry.default)} | ${allowed} | ${renderProse(row.entry.description)} |`,
153
+ );
154
+ }
155
+ out.push('');
156
+ }
157
+
158
+ out.push('## See also');
159
+ out.push('');
160
+ out.push('- [`settings-classes`](contracts/settings-classes.md) — the class contract this page reads.');
161
+ out.push('- [`settings-api`](contracts/settings-api.md) — the read/write surface.');
162
+ out.push('');
163
+ return out.join('\n');
164
+ }
165
+
166
+ export function generate(root: string): string {
167
+ const contract = fs.readFileSync(path.join(root, CONTRACT_REL), 'utf-8');
168
+ return renderPage(buildRows(contract));
169
+ }
170
+
171
+ interface Args {
172
+ check: boolean;
173
+ }
174
+
175
+ export function parse_args(argv: readonly string[]): Args {
176
+ const args: Args = { check: false };
177
+ for (const a of argv) {
178
+ if (a === '--check') {
179
+ args.check = true;
180
+ continue;
181
+ }
182
+ process.stderr.write(`generate_settings_reference: unknown argument: ${a}\n`);
183
+ process.exit(2);
184
+ }
185
+ return args;
186
+ }
187
+
188
+ export function main(argv?: readonly string[]): number {
189
+ const args = parse_args(argv ?? process.argv.slice(2));
190
+ const out_path = path.join(ROOT, OUTPUT_REL);
191
+
192
+ let rendered: string;
193
+ try {
194
+ rendered = generate(ROOT);
195
+ } catch (err) {
196
+ process.stderr.write(`generate_settings_reference: ${String(err)}\n`);
197
+ return 3;
198
+ }
199
+
200
+ if (args.check) {
201
+ let committed: string | null = null;
202
+ try {
203
+ committed = fs.readFileSync(out_path, 'utf-8');
204
+ } catch {
205
+ committed = null;
206
+ }
207
+ if (committed !== rendered) {
208
+ process.stderr.write(
209
+ `❌ ${OUTPUT_REL} is out of date.\n`
210
+ + ' Regenerate: ./scripts-run src/scripts/generate_settings_reference\n',
211
+ );
212
+ return 1;
213
+ }
214
+ process.stdout.write(`✅ ${OUTPUT_REL} is up to date.\n`);
215
+ return 0;
216
+ }
217
+
218
+ fs.mkdirSync(path.dirname(out_path), { recursive: true });
219
+ fs.writeFileSync(out_path, rendered, 'utf-8');
220
+ process.stdout.write(`✅ wrote ${OUTPUT_REL}\n`);
221
+ return 0;
222
+ }
223
+
224
+ function _invoked_directly(): boolean {
225
+ if (process.argv[1] === undefined) return false;
226
+ const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
227
+ return import.meta.url === argvUrl;
228
+ }
229
+
230
+ if (_invoked_directly()) {
231
+ process.exitCode = main();
232
+ }
@@ -83,16 +83,65 @@ export const ALL_OPS: readonly GitOp[] = [
83
83
  * cost latency on every turn and be unauditable.
84
84
  */
85
85
  const PHRASES: ReadonlyArray<{ op: GitOp; re: RegExp }> = [
86
- { op: "commit", re: /\b(commit(e|et|ten|ted|ting)?|committe|einchecken)\b/i },
87
- { op: "push", re: /\b(push(e|en|ed|ing)?|hochladen|hochschieben|raufschieben)\b/i },
88
- { op: "branch", re: /\b(branch(e|es)?|zweig|feature-branch|erstelle einen branch)\b/i },
89
- { op: "pr-create", re: /\b(pull[- ]request|pr\b|mach(e|en)? (einen|nen) pr|erstelle .{0,20}pr)\b/i },
90
- { op: "pr-merge", re: /\b(merge[nrd]?|mergen|zusammenführen|zusammenfuehren|reinmergen|gemerged)\b/i },
91
- { op: "tag", re: /\b(tag(ge|gen|ged|ging)?|version(iere|ieren)?)\b/i },
92
- { op: "release", re: /\b(release[nd]?|releasen|veröffentlich(e|en|ung)|veroeffentlich(e|en))\b/i },
93
- { op: "publish", re: /\b(publish(e|en|ed|ing)?|npm publish|publiziere[n]?)\b/i },
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 },
94
105
  ];
95
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
+
96
145
  /** Executable commands a user may paste, mapped to the op they authorize. */
97
146
  const PASTED_COMMANDS: ReadonlyArray<{ op: GitOp; re: RegExp }> = [
98
147
  { op: "publish", re: /\bnpm\s+publish\b/i },
@@ -155,8 +204,15 @@ export function classifyAuthorization(prompt: string): {
155
204
  const authorized = new Set<GitOp>();
156
205
  const evidence: Record<string, string> = {};
157
206
 
158
- for (const { op, re } of PHRASES) {
159
- const m = re.exec(prose);
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);
160
216
  if (m) {
161
217
  authorized.add(op);
162
218
  evidence[op] = `prose: "${m[0]}"`;
@@ -164,6 +220,15 @@ export function classifyAuthorization(prompt: string): {
164
220
  }
165
221
 
166
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
+ }
167
232
  for (const line of _commandLines(fence)) {
168
233
  for (const { op, re } of PASTED_COMMANDS) {
169
234
  if (re.test(line)) {
@@ -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
@@ -279,32 +298,73 @@ concerns:
279
298
  args: []
280
299
  fail_closed: false
281
300
  severity: blocking
282
- # session_start canary injection — deterministic carrier for the
301
+ # Canary injection — deterministic carrier for the
283
302
  # session-canary rule (greet-by-name at task start + reply-close markers
284
303
  # as a user-observable context-degradation signal). Gated on
285
304
  # `personal.canary_name` in .agent-settings.yml (empty = clean no-op),
286
305
  # so unconfigured consumers pay nothing. Emits a `context` block the
287
306
  # dispatcher forwards on session_start (hot-context mechanism).
307
+ # Bound in TWO slots on purpose: session_start carries the full contract once,
308
+ # user_prompt_submit carries a one-line beat. The obligation is per TASK and no
309
+ # host has a per-task slot (Cline maps TaskStart/TaskResume onto session_start;
310
+ # Claude Code has no task event), so per-turn — a strict superset — is the
311
+ # reachable covering period. Session scope alone measured a ~13-of-15 miss rate
312
+ # on task starts. Augment has no user_prompt_submit and is a DECLARED gap: its
313
+ # `stop` fires after the reply, so injecting there could not shape the reply the
314
+ # reminder is for, and counting it would be exactly the over-credit this pairing
315
+ # was added to remove.
288
316
  # Never blocks. fail_closed: false.
289
317
  session-canary:
290
318
  script: src/scripts/session_canary_hook.ts
291
319
  args: []
292
320
  fail_closed: false
293
321
  severity: advisory
322
+ # Shared per-session register (road-to-parallel-session-coordination) — writes
323
+ # this session into <git-common-dir>/agent-sessions/<session_id>.json so every
324
+ # other worktree of the repo can see it, and on session_start injects the live
325
+ # foreign sessions as a `context` block (hot-context mechanism).
326
+ #
327
+ # Bound in FOUR slots, with deliberately asymmetric meanings:
328
+ # session_start → register + prune expired + inject foreign sessions
329
+ # user_prompt_submit → heartbeat (pre-reply carrier)
330
+ # stop → heartbeat (post-reply carrier), NOT deregistration
331
+ # session_end → deregister
332
+ #
333
+ # `stop` is per-turn, not session end: on Claude Code the native Stop fires
334
+ # after every assistant reply (see _lib/obligation_frequency.ts), so
335
+ # deregistering there would kill a live session's own record after its first
336
+ # reply. The ONE exception is Cline, whose stop is TaskCancel and whose
337
+ # session_end is TaskComplete — a cancelled Cline task reaches neither, so it
338
+ # would leak its claim until TTL. That exception is an explicit platform
339
+ # allow-list in _lib/session_register.ts, never a condition computed from the
340
+ # frequency lattice: a future platform must be added by a human, deliberately.
341
+ #
342
+ # Correctness rests on heartbeat + TTL alone; deregistration only frees a claim
343
+ # sooner. So Windsurf's missing session_end costs latency, not correctness.
344
+ # Declared non-carriers: copilot (no hook surface), cursor CLI (per-turn slots
345
+ # are IDE-only), cowork (slots wired, lifecycle events inert) — a session there
346
+ # registers but expires by TTL rather than heartbeating, and the concern says so
347
+ # on stderr rather than letting the coverage join read green.
348
+ # Never blocks. fail_closed: false.
349
+ session-register:
350
+ script: src/scripts/session_register_hook.ts
351
+ args: []
352
+ fail_closed: false
353
+ severity: advisory
294
354
 
295
355
  platforms:
296
356
  augment:
297
- 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]
298
- session_end: [chat-history, memory-learn]
299
- stop: [chat-history, hot-context, verify-before-complete]
357
+ 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, session-register]
358
+ session_end: [chat-history, memory-learn, session-register]
359
+ stop: [chat-history, hot-context, verify-before-complete, session-register]
300
360
  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]
301
361
  post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
302
362
 
303
363
  claude:
304
- 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]
305
- session_end: [chat-history, memory-learn]
306
- stop: [chat-history, hot-context, verify-before-complete, team-review-gate]
307
- user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization]
364
+ 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, session-register]
365
+ session_end: [chat-history, memory-learn, session-register]
366
+ stop: [chat-history, hot-context, verify-before-complete, team-review-gate, session-register]
367
+ user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization, session-canary, session-register]
308
368
  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]
309
369
  post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
310
370
 
@@ -323,10 +383,10 @@ platforms:
323
383
  # Decision matrix + upstream blockers tracked in
324
384
  # agents/settings/contexts/chat-history-platform-hooks.md § Cowork.
325
385
  cowork:
326
- 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]
327
- session_end: [chat-history, memory-learn]
328
- stop: [chat-history, hot-context, verify-before-complete]
329
- user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization]
386
+ 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, session-register]
387
+ session_end: [chat-history, memory-learn, session-register]
388
+ stop: [chat-history, hot-context, verify-before-complete, session-register]
389
+ user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization, session-canary, session-register]
330
390
  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]
331
391
  post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
332
392
 
@@ -338,10 +398,10 @@ platforms:
338
398
  # IDE-only — CLI-only users fall back to /checkpoint per
339
399
  # agents/settings/contexts/chat-history-platform-hooks.md.
340
400
  cursor:
341
- 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]
342
- session_end: [chat-history, memory-learn]
343
- stop: [chat-history, hot-context, verify-before-complete]
344
- user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization]
401
+ 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, session-register]
402
+ session_end: [chat-history, memory-learn, session-register]
403
+ stop: [chat-history, hot-context, verify-before-complete, session-register]
404
+ user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization, session-canary, session-register]
345
405
  post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
346
406
 
347
407
  # Phase 7.6 — Cline. Hooks live under `.clinerules/hooks/<HookName>`
@@ -353,10 +413,10 @@ platforms:
353
413
  # both map to session_start. TaskCancel maps to stop because the
354
414
  # session is interrupted with partial state (mirrors Augment Stop).
355
415
  cline:
356
- 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]
357
- session_end: [chat-history, memory-learn]
358
- stop: [chat-history, hot-context, verify-before-complete]
359
- user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization]
416
+ 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, session-register]
417
+ session_end: [chat-history, memory-learn, session-register]
418
+ stop: [chat-history, hot-context, verify-before-complete, session-register]
419
+ user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization, session-canary, session-register]
360
420
  post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
361
421
 
362
422
  # Phase 7.7 — Windsurf (Cascade). Hooks live at `.windsurf/hooks.json`
@@ -372,11 +432,11 @@ platforms:
372
432
  # surface to record verification commands; documented limitation).
373
433
  # minimal-safe-diff is omitted entirely on Windsurf for the same reason.
374
434
  windsurf:
375
- session_start: [chat-history, hot-context, handoff-context, session-canary, first-run-gate, onboarding-gate, verify-before-complete, profile-staleness, wrapper-freshness, surface-probe]
376
- stop: [chat-history, hot-context, verify-before-complete]
435
+ session_start: [chat-history, hot-context, handoff-context, session-canary, first-run-gate, onboarding-gate, verify-before-complete, profile-staleness, wrapper-freshness, surface-probe, session-register]
436
+ stop: [chat-history, hot-context, verify-before-complete, session-register]
377
437
  # language-mirror IS bound here: unlike minimal-safe-diff it needs only the
378
438
  # user_prompt_submit slot, which Windsurf has.
379
- user_prompt_submit: [chat-history, verify-before-complete, language-mirror, git-authorization]
439
+ user_prompt_submit: [chat-history, verify-before-complete, language-mirror, git-authorization, session-canary, session-register]
380
440
 
381
441
  # Phase 7.8 — Gemini CLI. Hooks live at `.gemini/settings.json`
382
442
  # (project) or `~/.gemini/settings.json` (user). Per Gemini docs
@@ -389,10 +449,10 @@ platforms:
389
449
  # turn-check semantics. AfterAgent fires when the agent loop ends
390
450
  # — this is our `stop` slot.
391
451
  gemini:
392
- 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]
393
- session_end: [chat-history, memory-learn]
394
- stop: [chat-history, hot-context, verify-before-complete]
395
- user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization]
452
+ 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, session-register]
453
+ session_end: [chat-history, memory-learn, session-register]
454
+ stop: [chat-history, hot-context, verify-before-complete, session-register]
455
+ user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization, session-canary, session-register]
396
456
  post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
397
457
 
398
458
  # Phase 7.9 — Copilot has no hook surface. Concerns route through
@@ -329,6 +329,61 @@ function _is_blocked(git_tokens: string[]): [boolean, string] {
329
329
  return [false, ''];
330
330
  }
331
331
 
332
+ /**
333
+ * Does an unparseable command plausibly INVOKE git?
334
+ *
335
+ * The fail-closed branch below used to test `/\bgit\b/` against the raw command
336
+ * string. That is a mention, not an invocation, and the difference is not
337
+ * academic: measured in the round-5 audit (2026-08-07), a
338
+ * `mkdir -p … && cat > notes.md <<HEREDOC … HEREDOC` with no git anywhere in
339
+ * command position was refused, because the heredoc body named rules such as
340
+ * `git-history-discipline` and contained apostrophes — so shlex failed and the
341
+ * substring matched. The message's own remedy (`git commit -F <file>`) could not
342
+ * apply, because there was no commit.
343
+ *
344
+ * Fail-closed is still the right posture for the case it was written for: a
345
+ * genuinely unparseable command that DOES invoke git. So the test moves from
346
+ * "the string contains git" to "some command position is git", with heredoc
347
+ * bodies removed first — the same data-not-command distinction
348
+ * `block_unauthorized_git` already makes.
349
+ *
350
+ * Deliberately regex-shaped rather than tokenised: shlex has already failed by
351
+ * the time this runs, so no reliable token stream exists. It therefore stays
352
+ * conservative — a `git` at the head of any line or after any separator counts,
353
+ * quoted or not.
354
+ */
355
+ export function _looks_like_git_invocation(cmd: string, depth = 0): boolean {
356
+ if (depth > 3) {
357
+ return false;
358
+ }
359
+ const withoutHeredocs = cmd.replace(
360
+ /<<-?\s*(['"]?)([A-Za-z_][A-Za-z0-9_]*)\1[\s\S]*?^\s*\2\s*$/gm,
361
+ '<<HEREDOC',
362
+ );
363
+ for (const rawSeg of withoutHeredocs.split(/\n|;|&&|\|\||\||&|\$\(|`/)) {
364
+ // Drop leading env assignments: `GIT_DIR=x git status`.
365
+ const seg = rawSeg
366
+ .trim()
367
+ .replace(/^(?:[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|\S*)\s+)+/, '');
368
+ // A `sh -c '<payload>'` payload IS command context — recurse, or the
369
+ // narrowing above would open a bypass this guard did not have before.
370
+ // Caught by the adversarial case, not by review.
371
+ const wrapped =
372
+ /^(?:\S*\/)?(?:ba|z|k)?sh\s+-[a-z]*c\s+(?:"([^"]*)"|'([^']*)'|(\S+))/.exec(seg);
373
+ if (wrapped !== null) {
374
+ if (_looks_like_git_invocation(wrapped[1] ?? wrapped[2] ?? wrapped[3] ?? '', depth + 1)) {
375
+ return true;
376
+ }
377
+ continue;
378
+ }
379
+ // Command position: optional path prefix, then `git` as its own word.
380
+ if (/^(?:\S*\/)?git(?:\s|$)/.test(seg)) {
381
+ return true;
382
+ }
383
+ }
384
+ return false;
385
+ }
386
+
332
387
  /** Return (blocked, reason). Fail-closed on parse error for git commands. */
333
388
  function _check_command(cmd: string): [boolean, string] {
334
389
  let tokens: string[];
@@ -336,10 +391,12 @@ function _check_command(cmd: string): [boolean, string] {
336
391
  tokens = shlexSplit(cmd);
337
392
  } catch (e) {
338
393
  if (e instanceof ShlexError) {
339
- if (/\bgit\b/.test(cmd)) {
394
+ if (_looks_like_git_invocation(cmd)) {
340
395
  return [
341
396
  true,
342
- 'command parse failed (shlex) on a git-containing command — fail-closed (git-history-discipline)',
397
+ 'command parse failed (shlex) on a git-containing command — fail-closed (git-history-discipline). \n'
398
+ + 'Most common cause: an apostrophe or backtick inside a heredoc commit message in the SAME compound command as `git`. \n'
399
+ + 'Fix: write the message to a file and use `git commit -F <file>` — do not retry the heredoc.',
343
400
  ];
344
401
  }
345
402
  return [false, ''];