@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,579 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * report_skill_obligation_violations.ts — SK-2, *loaded-but-violated*: a skill
4
+ * body was in context and an obligation stated in it was violated in a later
5
+ * assistant turn of the same session.
6
+ *
7
+ * Executes round-6 Phase 3.2. Advisory: **it gates on nothing**, and it must not
8
+ * acquire a threshold — same reason as `report_skill_activation`, whose census
9
+ * this consumes rather than re-deriving.
10
+ *
11
+ * THE HEADLINE IS THE COVERAGE, NOT THE VIOLATION COUNT
12
+ * ----------------------------------------------------
13
+ * Phase 3.1 scoped SK-2 to the 30 skills carrying a deterministic
14
+ * `MUST`/`NEVER`/`ALWAYS`, so the coverage would be "legible rather than
15
+ * implied". Reading their bodies makes it legible in a way the phase did not
16
+ * anticipate. Across those 30 skills there are **110 obligation lines**, and of
17
+ * those:
18
+ *
19
+ * 4 name a concrete artefact — a repo path or a command literal
20
+ * 1 of those 4 names the PRESCRIBED alternative, not the forbidden thing
21
+ * 106 are deterministic in WORDING and require judgement to test
22
+ *
23
+ * "NEVER return `clean` out of politeness", "NEVER penalise an artifact for
24
+ * being short", "NEVER invent threat actors with unrealistic capabilities" — the
25
+ * verb is absolute and the violation is a reading. So the census's own number
26
+ * repeats the 8 → 30 correction one level down: `DETERMINISTIC_RE` matches the
27
+ * *sentence*, and being deterministic in wording is not the same property as
28
+ * being observable. This script measures the observable subset and publishes the
29
+ * ratio, because a detector over 3 obligations that reported only its own hit
30
+ * count would read as coverage of the 30.
31
+ *
32
+ * THE POLARITY TRAP, WHICH IS WHY THE ARTEFACT SPLIT IS NOT A GREP
33
+ * ---------------------------------------------------------------
34
+ * `using-git-worktrees` says *"NEVER `rm -rf` a worktree — use `git worktree
35
+ * remove`"*. A naive artefact extraction lifts `git worktree remove` and would
36
+ * flag the prescribed FIX as the violation. So an artefact found after a pivot
37
+ * (`—`, `use`, `instead`, `→`) is classified `prescribed` and excluded, by name.
38
+ * The forbidden half of that same obligation (`rm -rf` against a worktree path)
39
+ * stays unmechanised on purpose: `rm -rf` is legitimate against anything that is
40
+ * not a worktree, so a literal match would manufacture false positives — which
41
+ * is the failure 3.3 exists to catch and this script prefers not to create.
42
+ *
43
+ * WHAT COUNTS AS A VIOLATION
44
+ * --------------------------
45
+ * path artefact an EDIT/WRITE tool call naming that path after the skill
46
+ * loaded. A read never counts: the obligations say
47
+ * "hand-edit", and flagging a read would make the detector
48
+ * fire on the act of checking.
49
+ * command artefact a shell tool call whose command string contains the literal.
50
+ *
51
+ * Both require the skill to have been loaded EARLIER in the same session — that
52
+ * is the "loaded-but" half, and it is what separates SK-2 from a repo-wide grep.
53
+ *
54
+ * Exit: 0 always, except a usage/IO error (1). Deliberate.
55
+ */
56
+ import * as fs from 'node:fs';
57
+ import * as os from 'node:os';
58
+ import * as path from 'node:path';
59
+ import process from 'node:process';
60
+ import { fileURLToPath, pathToFileURL } from 'node:url';
61
+
62
+ import { entryText, isSidechain, toolUses as _toolUses } from './_lib/transcript_entry.js';
63
+ import { censusSkills, DETERMINISTIC_RE, SKILLS_ROOT } from './report_skill_activation.js';
64
+
65
+ const _HERE = fileURLToPath(import.meta.url);
66
+ const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
67
+
68
+ /**
69
+ * Every obligation line — DERIVED from the census's own pattern rather than
70
+ * restated, so "what counts as a deterministic obligation" has one definition.
71
+ * The census tests whether a skill has one (single match, `m`); this needs the
72
+ * whole line, every time (`.*`, `g`). A second literal here is how the 8 → 30
73
+ * spread happened in the first place: two defensible regexes, two numbers.
74
+ */
75
+ const OBLIGATION_LINE_RE = new RegExp(`${DETERMINISTIC_RE.source}.*`, 'gm');
76
+
77
+ /** A backticked repo path — needs a separator AND an extension to count. */
78
+ const PATH_RE = /`([A-Za-z0-9_./-]*\/[A-Za-z0-9_.-]+\.[a-z]{2,6})`/g;
79
+ /** A backticked command, gated on a known tool word in command position. */
80
+ const CMD_RE = /`([a-z][a-z0-9-]*(?: [a-z][a-z0-9:._-]*){1,3})`/g;
81
+ const CMD_HEADS = new Set([
82
+ 'cargo',
83
+ 'npm',
84
+ 'pnpm',
85
+ 'yarn',
86
+ 'git',
87
+ 'php',
88
+ 'composer',
89
+ 'task',
90
+ 'make',
91
+ 'npx',
92
+ 'gh',
93
+ 'python3',
94
+ 'docker',
95
+ ]);
96
+ // `vendor` was here and is unreachable: CMD_RE's token class excludes `/`, so
97
+ // `vendor/bin/phpunit` can never match. A head that cannot fire reads as coverage.
98
+
99
+ /**
100
+ * Where a line stops stating the prohibition and starts stating the remedy.
101
+ * Everything after the first pivot is prescriptive, so an artefact there is the
102
+ * fix, not the offence.
103
+ *
104
+ * DELIBERATELY NOT a bare `use` / `instead`. A first draft matched those anywhere
105
+ * in the line, which inverts the very polarity the pivot exists to get right:
106
+ * *"NEVER use `X`"* puts the pivot BEFORE the artefact, so the forbidden thing
107
+ * would be classified `prescribed` and silently dropped from the mechanisable
108
+ * set — biasing this report's own headline ratio downward. A pivot is a
109
+ * punctuation break (dash, arrow, semicolon) or a remedy phrase that follows one,
110
+ * never a verb that can appear inside the prohibition itself.
111
+ */
112
+ const PIVOT_RE = /\s—\s|\s–\s|→|;\s|\s—use\b|\buse instead\b/;
113
+
114
+ /**
115
+ * `forbidden` — a NEVER line's own artefact: its appearance IS the violation.
116
+ * `prescribed` — the remedy a NEVER line points at instead.
117
+ * `required` — a MUST / ALWAYS line's artefact. NOT testable here, and the
118
+ * distinction is load-bearing rather than tidy: for a positive obligation the
119
+ * violation is the artefact's **absence**, and a transcript cannot show that
120
+ * something never happened without also knowing it was due. Deciding polarity
121
+ * from position alone put `ALWAYS run \`task ci\`` into `forbidden`, so the
122
+ * detector would have flagged *compliance* as a violation. Verified against a
123
+ * fixture before the fix: `forbidden: ["task ci"]`.
124
+ */
125
+ export type Polarity = 'forbidden' | 'prescribed' | 'required';
126
+
127
+ /** The absolute a line opens with — the verb that decides what its artefact means. */
128
+ export type ObligationVerb = 'NEVER' | 'MUST' | 'ALWAYS';
129
+
130
+ export interface Obligation {
131
+ skill: string;
132
+ line: string;
133
+ verb: ObligationVerb;
134
+ artefact: string;
135
+ kind: 'path' | 'command';
136
+ polarity: Polarity;
137
+ }
138
+
139
+ export interface ObligationCensus {
140
+ /** The 30 (or however many the census names today). */
141
+ skills: string[];
142
+ totalLines: number;
143
+ /** Artefact-bearing, either polarity. One entry per ARTEFACT. */
144
+ withArtefact: Obligation[];
145
+ /**
146
+ * Obligation LINES carrying at least one artefact. Kept separate from
147
+ * `withArtefact.length` because a line can name two, and dividing an
148
+ * artefact count by a line count and calling it a share of lines overstates
149
+ * the coverage this report exists to be honest about.
150
+ */
151
+ linesWithArtefact: number;
152
+ /** The mechanisable subset — what this detector can actually test. */
153
+ forbidden: Obligation[];
154
+ prescribed: Obligation[];
155
+ /** MUST / ALWAYS artefacts: required, and their violation is unobservable here. */
156
+ required: Obligation[];
157
+ }
158
+
159
+ /** The absolute the line opens with. Defaults to NEVER only if none matched. */
160
+ function _verb(line: string): ObligationVerb {
161
+ const m = /\b(NEVER|MUST|ALWAYS)\b/.exec(line);
162
+ return (m?.[1] as ObligationVerb | undefined) ?? 'NEVER';
163
+ }
164
+
165
+ function _artefacts(line: string): Array<{ artefact: string; kind: 'path' | 'command' }> {
166
+ const out: Array<{ artefact: string; kind: 'path' | 'command' }> = [];
167
+ for (const m of line.matchAll(PATH_RE)) {
168
+ out.push({ artefact: m[1] as string, kind: 'path' });
169
+ }
170
+ for (const m of line.matchAll(CMD_RE)) {
171
+ const cmd = m[1] as string;
172
+ if (CMD_HEADS.has(cmd.split(' ')[0] as string)) {
173
+ out.push({ artefact: cmd, kind: 'command' });
174
+ }
175
+ }
176
+ return out;
177
+ }
178
+
179
+ /**
180
+ * Derive the obligations FROM the skill bodies — never a hand-written list.
181
+ *
182
+ * A literal list here would go stale the first time a skill's wording changed,
183
+ * and the detector would then police an obligation the skill no longer states.
184
+ * The census supplies the skill set for the same reason: one definition of "the
185
+ * 30".
186
+ */
187
+ export function extractObligations(repoRoot: string): ObligationCensus {
188
+ const skillsRoot = path.join(repoRoot, SKILLS_ROOT);
189
+ const census = censusSkills(skillsRoot);
190
+ const out: ObligationCensus = {
191
+ skills: census.withDeterministicObligation,
192
+ totalLines: 0,
193
+ withArtefact: [],
194
+ linesWithArtefact: 0,
195
+ forbidden: [],
196
+ prescribed: [],
197
+ required: [],
198
+ };
199
+ for (const skill of census.withDeterministicObligation) {
200
+ let text: string;
201
+ try {
202
+ text = fs.readFileSync(path.join(skillsRoot, skill, 'SKILL.md'), 'utf-8');
203
+ } catch {
204
+ continue;
205
+ }
206
+ const fmEnd = text.startsWith('---') ? text.indexOf('\n---', 3) : -1;
207
+ const body = fmEnd === -1 ? text : text.slice(fmEnd + 4);
208
+ for (const raw of body.match(OBLIGATION_LINE_RE) ?? []) {
209
+ const line = raw.trim();
210
+ out.totalLines += 1;
211
+ const verb = _verb(line);
212
+ const pivot = PIVOT_RE.exec(line);
213
+ const head = pivot === null ? line : line.slice(0, pivot.index);
214
+ const found = _artefacts(line);
215
+ if (found.length > 0) {
216
+ out.linesWithArtefact += 1;
217
+ }
218
+ for (const a of found) {
219
+ // The VERB decides what the artefact means; position only splits a
220
+ // prohibition from its remedy. Reading position alone classified
221
+ // `ALWAYS run \`task ci\`` as forbidden — the detector would have
222
+ // reported compliance as a violation.
223
+ const polarity: Polarity =
224
+ verb === 'NEVER'
225
+ ? head.includes(`\`${a.artefact}\``)
226
+ ? 'forbidden'
227
+ : 'prescribed'
228
+ : 'required';
229
+ const ob: Obligation = { skill, line, verb, artefact: a.artefact, kind: a.kind, polarity };
230
+ out.withArtefact.push(ob);
231
+ if (polarity === 'forbidden') out.forbidden.push(ob);
232
+ else if (polarity === 'prescribed') out.prescribed.push(ob);
233
+ else out.required.push(ob);
234
+ }
235
+ }
236
+ }
237
+ return out;
238
+ }
239
+
240
+ // ── transcript side ────────────────────────────────────────────────────────
241
+
242
+ type Entry = Record<string, unknown>;
243
+
244
+ /**
245
+ * Parse a session once. Both passes below need the same entries, and parsing the
246
+ * file three times for one report is the kind of waste that also invites the two
247
+ * passes to disagree about what they read.
248
+ */
249
+ function _parseSession(lines: readonly string[]): Array<Entry | null> {
250
+ return lines.map((line) => {
251
+ if (!line.trim()) return null;
252
+ try {
253
+ return JSON.parse(line) as Entry;
254
+ } catch {
255
+ return null;
256
+ }
257
+ });
258
+ }
259
+
260
+ /** Tools that WRITE. A read must never satisfy a "never hand-edit" obligation. */
261
+ const WRITE_TOOLS = new Set([
262
+ 'Edit',
263
+ 'Write',
264
+ 'NotebookEdit',
265
+ 'MultiEdit',
266
+ 'str-replace-editor',
267
+ 'str_replace_editor',
268
+ 'save-file',
269
+ 'save_file',
270
+ ]);
271
+ const SHELL_TOOLS = new Set(['Bash', 'BashTool', 'launch-process', 'launch_process']);
272
+
273
+ /** `…/skills/<name>` in an injected body, which is how a skill reaches context. */
274
+ const SKILL_BODY_RE = /Base directory for this skill:\s*\S*?\/skills\/([A-Za-z0-9_-]+)/;
275
+
276
+ /**
277
+ * Which skills were in context, and from which entry index onward.
278
+ *
279
+ * Two carriers, both real in this store: an injected body in the user role, and
280
+ * a `Skill` tool call. A slash-command form (`roadmap:process-full`) is
281
+ * normalised to its skill stem so both spellings resolve to one key.
282
+ */
283
+ export function loadedSkills(lines: readonly string[]): Map<string, number> {
284
+ return loadedSkillsFrom(_parseSession(lines));
285
+ }
286
+
287
+ /** As `loadedSkills`, over already-parsed entries. */
288
+ export function loadedSkillsFrom(entries: ReadonlyArray<Entry | null>): Map<string, number> {
289
+ const first = new Map<string, number>();
290
+ const note = (name: string, at: number): void => {
291
+ const key = name.replace(/:/g, '-');
292
+ if (!first.has(key)) first.set(key, at);
293
+ };
294
+ entries.forEach((entry, i) => {
295
+ // A sidechain turn did not happen in the main thread. Counting one lets a
296
+ // skill loaded inside a subagent be paired with an edit made outside it,
297
+ // which breaks the ordering premise that separates SK-2 from a grep.
298
+ if (entry === null || isSidechain(entry)) return;
299
+ if (entry['type'] === 'user') {
300
+ // `entryText`, not a string-only read: measured in one 30-session
301
+ // store, ALL 41 injected skill bodies arrive as content blocks and
302
+ // none as a bare string, so a string-only reader detects zero of
303
+ // them — and a detector with an empty loaded-set returns no findings,
304
+ // which is indistinguishable from compliance.
305
+ const m = SKILL_BODY_RE.exec(entryText(entry));
306
+ if (m !== null) note(m[1] as string, i);
307
+ return;
308
+ }
309
+ if (entry['type'] !== 'assistant') return;
310
+ for (const t of _toolUses(entry)) {
311
+ if (t.name !== 'Skill') continue;
312
+ const s = t.input['skill'];
313
+ if (typeof s === 'string' && s !== '') note(s, i);
314
+ }
315
+ });
316
+ return first;
317
+ }
318
+
319
+ export interface Flag {
320
+ session: string;
321
+ skill: string;
322
+ artefact: string;
323
+ kind: 'path' | 'command';
324
+ /** Entry index of the violating turn, and of the turn that loaded the skill. */
325
+ at: number;
326
+ loadedAt: number;
327
+ tool: string;
328
+ /** The matched literal in context — what makes the flag hand-checkable. */
329
+ evidence: string;
330
+ }
331
+
332
+ /**
333
+ * Which input fields name the TARGET of the act, per kind.
334
+ *
335
+ * Concatenating every string value would match a forbidden path that appears in
336
+ * `new_string` / `old_string` / `content` — i.e. flag an edit to some other file
337
+ * whose text merely MENTIONS `docs/THIRD-PARTY-NOTICES.md` as hand-editing it.
338
+ * The obligation is about the target, so only target-bearing fields are read; the
339
+ * same reasoning excludes a `description` that quotes a forbidden command.
340
+ */
341
+ const PATH_FIELDS = ['file_path', 'path', 'filePath', 'notebook_path', 'target_file'] as const;
342
+ const COMMAND_FIELDS = ['command', 'cmd'] as const;
343
+
344
+ function _targetText(input: Record<string, unknown>, kind: 'path' | 'command'): string {
345
+ const fields = kind === 'path' ? PATH_FIELDS : COMMAND_FIELDS;
346
+ return fields
347
+ .map((f) => input[f])
348
+ .filter((v): v is string => typeof v === 'string')
349
+ .join('\n');
350
+ }
351
+
352
+ export function scanSessionForViolations(
353
+ session: string,
354
+ lines: readonly string[],
355
+ forbidden: readonly Obligation[],
356
+ parsed?: ReadonlyArray<Entry | null>,
357
+ ): Flag[] {
358
+ // `parsed` lets `scanStore` reuse the single pass it already made — without it
359
+ // the file is parsed twice per session, which contradicts `_parseSession`'s
360
+ // own reason for existing.
361
+ const entries = parsed ?? _parseSession(lines);
362
+ const loaded = loadedSkillsFrom(entries);
363
+ if (loaded.size === 0) return [];
364
+ const relevant = forbidden.filter((o) => loaded.has(o.skill));
365
+ if (relevant.length === 0) return [];
366
+
367
+ const flags: Flag[] = [];
368
+ entries.forEach((entry, i) => {
369
+ if (entry === null || isSidechain(entry)) return;
370
+ if (entry['type'] !== 'assistant') return;
371
+ for (const t of _toolUses(entry)) {
372
+ const isWrite = WRITE_TOOLS.has(t.name);
373
+ const isShell = SHELL_TOOLS.has(t.name);
374
+ if (!isWrite && !isShell) continue;
375
+ for (const o of relevant) {
376
+ const loadedAt = loaded.get(o.skill) as number;
377
+ // "in a LATER assistant turn" — the load must precede the act,
378
+ // or a skill invoked to clean up after a violation would be read
379
+ // as having caused it.
380
+ if (i <= loadedAt) continue;
381
+ if (o.kind === 'path' && !isWrite) continue;
382
+ if (o.kind === 'command' && !isShell) continue;
383
+ const text = _targetText(t.input, o.kind);
384
+ if (!text.includes(o.artefact)) continue;
385
+ const at = text.indexOf(o.artefact);
386
+ flags.push({
387
+ session,
388
+ skill: o.skill,
389
+ artefact: o.artefact,
390
+ kind: o.kind,
391
+ at: i,
392
+ loadedAt,
393
+ tool: t.name,
394
+ evidence: text.slice(Math.max(0, at - 40), at + o.artefact.length + 40).replace(/\n/g, ' '),
395
+ });
396
+ }
397
+ }
398
+ });
399
+ return flags;
400
+ }
401
+
402
+ export interface Sk2Report {
403
+ store: string;
404
+ sessions: number;
405
+ sessionsWithASkill: number;
406
+ census: ObligationCensus;
407
+ flags: Flag[];
408
+ }
409
+
410
+ export function defaultStore(cwd: string): string {
411
+ return path.join(os.homedir(), '.claude', 'projects', cwd.replace(/[/.]/g, '-'));
412
+ }
413
+
414
+ export function scanStore(repoRoot: string, store: string, limit: number): Sk2Report {
415
+ const census = extractObligations(repoRoot);
416
+ const report: Sk2Report = {
417
+ store,
418
+ sessions: 0,
419
+ sessionsWithASkill: 0,
420
+ census,
421
+ flags: [],
422
+ };
423
+ let files: string[];
424
+ try {
425
+ files = fs
426
+ .readdirSync(store)
427
+ .filter((f) => f.endsWith('.jsonl'))
428
+ // mtime read ONCE per file, not twice per comparison — a comparator
429
+ // that stats is O(n log n) syscalls for an O(n) fact.
430
+ .map((f) => ({ p: path.join(store, f), m: fs.statSync(path.join(store, f)).mtimeMs }))
431
+ .sort((a, b) => b.m - a.m)
432
+ .slice(0, limit)
433
+ .map((x) => x.p);
434
+ } catch {
435
+ return report;
436
+ }
437
+ report.sessions = files.length;
438
+ for (const file of files) {
439
+ const lines = fs.readFileSync(file, 'utf-8').split('\n');
440
+ const id = path.basename(file).replace(/\.jsonl$/, '').slice(0, 8);
441
+ const entries = _parseSession(lines);
442
+ if (loadedSkillsFrom(entries).size > 0) report.sessionsWithASkill += 1;
443
+ report.flags.push(...scanSessionForViolations(id, lines, census.forbidden, entries));
444
+ }
445
+ return report;
446
+ }
447
+
448
+ function _pct(n: number, d: number): string {
449
+ return d === 0 ? 'n/a' : `${((100 * n) / d).toFixed(1)}%`;
450
+ }
451
+
452
+ export function render(r: Sk2Report): string {
453
+ const c = r.census;
454
+ const lines: string[] = [];
455
+ lines.push('SK-2 loaded-but-violated — advisory, gates on nothing');
456
+ lines.push('');
457
+ lines.push(' COVERAGE (the headline — read this before any violation count)');
458
+ lines.push(` skills with a deterministic obligation ${c.skills.length}`);
459
+ lines.push(` obligation lines in them ${c.totalLines}`);
460
+ lines.push(
461
+ ` …LINES naming a concrete artefact ${c.linesWithArtefact} (${_pct(c.linesWithArtefact, c.totalLines)})`,
462
+ );
463
+ lines.push(` …artefacts named across those lines ${c.withArtefact.length}`);
464
+ lines.push(` …of those, the FORBIDDEN artefact ${c.forbidden.length}`);
465
+ lines.push(` …the PRESCRIBED alternative (excluded) ${c.prescribed.length}`);
466
+ lines.push(` …REQUIRED by a MUST/ALWAYS (excluded) ${c.required.length}`);
467
+ lines.push(
468
+ ` testable without judgement ${c.forbidden.length} artefact(s) over ${c.totalLines} obligation line(s)`,
469
+ );
470
+ lines.push('');
471
+ lines.push(' The other lines are deterministic in wording and need a reading to test');
472
+ lines.push(' ("NEVER return `clean` out of politeness"). That is the FC-8 class this suite');
473
+ lines.push(' excludes, so it is reported as uncovered rather than approximated.');
474
+ lines.push('');
475
+ if (c.forbidden.length > 0) {
476
+ lines.push(' Mechanised obligations:');
477
+ for (const o of c.forbidden) {
478
+ lines.push(` [${o.skill}] ${o.kind}: ${o.artefact}`);
479
+ }
480
+ lines.push('');
481
+ }
482
+ if (c.prescribed.length > 0) {
483
+ lines.push(' Excluded as prescriptive (the artefact is the remedy, not the offence):');
484
+ for (const o of c.prescribed) {
485
+ lines.push(` [${o.skill}] ${o.artefact}`);
486
+ }
487
+ lines.push('');
488
+ }
489
+ if (c.required.length > 0) {
490
+ lines.push(' Excluded as REQUIRED — a MUST/ALWAYS line, so its violation is the');
491
+ lines.push(' artefact being ABSENT, and a transcript cannot show that something never');
492
+ lines.push(' happened without also knowing it was due:');
493
+ for (const o of c.required) {
494
+ lines.push(` [${o.skill}] ${o.verb} ${o.artefact}`);
495
+ }
496
+ lines.push('');
497
+ }
498
+ lines.push(
499
+ ` Scanned ${r.sessions} session(s); ${r.sessionsWithASkill} had a skill in context.`,
500
+ );
501
+ lines.push(` Flags: ${r.flags.length}`);
502
+ for (const f of r.flags) {
503
+ lines.push(` ${f.session} [${f.skill}] ${f.artefact} via ${f.tool} (entry ${f.at}, loaded ${f.loadedAt})`);
504
+ lines.push(` …${f.evidence}…`);
505
+ }
506
+ lines.push('');
507
+ lines.push(' PRECISION: every flag above is hand-read before any number from this script');
508
+ lines.push(' is cited (Phase 3.3). A detector that cannot state its false-positive rate');
509
+ lines.push(' ships as detection-only, and this one says so.');
510
+ return lines.join('\n');
511
+ }
512
+
513
+ export function main(argv?: readonly string[]): number {
514
+ const args = argv ?? process.argv.slice(2);
515
+ let limit = 30;
516
+ let json = false;
517
+ const stores: string[] = [];
518
+ for (let i = 0; i < args.length; i += 1) {
519
+ const a = args[i];
520
+ // A value must not itself be a flag: `--store --json` otherwise reads
521
+ // `--json` as a store path and reports 0 sessions, silently. The sibling
522
+ // report closes the same hole in this branch.
523
+ const value = (at: number): string | null => {
524
+ const v = args[at + 1];
525
+ return v === undefined || v.startsWith('-') ? null : v;
526
+ };
527
+ if (a === '--limit') {
528
+ const v = value(i);
529
+ if (v === null) {
530
+ process.stderr.write('report_skill_obligation_violations: --limit needs a number\n');
531
+ return 1;
532
+ }
533
+ limit = Number.parseInt(v, 10);
534
+ i += 1;
535
+ } else if (a === '--store') {
536
+ const v = value(i);
537
+ if (v === null) {
538
+ process.stderr.write('report_skill_obligation_violations: --store needs a path\n');
539
+ return 1;
540
+ }
541
+ stores.push(v);
542
+ i += 1;
543
+ } else if (a === '--json') {
544
+ json = true;
545
+ } else if (a === '--help' || a === '-h') {
546
+ process.stdout.write(
547
+ 'usage: report_skill_obligation_violations [--limit N] [--store PATH]... [--json]\n',
548
+ );
549
+ return 0;
550
+ } else if (a !== undefined && a.startsWith('--') && a !== '--quiet') {
551
+ process.stderr.write(`report_skill_obligation_violations: unknown flag ${a}\n`);
552
+ return 1;
553
+ }
554
+ }
555
+ if (!Number.isFinite(limit) || limit <= 0) {
556
+ process.stderr.write('report_skill_obligation_violations: --limit must be a positive integer\n');
557
+ return 1;
558
+ }
559
+ if (stores.length === 0) stores.push(defaultStore(REPO_ROOT));
560
+ for (const store of stores) {
561
+ const r = scanStore(REPO_ROOT, store, limit);
562
+ process.stdout.write(json ? `${JSON.stringify(r, null, 2)}\n` : `${render(r)}\n`);
563
+ }
564
+ return 0;
565
+ }
566
+
567
+ function _isCliEntry(): boolean {
568
+ if (process.argv[1] === undefined) return false;
569
+ if (import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) return true;
570
+ try {
571
+ return fs.realpathSync(_HERE) === fs.realpathSync(path.resolve(process.argv[1]));
572
+ } catch {
573
+ return false;
574
+ }
575
+ }
576
+
577
+ if (_isCliEntry()) {
578
+ process.exit(main());
579
+ }