@event4u/agent-config 9.26.0 → 9.28.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 (252) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/AGENTS.md +12 -11
  3. package/CHANGELOG.md +134 -207
  4. package/README.md +1 -1
  5. package/dist/agent-src/commands/analyze/conformance.md +43 -0
  6. package/dist/agent-src/commands/roadmap/next.md +65 -3
  7. package/dist/agent-src/contexts/execution/orchestration-telemetry.md +10 -3
  8. package/dist/agent-src/rules/active-remediation.md +2 -0
  9. package/dist/agent-src/rules/analysis-skill-routing.md +2 -0
  10. package/dist/agent-src/rules/architecture.md +2 -0
  11. package/dist/agent-src/rules/artifact-drafting-protocol.md +2 -0
  12. package/dist/agent-src/rules/artifact-engagement-recording.md +6 -1
  13. package/dist/agent-src/rules/augment-edit-discipline.md +2 -0
  14. package/dist/agent-src/rules/autonomous-execution.md +3 -1
  15. package/dist/agent-src/rules/brand-consistency.md +2 -0
  16. package/dist/agent-src/rules/brand-source-of-truth.md +35 -6
  17. package/dist/agent-src/rules/broken-access-control.md +2 -0
  18. package/dist/agent-src/rules/cli-output-handling.md +2 -0
  19. package/dist/agent-src/rules/code-comment-discipline.md +2 -0
  20. package/dist/agent-src/rules/code-provenance.md +2 -0
  21. package/dist/agent-src/rules/command-suggestion-policy.md +2 -0
  22. package/dist/agent-src/rules/commit-conventions.md +2 -0
  23. package/dist/agent-src/rules/communication-through-line.md +2 -0
  24. package/dist/agent-src/rules/content-quoting-floor.md +2 -0
  25. package/dist/agent-src/rules/context-hygiene.md +13 -1
  26. package/dist/agent-src/rules/copilot-routing.md +2 -0
  27. package/dist/agent-src/rules/council-availability.md +93 -0
  28. package/dist/agent-src/rules/cross-source-consistency.md +5 -0
  29. package/dist/agent-src/rules/decision-revisit-gate.md +2 -0
  30. package/dist/agent-src/rules/delegation-policy.md +31 -5
  31. package/dist/agent-src/rules/design-fidelity.md +5 -0
  32. package/dist/agent-src/rules/design-review-after-ui-write.md +4 -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 +14 -3
  43. package/dist/agent-src/rules/external-code-graph-interop.md +4 -1
  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 +3 -1
  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 +5 -1
  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 +3 -1
  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 +5 -1
  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 +5 -1
  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 +6 -1
  83. package/dist/agent-src/rules/roadmap-progress-sync.md +2 -0
  84. package/dist/agent-src/rules/role-mode-adherence.md +3 -1
  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 +5 -1
  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/self-repair-loop.md +103 -0
  91. package/dist/agent-src/rules/senior-engineering-discipline.md +2 -0
  92. package/dist/agent-src/rules/session-canary.md +62 -19
  93. package/dist/agent-src/rules/settings-ask-protocol.md +4 -0
  94. package/dist/agent-src/rules/size-enforcement.md +2 -0
  95. package/dist/agent-src/rules/skill-improvement-trigger.md +2 -0
  96. package/dist/agent-src/rules/skill-quality.md +2 -0
  97. package/dist/agent-src/rules/slash-command-routing-policy.md +2 -0
  98. package/dist/agent-src/rules/source-confidentiality.md +2 -0
  99. package/dist/agent-src/rules/source-discovery-gate.md +2 -0
  100. package/dist/agent-src/rules/source-of-truth.md +2 -0
  101. package/dist/agent-src/rules/spreadsheet-source-quality.md +2 -0
  102. package/dist/agent-src/rules/strategy-safety-floor.md +3 -1
  103. package/dist/agent-src/rules/symfony-routing.md +2 -0
  104. package/dist/agent-src/rules/think-before-action.md +2 -0
  105. package/dist/agent-src/rules/token-budget-discipline.md +5 -1
  106. package/dist/agent-src/rules/token-efficiency.md +2 -0
  107. package/dist/agent-src/rules/token-optimizer-maintenance.md +2 -0
  108. package/dist/agent-src/rules/tool-safety.md +7 -1
  109. package/dist/agent-src/rules/ui-audit-gate.md +4 -0
  110. package/dist/agent-src/rules/untrusted-input-defense.md +2 -0
  111. package/dist/agent-src/rules/upstream-proposal.md +2 -0
  112. package/dist/agent-src/rules/user-interaction.md +2 -0
  113. package/dist/agent-src/rules/user-interrupt-priority.md +2 -0
  114. package/dist/agent-src/skills/adversarial-review/SKILL.md +1 -1
  115. package/dist/agent-src/skills/analysis-autonomous-mode/SKILL.md +1 -1
  116. package/dist/agent-src/skills/performance-analysis/SKILL.md +1 -1
  117. package/dist/agent-src/skills/persona-improvement/SKILL.md +1 -1
  118. package/dist/agent-src/skills/project-analyzer/SKILL.md +1 -1
  119. package/dist/agent-src/skills/security-audit/SKILL.md +1 -1
  120. package/dist/agent-src/skills/sequential-thinking/SKILL.md +1 -1
  121. package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +1 -1
  122. package/dist/agent-src/skills/universal-project-analysis/SKILL.md +1 -1
  123. package/dist/agent-src/templates/AGENTS.md +6 -10
  124. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  125. package/dist/cli/registry.js +9 -0
  126. package/dist/cli/registry.js.map +1 -1
  127. package/dist/cli-delegate/chunk-22GCIJ2O.js +270 -0
  128. package/dist/cli-delegate/{chunk-BSLUHV2F.js → chunk-2BE5I2MN.js} +156 -141
  129. package/dist/cli-delegate/{chunk-AIIUU73N.js → chunk-7OMQCH4V.js} +1 -1
  130. package/dist/cli-delegate/{chunk-HJ6DAJHR.js → chunk-GB2FSUEE.js} +57 -7
  131. package/dist/cli-delegate/{chunk-WR6GTRHJ.js → chunk-JXARCGVN.js} +3 -3
  132. package/dist/cli-delegate/{chunk-SJQMJI7M.js → chunk-PQSAQKHW.js} +467 -305
  133. package/dist/cli-delegate/chunk-REKCMYOU.js +2991 -0
  134. package/dist/cli-delegate/cmd_analyze_session.js +1 -1
  135. package/dist/cli-delegate/cmd_brand_status.js +134 -0
  136. package/dist/cli-delegate/cmd_conformance.js +7 -7
  137. package/dist/cli-delegate/cmd_converge.js +3 -3
  138. package/dist/cli-delegate/cmd_doctor.js +4 -4
  139. package/dist/cli-delegate/cmd_explain.js +80 -330
  140. package/dist/cli-delegate/cmd_export.js +5 -5
  141. package/dist/cli-delegate/cmd_fleet.js +3 -3
  142. package/dist/cli-delegate/cmd_handoff.js +2 -2
  143. package/dist/cli-delegate/cmd_mcp_available.js +433 -0
  144. package/dist/cli-delegate/cmd_migrate.js +3 -3
  145. package/dist/cli-delegate/cmd_packs_active.js +153 -0
  146. package/dist/cli-delegate/cmd_preflight.js +1 -1
  147. package/dist/cli-delegate/cmd_prune.js +5 -5
  148. package/dist/cli-delegate/cmd_route_audit.js +8 -8
  149. package/dist/cli-delegate/cmd_route_explain.js +4 -4
  150. package/dist/cli-delegate/cmd_settings_get.js +272 -0
  151. package/dist/cli-delegate/cmd_settings_migrate.js +5 -5
  152. package/dist/cli-delegate/cmd_settings_set.js +7 -2967
  153. package/dist/cli-delegate/cmd_sync.js +5 -5
  154. package/dist/cli-delegate/cmd_uninstall.js +5 -5
  155. package/dist/cli-delegate/cmd_update.js +4 -4
  156. package/dist/cli-delegate/cmd_upgrade.js +4 -4
  157. package/dist/cli-delegate/cmd_validate.js +5 -5
  158. package/dist/cli-delegate/cmd_versions.js +4 -4
  159. package/dist/discovery/deprecation-report.md +1 -1
  160. package/dist/discovery/discovery-manifest.json +184 -124
  161. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  162. package/dist/discovery/discovery-manifest.summary.md +3 -3
  163. package/dist/discovery/orphan-report.md +1 -1
  164. package/dist/discovery/packs.json +7 -5
  165. package/dist/discovery/trust-report.md +11 -11
  166. package/dist/discovery/workspaces.json +6 -4
  167. package/dist/hooks/dispatch.js +9080 -5701
  168. package/dist/install/install.mjs +698 -508
  169. package/dist/mcp/registry-manifest.json +3 -3
  170. package/dist/mcp/server.json +2 -2
  171. package/dist/mcp/server.mjs +698 -437
  172. package/dist/router.json +1 -1
  173. package/dist/scripts/_lib/agent_settings.js +80 -15
  174. package/dist/scripts/_lib/agent_settings.js.map +1 -1
  175. package/dist/scripts/condense.js +33 -2
  176. package/dist/scripts/condense.js.map +1 -1
  177. package/dist/server/io/yamlIO.js +85 -0
  178. package/dist/server/io/yamlIO.js.map +1 -1
  179. package/dist/server/routes/settings.js +1 -12
  180. package/dist/server/routes/settings.js.map +1 -1
  181. package/dist/server/routes/wizard.js +52 -2
  182. package/dist/server/routes/wizard.js.map +1 -1
  183. package/dist/shared/settingsCarveOut.js +118 -0
  184. package/dist/shared/settingsCarveOut.js.map +1 -0
  185. package/dist/shared/settingsSurface.js +7 -1
  186. package/dist/shared/settingsSurface.js.map +1 -1
  187. package/dist/ui/assets/index-DmR0mbk4.js.map +1 -1
  188. package/docs/contracts/settings-classes.md +29 -8
  189. package/docs/guidelines/agent-infra/memory-access.md +14 -3
  190. package/package.json +1 -1
  191. package/src/agent-src/templates/AGENTS.md +6 -10
  192. package/src/config/budgets.yml +29 -0
  193. package/src/config/evaluator-budgets.json +4 -4
  194. package/src/config/gate-coverage.yml +26 -0
  195. package/src/scripts/_cli/cmd_brand_status.ts +174 -0
  196. package/src/scripts/_cli/cmd_doctor.ts +6 -21
  197. package/src/scripts/_cli/cmd_mcp_available.ts +266 -0
  198. package/src/scripts/_cli/cmd_packs_active.ts +225 -0
  199. package/src/scripts/_cli/cmd_settings_get.ts +324 -0
  200. package/src/scripts/_dispatch.bash +75 -0
  201. package/src/scripts/_lib/agent_settings.ts +82 -16
  202. package/src/scripts/_lib/carrier_divergence.ts +135 -0
  203. package/src/scripts/_lib/git_common_dir.ts +128 -0
  204. package/src/scripts/_lib/host_capability.ts +84 -1
  205. package/src/scripts/_lib/obligation_frequency.ts +391 -0
  206. package/src/scripts/_lib/prompt_shape.ts +88 -0
  207. package/src/scripts/_lib/review_skipped_record.ts +102 -0
  208. package/src/scripts/_lib/rule_layer_overlap.ts +254 -0
  209. package/src/scripts/_lib/self_repair.ts +342 -0
  210. package/src/scripts/_lib/self_repair_store.ts +100 -0
  211. package/src/scripts/_lib/session_register.ts +361 -0
  212. package/src/scripts/_lib/transcript_entry.ts +80 -0
  213. package/src/scripts/check_branch_freshness.ts +255 -0
  214. package/src/scripts/check_council_config_location.ts +125 -1
  215. package/src/scripts/check_enforcement_coverage.ts +192 -7
  216. package/src/scripts/check_rule_projection_integrity.ts +364 -0
  217. package/src/scripts/check_standing_rule_delivery.ts +314 -0
  218. package/src/scripts/condense.ts +36 -2
  219. package/src/scripts/config/profiles.ts +13 -3
  220. package/src/scripts/conformance_scan.ts +307 -24
  221. package/src/scripts/council_availability_hook.ts +188 -0
  222. package/src/scripts/council_cli.ts +126 -7
  223. package/src/scripts/generate_settings_reference.ts +232 -0
  224. package/src/scripts/hook_manifest.yaml +155 -33
  225. package/src/scripts/hooks/block_no_verify.ts +95 -3
  226. package/src/scripts/hooks/block_unauthorized_git.ts +217 -2
  227. package/src/scripts/hooks/concern_registry.ts +12 -0
  228. package/src/scripts/hooks/delegation_nudge_hook.ts +450 -0
  229. package/src/scripts/hooks/end_review_nudge_hook.ts +726 -0
  230. package/src/scripts/hooks/orchestration_record_hook.ts +342 -0
  231. package/src/scripts/install.ts +151 -0
  232. package/src/scripts/language_mirror_hook.ts +49 -6
  233. package/src/scripts/measure_scope_dedup.ts +19 -13
  234. package/src/scripts/onboarding_gate_hook.ts +40 -1
  235. package/src/scripts/orchestration_backfill.ts +354 -0
  236. package/src/scripts/orchestration_record.ts +4 -1
  237. package/src/scripts/preamble_byte_census.ts +9 -20
  238. package/src/scripts/release.ts +142 -36
  239. package/src/scripts/report_carrier_divergence.ts +364 -0
  240. package/src/scripts/report_skill_activation.ts +257 -0
  241. package/src/scripts/report_skill_obligation_violations.ts +579 -0
  242. package/src/scripts/routing_doctor.ts +77 -6
  243. package/src/scripts/schemas/rule.schema.json +5 -0
  244. package/src/scripts/self_repair_cli.ts +240 -0
  245. package/src/scripts/self_repair_hook.ts +241 -0
  246. package/src/scripts/session_canary_hook.ts +35 -3
  247. package/src/scripts/session_register_hook.ts +288 -0
  248. package/src/scripts/sessions_cli.ts +140 -0
  249. package/src/scripts/validate_frontmatter.ts +68 -0
  250. package/src/server/io/yamlIO.ts +87 -0
  251. package/src/shared/settingsCarveOut.ts +143 -0
  252. package/src/shared/settingsSurface.ts +7 -1
