@event4u/agent-config 9.26.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 (174) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +57 -229
  3. package/dist/agent-src/commands/analyze/conformance.md +19 -0
  4. package/dist/agent-src/commands/roadmap/next.md +65 -3
  5. package/dist/agent-src/rules/active-remediation.md +2 -0
  6. package/dist/agent-src/rules/analysis-skill-routing.md +2 -0
  7. package/dist/agent-src/rules/architecture.md +2 -0
  8. package/dist/agent-src/rules/artifact-drafting-protocol.md +2 -0
  9. package/dist/agent-src/rules/artifact-engagement-recording.md +2 -0
  10. package/dist/agent-src/rules/augment-edit-discipline.md +2 -0
  11. package/dist/agent-src/rules/autonomous-execution.md +2 -0
  12. package/dist/agent-src/rules/brand-consistency.md +2 -0
  13. package/dist/agent-src/rules/brand-source-of-truth.md +2 -0
  14. package/dist/agent-src/rules/broken-access-control.md +2 -0
  15. package/dist/agent-src/rules/cli-output-handling.md +2 -0
  16. package/dist/agent-src/rules/code-comment-discipline.md +2 -0
  17. package/dist/agent-src/rules/code-provenance.md +2 -0
  18. package/dist/agent-src/rules/command-suggestion-policy.md +2 -0
  19. package/dist/agent-src/rules/commit-conventions.md +2 -0
  20. package/dist/agent-src/rules/communication-through-line.md +2 -0
  21. package/dist/agent-src/rules/content-quoting-floor.md +2 -0
  22. package/dist/agent-src/rules/context-hygiene.md +10 -0
  23. package/dist/agent-src/rules/copilot-routing.md +2 -0
  24. package/dist/agent-src/rules/cross-source-consistency.md +2 -0
  25. package/dist/agent-src/rules/decision-revisit-gate.md +2 -0
  26. package/dist/agent-src/rules/delegation-policy.md +2 -0
  27. package/dist/agent-src/rules/design-fidelity.md +2 -0
  28. package/dist/agent-src/rules/design-review-after-ui-write.md +4 -0
  29. package/dist/agent-src/rules/devcontainer-routing.md +2 -0
  30. package/dist/agent-src/rules/doc-screenshot-hygiene.md +2 -0
  31. package/dist/agent-src/rules/docker-commands.md +2 -0
  32. package/dist/agent-src/rules/domain-adoption-policy.md +2 -0
  33. package/dist/agent-src/rules/domain-safety-disclaimer.md +2 -0
  34. package/dist/agent-src/rules/domain-safety-pii.md +2 -0
  35. package/dist/agent-src/rules/domain-safety-retention.md +2 -0
  36. package/dist/agent-src/rules/downstream-changes.md +2 -0
  37. package/dist/agent-src/rules/engineering-safety-floor.md +2 -0
  38. package/dist/agent-src/rules/evaluator-independence.md +13 -3
  39. package/dist/agent-src/rules/external-code-graph-interop.md +2 -0
  40. package/dist/agent-src/rules/external-reference-deep-dive.md +2 -0
  41. package/dist/agent-src/rules/fast-path-marker-visibility.md +2 -0
  42. package/dist/agent-src/rules/finance-safety-floor.md +2 -0
  43. package/dist/agent-src/rules/framework-neutrality-in-generic-skills.md +2 -0
  44. package/dist/agent-src/rules/git-history-discipline.md +3 -1
  45. package/dist/agent-src/rules/guidelines.md +2 -0
  46. package/dist/agent-src/rules/history-discipline.md +2 -0
  47. package/dist/agent-src/rules/icon-consistency.md +2 -0
  48. package/dist/agent-src/rules/image-likeness-and-rights.md +2 -0
  49. package/dist/agent-src/rules/improve-before-implement.md +2 -0
  50. package/dist/agent-src/rules/invite-challenge.md +2 -0
  51. package/dist/agent-src/rules/laravel-routing.md +2 -0
  52. package/dist/agent-src/rules/laravel-translations.md +2 -0
  53. package/dist/agent-src/rules/legal-safety-floor.md +2 -0
  54. package/dist/agent-src/rules/lethal-trifecta-guard.md +2 -0
  55. package/dist/agent-src/rules/linked-projects-onboarding-gate.md +2 -0
  56. package/dist/agent-src/rules/low-impact-corpus-privacy-floor.md +2 -0
  57. package/dist/agent-src/rules/markdown-safe-codeblocks.md +2 -0
  58. package/dist/agent-src/rules/media-governance-routing.md +2 -0
  59. package/dist/agent-src/rules/media-sync-ground-truth.md +2 -0
  60. package/dist/agent-src/rules/minimal-safe-diff.md +2 -0
  61. package/dist/agent-src/rules/missing-tool-handling.md +2 -0
  62. package/dist/agent-src/rules/model-recommendation.md +2 -0
  63. package/dist/agent-src/rules/no-attribution-footers.md +2 -0
  64. package/dist/agent-src/rules/no-decorative-emojis-in-git-surfaces.md +2 -0
  65. package/dist/agent-src/rules/no-pr-progress-comments.md +2 -0
  66. package/dist/agent-src/rules/no-roadmap-references.md +2 -0
  67. package/dist/agent-src/rules/notes-first-reasoning.md +2 -0
  68. package/dist/agent-src/rules/onboarding-gate.md +2 -0
  69. package/dist/agent-src/rules/output-discipline.md +2 -0
  70. package/dist/agent-src/rules/package-ci-checks.md +2 -0
  71. package/dist/agent-src/rules/persona-governance.md +2 -0
  72. package/dist/agent-src/rules/php-coding.md +2 -0
  73. package/dist/agent-src/rules/prefer-enums-over-literals.md +2 -0
  74. package/dist/agent-src/rules/preservation-guard.md +2 -0
  75. package/dist/agent-src/rules/provider-lifecycle-discipline.md +2 -0
  76. package/dist/agent-src/rules/question-not-instruction.md +2 -0
  77. package/dist/agent-src/rules/reviewer-awareness.md +2 -0
  78. package/dist/agent-src/rules/roadmap-ci-steps-policy.md +2 -0
  79. package/dist/agent-src/rules/roadmap-progress-sync.md +2 -0
  80. package/dist/agent-src/rules/role-mode-adherence.md +2 -0
  81. package/dist/agent-src/rules/rule-type-governance.md +2 -0
  82. package/dist/agent-src/rules/runtime-safety.md +2 -0
  83. package/dist/agent-src/rules/scale-discipline.md +2 -0
  84. package/dist/agent-src/rules/secret-vcs-guard.md +2 -0
  85. package/dist/agent-src/rules/security-sensitive-stop.md +2 -0
  86. package/dist/agent-src/rules/senior-engineering-discipline.md +2 -0
  87. package/dist/agent-src/rules/session-canary.md +61 -19
  88. package/dist/agent-src/rules/settings-ask-protocol.md +4 -0
  89. package/dist/agent-src/rules/size-enforcement.md +2 -0
  90. package/dist/agent-src/rules/skill-improvement-trigger.md +2 -0
  91. package/dist/agent-src/rules/skill-quality.md +2 -0
  92. package/dist/agent-src/rules/slash-command-routing-policy.md +2 -0
  93. package/dist/agent-src/rules/source-confidentiality.md +2 -0
  94. package/dist/agent-src/rules/source-discovery-gate.md +2 -0
  95. package/dist/agent-src/rules/source-of-truth.md +2 -0
  96. package/dist/agent-src/rules/spreadsheet-source-quality.md +2 -0
  97. package/dist/agent-src/rules/strategy-safety-floor.md +2 -0
  98. package/dist/agent-src/rules/symfony-routing.md +2 -0
  99. package/dist/agent-src/rules/think-before-action.md +2 -0
  100. package/dist/agent-src/rules/token-budget-discipline.md +2 -0
  101. package/dist/agent-src/rules/token-efficiency.md +2 -0
  102. package/dist/agent-src/rules/token-optimizer-maintenance.md +2 -0
  103. package/dist/agent-src/rules/tool-safety.md +2 -0
  104. package/dist/agent-src/rules/ui-audit-gate.md +4 -0
  105. package/dist/agent-src/rules/untrusted-input-defense.md +2 -0
  106. package/dist/agent-src/rules/upstream-proposal.md +2 -0
  107. package/dist/agent-src/rules/user-interaction.md +2 -0
  108. package/dist/agent-src/rules/user-interrupt-priority.md +2 -0
  109. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  110. package/dist/cli/registry.js +2 -0
  111. package/dist/cli/registry.js.map +1 -1
  112. package/dist/cli-delegate/{chunk-SJQMJI7M.js → chunk-3KJIMFTB.js} +1 -1
  113. package/dist/cli-delegate/{chunk-BSLUHV2F.js → chunk-BCPIOAGV.js} +148 -133
  114. package/dist/cli-delegate/cmd_conformance.js +1 -1
  115. package/dist/cli-delegate/cmd_doctor.js +1 -1
  116. package/dist/cli-delegate/cmd_export.js +1 -1
  117. package/dist/cli-delegate/cmd_prune.js +1 -1
  118. package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
  119. package/dist/cli-delegate/cmd_sync.js +1 -1
  120. package/dist/cli-delegate/cmd_uninstall.js +1 -1
  121. package/dist/cli-delegate/cmd_validate.js +1 -1
  122. package/dist/discovery/deprecation-report.md +1 -1
  123. package/dist/discovery/discovery-manifest.json +109 -109
  124. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  125. package/dist/discovery/discovery-manifest.summary.md +1 -1
  126. package/dist/discovery/orphan-report.md +1 -1
  127. package/dist/discovery/packs.json +2 -2
  128. package/dist/discovery/trust-report.md +1 -1
  129. package/dist/discovery/workspaces.json +2 -2
  130. package/dist/hooks/dispatch.js +796 -289
  131. package/dist/install/install.mjs +1 -1
  132. package/dist/mcp/registry-manifest.json +2 -2
  133. package/dist/mcp/server.json +2 -2
  134. package/dist/mcp/server.mjs +571 -417
  135. package/dist/scripts/condense.js +33 -2
  136. package/dist/scripts/condense.js.map +1 -1
  137. package/dist/server/io/yamlIO.js +85 -0
  138. package/dist/server/io/yamlIO.js.map +1 -1
  139. package/dist/server/routes/settings.js +1 -12
  140. package/dist/server/routes/settings.js.map +1 -1
  141. package/dist/server/routes/wizard.js +52 -2
  142. package/dist/server/routes/wizard.js.map +1 -1
  143. package/dist/shared/settingsCarveOut.js +118 -0
  144. package/dist/shared/settingsCarveOut.js.map +1 -0
  145. package/dist/shared/settingsSurface.js +7 -1
  146. package/dist/shared/settingsSurface.js.map +1 -1
  147. package/dist/ui/assets/index-DmR0mbk4.js.map +1 -1
  148. package/package.json +1 -1
  149. package/src/config/evaluator-budgets.json +4 -4
  150. package/src/scripts/_cli/cmd_doctor.ts +6 -21
  151. package/src/scripts/_dispatch.bash +18 -0
  152. package/src/scripts/_lib/git_common_dir.ts +128 -0
  153. package/src/scripts/_lib/obligation_frequency.ts +391 -0
  154. package/src/scripts/_lib/session_register.ts +361 -0
  155. package/src/scripts/check_enforcement_coverage.ts +192 -7
  156. package/src/scripts/check_rule_projection_integrity.ts +364 -0
  157. package/src/scripts/condense.ts +36 -2
  158. package/src/scripts/conformance_scan.ts +35 -0
  159. package/src/scripts/generate_settings_reference.ts +232 -0
  160. package/src/scripts/hook_manifest.yaml +68 -27
  161. package/src/scripts/hooks/block_no_verify.ts +56 -1
  162. package/src/scripts/hooks/block_unauthorized_git.ts +67 -1
  163. package/src/scripts/hooks/concern_registry.ts +2 -0
  164. package/src/scripts/language_mirror_hook.ts +56 -6
  165. package/src/scripts/orchestration_backfill.ts +354 -0
  166. package/src/scripts/release.ts +142 -36
  167. package/src/scripts/schemas/rule.schema.json +5 -0
  168. package/src/scripts/session_canary_hook.ts +35 -3
  169. package/src/scripts/session_register_hook.ts +288 -0
  170. package/src/scripts/sessions_cli.ts +140 -0
  171. package/src/scripts/validate_frontmatter.ts +68 -0
  172. package/src/server/io/yamlIO.ts +87 -0
  173. package/src/shared/settingsCarveOut.ts +143 -0
  174. 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
