session-orchestrator 5.3.0 → 5.5.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 (260) hide show
  1. package/.agents/skills/claude-md-drift-check/SKILL.md +1 -1
  2. package/.agents/skills/test-audit/SKILL.md +21 -0
  3. package/.agents/skills/test-audit/agents/openai.yaml +5 -0
  4. package/.claude-plugin/marketplace.json +1 -1
  5. package/.claude-plugin/plugin.json +1 -1
  6. package/.codex-plugin/plugin.json +1 -1
  7. package/.codex-plugin/skills/claude-md-drift-check/SKILL.md +1 -1
  8. package/.codex-plugin/skills/test-audit/SKILL.md +21 -0
  9. package/.codex-plugin/skills/test-audit/agents/openai.yaml +5 -0
  10. package/.cursor/commands/test-audit.md +14 -0
  11. package/.cursor/rules/060-evolve.mdc +28 -13
  12. package/.cursor/skills/claude-md-drift-check/SKILL.md +1 -1
  13. package/.cursor/skills/test-audit/SKILL.md +12 -0
  14. package/.cursor-plugin/plugin.json +1 -1
  15. package/.orchestrator/policy/blocked-commands.json +1 -1
  16. package/AGENTS.md +9 -31
  17. package/CHANGELOG.md +162 -0
  18. package/README.md +14 -10
  19. package/docs/agent-authoring.md +2 -2
  20. package/docs/baseline.md +10 -1
  21. package/docs/ci-setup.md +154 -4
  22. package/docs/components.md +11 -11
  23. package/docs/events-schema.md +25 -19
  24. package/docs/install.md +14 -2
  25. package/docs/migration-v4.md +24 -17
  26. package/docs/persona-panel.md +5 -4
  27. package/docs/plugin-architecture-v3.md +1 -1
  28. package/docs/rule-authoring.md +27 -12
  29. package/docs/scope-collision-guard.md +40 -16
  30. package/docs/session-config-reference.md +61 -12
  31. package/docs/session-config-template.md +11 -2
  32. package/docs/vault-docs-architecture.md +8 -4
  33. package/hooks/_lib/hook-import-set.json +42 -29
  34. package/hooks/_lib/lock-bootstrap.mjs +118 -26
  35. package/hooks/config-protection.mjs +1 -1
  36. package/hooks/enforce-commands.mjs +28 -13
  37. package/hooks/enforce-scope.mjs +201 -23
  38. package/hooks/hooks-codex.json +1 -1
  39. package/hooks/hooks-cursor.json +5 -0
  40. package/hooks/hooks-pi.json +5 -0
  41. package/hooks/hooks.json +6 -1
  42. package/hooks/on-session-start.mjs +57 -2
  43. package/hooks/on-stop.mjs +70 -168
  44. package/hooks/post-bash-write-verify.mjs +98 -17
  45. package/hooks/post-edit-validate.mjs +15 -5
  46. package/hooks/post-tool-batch-wave-signal.mjs +32 -152
  47. package/hooks/pre-bash-memory-propose-audit.mjs +71 -10
  48. package/hooks/pre-bash-sessions-ledger-guard.mjs +28 -2
  49. package/hooks/pre-bash-staging-fence.mjs +29 -8
  50. package/hooks/pre-edit-sessions-ledger-guard.mjs +162 -0
  51. package/hooks/pre-task-scope-disjoint.mjs +1143 -279
  52. package/hooks/subagent-telemetry.mjs +96 -19
  53. package/hooks/wave-scope-commit-guard.mjs +168 -72
  54. package/package.json +2 -6
  55. package/pi/prompts/test-audit.md +12 -0
  56. package/rules/README.md +6 -1
  57. package/rules/_index.md +3 -3
  58. package/rules/always-on/bash-harness-pitfalls.md +7 -1
  59. package/rules/opt-in-domain/prompt-caching.md +5 -1
  60. package/rules/opt-in-stack/backend-data.md +9 -1
  61. package/rules/opt-in-stack/backend.md +7 -1
  62. package/rules/opt-in-stack/frontend.md +6 -1
  63. package/rules/opt-in-stack/security-web.md +7 -1
  64. package/rules/opt-in-stack/swift.md +5 -1
  65. package/scripts/apply-session-learnings.mjs +513 -0
  66. package/scripts/archive-closed-prds.mjs +40 -12
  67. package/scripts/autopilot.mjs +57 -54
  68. package/scripts/backfill-abandoned-sessions.mjs +81 -6
  69. package/scripts/backfill-learnings-from-vault.mjs +49 -22
  70. package/scripts/baseline-archetypes.mjs +1 -1
  71. package/scripts/check-package-manager.mjs +62 -5
  72. package/scripts/codex-install.mjs +3 -0
  73. package/scripts/cursor-install.mjs +47 -6
  74. package/scripts/emit-session.mjs +298 -2
  75. package/scripts/export-hw-learnings.mjs +40 -10
  76. package/scripts/fleet-checkin.mjs +170 -0
  77. package/scripts/lib/agent-frontmatter.mjs +3 -3
  78. package/scripts/lib/auto-dream.mjs +7 -6
  79. package/scripts/lib/autopilot/flags.mjs +6 -1
  80. package/scripts/lib/autopilot/loop.mjs +32 -13
  81. package/scripts/lib/autopilot/stall-sampler.mjs +13 -9
  82. package/scripts/lib/baseline-archetypes.mjs +41 -22
  83. package/scripts/lib/blocked-commands-policy.mjs +40 -0
  84. package/scripts/lib/common.mjs +41 -0
  85. package/scripts/lib/config/host-paths.mjs +79 -0
  86. package/scripts/lib/config/persona-gate-wave.mjs +3 -3
  87. package/scripts/lib/config/reaper.mjs +7 -6
  88. package/scripts/lib/config/vault-integration.mjs +151 -16
  89. package/scripts/lib/config-schema.mjs +13 -0
  90. package/scripts/lib/config.mjs +75 -10
  91. package/scripts/lib/eval/engine.mjs +13 -0
  92. package/scripts/lib/events-retention-banner.mjs +393 -0
  93. package/scripts/lib/events-rotation.mjs +21 -6
  94. package/scripts/lib/events.mjs +72 -5
  95. package/scripts/lib/fleet-protocol.mjs +288 -0
  96. package/scripts/lib/gates/gate-baseline.mjs +13 -3
  97. package/scripts/lib/gates/gate-full.mjs +7 -2
  98. package/scripts/lib/gates/gate-helpers.mjs +93 -2
  99. package/scripts/lib/gates/gate-incremental.mjs +23 -22
  100. package/scripts/lib/gates/gate-per-file.mjs +16 -7
  101. package/scripts/lib/hardening.mjs +1 -0
  102. package/scripts/lib/harness-audit/categories/category4.mjs +8 -6
  103. package/scripts/lib/instruction-budget-guard.mjs +77 -11
  104. package/scripts/lib/learnings/expiry-sweep.mjs +420 -22
  105. package/scripts/lib/learnings/io.mjs +274 -37
  106. package/scripts/lib/learnings/schema.mjs +163 -15
  107. package/scripts/lib/learnings/sizing-subject.mjs +2 -2
  108. package/scripts/lib/lock-reaper.mjs +38 -14
  109. package/scripts/lib/maintenance-due-banner.mjs +23 -8
  110. package/scripts/lib/memory-proposals/schema.mjs +11 -5
  111. package/scripts/lib/named-baseline-resolver.mjs +53 -28
  112. package/scripts/lib/named-vault-resolver.mjs +147 -5
  113. package/scripts/lib/orphan-reaper/defaults.mjs +41 -0
  114. package/scripts/lib/orphan-reaper/no-follow-append.mjs +87 -0
  115. package/scripts/lib/orphan-reaper/ps-snapshot.mjs +181 -0
  116. package/scripts/lib/orphan-reaper/reaper-audit.mjs +323 -0
  117. package/scripts/lib/orphan-reaper/reaper-decide.mjs +395 -0
  118. package/scripts/lib/orphan-reaper/scan-throttle.mjs +144 -0
  119. package/scripts/lib/orphan-reaper/scan.mjs +510 -0
  120. package/scripts/lib/orphan-reaper/trigger.mjs +173 -0
  121. package/scripts/lib/orphan-reaper.mjs +47 -1315
  122. package/scripts/lib/owner-yaml.mjs +104 -6
  123. package/scripts/lib/persona-panel/catalog-loader.mjs +1 -1
  124. package/scripts/lib/platform.mjs +206 -2
  125. package/scripts/lib/plugin-update-banner.mjs +3 -1
  126. package/scripts/lib/process-group.mjs +134 -22
  127. package/scripts/lib/project-hygiene.mjs +21 -1
  128. package/scripts/lib/quality-gate.mjs +45 -5
  129. package/scripts/lib/resource-probe.mjs +1 -1
  130. package/scripts/lib/rule-loader.mjs +100 -62
  131. package/scripts/lib/rules-sync.mjs +27 -12
  132. package/scripts/lib/scope-baseline.mjs +119 -29
  133. package/scripts/lib/scope-echo.mjs +7 -0
  134. package/scripts/lib/scope-gate.mjs +194 -27
  135. package/scripts/lib/session-close-backfill.mjs +129 -4
  136. package/scripts/lib/session-end/tail-runner.mjs +10 -5
  137. package/scripts/lib/session-lock-shape.mjs +6 -3
  138. package/scripts/lib/session-lock.mjs +320 -67
  139. package/scripts/lib/session-record-repair.mjs +183 -26
  140. package/scripts/lib/session-registry.mjs +67 -17
  141. package/scripts/lib/session-schema/constants.mjs +30 -4
  142. package/scripts/lib/session-schema/validator.mjs +28 -1
  143. package/scripts/lib/session-start-probes.mjs +242 -68
  144. package/scripts/lib/session-token-rollup.mjs +252 -54
  145. package/scripts/lib/sessions-canonical.mjs +100 -9
  146. package/scripts/lib/sessions-staleness-banner.mjs +3 -1
  147. package/scripts/lib/sh-quote.mjs +29 -0
  148. package/scripts/lib/subagents-schema.mjs +30 -3
  149. package/scripts/lib/tail-window.mjs +27 -3
  150. package/scripts/lib/telemetry/pricing.mjs +121 -71
  151. package/scripts/lib/telemetry/sync.mjs +17 -0
  152. package/scripts/lib/tests-src-ratio.mjs +215 -23
  153. package/scripts/lib/tmux-layout/layouts.mjs +1 -11
  154. package/scripts/lib/tmux-layout/telemetry-stats.mjs +12 -0
  155. package/scripts/lib/tmux-layout/tmux-shell.mjs +2 -11
  156. package/scripts/lib/ux-grill/reconcile.mjs +1 -1
  157. package/scripts/lib/validate/check-agents.mjs +1 -1
  158. package/scripts/lib/validate/check-hooks-symmetry.mjs +5 -0
  159. package/scripts/lib/validate/check-learnings-shell-writes.mjs +214 -0
  160. package/scripts/lib/validate/check-test-value-bans.mjs +388 -27
  161. package/scripts/lib/validate/check-unicode-safety.mjs +1 -0
  162. package/scripts/lib/validate/check-unwired-features.mjs +1 -1
  163. package/scripts/lib/validate/check-vcs-repo-flag.mjs +1 -1
  164. package/scripts/lib/validate-vendored-rules.mjs +206 -81
  165. package/scripts/lib/vault-mirror/auto-commit.mjs +5 -3
  166. package/scripts/lib/vault-mirror/namespace.mjs +47 -26
  167. package/scripts/lib/vault-mirror/process.mjs +350 -62
  168. package/scripts/lib/vault-mirror/pseudonym-map.mjs +14 -5
  169. package/scripts/lib/vault-mirror/render-learnings.mjs +66 -3
  170. package/scripts/lib/vault-mirror/render-sessions.mjs +28 -4
  171. package/scripts/lib/vault-mirror/telemetry.mjs +25 -11
  172. package/scripts/lib/vault-status/board-writer.mjs +53 -12
  173. package/scripts/lib/vault-status/narrative-mirror.mjs +40 -9
  174. package/scripts/lib/vcs-repo-spec.mjs +8 -0
  175. package/scripts/lib/wave-executor/dispatch-common.mjs +10 -18
  176. package/scripts/lib/wave-executor/foreign-dispatch.mjs +1 -1
  177. package/scripts/lib/wave-executor/remote-dispatch.mjs +1 -1
  178. package/scripts/lib/wave-resource-gate.mjs +34 -8
  179. package/scripts/materialize-wave-scope.mjs +7 -4
  180. package/scripts/memory-propose.mjs +18 -5
  181. package/scripts/release-session-lock.mjs +2 -1
  182. package/scripts/release.mjs +11 -3
  183. package/scripts/repair-invalid-sessions.mjs +37 -4
  184. package/scripts/run-migrate-v2-cross-repo.mjs +171 -31
  185. package/scripts/run-quality-gate.mjs +331 -307
  186. package/scripts/self-update.mjs +229 -0
  187. package/scripts/site-numbers.mjs +163 -5
  188. package/scripts/sweep-expired-learnings.mjs +603 -20
  189. package/scripts/sweep-expired-rules.mjs +2 -1
  190. package/scripts/validate-plugin.mjs +7 -0
  191. package/scripts/validate-wave-scope.mjs +21 -2
  192. package/scripts/vault-backfill.mjs +50 -37
  193. package/scripts/vault-integration-watcher.mjs +2 -1
  194. package/scripts/vault-mirror.mjs +238 -75
  195. package/scripts/wave-scope-binding.mjs +2 -1
  196. package/skills/_shared/config-reading.md +3 -3
  197. package/skills/_shared/fleet-protocol.md +134 -0
  198. package/skills/_shared/state-ownership.md +41 -12
  199. package/skills/autopilot/SKILL.md +25 -11
  200. package/skills/bootstrap/SKILL.md +6 -4
  201. package/skills/bootstrap/_shared-template.md +97 -2
  202. package/skills/bootstrap/fast-template.md +4 -1
  203. package/skills/bootstrap/private-contract.md +2 -1
  204. package/skills/bootstrap/public-fallback.md +6 -3
  205. package/skills/bootstrap/references/bootstrap-refresh-lock-flow.md +5 -3
  206. package/skills/bootstrap/references/bootstrap-retroactive-flow.md +2 -1
  207. package/skills/bootstrap/standard-template.md +2 -0
  208. package/skills/claude-md-drift-check/SKILL.md +10 -10
  209. package/skills/claude-md-drift-check/checker.mjs +26 -18
  210. package/skills/discovery/probes/ssot-code-diff.mjs +12 -6
  211. package/skills/discovery/probes/vault-narrative-staleness.mjs +6 -2
  212. package/skills/discovery/probes/vault-staleness.mjs +9 -3
  213. package/skills/docs-orchestrator/SKILL.md +12 -5
  214. package/skills/docs-orchestrator/audience-mapping.md +5 -3
  215. package/skills/evolve/SKILL.md +13 -4
  216. package/skills/evolve/references/evolve-analyze-mode.md +42 -18
  217. package/skills/memory-cleanup/SKILL.md +1 -1
  218. package/skills/persona-panel/SKILL.md +2 -2
  219. package/skills/persona-panel/persona-format.md +2 -2
  220. package/skills/persona-panel/presets/designer-lens.md +1 -1
  221. package/skills/persona-panel/presets/engineer-lens.md +1 -1
  222. package/skills/persona-panel/presets/pm-lens.md +1 -1
  223. package/skills/plan/mode-retro.md +19 -4
  224. package/skills/quality-gates/SKILL.md +6 -4
  225. package/skills/remote-offload/SKILL.md +4 -2
  226. package/skills/session-end/learning-patterns.md +23 -13
  227. package/skills/session-end/metrics-collection.md +6 -7
  228. package/skills/session-end/phase-3-2-docs-verification.md +7 -6
  229. package/skills/session-end/plan-verification.md +25 -9
  230. package/skills/session-end/references/phase-3-documentation-updates.md +13 -3
  231. package/skills/session-end/references/phase-4a-worktree-cleanup.md +2 -0
  232. package/skills/session-end/references/session-summary-template.md +1 -1
  233. package/skills/session-end/session-metrics-write.md +65 -68
  234. package/skills/session-plan/SKILL.md +0 -3
  235. package/skills/session-start/SKILL.md +8 -2
  236. package/skills/session-start/references/operations-contract.md +6 -0
  237. package/skills/session-start/references/phase-1-2-session-lock.md +32 -7
  238. package/skills/session-start/references/phase-1-7-vault-status-board.md +1 -1
  239. package/skills/session-start/references/phase-4-ssot-environment-check.md +10 -3
  240. package/skills/test-audit/SKILL.md +152 -0
  241. package/skills/test-audit/references/campaign.md +111 -0
  242. package/skills/test-audit/references/mutate.sh +50 -0
  243. package/skills/vault-mirror/SKILL.md +21 -11
  244. package/skills/wave-executor/SKILL.md +2 -2
  245. package/skills/wave-executor/circuit-breaker.md +1 -1
  246. package/skills/wave-executor/references/wave-executor-state-init.md +3 -0
  247. package/skills/wave-executor/references/wave-loop-dispatch.md +11 -1
  248. package/skills/wave-executor/references/wave-loop-review.md +4 -6
  249. package/skills/wave-executor/references/wave-loop-scope-manifest.md +13 -5
  250. package/templates/personas/README.md +5 -5
  251. package/templates/personas/accounting-compliance.v1.md +1 -1
  252. package/templates/personas/accounting-tax-advisor.v1.md +1 -1
  253. package/templates/personas/buyer-p1-cto.v1.md +1 -1
  254. package/templates/personas/buyer-p2-kanzlei.v1.md +1 -1
  255. package/templates/personas/buyer-p3-build.v1.md +1 -1
  256. package/templates/personas/buyer-p4-tech-veto.v1.md +1 -1
  257. package/templates/personas/buyer-p5-solo.v1.md +1 -1
  258. package/templates/personas/buyer-p6-ld.v1.md +1 -1
  259. package/templates/personas/klima-ai-expert.v1.md +1 -1
  260. package/templates/personas/klima-physicist.v1.md +1 -1