@@ -298,34 +298,146 @@ 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
+ # Council-availability fact. Carries whether a council is configured and from
323
+ # where, because the always-loaded layer refers to the council conditionally
324
+ # and never says how to check. Measured 2026-08-08: an agent announced "kein
325
+ # Council konfiguriert (keine .agent-settings.yml)" and substituted a weaker
326
+ # path; the council was configured user-globally the whole time, and the
327
+ # project tree is never searched for it (ADR-104). Advisory: supplies a fact,
328
+ # never blocks.
329
+ council-availability:
330
+ script: src/scripts/council_availability_hook.ts
331
+ args: []
332
+ fail_closed: false
333
+ severity: advisory
334
+ # Self-repair intake (road-to-self-repair Phase 1). Two slots, one queue:
335
+ # user_prompt_submit opens a record when the user states the agent worked
336
+ # wrongly and injects the open-queue line; stop runs the deterministic
337
+ # detectors over the finished turn and records silently (a reminder fired
338
+ # after the reply cannot shape it). ADVISORY by construction — it records,
339
+ # it never gates; the outward step is a separate, user-run CLI command.
340
+ self-repair:
341
+ script: src/scripts/self_repair_hook.ts
342
+ args: []
343
+ fail_closed: false
344
+ severity: advisory
345
+ # Shared per-session register (road-to-parallel-session-coordination) — writes
346
+ # this session into <git-common-dir>/agent-sessions/<session_id>.json so every
347
+ # other worktree of the repo can see it, and on session_start injects the live
348
+ # foreign sessions as a `context` block (hot-context mechanism).
349
+ #
350
+ # Bound in FOUR slots, with deliberately asymmetric meanings:
351
+ # session_start → register + prune expired + inject foreign sessions
352
+ # user_prompt_submit → heartbeat (pre-reply carrier)
353
+ # stop → heartbeat (post-reply carrier), NOT deregistration
354
+ # session_end → deregister
355
+ #
356
+ # `stop` is per-turn, not session end: on Claude Code the native Stop fires
357
+ # after every assistant reply (see _lib/obligation_frequency.ts), so
358
+ # deregistering there would kill a live session's own record after its first
359
+ # reply. The ONE exception is Cline, whose stop is TaskCancel and whose
360
+ # session_end is TaskComplete — a cancelled Cline task reaches neither, so it
361
+ # would leak its claim until TTL. That exception is an explicit platform
362
+ # allow-list in _lib/session_register.ts, never a condition computed from the
363
+ # frequency lattice: a future platform must be added by a human, deliberately.
364
+ #
365
+ # Correctness rests on heartbeat + TTL alone; deregistration only frees a claim
366
+ # sooner. So Windsurf's missing session_end costs latency, not correctness.
367
+ # Declared non-carriers: copilot (no hook surface), cursor CLI (per-turn slots
368
+ # are IDE-only), cowork (slots wired, lifecycle events inert) — a session there
369
+ # registers but expires by TTL rather than heartbeating, and the concern says so
370
+ # on stderr rather than letting the coverage join read green.
371
+ # Never blocks. fail_closed: false.
372
+ session-register:
373
+ script: src/scripts/session_register_hook.ts
374
+ args: []
375
+ fail_closed: false
376
+ severity: advisory
377
+ # road-to-orchestrator-discipline-carriers Phase 3 (F6) — deterministic
378
+ # PostToolUse orchestration-telemetry capture. Fires only on an Agent/Task
379
+ # tool-use completion (the two observed subagent-dispatch tool names) and
380
+ # appends one `orchestration_record` line — no model step, replacing the
381
+ # measured 0.27% model-carried capture rate for future dispatches. Silent
382
+ # on every path (never writes to stdout); exit 0 always, including a
383
+ # malformed envelope or a write failure. fail_closed: false.
384
+ orchestration-record:
385
+ script: src/scripts/hooks/orchestration_record_hook.ts
386
+ args: []
387
+ fail_closed: false
388
+ severity: advisory
389
+ tools: [Agent, Task]
390
+ # road-to-orchestrator-discipline-carriers Phase 4 (F3-lite) — conditional
391
+ # delegation nudge. Extracts cheap structural signals from the SUBMITTED
392
+ # prompt (enumerated file lists, "for each"/"alle …" shapes, explicit slice
393
+ # counts, ordered-plan markers, multi-deliverable conjunctions), classifies
394
+ # via `classifyTask`, and — only on a positive do-in-parallel/do-in-steps
395
+ # verdict — reports `{decision:"warn", reason, additional_context}` at exit
396
+ # 2, mirroring `language-mirror`'s shipped delivery pattern so the verdict
397
+ # line actually reaches `hookSpecificOutput.additionalContext` on the
398
+ # verified `claude` platform. Silence is the anti-canary default: no
399
+ # enumerated signal → no injection at all. Never blocks the turn — see the
400
+ # hook's file header for the `host_semantics.emitFor` proof. fail_closed:
401
+ # false.
402
+ delegation-nudge:
403
+ script: src/scripts/hooks/delegation_nudge_hook.ts
404
+ args: []
405
+ fail_closed: false
406
+ severity: advisory
407
+ # road-to-orchestrator-discipline-carriers Phase 5 (F4-lite) — end-review
408
+ # nudge. Fires ONLY when the session mutated tracked+untracked, non-doc
409
+ # files beyond MUTATION_LINE_THRESHOLD (50 lines; untracked files added
410
+ # post-review, F6) AND no reviewer ran this session (scanned from the
411
+ # Stop payload's `transcript_path`) AND this session has not already
412
+ # fired once (F2 — a once-per-session gate; state under
413
+ # agents/runtime/state/end-review-nudge/, gitignored). On fire, reports
414
+ # `{decision:"warn", reason, additional_context}` at exit 2 (same delivery
415
+ # pattern as `language-mirror`/`delegation-nudge` above) and appends one
416
+ # `review_skipped` telemetry line — hook-carried, not model-carried — so a
417
+ # future blocking decision has a measured distribution to cite. Never
418
+ # blocks the turn — see the hook's file header for the `host_semantics.
419
+ # emitFor` proof. fail_closed: false.
420
+ end-review-nudge:
421
+ script: src/scripts/hooks/end_review_nudge_hook.ts
422
+ args: []
423
+ fail_closed: false
424
+ severity: advisory
313
425
 