+ }
@@ -298,32 +298,73 @@ concerns:
298
298
  args: []
299
299
  fail_closed: false
300
300
  severity: blocking
301
- # session_start canary injection — deterministic carrier for the
301
+ # Canary injection — deterministic carrier for the
302
302
  # session-canary rule (greet-by-name at task start + reply-close markers
303
303
  # as a user-observable context-degradation signal). Gated on
304
304
  # `personal.canary_name` in .agent-settings.yml (empty = clean no-op),
305
305
  # so unconfigured consumers pay nothing. Emits a `context` block the
306
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.
307
316
  # Never blocks. fail_closed: false.
308
317
  session-canary:
309
318
  script: src/scripts/session_canary_hook.ts
310
319
  args: []
311
320
  fail_closed: false
312
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
313
354
 
314
355
  platforms:
315
356
  augment:
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]
317
- session_end: [chat-history, memory-learn]
318
- 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]
319
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]
320
361
  post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
321
362
 
322
363
  claude:
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]
324
- session_end: [chat-history, memory-learn]
325
- stop: [chat-history, hot-context, verify-before-complete, team-review-gate]
326
- 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]
327
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]
328
369
  post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
329
370
 
@@ -342,10 +383,10 @@ platforms:
342
383
  # Decision matrix + upstream blockers tracked in
