@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
@@ -0,0 +1,391 @@
1
+ /**
2
+ * Obligation frequency, carrier frequency, and the coverage lattice between them.
3
+ *
4
+ * `enforced_by` answers *does something carry this rule*. It does not answer
5
+ * *does it carry it often enough*, and the difference is not academic:
6
+ * `session-canary` declares `hook:session-canary`, the hook exists, is wired and
7
+ * fires — so the coverage instrument counts the rule as enforced — while the hook
8
+ * sits in the `session_start` slot and the rule's obligation is per task. The
9
+ * rule's own 30-session audit measured the opening canary dropped on ~13 of 15
10
+ * task starts. Enforced on paper, broken in fact.
11
+ *
12
+ * This module supplies the missing half: a declared obligation period per rule
13
+ * (frontmatter `obligation_frequency`), a carrier period resolved per slot AND
14
+ * per platform, and a `covers()` relation that is honest about the two ways the
15
+ * intuitive linear ordering produces FALSE GREENS.
16
+ *
17
+ * ## The value set is a forest, not a chain
18
+ *
19
+ * Four roots. A carrier in one root never covers an obligation in another:
20
+ *
21
+ * lifecycle per-turn ⊃ per-task ⊃ per-session
22
+ * tool-call per-edit ⊃ per-file-write
23
+ * repository per-commit
24
+ * external-event per-event
25
+ *
26
+ * Two corrections that a magnitude-ordered chain would get wrong:
27
+ *
28
+ * - `per-edit` and `per-turn` are INCOMPARABLE. `pre_tool_use`/`post_tool_use`
29
+ * fire per tool call, so a plain conversational reply fires them zero times.
30
+ * A per-turn obligation carried only by a per-edit hook is uncovered on
31
+ * exactly those turns — and a chain would paint that green.
32
+ * - `per-event` is a SEPARATE ROOT, not the bottom of the lifecycle chain. An
33
+ * external event fires on its own clock: a CI gate firing three times inside
34
+ * one session is not covered by a `session_start` check, yet a chain with
35
+ * `per-event` at the bottom would accept the session-scoped carrier as
36
+ * dominating it and report green.
37
+ *
38
+ * ## Point carriers vs sweep carriers — the third correction
39
+ *
40
+ * The forest rule above is stated for carriers that fire AT an event and see
41
+ * only that event: a hook. A CI validator is not that shape. It fires once per
42
+ * commit and inspects the whole tree, so it catches every edit inside the commit
43
+ * regardless of how many there were. Treating it as a `per-commit` point carrier
44
+ * would make every `validator:`-carried rule with a per-edit obligation a
45
+ * finding — roughly a fifth of the declared corpus at once, all from one
46
+ * modelling error, which is precisely the unusable-first-run failure this audit
47
+ * must avoid.
48
+ *
49
+ * So carriers have a MODE. A `sweep` carrier covers any obligation whose
50
+ * violations land in a durable artefact (`per-edit`, `per-file-write`,
51
+ * `per-commit`). It covers none of the transient ones (`per-turn`, `per-task`,
52
+ * `per-session`, `per-event`), because a reply's language, a greeting, or a
53
+ * fetched-content decision leaves nothing in the tree for a sweep to read.
54
+ */
55
+
56
+ // ------------------------------------------------------------------ vocabulary
57
+
58
+ export type Frequency =
59
+ | 'per-edit'
60
+ | 'per-turn'
61
+ | 'per-task'
62
+ | 'per-session'
63
+ | 'per-event'
64
+ | 'per-commit'
65
+ | 'per-file-write'
66
+ | 'none';
67
+
68
+ export const FREQUENCIES: readonly Frequency[] = [
69
+ 'per-edit',
70
+ 'per-turn',
71
+ 'per-task',
72
+ 'per-session',
73
+ 'per-event',
74
+ 'per-commit',
75
+ 'per-file-write',
76
+ 'none',
77
+ ] as const;
78
+
79
+ export function is_frequency(v: unknown): v is Frequency {
80
+ return typeof v === 'string' && (FREQUENCIES as readonly string[]).includes(v);
81
+ }
82
+
83
+ export type Root = 'lifecycle' | 'tool-call' | 'repository' | 'external-event' | 'none';
84
+
85
+ const ROOT_OF: Record<Frequency, Root> = {
86
+ 'per-turn': 'lifecycle',
87
+ 'per-task': 'lifecycle',
88
+ 'per-session': 'lifecycle',
89
+ 'per-edit': 'tool-call',
90
+ 'per-file-write': 'tool-call',
91
+ 'per-commit': 'repository',
92
+ 'per-event': 'external-event',
93
+ none: 'none',
94
+ };
95
+
96
+ /**
97
+ * Cross-root coverage that IS real, enumerated rather than inferred.
98
+ *
99
+ * `per-commit` keeps its own root because it is not a lifecycle event — a
100
+ * `--fixup` during a rebase has no preceding user turn, so no lifecycle carrier
101
+ * covers it. But in an agent runtime every commit the agent makes is issued
102
+ * through a tool call, so a point carrier in the tool-call root DOES observe it:
103
+ * `block-no-verify` sits in `pre_tool_use` and inspects the very `git commit`
104
+ * command. Refusing that edge on a tidy root rule would report the commit guard
105
+ * as failing to carry the commit rule, which is the opposite of true.
106
+ *
107
+ * The relation is one-directional. A per-commit carrier does not cover a
108
+ * per-edit obligation: one commit can hold twenty edits, and a point carrier at
109
+ * the commit sees the command, not each edit that preceded it.
110
+ */
111
+ const CROSS_ROOT_COVERAGE: ReadonlyArray<readonly [Frequency, Frequency]> = [
112
+ ['per-edit', 'per-commit'],
113
+ ];
114
+
115
+ export function root_of(f: Frequency): Root {
116
+ return ROOT_OF[f];
117
+ }
118
+
119
+ /**
120
+ * Within-root inclusion depth: a SMALLER number fires more often, so it is a
121
+ * superset of every larger number in the same root. Cross-root comparison is
122
+ * rejected before this table is consulted.
123
+ */
124
+ const DEPTH: Record<Frequency, number> = {
125
+ 'per-turn': 0,
126
+ 'per-task': 1,
127
+ 'per-session': 2,
128
+ 'per-edit': 0,
129
+ 'per-file-write': 1,
130
+ 'per-commit': 0,
131
+ 'per-event': 0,
132
+ none: 0,
133
+ };
134
+
135
+ // -------------------------------------------------------------------- carriers
136
+
137
+ /**
138
+ * How a carrier observes the world.
139
+ *
140
+ * `point` — fires at an event and sees that event (a hook).
141
+ * `sweep` — fires once and inspects the whole artefact tree (a CI validator, a
142
+ * test suite). Its reach is bounded by what lands in the tree, not by
143
+ * how often it runs.
144
+ */
145
+ export type CarrierMode = 'point' | 'sweep';
146
+
147
+ /** Obligations whose violations leave a durable trace a sweep can read. */
148
+ const SWEEPABLE: ReadonlySet<Frequency> = new Set<Frequency>([
149
+ 'per-edit',
150
+ 'per-file-write',
151
+ 'per-commit',
152
+ ]);
153
+
154
+ /**
155
+ * Does `carrier` cover `obligation`?
156
+ *
157
+ * An obligation of `none` is covered vacuously — there is nothing recurring to
158
+ * carry, which is what `none` means. Everything else is decided by mode first,
159
+ * then root, then within-root depth.
160
+ */
161
+ export function covers(
162
+ carrier: { frequency: Frequency; mode: CarrierMode },
163
+ obligation: Frequency,
164
+ ): boolean {
165
+ if (obligation === 'none') return true;
166
+ if (carrier.mode === 'sweep') return SWEEPABLE.has(obligation);
167
+ if (carrier.frequency === 'none') return false;
168
+ if (CROSS_ROOT_COVERAGE.some(([c, o]) => c === carrier.frequency && o === obligation)) {
169
+ return true;
170
+ }
171
+ if (ROOT_OF[carrier.frequency] !== ROOT_OF[obligation]) return false;
172
+ return DEPTH[carrier.frequency] <= DEPTH[obligation];
173
+ }
174
+
175
+ /** True when ANY of a carrier's firing periods covers the obligation. */
176
+ export function covers_any(frequencies: readonly Frequency[], obligation: Frequency): boolean {
177
+ return frequencies.some((f) => covers({ frequency: f, mode: 'point' }, obligation));
178
+ }
179
+
180
+ // ------------------------------------------------------------- the decay guard
181
+
182
+ /**
183
+ * Prose signals that suggest a period, keyed to the value they suggest.
184
+ *
185
+ * A declared value can be right the day it is written and wrong two edits later,
186
+ * and nothing in the join notices — the field would then be a stale claim
187
+ * wearing a schema. This heuristic reads the rule's own prose and reports when
188
+ * it demands a TIGHTER period than the frontmatter declares.
189
+ *
190
+ * Deliberately one-directional. Prose looser than the declaration is not a
191
+ * finding: a rule may state a per-session cache and still carry a per-turn
192
+ * obligation elsewhere in its body, and flagging that would fire on most of the
193
+ * corpus. Prose demanding MORE often than declared is the direction where the
194
+ * join silently over-credits a carrier.
195
+ */
196
+ const PROSE_SIGNALS: ReadonlyArray<readonly [Frequency, RegExp]> = [
197
+ ['per-turn', /\b(every|each) (reply|turn|answer|response)\b/i],
198
+ ['per-turn', /\bper (reply|turn)\b/i],
199
+ ['per-task', /\b(every|each) (new )?task\b/i],
200
+ ['per-edit', /\bafter every (code )?edit\b/i],
201
+ ['per-edit', /\b(every|each) edit\b/i],
202
+ ['per-commit', /\b(in the )?same commit\b/i],
203
+ ['per-commit', /\b(every|each) commit\b/i],
204
+ ];
205
+
206
+ /**
207
+ * Frequencies the prose demands but the declared value does not cover.
208
+ *
209
+ * Empty is the passing answer. A non-empty result is a WARNING — the heuristic
210
+ * is a keyword match over prose and will be noisy, so it never fails a build;
211
+ * an audited mismatch is silenced with an explicit `# frequency-override:`
212
+ * comment, which makes a suppression a decision someone signed rather than
213
+ * silence.
214
+ */
215
+ export function frequency_prose_conflicts(body: string, declared: Frequency): Frequency[] {
216
+ const out: Frequency[] = [];
217
+ for (const [freq, re] of PROSE_SIGNALS) {
218
+ if (!re.test(body)) continue;
219
+ if (covers({ frequency: declared, mode: 'point' }, freq)) continue;
220
+ if (!out.includes(freq)) out.push(freq);
221
+ }
222
+ return out;
223
+ }
224
+
225
+ /** True when the file carries an audited `# frequency-override: <reason>` note. */
226
+ export function has_frequency_override(text: string): boolean {
227
+ return /^#\s*frequency-override:\s*\S/m.test(text);
228
+ }
229
+
230
+ // --------------------------------------------------------- slot × platform map
231
+
232
+ export type Slot =
233
+ | 'session_start'
234
+ | 'session_end'
235
+ | 'stop'
236
+ | 'user_prompt_submit'
237
+ | 'pre_tool_use'
238
+ | 'post_tool_use';
239
+
240
+ /**
241
+ * Default firing period per slot, before per-platform native-event corrections.
242
+ *
243
+ * `stop` is per-turn, NOT session end. On Claude Code the native `Stop` event
244
+ * fires after every assistant reply — `hook_manifest.yaml` says so itself,
245
+ * describing the `stop` write as a "deterministic … overwrite of hot-context.md",
246
+ * i.e. a working-memory refresh per reply. True session end is the separate
247
+ * `session_end` slot.
248
+ */
249
+ const SLOT_FREQUENCY_DEFAULT: Record<Slot, Frequency> = {
250
+ session_start: 'per-session',
251
+ session_end: 'per-session',
252
+ stop: 'per-turn',
253
+ user_prompt_submit: 'per-turn',
254
+ pre_tool_use: 'per-edit',
255
+ post_tool_use: 'per-edit',
256
+ };
257
+
258
+ /**
259
+ * Per-platform overrides where the native event a slot is mapped from means
260
+ * something other than the default.
261
+ *
262
+ * Cline maps `stop` from `TaskCancel` — the session is interrupted with partial
263
+ * state (`hook_manifest.yaml`: "TaskCancel maps to stop because the session is
264
+ * interrupted with partial state"). That is an interruption on its own clock,
265
+ * not a per-reply beat, so it belongs to the external-event root and covers no
266
+ * lifecycle obligation. Collapsing it into per-turn is the semantic type error
267
+ * a one-dimensional slot→frequency table cannot express.
268
+ */
269
+ const SLOT_FREQUENCY_OVERRIDE: Record<string, Partial<Record<Slot, Frequency>>> = {
270
+ cline: { stop: 'per-event' },
271
+ };
272
+
273
+ export function slot_frequency(platform: string, slot: Slot): Frequency {
274
+ return SLOT_FREQUENCY_OVERRIDE[platform]?.[slot] ?? SLOT_FREQUENCY_DEFAULT[slot];
275
+ }
276
+
277
+ /**
278
+ * Platforms with no hook surface at all.
279
+ *
280
+ * Copilot declares `fallback_only: true` in the manifest — concerns route
281
+ * through rule-only fallback and the dispatcher no-ops. Every hook-declared
282
+ * carrier fires zero times there, for every rule equally. Including it in the
283
+ * join would turn one structural fact into a finding on every hook-carried rule
284
+ * at once, which reports a platform property as if it were a per-rule defect.
285
+ * It is excluded by declaration, and the exclusion is named in the report rather
286
+ * than hidden.
287
+ */
288
+ export function is_hook_capable(platform_block: Record<string, unknown>): boolean {
289
+ return platform_block['fallback_only'] !== true;
290
+ }
291
+
292
+ // ------------------------------------------------- hook manifest platform read
293
+
294
+ export interface PlatformBinding {
295
+ /** Platform id → slot → concern ids bound in that slot. */
296
+ slots: Map<string, Map<Slot, string[]>>;
297
+ /** Platform ids that carry no hook surface at all. */
298
+ fallback_only: Set<string>;
299
+ }
300
+
301
+ const SLOT_NAMES: ReadonlySet<string> = new Set<string>([
302
+ 'session_start',
303
+ 'session_end',
304
+ 'stop',
305
+ 'user_prompt_submit',
306
+ 'pre_tool_use',
307
+ 'post_tool_use',
308
+ ]);
309
+
310
+ /**
311
+ * Read the `platforms:` block of `hook_manifest.yaml`.
312
+ *
313
+ * A narrow reader, matching the one `check_enforcement_coverage.parse_hook_manifest`
314
+ * already uses for `concerns:` — the block is a fixed two-level shape
315
+ * (`platform: slot: [concern, …]`) and the only facts needed are slot membership
316
+ * and the `fallback_only` sentinel. Deliberately NOT a second YAML dependency.
317
+ */
318
+ export function parse_hook_platforms(text: string): PlatformBinding {
319
+ const slots = new Map<string, Map<Slot, string[]>>();
320
+ const fallback_only = new Set<string>();
321
+ let in_platforms = false;
322
+ let current: string | null = null;
323
+
324
+ for (const raw of text.split('\n')) {
325
+ const line = raw.replace(/\s+$/, '');
326
+ if (/^platforms:\s*$/.test(line)) {
327
+ in_platforms = true;
328
+ continue;
329
+ }
330
+ if (!in_platforms) continue;
331
+ if (/^\S/.test(line)) break; // left the platforms block
332
+ if (line.trim() === '' || /^\s*#/.test(line)) continue;
333
+
334
+ const platform = /^ {2}([a-z0-9_-]+):\s*$/.exec(line)?.[1];
335
+ if (platform !== undefined) {
336
+ current = platform;
337
+ slots.set(current, new Map());
338
+ continue;
339
+ }
340
+ if (current === null) continue;
341
+
342
+ if (/^\s+fallback_only:\s*true\s*$/.test(line)) {
343
+ fallback_only.add(current);
344
+ continue;
345
+ }
346
+ const m = /^\s+([a-z_]+):\s*\[(.*)\]\s*$/.exec(line);
347
+ if (m === null) continue;
348
+ const slot = m[1] as string;
349
+ if (!SLOT_NAMES.has(slot)) continue;
350
+ const concerns = (m[2] as string)
351
+ .split(',')
352
+ .map((s) => s.trim())
353
+ .filter((s) => s.length > 0);
354
+ slots.get(current)?.set(slot as Slot, concerns);
355
+ }
356
+ return { slots, fallback_only };
357
+ }
358
+
359
+ /**
360
+ * Resolve, per platform, EVERY period at which `concern` fires — a set, never a
361
+ * scalar.
362
+ *
363
+ * A concern bound in more than one slot fires at the UNION of their periods, and
364
+ * those periods can sit in different roots: `minimal-safe-diff` is bound in
365
+ * `session_start` (per-session), `user_prompt_submit` (per-turn) AND
366
+ * `post_tool_use` (per-edit). Collapsing that to one "strongest" value has no
367
+ * correct answer, because per-turn and per-edit are incomparable by
368
+ * construction — and picking either drops a root the carrier really does cover.
369
+ * Doing so reported `minimal-safe-diff` as failing to carry its own per-edit
370
+ * obligation while a post_tool_use binding was sitting right there.
371
+ *
372
+ * An empty array means the concern is bound in no slot on that platform, which
373
+ * is a distinct answer from any frequency and must not be collapsed into one.
374
+ */
375
+ export function carrier_frequency_by_platform(
376
+ concern: string,
377
+ binding: PlatformBinding,
378
+ ): Record<string, Frequency[]> {
379
+ const out: Record<string, Frequency[]> = {};
380
+ for (const [platform, slot_map] of binding.slots) {
381
+ if (binding.fallback_only.has(platform)) continue; // named in the report, not joined
382
+ const found: Frequency[] = [];
383
+ for (const [slot, concerns] of slot_map) {
384
+ if (!concerns.includes(concern)) continue;
385
+ const f = slot_frequency(platform, slot);
386
+ if (!found.includes(f)) found.push(f);
387
+ }
388
+ out[platform] = found;
389
+ }
390
+ return out;
391
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * What a "prompt" actually is — shared by the language hook and the conformance
3
+ * scanner so the two cannot classify the same entry differently.
4
+ *
5
+ * They did. Round 6 measured it: `isSyntheticPrompt` lived in
6
+ * `language_mirror_hook` and was never referenced from `conformance_scan`, whose
7
+ * only general net was `length > 2500 && english`. So the hook skipped
8
+ * harness-generated turns and the scanner counted them, and every era split
9
+ * argued about two different populations without knowing it.
10
+ *
11
+ * The asymmetry ran the other way too, and that half had no net on either side:
12
+ * a prompt whose bulk is pasted content in another language pinned to the paste.
13
+ * That fired on the very session that received the round-6 review — the pin read
14
+ * English because an English draft was pasted below German prose.
15
+ *
16
+ * Nothing here is language-specific. A rule that names a language cannot answer
17
+ * a bidirectional question.
18
+ */
19
+
20
+ /**
21
+ * Is this "prompt" a harness-generated turn rather than something a human typed?
22
+ *
23
+ * MEASURED, IN THE LANGUAGE HOOK'S OWN AUDIT (round 5, 2026-08-07). The state
24
+ * file read mid-session:
25
+ *
26
+ * { "language": "en", "source": "prompt", "prompt_chars": 6627,
27
+ * "de_markers": 0, "en_markers": 63 }
28
+ *
29
+ * No human wrote those 6,627 characters. They were a background-task completion
30
+ * notification, injected as a user turn, which `user_prompt_submit` sees and the
31
+ * hook classified as the trigger — flipping a German session to `en` for every
32
+ * later turn, and recording `source: "prompt"`, a false provenance claim.
33
+ *
34
+ * The markers are structural, not linguistic: the harness stamps these turns
35
+ * with fixed envelope tags. Matching prose would misfire on a human quoting a
36
+ * notification; matching the tags cannot, because a human typing
37
+ * `<task-notification>` is quoting, and a quote does not open at character zero.
38
+ *
39
+ * Deliberately conservative: an unrecognised synthetic shape falls through and
40
+ * is treated as a chat message. Under-filtering keeps prior behaviour;
41
+ * over-filtering would silently stop pinning real prompts, which is worse.
42
+ * Measured across 1 540 user-role entries: 509 open at character zero with a
43
+ * marker, and in 0 of them does human text survive wrapper removal.
44
+ */
45
+ export function isSyntheticPrompt(prompt: string): boolean {
46
+ const head = prompt.trimStart().slice(0, 400);
47
+ return (
48
+ head.startsWith("[SYSTEM NOTIFICATION - NOT USER INPUT]") ||
49
+ head.startsWith("<task-notification>") ||
50
+ head.startsWith("<system-reminder>") ||
51
+ head.startsWith("<local-command-caveat>") ||
52
+ /^\[SYSTEM NOTIFICATION\b/.test(head)
53
+ );
54
+ }
55
+
56
+ /**
57
+ * A line that starts pasted DOCUMENT content rather than typed instruction.
58
+ *
59
+ * Distinct from the output-shaped heads `instructionText` already removes (stack
60
+ * traces, diffs, `remote:` lines). Those look like machine output. This catches
61
+ * the other paste shape, which looks like prose: a roadmap, a review, a spec —
62
+ * pasted whole, under its own headings, below one typed sentence.
63
+ *
64
+ * The markers are structural for the same reason as above: a heading, a
65
+ * frontmatter fence, or a bare tag line is document furniture. A typed chat turn
66
+ * rarely carries one; a pasted document essentially always does.
67
+ */
68
+ const DOCUMENT_HEAD = /^\s*(#{1,6}\s|-{3,}\s*$|={3,}\s*$|<[A-Za-z][\w-]*>\s*$)/;
69
+
70
+ /**
71
+ * The part of a prompt a human plausibly typed: everything before the first
72
+ * pasted document begins.
73
+ *
74
+ * Callers pass text that has already had output-shaped pastes removed. Returning
75
+ * the LEAD rather than a filtered whole is deliberate — a paste can be
76
+ * interleaved, and reassembling the human fragments would invent a text nobody
77
+ * wrote. The lead is the one span whose authorship is not in question.
78
+ */
79
+ export function humanAuthoredLead(text: string): string {
80
+ const kept: string[] = [];
81
+ for (const line of text.split("\n")) {
82
+ if (DOCUMENT_HEAD.test(line)) {
83
+ break;
84
+ }
85
+ kept.push(line);
86
+ }
87
+ return kept.join("\n");
88
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * `review_skipped` telemetry line builder (pure, no I/O).
3
+ *
4
+ * Purpose-built, MINIMAL extension of the audit-log-v1 envelope
5
+ * (`docs/contracts/audit-log-v1.md`) for the F4-lite end-review-nudge concern
6
+ * (road-to-orchestrator-discipline-carriers Phase 5). It reuses the same
7
+ * envelope shape and the same file location
8
+ * (`agents/runtime/state/audit/<YYYY-MM>.jsonl`) as
9
+ * `_lib/orchestration_record.ts`'s `orchestration` object, but does not
10
+ * import that module: `buildOrchestrationLine` requires `spawn_count`, which
11
+ * has no meaning for a review-skip event, and its enum-constant tables are
12
+ * private to that file. Adding a second additive top-level object
13
+ * (`review_skipped`) is exactly the pattern the contract already uses for
14
+ * `orchestration` — "Unknown trailing fields are forward-compat extensions;
15
+ * readers MUST NOT raise on them."
16
+ *
17
+ * PRIVACY BY CONSTRUCTION: the `review_skipped` object carries `diff_lines`
18
+ * (a non-negative integer count) and `mutation_measure` (a closed 2-value
19
+ * enum — never free text). There is no field capable of holding a file
20
+ * path, a prompt, or any free-form content — never add one (mirrors
21
+ * `domain-safety-pii` § Surface 2 / the `artifact-engagement` event shape).
22
+ */
23
+
24
+ /**
25
+ * Whether `diff_lines` is an exact sum or a guaranteed-over-threshold
26
+ * approximation (`end_review_nudge_hook.ts`'s `UNTRACKED_FILE_CAP` path —
27
+ * past that many untracked non-doc files, the hook refuses to spawn one
28
+ * `git diff --no-index` subprocess per file and reports a value merely
29
+ * guaranteed to exceed the fire threshold, not an exact count). Mixing
30
+ * exact and approximated counts in this telemetry stream, unlabeled, would
31
+ * corrupt any future blocking-threshold calibration that reads it — hence
32
+ * this field, rather than folding the approximation silently into
33
+ * `diff_lines`.
34
+ */
35
+ export type MutationMeasure = 'exact' | 'capped_approximation';
36
+
37
+ export interface ReviewSkippedInput {
38
+ /** Non-doc tracked-file mutation size that triggered the nudge (count only). */
39
+ diff_lines: number;
40
+ /** Whether `diff_lines` is exact or a capped approximation — see above. */
41
+ mutation_measure: MutationMeasure;
42
+ /** ISO-8601 UTC timestamp; caller supplies (keeps this fn pure/deterministic). */
43
+ ts: string;
44
+ /** Stable id (ULID, UUID, or content hash); caller supplies. */
45
+ id: string;
46
+ work_id?: string | undefined;
47
+ }
48
+
49
+ export interface BuiltReviewSkippedLine {
50
+ line: Record<string, unknown> | null;
51
+ errors: string[];
52
+ }
53
+
54
+ function isNonNegInt(n: unknown): n is number {
55
+ return typeof n === 'number' && Number.isFinite(n) && Number.isInteger(n) && n >= 0;
56
+ }
57
+
58
+ /**
59
+ * High-risk threshold for the line's own `risk_class` (audit-log-v1 field,
60
+ * not the hook's fire threshold): a skipped review over a large diff carries
61
+ * more residual risk than one just past the fire threshold.
62
+ */
63
+ const HIGH_RISK_DIFF_LINES = 200;
64
+
65
+ /**
66
+ * Build + validate ONE audit-log-v1 line carrying a `review_skipped` object.
67
+ * Returns `{line, errors}`; when `errors` is non-empty, `line` is null and
68
+ * the caller must NOT write.
69
+ */
70
+ export function buildReviewSkippedLine(input: ReviewSkippedInput): BuiltReviewSkippedLine {
71
+ const errors: string[] = [];
72
+ if (!isNonNegInt(input.diff_lines)) {
73
+ errors.push('diff_lines must be a non-negative integer');
74
+ }
75
+ if (input.mutation_measure !== 'exact' && input.mutation_measure !== 'capped_approximation') {
76
+ errors.push('mutation_measure must be "exact" or "capped_approximation"');
77
+ }
78
+ if (!input.ts) errors.push('ts (ISO-8601 UTC) is required');
79
+ if (!input.id) errors.push('id (ULID, UUID, or content hash) is required');
80
+
81
+ if (errors.length) return { line: null, errors };
82
+
83
+ const line: Record<string, unknown> = {
84
+ schema_version: 1,
85
+ id: input.id,
86
+ ts: input.ts,
87
+ work_id: input.work_id ?? `review-skipped-${input.ts}`,
88
+ phase: 'report',
89
+ outcome: 'skipped',
90
+ confidence_band: 'medium',
91
+ risk_class: input.diff_lines >= HIGH_RISK_DIFF_LINES ? 'high' : 'medium',
92
+ memory: { asks: 0, hits: 0 },
93
+ verify: { claims: 0, first_try_passes: 0 },
94
+ rules_applied: ['delegation-policy'],
95
+ persona: null,
96
+ input_kind: 'prompt',
97
+ type: 'note',
98
+ review_skipped: { diff_lines: input.diff_lines, mutation_measure: input.mutation_measure },
99
+ };
100
+
101
+ return { line, errors: [] };
102
+ }