314
426
  platforms:
315
427
  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]
428
+ session_start: [chat-history, hot-context, handoff-context, session-canary, council-availability, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe, session-register]
429
+ session_end: [chat-history, memory-learn, session-register]
430
+ stop: [chat-history, hot-context, verify-before-complete, self-repair, session-register]
319
431
  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
- post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
432
+ post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder, orchestration-record]
321
433
 
322
434
  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]
435
+ session_start: [chat-history, hot-context, handoff-context, session-canary, council-availability, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe, session-register]
436
+ session_end: [chat-history, memory-learn, session-register]
437
+ stop: [chat-history, hot-context, verify-before-complete, team-review-gate, end-review-nudge, self-repair, session-register]
438
+ user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, delegation-nudge, git-authorization, session-canary, self-repair, session-register]
327
439
  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
- post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
440
+ post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder, orchestration-record]
329
441
 
330
442
  # Cowork — the Claude desktop app's local-agent-mode runtime, built
331
443
  # on top of the Claude Code CLI. Same lifecycle vocabulary, same
@@ -342,12 +454,22 @@ platforms:
342
454
  # Decision matrix + upstream blockers tracked in
343
455
  # agents/settings/contexts/chat-history-platform-hooks.md § Cowork.
344
456
  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]
457
+ session_start: [chat-history, hot-context, handoff-context, session-canary, council-availability, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe, session-register]
458
+ session_end: [chat-history, memory-learn, session-register]
459
+ # end-review-nudge and delegation-nudge are DELIBERATELY NOT bound on
460
+ # this platform (F10, review) — cowork-dispatcher.sh's own header states
461
+ # it "always exit[s] 0" and its dispatch line discards the dispatcher's
462
+ # exit code AND stdout unconditionally (`>/dev/null 2>&1 || true`). Both
463
+ # concerns' delivery depends entirely on `additional_context` reaching
464
+ # the model via stdout at the verified exit-2 warn path — a delivery this
465
+ # trampoline makes structurally impossible regardless of which platform
466
+ # is later verified to forward it. Binding them here would be dead
467
+ # weight: the concern still runs (git diff, transcript scan, classifier),
468
+ # still costs the dispatch, and its entire output is thrown away.
469
+ stop: [chat-history, hot-context, verify-before-complete, self-repair, session-register]
470
+ user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization, session-canary, self-repair, session-register]
349
471
  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