343
384
  # agents/settings/contexts/chat-history-platform-hooks.md § Cowork.
344
385
  cowork:
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]
346
- session_end: [chat-history, memory-learn]
347
- stop: [chat-history, hot-context, verify-before-complete]
348
- 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]
349
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]
350
391
  post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
351
392
 
@@ -357,10 +398,10 @@ platforms:
357
398
  # IDE-only — CLI-only users fall back to /checkpoint per
358
399
  # agents/settings/contexts/chat-history-platform-hooks.md.
359
400
  cursor:
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]
361
- session_end: [chat-history, memory-learn]
362
- stop: [chat-history, hot-context, verify-before-complete]
363
- 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]
364
405
  post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
365
406
 
366
407
  # Phase 7.6 — Cline. Hooks live under `.clinerules/hooks/<HookName>`
@@ -372,10 +413,10 @@ platforms:
372
413
  # both map to session_start. TaskCancel maps to stop because the
373
414
  # session is interrupted with partial state (mirrors Augment Stop).
374
415
  cline:
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]
376
- session_end: [chat-history, memory-learn]
377
- stop: [chat-history, hot-context, verify-before-complete]
378
- 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]
379
420
  post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
380
421
 
381
422
  # Phase 7.7 — Windsurf (Cascade). Hooks live at `.windsurf/hooks.json`