@@ -0,0 +1,214 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Check: no instruction prose tells an agent to write `learnings.jsonl` with a
4
+ * shell redirect (GitLab #1446 / GitHub #69). BLOCKING.
5
+ *
6
+ * ## Why
7
+ *
8
+ * The store is gitignored, so a bad write has no VCS restore. The sanctioned
9
+ * write paths — `scripts/apply-session-learnings.mjs` (session-end, retro) and
10
+ * `scripts/sweep-expired-learnings.mjs --prune` (`/evolve`) — validate every
11
+ * record, snapshot `.bak-<ISO>`, and archive what leaves the store. A shell
12
+ * `>` / `>>` does none of that. The prose producers this change closed, all
13
+ * measured at ebf63bd1:
14
+ *
15
+ * - `skills/session-end/learning-patterns.md` Phase 3.6 step 4g ("atomic
16
+ * rewrite with `>`") — the path that wrote a file PATH into `scope` (GH#69);
17
+ * - `skills/plan/mode-retro.md` § 3.3 — an unnamed "Append new patterns to
18
+ * `learnings.jsonl`" with no mechanism at all;
19
+ * - `.cursor/rules/060-evolve.mdc` — three sites ("Write entire result back
20
+ * with `>`", "write all with `>`" twice). None of the three names
21
+ * `learnings.jsonl` on its own line, which is why R2 is windowed.
22
+ *
23
+ * ## Rules
24
+ *
25
+ * R1 shell redirect — `>`/`>>` directly in front of a `…learnings.jsonl`
26
+ * target on the same line. The leading boundary (start,
27
+ * whitespace, `|`, `;`, `&`, `(`) is load-bearing:
28
+ * without it 9 of 12 hits were `<state-dir>/…`
29
+ * placeholders. A Markdown blockquote marker (`> ` at
30
+ * line start, outside a fence) is not a redirect and is
31
+ * stripped first; inside a fence it IS a truncation.
32
+ * R3 tee — `tee [-a] …learnings.jsonl` on the same line.
33
+ * R2 prose operator — a backticked `` `>` `` / `` `>>` `` within ±3 lines
34
+ * (WINDOW) of an anchor: `learnings.jsonl`, "the store",
35
+ * "learnings store", "result back", "atomic rewrite".
36
+ * The last two are the retired producers' own wording.
37
+ * An operator inside an ASCII double-quoted span is a
38
+ * quotation of former prose, not an instruction.
39
+ *
40
+ * Exempt (per operator): a prohibition keyword (NEVER / do not / FORBIDDEN /
41
+ * verboten …) BEFORE the operator in the same sentence — a keyword after it
42
+ * (`… > learnings.jsonl # never run this twice`) or in an earlier sentence
43
+ * (`Never skip review. Then run: … >> learnings.jsonl`) does not count.
44
+ * Exempt (whole line): the last sentence of the previous non-empty line states
45
+ * a prohibition before any operator, an empty-file create (`: > file`), or the
46
+ * marker `<!-- learnings-write-check: prohibition -->` on the line or the
47
+ * previous non-empty line. Fenced blocks ARE scanned — a fenced shell snippet
48
+ * is exactly a shell write.
49
+ *
50
+ * @module scripts/lib/validate/check-learnings-shell-writes
51
+ */
52
+
53
+ import { readFileSync } from 'node:fs';
54
+ import path from 'node:path';
55
+ import { enumerateRepoFiles } from './enumerate-repo-files.mjs';
56
+ import { isMainModule } from '../is-main-module.mjs';
57
+
58
+ /** Instruction corpora that tell an agent what to run. */
59
+ export const SCAN_DIRS = Object.freeze(['skills', 'commands', 'agents', 'docs', '.claude/rules', '.cursor/rules']);
60
+
61
+ /** Lines on either side of an R2 operator searched for an anchor. */
62
+ const WINDOW = 3;
63
+
64
+ const MENTION_RE = /learnings\.jsonl/;
65
+ const ANCHOR_RE = /learnings\.jsonl|\b(?:the|learnings) store\b|\bresult back\b|\batomic rewrite\b/i;
66
+ const SHELL_REDIRECT_RE = /(?:^|[\s|;&(])(>{1,2})[ \t]*["'`]?[^\s"'`<>]*learnings\.jsonl\b/dg;
67
+ const TEE_RE = /\btee\b(?:[ \t]+-{1,2}[\w-]+)*[ \t]+["'`]?[^\s"'`|]*learnings\.jsonl\b/g;
68
+ const PROSE_OPERATOR_RE = /`>{1,2}`/g;
69
+ const ANY_OPERATOR_RE = /`>{1,2}`|(?:^|[\s|;&(])>{1,2}|\btee\b/;
70
+ const PROHIBITION_RE = /\b(NEVER|[Nn]ever|DO NOT|[Dd]o not|[Dd]on't|FORBIDDEN|forbidden|verboten)\b/;
71
+ // A sentence ends at `.`/`!`/`?` + whitespace + a capital (markup allowed):
72
+ // `jq . next` and `jq | ... >` are not sentence breaks.
73
+ const SENTENCE_BREAK_RE = /[.!?]\s+(?=[*_`"']*[A-Z])/;
74
+ const EMPTY_CREATE_RE = /^\s*:\s*>/;
75
+ const BLOCKQUOTE_RE = /^\s*(?:>[ \t])+/;
76
+ const FENCE_RE = /^\s*(?:```|~~~)/;
77
+ const MARKER = '<!-- learnings-write-check: prohibition -->';
78
+
79
+ /** The sentence a position sits in, cut off at that position. */
80
+ function clauseBefore(text, pos) {
81
+ return text.slice(0, pos).split(SENTENCE_BREAK_RE).pop();
82
+ }
83
+
84
+ /** True when a prohibition keyword precedes `pos` within its own sentence. */
85
+ function prohibitedBefore(text, pos) {
86
+ return PROHIBITION_RE.test(clauseBefore(text, pos));
87
+ }
88
+
89
+ /** True when the last sentence of `line` states a prohibition before any operator. */
90
+ function isProhibitionSentence(line) {
91
+ const last = line.replace(BLOCKQUOTE_RE, '').trimEnd().split(SENTENCE_BREAK_RE).pop();
92
+ const k = last.search(PROHIBITION_RE);
93
+ if (k < 0) return false;
94
+ const o = last.search(ANY_OPERATOR_RE);
95
+ return o < 0 || k < o;
96
+ }
97
+
98
+ /** True when `pos` sits inside an ASCII double-quoted span of `text`. */
99
+ function insideDoubleQuotes(text, pos) {
100
+ return (text.slice(0, pos).match(/"/g) ?? []).length % 2 === 1;
101
+ }
102
+
103
+ /**
104
+ * Every operator on the line as `{rule, pos}`, positions relative to `body`.
105
+ * R2 candidates are returned only when the ±WINDOW neighbourhood holds an anchor.
106
+ */
107
+ function operatorsOf(body, lines, i) {
108
+ const ops = [];
109
+ if (MENTION_RE.test(body)) {
110
+ for (const m of body.matchAll(SHELL_REDIRECT_RE)) ops.push({ rule: 'R1', pos: m.indices[1][0] });
111
+ for (const m of body.matchAll(TEE_RE)) ops.push({ rule: 'R3', pos: m.index });
112
+ }
113
+ const prose = [...body.matchAll(PROSE_OPERATOR_RE)];
114
+ if (prose.length > 0) {
115
+ const from = Math.max(0, i - WINDOW);
116
+ const anchored = lines.slice(from, i + WINDOW + 1).some((l) => ANCHOR_RE.test(l));
117
+ if (anchored) {
118
+ for (const m of prose) {
119
+ if (!insideDoubleQuotes(body, m.index)) ops.push({ rule: 'R2', pos: m.index });
120
+ }
121
+ }
122
+ }
123
+ return ops;
124
+ }
125
+
126
+ /**
127
+ * Scan the instruction corpora for shell writes to `learnings.jsonl`.
128
+ *
129
+ * @param {{ pluginRoot: string, dirs?: readonly string[] }} opts
130
+ * @returns {{ ok: boolean, findings: {file: string, line: number, rule: string, text: string}[],
131
+ * summary: { filesScanned: number, mentions: number, exempt: number } }}
132
+ */
133
+ export function scanLearningsShellWrites({ pluginRoot, dirs = SCAN_DIRS }) {
134
+ const files = enumerateRepoFiles({ repoRoot: pluginRoot, dirs: [...dirs], exts: ['.md', '.mdc'] });
135
+ const findings = [];
136
+ let mentions = 0;
137
+ let exempt = 0;
138
+ for (const abs of files) {
139
+ const lines = readFileSync(abs, 'utf8').split('\n');
140
+ const rel = path.relative(pluginRoot, abs);
141
+ let inFence = false;
142
+ let prev = '';
143
+ lines.forEach((text, i) => {
144
+ const prevNonEmpty = prev;
145
+ if (text.trim().length > 0) prev = text;
146
+ if (FENCE_RE.test(text)) {
147
+ inFence = !inFence;
148
+ return;
149
+ }
150
+ if (MENTION_RE.test(text)) mentions += 1;
151
+ const body = inFence ? text : text.replace(BLOCKQUOTE_RE, '');
152
+ const ops = operatorsOf(body, lines, i);
153
+ if (ops.length === 0) return;
154
+ const lineExempt =
155
+ EMPTY_CREATE_RE.test(body) ||
156
+ text.includes(MARKER) ||
157
+ prevNonEmpty.includes(MARKER) ||
158
+ isProhibitionSentence(prevNonEmpty);
159
+ const failing = lineExempt ? [] : ops.filter((op) => !prohibitedBefore(body, op.pos));
160
+ if (failing.length === 0) {
161
+ exempt += 1;
162
+ return;
163
+ }
164
+ const rule = ['R1', 'R3', 'R2'].find((r) => failing.some((op) => op.rule === r));
165
+ findings.push({ file: rel, line: i + 1, rule, text: text.trim() });
166
+ });
167
+ }
168
+ return { ok: findings.length === 0, findings, summary: { filesScanned: files.length, mentions, exempt } };
169
+ }
170
+
171
+ const RULE_TEXT = Object.freeze({
172
+ R1: 'shell redirect into learnings.jsonl',
173
+ R2: 'redirect operator near a learnings.jsonl / store-write mention',
174
+ R3: 'tee into learnings.jsonl',
175
+ });
176
+
177
+ /**
178
+ * Run the human-readable validator CLI.
179
+ *
180
+ * @param {string} pluginRoot absolute plugin root
181
+ * @returns {number} 0 = clean, 1 = findings
182
+ */
183
+ export function runCheckLearningsShellWrites(pluginRoot) {
184
+ console.log('--- Check: no shell-redirect writes to learnings.jsonl in instruction prose ---');
185
+ const inspection = scanLearningsShellWrites({ pluginRoot });
186
+ for (const f of inspection.findings) {
187
+ console.log(
188
+ ` FAIL: [${f.rule}] ${f.file}:${f.line} ${RULE_TEXT[f.rule]} — route the write through ` +
189
+ '`scripts/apply-session-learnings.mjs` or `sweep-expired-learnings.mjs --prune`',
190
+ );
191
+ }
192
+ const s = inspection.summary;
193
+ if (inspection.ok) {
194
+ console.log(
195
+ ` PASS: ${s.mentions} learnings.jsonl mention(s) in ${s.filesScanned} file(s), ` +
196
+ `${s.exempt} exempt as prohibition`,
197
+ );
198
+ }
199
+ console.log('');
200
+ console.log(`Results: ${inspection.ok ? 1 : 0} passed, ${inspection.findings.length} failed`);
201
+ return inspection.ok ? 0 : 1;
202
+ }
203
+
204
+ if (isMainModule(import.meta.url)) {
205
+ const args = process.argv.slice(2).filter((arg) => arg !== '--json');
206
+ const root = path.resolve(args[0] || process.cwd());
207
+ if (process.argv.includes('--json')) {
208
+ const inspection = scanLearningsShellWrites({ pluginRoot: root });
209
+ process.stdout.write(`${JSON.stringify(inspection, null, 2)}\n`);
210
+ process.exitCode = inspection.ok ? 0 : 1;
211
+ } else {
212
+ process.exitCode = runCheckLearningsShellWrites(root);
213
+ }
214
+ }