- post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
472
+ post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder, orchestration-record]
351
473
 
352
474
  # Phase 7.5 — Cursor. `.cursor/hooks.json` (project) is read by the
353
475
  # IDE and CLI; `~/.cursor/hooks.json` (user) is opt-in via
@@ -357,11 +479,11 @@ platforms:
357
479
  # IDE-only — CLI-only users fall back to /checkpoint per
358
480
  # agents/settings/contexts/chat-history-platform-hooks.md.
359
481
  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]
364
- post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
482
+ session_start: [chat-history, hot-context, handoff-context, session-canary, council-availability, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe, session-register]
483
+ session_end: [chat-history, memory-learn, session-register]
484
+ stop: [chat-history, hot-context, verify-before-complete, self-repair, session-register]
485
+ user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization, session-canary, self-repair, session-register]
486
+ post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder, orchestration-record]
365
487
 
366
488
  # Phase 7.6 — Cline. Hooks live under `.clinerules/hooks/<HookName>`
367
489
  # (project, no file extension, must be executable per Cline docs) or
@@ -372,11 +494,11 @@ platforms:
372
494
  # both map to session_start. TaskCancel maps to stop because the
373
495
  # session is interrupted with partial state (mirrors Augment Stop).
374
496
  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]
379
- post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
497
+ session_start: [chat-history, hot-context, handoff-context, session-canary, council-availability, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe, session-register]
498
+ session_end: [chat-history, memory-learn, session-register]
499
+ stop: [chat-history, hot-context, verify-before-complete, self-repair, session-register]
500
+ user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization, session-canary, self-repair, session-register]
501
+ post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder, orchestration-record]
380
502
 