@@ -391,11 +432,11 @@ platforms:
391
432
  # surface to record verification commands; documented limitation).
392
433
  # minimal-safe-diff is omitted entirely on Windsurf for the same reason.
393
434
  windsurf:
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]
395
- 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]
396
437
  # language-mirror IS bound here: unlike minimal-safe-diff it needs only the
397
438
  # user_prompt_submit slot, which Windsurf has.
398
- 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]
399
440
 
400
441
  # Phase 7.8 — Gemini CLI. Hooks live at `.gemini/settings.json`
401
442
  # (project) or `~/.gemini/settings.json` (user). Per Gemini docs
@@ -408,10 +449,10 @@ platforms:
408
449
  # turn-check semantics. AfterAgent fires when the agent loop ends
409
450
  # — this is our `stop` slot.
410
451
  gemini:
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]
412
- session_end: [chat-history, memory-learn]
413
- stop: [chat-history, hot-context, verify-before-complete]
414
- 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]
415
456
  post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
416
457
 
417
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,7 +391,7 @@ 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
397
  'command parse failed (shlex) on a git-containing command — fail-closed (git-history-discipline). \n'
@@ -143,6 +143,62 @@ export function extractCommand(envelope: JsonObject): string | null {
143
143
  return null;
144
144
  }
145
145
 