381
503
  # Phase 7.7 — Windsurf (Cascade). Hooks live at `.windsurf/hooks.json`
382
504
  # (project) or `~/.codeium/windsurf/hooks.json` (user). Cascade has
@@ -391,11 +513,11 @@ platforms:
391
513
  # surface to record verification commands; documented limitation).
392
514
  # minimal-safe-diff is omitted entirely on Windsurf for the same reason.
393
515
  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]
516
+ session_start: [chat-history, hot-context, handoff-context, session-canary, council-availability, first-run-gate, onboarding-gate, verify-before-complete, profile-staleness, wrapper-freshness, surface-probe, session-register]
517
+ stop: [chat-history, hot-context, verify-before-complete, self-repair, session-register]
396
518
  # language-mirror IS bound here: unlike minimal-safe-diff it needs only the
397
519
  # user_prompt_submit slot, which Windsurf has.
398
- user_prompt_submit: [chat-history, verify-before-complete, language-mirror, git-authorization]
520
+ user_prompt_submit: [chat-history, verify-before-complete, language-mirror, git-authorization, session-canary, self-repair, session-register]
399
521
 
400
522
  # Phase 7.8 — Gemini CLI. Hooks live at `.gemini/settings.json`
401
523
  # (project) or `~/.gemini/settings.json` (user). Per Gemini docs
@@ -408,11 +530,11 @@ platforms:
408
530
  # turn-check semantics. AfterAgent fires when the agent loop ends
409
531
  # — this is our `stop` slot.
410
532
  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]
415
- post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder]
533
+ session_start: [chat-history, hot-context, handoff-context, session-canary, council-availability, first-run-gate, onboarding-gate, verify-before-complete, minimal-safe-diff, profile-staleness, wrapper-freshness, surface-probe, session-register]
534
+ session_end: [chat-history, memory-learn, session-register]
535
+ stop: [chat-history, hot-context, verify-before-complete, self-repair, session-register]
536
+ user_prompt_submit: [chat-history, verify-before-complete, minimal-safe-diff, language-mirror, git-authorization, session-canary, self-repair, session-register]
537
+ post_tool_use: [chat-history, roadmap-progress, context-hygiene, verify-before-complete, minimal-safe-diff, injection-scan, pr-url-reminder, orchestration-record]
416
538
 
417
539
  # Phase 7.9 — Copilot has no hook surface. Concerns route through
418
540
  # rule-only fallback; the dispatcher silently no-ops on --platform copilot.
@@ -20,12 +20,21 @@
20
20
  * 2 — warn (not used by this guard)
21
21
  *
22
22
  * No ALLOW_NO_VERIFY-style env bypass is provided. See src/rules/git-history-discipline.md
23
+ *
24
+ * WHAT THIS GUARD DOES NOT SEE. Command substitutions ARE now checked — see
25
+ * `_check_command` for why they were not, and why the well-formed case was the
26
+ * unprotected one. What remains uncovered is an invocation whose command word
27
+ * only exists at runtime (`P=commit; git $P --no-verify` via indirection, or a
28
+ * word composed by `xargs`). Those execute under bash and are not classified
29
+ * here. Stated rather than left implied: this file's own round-5 defect was a
30
+ * docstring that described coverage it did not have.
23
31
  */
24
32
 
25
33
  import * as fs from 'node:fs';
26
34
  import { fileURLToPath, pathToFileURL } from 'node:url';
27
35
  import * as path from 'node:path';
28
36
  import { readHookStdin } from './hook_stdin.js';
37
+ import { substitutionPayloads } from './block_unauthorized_git.js';
29
38
 
30
39
  const _HERE = fileURLToPath(import.meta.url);
31
40
 
@@ -329,14 +338,97 @@ function _is_blocked(git_tokens: string[]): [boolean, string] {
329
338
  return [false, ''];
330
339
  }
331
340
 
332
- /** Return (blocked, reason). Fail-closed on parse error for git commands. */
333
- function _check_command(cmd: string): [boolean, string] {
341
+ /**
342
+ * Does an unparseable command plausibly INVOKE git?
343
+ *
344
+ * The fail-closed branch below used to test `/\bgit\b/` against the raw command
345
+ * string. That is a mention, not an invocation, and the difference is not
346
+ * academic: measured in the round-5 audit (2026-08-07), a
347
+ * `mkdir -p … && cat > notes.md <<HEREDOC … HEREDOC` with no git anywhere in
348
+ * command position was refused, because the heredoc body named rules such as
349
+ * `git-history-discipline` and contained apostrophes — so shlex failed and the
350
+ * substring matched. The message's own remedy (`git commit -F <file>`) could not
351
+ * apply, because there was no commit.
352
+ *
353
+ * Fail-closed is still the right posture for the case it was written for: a
354
+ * genuinely unparseable command that DOES invoke git. So the test moves from
355
+ * "the string contains git" to "some command position is git", with heredoc
356
+ * bodies removed first — the same data-not-command distinction
357
+ * `block_unauthorized_git` already makes.
358
+ *
359
+ * Deliberately regex-shaped rather than tokenised: shlex has already failed by
360
+ * the time this runs, so no reliable token stream exists. It therefore stays
361
+ * conservative — a `git` at the head of any line or after any separator counts,
362
+ * quoted or not.
363
+ */
364
+ export function _looks_like_git_invocation(cmd: string, depth = 0): boolean {
365
+ if (depth > 3) {
366
+ return false;
367
+ }
368
+ const withoutHeredocs = cmd.replace(
369
+ /<<-?\s*(['"]?)([A-Za-z_][A-Za-z0-9_]*)\1[\s\S]*?^\s*\2\s*$/gm,
370
+ '<<HEREDOC',
371
+ );
372
+ for (const rawSeg of withoutHeredocs.split(/\n|;|&&|\|\||\||&|\$\(|`/)) {
373
+ // Drop leading env assignments: `GIT_DIR=x git status`.
374
+ const seg = rawSeg
375
+ .trim()
376
+ .replace(/^(?:[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|\S*)\s+)+/, '');
377
+ // A `sh -c '<payload>'` payload IS command context — recurse, or the
378
+ // narrowing above would open a bypass this guard did not have before.
379
+ // Caught by the adversarial case, not by review.
380
+ const wrapped =
381
+ /^(?:\S*\/)?(?:ba|z|k)?sh\s+-[a-z]*c\s+(?:"([^"]*)"|'([^']*)'|(\S+))/.exec(seg);
382
+ if (wrapped !== null) {
383
+ if (_looks_like_git_invocation(wrapped[1] ?? wrapped[2] ?? wrapped[3] ?? '', depth + 1)) {
384
+ return true;
385
+ }
386
+ continue;
387
+ }
388
+ // Command position: optional path prefix, then `git` as its own word.
389
+ if (/^(?:\S*\/)?git(?:\s|$)/.test(seg)) {
390
+ return true;
391
+ }
392
+ }
393
+ return false;
394
+ }
395
+
396
+ /**
397
+ * Return (blocked, reason). Fail-closed on parse error for git commands.
398
+ *
399
+ * COMMAND SUBSTITUTIONS ARE CHECKED SEPARATELY, and the reason they were not is
400
+ * the shape of the hole: this file does split on `$(` and backtick, but only
401
+ * inside `_looks_like_git_invocation`, which runs exclusively in the
402
+ * shlex-failure branch. A well-formed `echo "$(git commit --no-verify)"` parses
403
+ * cleanly, so shlex succeeds, the substitution stays inside one token, and
404
+ * `_git_base` never sees git. The more parseable the command, the less it was
405
+ * protected. Classification inside the payload is by command position, so a
406
+ * substitution that INVOKES git is checked and one that merely names it in an
407
+ * argument is not — matching what `block_unauthorized_git` does one level up.
408
+ *
409
+ * Heredoc bodies are removed before extraction: a backtick or `$(` inside a
410
+ * commit message being written to a file is data, and feeding it back through
411
+ * this function is exactly the false positive the round-5 repair closed.
412
+ */
413
+ function _check_command(cmd: string, depth = 0): [boolean, string] {
414
+ if (depth <= 3) {
415
+ const withoutHeredocs = cmd.replace(
416
+ /<<-?\s*(['"]?)([A-Za-z_][A-Za-z0-9_]*)\1[\s\S]*?^\s*\2\s*$/gm,
417
+ '<<HEREDOC',
418
+ );
419
+ for (const payload of substitutionPayloads(withoutHeredocs)) {
420
+ const [blocked, reason] = _check_command(payload, depth + 1);
421
+ if (blocked) {
422
+ return [true, reason];
423
+ }
424
+ }
425
+ }
334
426
  let tokens: string[];
335
427
  try {
336
428
  tokens = shlexSplit(cmd);
337
429
  } catch (e) {
338
430
  if (e instanceof ShlexError) {
339
- if (/\bgit\b/.test(cmd)) {
431
+ if (_looks_like_git_invocation(cmd)) {
340
432
  return [
341
433
  true,
342
434
  'command parse failed (shlex) on a git-containing command — fail-closed (git-history-discipline). \n'
@@ -38,6 +38,23 @@
38
38
  * Exit codes (dispatcher contract): 0 allow · 2 block (stderr carries the
39
39
  * reason on block-capable events) — the dispatcher's `host_semantics`
40
40
  * translation owns the per-platform mapping.
41
+ *
42
+ * WHAT THIS GUARD DOES NOT SEE — stated because the previous version of this
43
+ * header stated only what it covers, and a coverage claim that omits its
44
+ * residue reads as completeness. The classifier reads the command TEXT; it does
45
+ * not interpret the shell. So an invocation whose command word is assembled at
46
+ * runtime is not classified, measured and still open:
47
+ *
48
+ * P=publish; npm $P — variable indirection
49
+ * echo publish | xargs npm — the command word is composed by xargs
50
+ *
51
+ * Both execute under bash. Closing them needs a different mechanism than a
52
+ * longer pattern, and `Measured, deferred, and why` in the round-6 roadmap
53
+ * draws that line deliberately. Covered, by contrast, and each pinned by a
54
+ * vector whose ground truth came from running bash rather than from reading
55
+ * this file: ANSI-C and locale quoting, unbalanced quotes, command and process
56
+ * substitution in every position tried, `sh -c` and `eval` payloads, env
57
+ * assignment prefixes, and quotes inside the command word.
41
58
  */
42
59
  import fs from "node:fs";
43
60
  import path from "node:path";
@@ -143,6 +160,174 @@ export function extractCommand(envelope: JsonObject): string | null {
143
160
  return null;
144
161
  }
145
162
 
163
+ /**
164
+ * Split on shell separators that are OUTSIDE quotes.
165
+ *
166
+ * Separators recognised, matching the raw regex this replaces: newline, `;`,
167
+ * `&&`, `||`, `|`, `&`. A separator inside `'…'` or `"…"` is a literal, so it
168
+ * does not split. Backslash escapes the next character outside single quotes,
169
+ * per POSIX.
170
+ *
171
+ * Deliberately NOT a full tokeniser. `shlexSplit` in `block_no_verify` throws on
172
+ * an unbalanced quote, which is the right behaviour for a guard that fail-closes;
173
+ * this guard must classify every command it sees, including a malformed one.
174
+ *
175
+ * THE UNTERMINATED-QUOTE POSTURE IS FAIL-CLOSED, AND THE ARGUMENT FOR THE
176
+ * PREVIOUS ONE WAS INVERTED. Round 5 shipped this docstring: "an unterminated
177
+ * quote here degrades to 'the rest of the string is quoted', which yields one
178
+ * segment starting at the real command word — the conservative outcome". It is
179
+ * the opposite. In `echo 'oops && npm publish` the surviving segment's command
180
+ * word is `echo`, so everything after `&&` is swallowed and the publish is never
181
+ * seen. Measured, not reasoned: that command was blocked before #1208 and
182
+ * allowed after it. So an unbalanced quote now re-splits the trailing segment
183
+ * WITHOUT quote awareness and classifies every piece. A false positive on input
184
+ * bash would itself refuse to run is acceptable; a false negative on input bash
185
+ * does run is the failure this guard exists for.
186
+ *
187
+ * ANSI-C quoting (`$'…'`) is a distinct opener, and treating its `$` as ordinary
188
+ * text was the second half of the same regress. In `$'don\'t'` the `\'` does NOT
189
+ * close the quote — C escape semantics apply inside — so a plain single-quote
190
+ * reading closes early, re-opens on the trailing `'`, and swallows the tail.
191
+ * `$"…"` is locale translation and behaves as `"…"`.
192
+ */
193
+ export function splitOutsideQuotes(command: string): string[] {
194
+ const parts: string[] = [];
195
+ let cur = "";
196
+ // `$'` is tracked distinctly from `'` because only it honours `\` escapes.
197
+ let quote: '"' | "'" | "$'" | null = null;
198
+ for (let i = 0; i < command.length; i += 1) {
199
+ const ch = command[i] as string;
200
+ if (quote !== null) {
201
+ if (quote === "$'" && ch === "\\" && i + 1 < command.length) {
202
+ // C-style escape: `\'` is a literal quote, not a terminator.
203
+ cur += ch + (command[i + 1] as string);
204
+ i += 1;
205
+ continue;
206
+ }
207
+ cur += ch;
208
+ if (ch === (quote === "$'" ? "'" : quote)) {
209
+ quote = null;
210
+ } else if (ch === "\\" && quote === '"' && i + 1 < command.length) {
211
+ cur += command[i + 1] as string;
212
+ i += 1;
213
+ }
214
+ continue;
215
+ }
216
+ // `$'…'` / `$"…"` — the `$` belongs to the opener, not to the payload.
217
+ if (ch === "$" && (command[i + 1] === "'" || command[i + 1] === '"')) {
218
+ quote = command[i + 1] === "'" ? "$'" : '"';
219
+ cur += ch + (command[i + 1] as string);
220
+ i += 1;
221
+ continue;
222
+ }
223
+ if (ch === '"' || ch === "'") {
224
+ quote = ch;
225
+ cur += ch;
226
+ continue;
227
+ }
228
+ if (ch === "\\" && i + 1 < command.length) {
229
+ cur += ch + (command[i + 1] as string);
230
+ i += 1;
231
+ continue;
232
+ }
233
+ if (ch === "\n" || ch === ";" || ch === "|" || ch === "&") {
234
+ // `&&` and `||` are two-character separators; consume the pair.
235
+ if ((ch === "|" || ch === "&") && command[i + 1] === ch) {
236
+ i += 1;
237
+ }
238
+ parts.push(cur);
239
+ cur = "";
240
+ continue;
241
+ }
242
+ cur += ch;
243
+ }
244
+ if (quote !== null) {
245
+ // Unbalanced: the trailing segment carries an opener that never closed, so
246
+ // its separators were read as data. Re-read them as separators. Segments
247
+ // completed BEFORE the opener keep their quote-aware split — the round-5
248
+ // false positive (a `grep -E` alternation) balances its quotes and never
249
+ // reaches this branch.
250
+ parts.push(...splitIgnoringQuotes(cur));
251
+ return parts;
252
+ }
253
+ parts.push(cur);
254
+ return parts;
255
+ }
256
+
257
+ /**
258
+ * Separator split with no quote awareness at all — the fail-closed fallback for
259
+ * a command whose quotes do not balance. Backslash still escapes, because a
260
+ * `\|` is a literal pipe under every reading.
261
+ */
262
+ function splitIgnoringQuotes(fragment: string): string[] {
263
+ const parts: string[] = [];
264
+ let cur = "";
265
+ for (let i = 0; i < fragment.length; i += 1) {
266
+ const ch = fragment[i] as string;
267
+ if (ch === "\\" && i + 1 < fragment.length) {
268
+ cur += ch + (fragment[i + 1] as string);
269
+ i += 1;
270
+ continue;
271
+ }
272
+ if (ch === "\n" || ch === ";" || ch === "|" || ch === "&") {
273
+ if ((ch === "|" || ch === "&") && fragment[i + 1] === ch) {
274
+ i += 1;
275
+ }
276
+ parts.push(cur);
277
+ cur = "";
278
+ continue;
279
+ }
280
+ cur += ch;
281
+ }
282
+ parts.push(cur);
283
+ return parts;
284
+ }
285
+
286
+ /**
287
+ * Extract the payload of every command substitution in a segment — `$(…)` with
288
+ * balanced parentheses, and backtick form.
289
+ *
290
+ * An unterminated substitution yields everything to the end of the segment
291
+ * rather than nothing: the same fail-closed direction the quote posture takes.
292
+ */
293
+ export function substitutionPayloads(segment: string): string[] {
294
+ const out: string[] = [];
295
+ for (let i = 0; i < segment.length; i += 1) {
296
+ const ch = segment[i];
297
+ if (ch === "\\") {
298
+ i += 1;
299
+ continue;
300
+ }
301
+ // `$(…)` command substitution and `<(…)` / `>(…)` process substitution:
302
+ // different syntax, same fact — the parenthesised payload is a command that
303
+ // runs. Measured: `cat <(npm publish)` executes the publish.
304
+ if ((ch === "$" || ch === "<" || ch === ">") && segment[i + 1] === "(") {
305
+ let open = 1;
306
+ let j = i + 2;
307
+ const start = j;
308
+ for (; j < segment.length && open > 0; j += 1) {
309
+ const c = segment[j];
310
+ if (c === "\\") {
311
+ j += 1;
312
+ } else if (c === "(") {
313
+ open += 1;
314
+ } else if (c === ")") {
315
+ open -= 1;
316
+ }
317
+ }
318
+ out.push(segment.slice(start, open === 0 ? j - 1 : segment.length));
319
+ i = j - 1;
320
+ continue;
321
+ }
322
+ if (ch === "`") {
323
+ const end = segment.indexOf("`", i + 1);
324
+ out.push(segment.slice(i + 1, end === -1 ? segment.length : end));
325
+ i = end === -1 ? segment.length : end;
326
+ }
327
+ }
328
+ return out;
329
+ }
330
+
146
331
  /**
147
332
  * Split a shell command into the segments that are actually INVOKED.
148
333
  *
@@ -159,6 +344,16 @@ export function extractCommand(envelope: JsonObject): string | null {
159
344
  * Quoted payloads are NOT simply discarded — `sh -c "npm publish"` really does
160
345
  * publish. Those are unwrapped and re-split, so closing the false positive does
161
346
  * not open a bypass.
347
+ *
348
+ * The separator split is QUOTE-AWARE, and that is not a refinement — it is the
349
+ * same false-positive class the paragraph above describes, surviving in the one
350
+ * place the round-2 fix did not reach. Measured in the round-5 audit
351
+ * (2026-08-07): a read-only `grep -n -E "…|npm publish|…"` was refused with
352
+ * "Blocked: `publish` with no authorization in this turn's prompt", because a
353
+ * raw `split` on `|` cut the quoted alternation into a segment beginning with
354
+ * `npm`. A `|` inside quotes is data, exactly like a heredoc body; only an
355
+ * unquoted one is a pipe. Splitting outside quotes closes that without
356
+ * discarding quoted payloads, so `sh -c "npm publish"` still unwraps below.
162
357
  */
163
358
  export function invokedSegments(command: string, depth = 0): string[] {
164
359
  if (depth > 3) {
@@ -172,20 +367,40 @@ export function invokedSegments(command: string, depth = 0): string[] {
172
367
  /<<-?\s*(['"]?)([A-Za-z_][A-Za-z0-9_]*)\1[\s\S]*?^\s*\2\s*$/gm,
173
368
  "<<HEREDOC",
174
369
  );
175
- for (const raw of withoutHeredocs.split(/\n|;|&&|\|\||\||&/)) {
370
+ for (const raw of splitOutsideQuotes(withoutHeredocs)) {
176
371
  let seg = raw.trim();
177
372
  if (!seg) {
178
373
  continue;
179
374
  }
180
375
  // Drop leading env assignments: `FOO=bar BAZ=1 npm publish`.
181
376
  seg = seg.replace(/^(?:[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|\S*)\s+)+/, "");
377
+ // A command substitution is an invocation in its own right, and it runs
378
+ // whether or not the enclosing segment's command word is harmless:
379
+ // `echo "$(npm publish)"` publishes. Classify INSIDE the substitution by
380
+ // command position, so the substitution that *invokes* a blocked op is
381
+ // refused and the one that merely *mentions* it in an argument
382
+ // (`echo "$(grep -c 'npm publish' f)"`) is not — the same rule `commandOp`
383
+ // already applies one level up, applied one level deeper.
384
+ for (const payload of substitutionPayloads(seg)) {
385
+ out.push(...invokedSegments(payload, depth + 1));
386
+ }
182
387
  // Unwrap `sh -c '<payload>'` / `bash -c "<payload>"` and recurse.
183
388
  const wrapped = /^(?:\S*\/)?(?:ba|z|k)?sh\s+-[a-z]*c\s+(?:"([^"]*)"|'([^']*)'|(\S+))/.exec(seg);
184
389
  if (wrapped) {
185
390
  out.push(...invokedSegments(wrapped[1] ?? wrapped[2] ?? wrapped[3] ?? "", depth + 1));
186
391
  continue;
187
392
  }
188
- out.push(seg);
393
+ // `eval` is the same shape as `sh -c`: its argument is command context, and
394
+ // it takes the rest of the segment rather than one word.
395
+ const evaled = /^eval\s+(?:"([^"]*)"|'([^']*)'|(.+))$/.exec(seg);
396
+ if (evaled) {
397
+ out.push(...invokedSegments(evaled[1] ?? evaled[2] ?? evaled[3] ?? "", depth + 1));
398
+ continue;
399
+ }
400
+ // Quotes inside the command WORD are removed by the shell before it looks
401
+ // the command up, so `np''m publish` invokes npm. Strip them from the head
402
+ // only — an argument's quotes are data and stay.
403
+ out.push(seg.replace(/^(\S+)/, (w) => w.replace(/['"]/g, "")));
189
404
  }
190
405
  return out;
191
406
  }