146
+ /**
147
+ * Split on shell separators that are OUTSIDE quotes.
148
+ *
149
+ * Separators recognised, matching the raw regex this replaces: newline, `;`,
150
+ * `&&`, `||`, `|`, `&`. A separator inside `'…'` or `"…"` is a literal, so it
151
+ * does not split. Backslash escapes the next character outside single quotes,
152
+ * per POSIX.
153
+ *
154
+ * Deliberately NOT a full tokeniser. `shlexSplit` in `block_no_verify` throws on
155
+ * an unbalanced quote, which is the right behaviour for a guard that fail-closes;
156
+ * this guard must classify every command it sees, including a malformed one. So
157
+ * an unterminated quote here degrades to "the rest of the string is quoted",
158
+ * which yields one segment starting at the real command word — the conservative
159
+ * outcome, since the command word is what `commandOp` matches on.
160
+ */
161
+ export function splitOutsideQuotes(command: string): string[] {
162
+ const parts: string[] = [];
163
+ let cur = "";
164
+ let quote: '"' | "'" | null = null;
165
+ for (let i = 0; i < command.length; i += 1) {
166
+ const ch = command[i] as string;
167
+ if (quote !== null) {
168
+ cur += ch;
169
+ if (ch === quote) {
170
+ quote = null;
171
+ } else if (ch === "\\" && quote === '"' && i + 1 < command.length) {
172
+ cur += command[i + 1] as string;
173
+ i += 1;
174
+ }
175
+ continue;
176
+ }
177
+ if (ch === '"' || ch === "'") {
178
+ quote = ch;
179
+ cur += ch;
180
+ continue;
181
+ }
182
+ if (ch === "\\" && i + 1 < command.length) {
183
+ cur += ch + (command[i + 1] as string);
184
+ i += 1;
185
+ continue;
186
+ }
187
+ if (ch === "\n" || ch === ";" || ch === "|" || ch === "&") {
188
+ // `&&` and `||` are two-character separators; consume the pair.
189
+ if ((ch === "|" || ch === "&") && command[i + 1] === ch) {
190
+ i += 1;
191
+ }
192
+ parts.push(cur);
193
+ cur = "";
194
+ continue;
195
+ }
196
+ cur += ch;
197
+ }
198
+ parts.push(cur);
199
+ return parts;
200
+ }
201
+
146
202
  /**
147
203
  * Split a shell command into the segments that are actually INVOKED.
148
204
  *
@@ -159,6 +215,16 @@ export function extractCommand(envelope: JsonObject): string | null {
159
215
  * Quoted payloads are NOT simply discarded — `sh -c "npm publish"` really does
160
216
  * publish. Those are unwrapped and re-split, so closing the false positive does
161
217
  * not open a bypass.
218
+ *
219
+ * The separator split is QUOTE-AWARE, and that is not a refinement — it is the
220
+ * same false-positive class the paragraph above describes, surviving in the one
221
+ * place the round-2 fix did not reach. Measured in the round-5 audit
222
+ * (2026-08-07): a read-only `grep -n -E "…|npm publish|…"` was refused with
223
+ * "Blocked: `publish` with no authorization in this turn's prompt", because a
224
+ * raw `split` on `|` cut the quoted alternation into a segment beginning with
225
+ * `npm`. A `|` inside quotes is data, exactly like a heredoc body; only an
226
+ * unquoted one is a pipe. Splitting outside quotes closes that without
227
+ * discarding quoted payloads, so `sh -c "npm publish"` still unwraps below.
162
228
  */
163
229
  export function invokedSegments(command: string, depth = 0): string[] {
164
230
  if (depth > 3) {
@@ -172,7 +238,7 @@ export function invokedSegments(command: string, depth = 0): string[] {
172
238
  /<<-?\s*(['"]?)([A-Za-z_][A-Za-z0-9_]*)\1[\s\S]*?^\s*\2\s*$/gm,
173
239
  "<<HEREDOC",
174
240
  );
175
- for (const raw of withoutHeredocs.split(/\n|;|&&|\|\||\||&/)) {
241
+ for (const raw of splitOutsideQuotes(withoutHeredocs)) {
176
242
  let seg = raw.trim();
177
243
  if (!seg) {
178
244
  continue;
@@ -41,6 +41,7 @@ import { main as surfaceProbeMain } from '../surface_probe_hook.js';
41
41
  import { main as teamReviewGateMain } from '../team_review_gate_hook.js';
42
42
  import { main as prUrlReminderMain } from '../pr_url_reminder_hook.js';
43
43
  import { main as sessionCanaryMain } from '../session_canary_hook.js';
44
+ import { main as sessionRegisterMain } from '../session_register_hook.js';
44
45
  import { main as languageMirrorMain } from '../language_mirror_hook.js';
45
46
  import { main as gitAuthorizationMain } from '../git_authorization_hook.js';
46
47
  import { main as blockUnauthorizedGitMain } from './block_unauthorized_git.js';
@@ -74,6 +75,7 @@ export const CONCERN_REGISTRY: Readonly<Record<string, ConcernMain>> = {
74
75
  'src/scripts/team_review_gate_hook.ts': teamReviewGateMain as ConcernMain,
75
76
  'src/scripts/pr_url_reminder_hook.ts': prUrlReminderMain as ConcernMain,
76
77
  'src/scripts/session_canary_hook.ts': sessionCanaryMain as ConcernMain,
78
+ 'src/scripts/session_register_hook.ts': sessionRegisterMain as ConcernMain,
77
79
  'src/scripts/language_mirror_hook.ts': languageMirrorMain as ConcernMain,
78
80
  'src/scripts/git_authorization_hook.ts': gitAuthorizationMain as ConcernMain,
79
81
  'src/scripts/hooks/block_unauthorized_git.ts': blockUnauthorizedGitMain as ConcernMain,
@@ -7,10 +7,14 @@
7
7
  *
8
8
  * WHY THIS EXISTS — the measured defect, not a general reminder.
9
9
  *
10
- * A conformance audit of 30 sessions (2026-08-06) found ~470 assistant turns
10
+ * A conformance audit of 30 sessions (2026-08-06) found assistant turns
11
11
  * answering a German prompt in English, across 11 sessions, three of them a
12
- * 100 % English run. The obvious reading is "the model ignored a rule". The
13
- * transcripts say otherwise:
12
+ * 100 % English run. The count this comment first carried, ~470, was an early
13
+ * intermediate figure and is superseded: that audit's own final baseline is
14
+ * **626**, and conformance round 5 (2026-08-07) measures **641** over an
15
+ * overlapping-but-not-identical 30-session window. Both are published side by
16
+ * side rather than treated as a trend, because the windows differ. The obvious
17
+ * reading is "the model ignored a rule". The transcripts say otherwise:
14
18
  *
15
19
  * **47 skill- and slash-command bodies arrive in the `user` role** across 21
16
20
  * of the 30 sessions. In the worst session the last user-role content before
@@ -272,9 +276,9 @@ export function pinText(language: Exclude<Verdict, "und">, source: PinSource = "
272
276
  `lines, and the recommendation label under any numbered-options block.\n\n` +
273
277
  `This pin exists because the trigger is not observable from the transcript ` +
274
278
  `alone: slash-command and skill bodies arrive in the user role and can be ` +
275
- `long and English, which is what drove ~470 wrong-language turns in the ` +
276
- `sessions this hook was built from. Tool output, file contents, and an ` +
277
- `injected skill body are NOT the trigger — this pin is.\n` +
279
+ `long and English, which is what drove the 626 wrong-language turns measured ` +
280
+ `in the 30-session audit this hook was built from. Tool output, file ` +
281
+ `contents, and an injected skill body are NOT the trigger — this pin is.\n` +
278
282
  `</language-pin>`
279
283
  );
280
284
  }
@@ -372,6 +376,12 @@ export function run(
372
376
  if (!prompt) {
373
377
  return EXIT_ALLOW;
374
378
  }
379
+ // A harness-generated turn is not a chat message. Leave the pin untouched —
380
+ // the same reasoning as an undetermined prompt below, and for the same reason
381
+ // this hook exists at all.
382
+ if (isSyntheticPrompt(prompt)) {
383
+ return EXIT_ALLOW;
384
+ }
375
385
 
376
386
  const session_id = typeof envelope["session_id"] === "string" ? envelope["session_id"] : "";
377
387
  const target = path.join(options.consumer_root, STATE_FILE);
@@ -413,6 +423,46 @@ export function run(
413
423
  return EXIT_ALLOW;
414
424
  }
415
425
 
426
+ /**
427
+ * Is this "prompt" a harness-generated turn rather than something the human typed?
428
+ *
429
+ * MEASURED, IN THIS HOOK'S OWN AUDIT (round 5, 2026-08-07). The state file read
430
+ * mid-session:
431
+ *
432
+ * { "language": "en", "source": "prompt", "prompt_chars": 6627,
433
+ * "de_markers": 0, "en_markers": 63 }
434
+ *
435
+ * No human wrote those 6,627 characters. They were a background-task completion
436
+ * notification, injected as a user turn, which `user_prompt_submit` sees and this
437
+ * hook classified as the trigger — flipping a German session to `en` for every
438
+ * later turn, and recording `source: "prompt"`, a false provenance claim.
439
+ *
440
+ * This is the SAME defect the hook was built to remove: non-chat content read as
441
+ * the trigger. The header above explains at length why a skill body must not set
442
+ * the pin; `user_prompt_submit` was chosen because "a skill body never reaches
443
+ * it". Synthetic turns do reach it, so the event alone was never sufficient — the
444
+ * content test has to exist too.
445
+ *
446
+ * The markers are structural, not linguistic: the harness stamps these turns
447
+ * with fixed envelope tags. Matching prose would misfire on a human quoting a
448
+ * notification; matching the tags cannot, because a human typing `<task-notification>`
449
+ * is quoting, and a quote does not open at character zero of the turn.
450
+ *
451
+ * Deliberately conservative: an unrecognised synthetic shape falls through and
452
+ * pins as before. Under-filtering keeps today's behaviour; over-filtering would
453
+ * silently stop pinning real prompts, which is the worse failure.
454
+ */
455
+ export function isSyntheticPrompt(prompt: string): boolean {
456
+ const head = prompt.trimStart().slice(0, 400);
457
+ return (
458
+ head.startsWith("[SYSTEM NOTIFICATION - NOT USER INPUT]") ||
459
+ head.startsWith("<task-notification>") ||
460
+ head.startsWith("<system-reminder>") ||
461
+ head.startsWith("<local-command-caveat>") ||
462
+ /^\[SYSTEM NOTIFICATION\b/.test(head)
463
+ );
464
+ }
465
+
416
466
  export function main(argv?: string[]): number {
417
467
  const args = argv ?? process.argv.slice(2);
418
468
  let consumer_root = process.cwd();