session-orchestrator 3.22.0 → 3.24.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 (316) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.cursor/commands/autopilot-multi.md +14 -0
  5. package/.cursor/commands/autopilot.md +14 -0
  6. package/.cursor/commands/bootstrap.md +14 -0
  7. package/.cursor/commands/brainstorm.md +14 -0
  8. package/.cursor/commands/close.md +13 -0
  9. package/.cursor/commands/contract-version-bump.md +14 -0
  10. package/.cursor/commands/debug.md +14 -0
  11. package/.cursor/commands/discovery.md +14 -0
  12. package/.cursor/commands/dispatcher.md +14 -0
  13. package/.cursor/commands/eli5.md +14 -0
  14. package/.cursor/commands/eval.md +14 -0
  15. package/.cursor/commands/evolve.md +14 -0
  16. package/.cursor/commands/go.md +14 -0
  17. package/.cursor/commands/grill.md +14 -0
  18. package/.cursor/commands/harness-audit.md +13 -0
  19. package/.cursor/commands/journey-audit.md +14 -0
  20. package/.cursor/commands/memory-cleanup.md +14 -0
  21. package/.cursor/commands/persona-panel.md +14 -0
  22. package/.cursor/commands/plan.md +14 -0
  23. package/.cursor/commands/portfolio.md +14 -0
  24. package/.cursor/commands/reconcile.md +14 -0
  25. package/.cursor/commands/release.md +14 -0
  26. package/.cursor/commands/repo-audit.md +13 -0
  27. package/.cursor/commands/session.md +14 -0
  28. package/.cursor/commands/spinout.md +14 -0
  29. package/.cursor/commands/sunset-review.md +14 -0
  30. package/.cursor/commands/templates-ack.md +14 -0
  31. package/.cursor/commands/test.md +14 -0
  32. package/.cursor/hooks.json +60 -0
  33. package/.cursor/rules/000-session-orchestrator.mdc +8 -0
  34. package/.cursor/rules/010-session-workflow.mdc +9 -1
  35. package/.cursor/rules/020-quality-gates.mdc +1 -1
  36. package/.cursor/rules/030-wave-execution.mdc +1 -1
  37. package/.cursor/rules/050-plan.mdc +2 -2
  38. package/.cursor/rules/070-gitlab-ops.mdc +73 -57
  39. package/.cursor/rules/080-ecosystem-health.mdc +7 -7
  40. package/.cursor/skills/architecture/SKILL.md +13 -0
  41. package/.cursor/skills/autopilot/SKILL.md +12 -0
  42. package/.cursor/skills/bootstrap/SKILL.md +12 -0
  43. package/.cursor/skills/brainstorm/SKILL.md +13 -0
  44. package/.cursor/skills/claude-md-drift-check/SKILL.md +13 -0
  45. package/.cursor/skills/contract-version-bump/SKILL.md +12 -0
  46. package/.cursor/skills/convergence-monitoring/SKILL.md +12 -0
  47. package/.cursor/skills/daily/SKILL.md +12 -0
  48. package/.cursor/skills/debug/SKILL.md +13 -0
  49. package/.cursor/skills/discovery/SKILL.md +13 -0
  50. package/.cursor/skills/dispatcher/SKILL.md +13 -0
  51. package/.cursor/skills/docs-orchestrator/SKILL.md +13 -0
  52. package/.cursor/skills/domain-model/SKILL.md +13 -0
  53. package/.cursor/skills/ecosystem-health/SKILL.md +13 -0
  54. package/.cursor/skills/eli5/SKILL.md +13 -0
  55. package/.cursor/skills/eval/SKILL.md +12 -0
  56. package/.cursor/skills/evolve/SKILL.md +13 -0
  57. package/.cursor/skills/frontmatter-guard/SKILL.md +13 -0
  58. package/.cursor/skills/gitlab-ops/SKILL.md +13 -0
  59. package/.cursor/skills/gitlab-portfolio/SKILL.md +13 -0
  60. package/.cursor/skills/grill/SKILL.md +13 -0
  61. package/.cursor/skills/hook-development/SKILL.md +13 -0
  62. package/.cursor/skills/journey-audit/SKILL.md +13 -0
  63. package/.cursor/skills/mcp-builder/SKILL.md +13 -0
  64. package/.cursor/skills/memory-cleanup/SKILL.md +12 -0
  65. package/.cursor/skills/mode-selector/SKILL.md +13 -0
  66. package/.cursor/skills/npm-publish/SKILL.md +12 -0
  67. package/.cursor/skills/peekaboo-driver/SKILL.md +13 -0
  68. package/.cursor/skills/persona-panel/SKILL.md +12 -0
  69. package/.cursor/skills/plan/SKILL.md +13 -0
  70. package/.cursor/skills/playwright-driver/SKILL.md +13 -0
  71. package/.cursor/skills/quality-gates/SKILL.md +13 -0
  72. package/.cursor/skills/reconcile/SKILL.md +12 -0
  73. package/.cursor/skills/remote-offload/SKILL.md +13 -0
  74. package/.cursor/skills/repo-audit/SKILL.md +13 -0
  75. package/.cursor/skills/session-end/SKILL.md +13 -0
  76. package/.cursor/skills/session-plan/SKILL.md +13 -0
  77. package/.cursor/skills/session-start/SKILL.md +13 -0
  78. package/.cursor/skills/skill-creator/SKILL.md +13 -0
  79. package/.cursor/skills/spinout/SKILL.md +12 -0
  80. package/.cursor/skills/sunset-review/SKILL.md +13 -0
  81. package/.cursor/skills/test-runner/SKILL.md +13 -0
  82. package/.cursor/skills/tmux-layout/SKILL.md +13 -0
  83. package/.cursor/skills/ubiquitous-language/SKILL.md +13 -0
  84. package/.cursor/skills/using-orchestrator/SKILL.md +13 -0
  85. package/.cursor/skills/vault-mirror/SKILL.md +13 -0
  86. package/.cursor/skills/vault-sync/SKILL.md +13 -0
  87. package/.cursor/skills/wave-executor/SKILL.md +13 -0
  88. package/.cursor/skills/write-executable-plan/SKILL.md +13 -0
  89. package/.mcp.json +4 -1
  90. package/CHANGELOG.md +446 -0
  91. package/README.md +22 -17
  92. package/agents/AGENTS.md +23 -4
  93. package/agents/code-implementer.md +2 -1
  94. package/agents/db-specialist.md +2 -2
  95. package/agents/docs-writer.md +3 -1
  96. package/agents/eval-judge.md +1 -1
  97. package/agents/session-reviewer.md +7 -1
  98. package/agents/test-writer.md +2 -1
  99. package/agents/ui-developer.md +2 -1
  100. package/commands/bootstrap.md +2 -2
  101. package/commands/close.md +3 -1
  102. package/commands/go.md +1 -1
  103. package/commands/journey-audit.md +43 -0
  104. package/docs/USER-GUIDE.md +2 -2
  105. package/docs/ci-setup.md +194 -25
  106. package/docs/codex-setup.md +64 -0
  107. package/docs/components.md +7 -7
  108. package/docs/cursor-setup.md +26 -47
  109. package/docs/events-schema.md +120 -10
  110. package/docs/github-mirror-protection.md +197 -0
  111. package/docs/pi-setup.md +2 -0
  112. package/docs/rule-authoring.md +3 -1
  113. package/docs/scope-collision-guard.md +49 -2
  114. package/docs/session-config-reference.md +89 -9
  115. package/docs/session-config-template.md +38 -7
  116. package/docs/telemetry/telemetry-claims.md +11 -10
  117. package/docs/telemetry.md +52 -1
  118. package/hooks/_lib/atomic-json.mjs +111 -0
  119. package/hooks/_lib/lock-bootstrap.mjs +8 -4
  120. package/hooks/_lib/subagent-paths.mjs +143 -0
  121. package/hooks/_lib/vcs-create-matcher.mjs +397 -38
  122. package/hooks/cwd-change-restore.mjs +9 -29
  123. package/hooks/enforce-scope.mjs +93 -0
  124. package/hooks/hooks-codex.json +1 -1
  125. package/hooks/hooks-cursor.json +201 -20
  126. package/hooks/hooks-pi.json +1 -1
  127. package/hooks/hooks.json +2 -2
  128. package/hooks/on-session-end.mjs +486 -19
  129. package/hooks/on-session-start.mjs +263 -12
  130. package/hooks/on-stop.mjs +392 -24
  131. package/hooks/post-bash-write-verify.mjs +104 -4
  132. package/hooks/post-subagent-discovery-validator.mjs +182 -21
  133. package/hooks/post-tool-batch-wave-signal.mjs +165 -42
  134. package/hooks/post-tool-failure-corrective-context.mjs +9 -32
  135. package/hooks/pre-bash-issue-budget.mjs +117 -4
  136. package/hooks/pre-bash-memory-propose-audit.mjs +13 -7
  137. package/hooks/pre-bash-sessions-ledger-guard.mjs +159 -0
  138. package/hooks/pre-bash-staging-fence.mjs +4 -0
  139. package/hooks/pre-task-scope-disjoint.mjs +368 -35
  140. package/hooks/skill-invocation-telemetry.mjs +21 -10
  141. package/hooks/subagent-telemetry.mjs +11 -26
  142. package/monitors/monitors.json +6 -0
  143. package/package.json +1 -1
  144. package/pi/prompts/journey-audit.md +12 -0
  145. package/rules/_index.md +9 -1
  146. package/rules/always-on/ask-via-tool.md +62 -0
  147. package/rules/always-on/bash-harness-pitfalls.md +168 -0
  148. package/rules/always-on/build-value.md +47 -0
  149. package/rules/always-on/cross-session-messaging.md +59 -0
  150. package/rules/always-on/loop-and-monitor.md +221 -0
  151. package/rules/always-on/parallel-sessions.md +142 -12
  152. package/rules/always-on/receiving-review.md +108 -0
  153. package/rules/always-on/test-value.md +40 -0
  154. package/rules/always-on/verification-before-completion.md +77 -0
  155. package/scripts/archive-closed-prds.mjs +258 -18
  156. package/scripts/autopilot.mjs +31 -12
  157. package/scripts/backfill-abandoned-sessions.mjs +80 -11
  158. package/scripts/backfill-evidence-digest.mjs +376 -0
  159. package/scripts/cursor-install.mjs +89 -48
  160. package/scripts/emit-event.mjs +10 -2
  161. package/scripts/export-hw-learnings.mjs +143 -2
  162. package/scripts/express-path.mjs +299 -0
  163. package/scripts/generate-cursor-adapter.mjs +253 -0
  164. package/scripts/github-protection-audit.mjs +358 -0
  165. package/scripts/lib/auq/parse.mjs +5 -29
  166. package/scripts/lib/auto-dialectic.mjs +68 -0
  167. package/scripts/lib/autopilot/worktree-pipeline.mjs +318 -18
  168. package/scripts/lib/build-live-signals.mjs +49 -27
  169. package/scripts/lib/ci-status-banner.mjs +158 -11
  170. package/scripts/lib/cold-start-detector.mjs +23 -14
  171. package/scripts/lib/command-blocker.mjs +70 -0
  172. package/scripts/lib/config/block-header.mjs +55 -0
  173. package/scripts/lib/config/discovery-validator.mjs +7 -2
  174. package/scripts/lib/config/health-endpoints.mjs +383 -0
  175. package/scripts/lib/config/reconcile.mjs +79 -4
  176. package/scripts/lib/config/remote-hosts.mjs +233 -0
  177. package/scripts/lib/config/section-extractor.mjs +235 -36
  178. package/scripts/lib/config-schema.mjs +9 -1
  179. package/scripts/lib/config.mjs +87 -8
  180. package/scripts/lib/convergence-monitor.mjs +13 -2
  181. package/scripts/lib/cursor-hook-bridge.mjs +443 -0
  182. package/scripts/lib/dispatcher/cli.mjs +2 -2
  183. package/scripts/lib/dispatcher/enumerate.mjs +2 -17
  184. package/scripts/lib/events-schema.mjs +48 -0
  185. package/scripts/lib/events.mjs +238 -5
  186. package/scripts/lib/evolve/autonomy-verdict.mjs +9 -4
  187. package/scripts/lib/evolve/autopilot-effectiveness.mjs +18 -1
  188. package/scripts/lib/express-path.mjs +327 -0
  189. package/scripts/lib/file-lock.mjs +22 -4
  190. package/scripts/lib/gates/gate-full.mjs +81 -8
  191. package/scripts/lib/gates/gate-helpers.mjs +76 -15
  192. package/scripts/lib/git-config-drift.mjs +134 -5
  193. package/scripts/lib/gitlab-portfolio/cli.mjs +3 -15
  194. package/scripts/lib/harness-audit/categories/category1.mjs +17 -6
  195. package/scripts/lib/host-identity.mjs +247 -2
  196. package/scripts/lib/instruction-budget-guard.mjs +31 -1
  197. package/scripts/lib/issue-budget.mjs +229 -30
  198. package/scripts/lib/learnings/io.mjs +55 -10
  199. package/scripts/lib/learnings/schema.mjs +95 -28
  200. package/scripts/lib/lock-reaper.mjs +7 -1
  201. package/scripts/lib/locks/staging-fence-lock.mjs +5 -1
  202. package/scripts/lib/locks/state-md-lock.mjs +8 -1
  203. package/scripts/lib/memory-banner.mjs +25 -10
  204. package/scripts/lib/memory-paths.mjs +15 -6
  205. package/scripts/lib/mode-selector/scoring.mjs +53 -6
  206. package/scripts/lib/peer-discovery.mjs +20 -2
  207. package/scripts/lib/platform.mjs +72 -9
  208. package/scripts/lib/plugin-root.mjs +143 -19
  209. package/scripts/lib/project-hygiene.mjs +43 -3
  210. package/scripts/lib/quality-gate.mjs +271 -13
  211. package/scripts/lib/reconcile/emitter.mjs +87 -19
  212. package/scripts/lib/reconcile/engine.mjs +517 -18
  213. package/scripts/lib/reconcile/idempotency.mjs +102 -1
  214. package/scripts/lib/reconcile/renderer.mjs +148 -3
  215. package/scripts/lib/reconcile/sanitize.mjs +40 -17
  216. package/scripts/lib/reconcile/writer.mjs +415 -84
  217. package/scripts/lib/rule-loader.mjs +37 -2
  218. package/scripts/lib/rules-sync.mjs +51 -8
  219. package/scripts/lib/scope-gate.mjs +126 -0
  220. package/scripts/lib/session-close-backfill.mjs +427 -37
  221. package/scripts/lib/session-discovery.mjs +69 -5
  222. package/scripts/lib/session-end/phase-skip.mjs +38 -5
  223. package/scripts/lib/session-end/worktree-cleanup.mjs +154 -7
  224. package/scripts/lib/session-id.mjs +30 -14
  225. package/scripts/lib/session-identity/own-session.mjs +220 -0
  226. package/scripts/lib/session-lock.mjs +85 -30
  227. package/scripts/lib/session-schema/normalizer.mjs +70 -3
  228. package/scripts/lib/session-schema/validator.mjs +40 -0
  229. package/scripts/lib/session-start-probes.mjs +608 -0
  230. package/scripts/lib/session-transition.mjs +277 -0
  231. package/scripts/lib/sessions-canonical.mjs +446 -0
  232. package/scripts/lib/sessions-staleness-banner.mjs +124 -57
  233. package/scripts/lib/spiral-carryover.mjs +90 -9
  234. package/scripts/lib/state-md/frontmatter-mutators.mjs +41 -8
  235. package/scripts/lib/state-md/mission-status.mjs +350 -52
  236. package/scripts/lib/state-md/yaml-parser.mjs +145 -16
  237. package/scripts/lib/state-md.mjs +12 -2
  238. package/scripts/lib/telemetry/schema.mjs +74 -8
  239. package/scripts/lib/telemetry/sync.mjs +91 -16
  240. package/scripts/lib/tmux-layout/telemetry.mjs +14 -2
  241. package/scripts/lib/validate/check-agents.mjs +66 -0
  242. package/scripts/lib/validate/check-cursor-adapter.mjs +102 -0
  243. package/scripts/lib/validate/check-dead-bridge.mjs +24 -2
  244. package/scripts/lib/validate/check-doc-cli-commands.mjs +25 -65
  245. package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +370 -0
  246. package/scripts/lib/validate/check-hooks-symmetry.mjs +29 -63
  247. package/scripts/lib/validate/check-playwright-mcp-canary.mjs +13 -22
  248. package/scripts/lib/validate/check-plugin-monitors.mjs +10 -4
  249. package/scripts/lib/validate/check-skill-script-paths.mjs +436 -0
  250. package/scripts/lib/validate/check-test-value-bans.mjs +165 -17
  251. package/scripts/lib/validate/check-untracked-test-deps.mjs +10 -0
  252. package/scripts/lib/validate/check-unwired-features.mjs +333 -32
  253. package/scripts/lib/validate/check-validator-registration.mjs +248 -0
  254. package/scripts/lib/validate/check-vcs-repo-flag.mjs +6 -28
  255. package/scripts/lib/validate/markdown-fences.mjs +196 -0
  256. package/scripts/lib/validate/repo-files.mjs +275 -0
  257. package/scripts/lib/validate-vendored-rules.mjs +229 -7
  258. package/scripts/lib/vault-mirror/process.mjs +99 -43
  259. package/scripts/lib/vault-mirror/telemetry.mjs +210 -0
  260. package/scripts/lib/vault-staleness-banner.mjs +76 -6
  261. package/scripts/lib/vault-status/board-lock.mjs +185 -0
  262. package/scripts/lib/vault-status/board-writer.mjs +381 -141
  263. package/scripts/lib/vault-status/narrative-mirror.mjs +190 -27
  264. package/scripts/lib/wave-executor/foreign-dispatch.mjs +832 -0
  265. package/scripts/lib/wave-executor/remote-dispatch.mjs +504 -0
  266. package/scripts/lib/wave-resource-gate.mjs +127 -7
  267. package/scripts/lib/wave-transcript-tail.mjs +889 -0
  268. package/scripts/materialize-wave-scope.mjs +228 -15
  269. package/scripts/mcp-server.sh +11 -2
  270. package/scripts/memory-propose.mjs +132 -8
  271. package/scripts/parse-config.mjs +65 -0
  272. package/scripts/promote-vault-strict.mjs +4 -15
  273. package/scripts/site-numbers.mjs +36 -4
  274. package/scripts/token-audit.sh +9 -2
  275. package/scripts/validate-plugin.mjs +29 -0
  276. package/scripts/validate-wave-scope.mjs +67 -0
  277. package/scripts/vault-consolidate.mjs +3 -11
  278. package/scripts/vault-integration-watcher.mjs +2 -4
  279. package/scripts/vault-mirror.mjs +305 -51
  280. package/skills/_shared/monitor-patterns.md +31 -5
  281. package/skills/_shared/parallel-aware-auq.md +31 -2
  282. package/skills/_shared/parallel-aware-preamble.md +19 -4
  283. package/skills/_shared/platform-tools.md +11 -5
  284. package/skills/_shared/state-ownership.md +29 -2
  285. package/skills/autopilot/SKILL.md +5 -1
  286. package/skills/bootstrap/SKILL.md +3 -3
  287. package/skills/bootstrap/_shared-template.md +18 -10
  288. package/skills/bootstrap/deep-template.md +10 -6
  289. package/skills/bootstrap/fast-template.md +15 -8
  290. package/skills/bootstrap/standard-template.md +10 -6
  291. package/skills/claude-md-drift-check/checker.mjs +39 -11
  292. package/skills/contract-version-bump/SKILL.md +1 -1
  293. package/skills/dispatcher/SKILL.md +1 -1
  294. package/skills/ecosystem-health/SKILL.md +4 -1
  295. package/skills/ecosystem-health/wizard.md +5 -0
  296. package/skills/evolve/SKILL.md +38 -1
  297. package/skills/journey-audit/SKILL.md +270 -0
  298. package/skills/peekaboo-driver/SKILL.md +15 -3
  299. package/skills/persona-panel/SKILL.md +1 -1
  300. package/skills/reconcile/SKILL.md +46 -3
  301. package/skills/remote-offload/SKILL.md +89 -0
  302. package/skills/session-end/SKILL.md +17 -4
  303. package/skills/session-end/metrics-collection.md +7 -4
  304. package/skills/session-end/phase-3-6-tail.md +20 -9
  305. package/skills/session-end/phase-3-7a-recommendations.md +16 -2
  306. package/skills/session-plan/SKILL.md +6 -1
  307. package/skills/session-plan/wave-template.md +1 -0
  308. package/skills/session-start/SKILL.md +54 -17
  309. package/skills/session-start/phase-7-5-mode-selector.md +15 -3
  310. package/skills/session-start/phase-8-5-express-path.md +77 -12
  311. package/skills/vault-sync/validator.mjs +31 -0
  312. package/skills/wave-executor/SKILL.md +5 -3
  313. package/skills/wave-executor/circuit-breaker.md +34 -9
  314. package/skills/wave-executor/wave-loop.md +143 -22
  315. package/templates/_shared/journey-manifest.md +110 -0
  316. package/templates/_shared/rules/parallel-sessions.md +0 -77
@@ -0,0 +1,376 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * backfill-evidence-digest.mjs — seal existing generated rules (#1101).
4
+ *
5
+ * `.orchestrator/metrics/learnings.jsonl` is GITIGNORED (`.gitignore:40`). Every
6
+ * `.claude/rules/*.md` written by the reconciliation engine before #1101 points
7
+ * at that file through its `learning-key` and carries nothing else, so in a
8
+ * fresh clone the pointer resolves to nothing and the drift-checker's Check 8
9
+ * warns on ALL of them (measured 2026-08-26: 23 of 23; in this working copy 3,
10
+ * whose keys no longer resolve even here).
11
+ *
12
+ * `renderRule` now emits an `evidence-digest` for every NEW rule. This script is
13
+ * the one-off that gives the EXISTING corpus the same seal, computed with the
14
+ * very same {@link computeEvidenceDigest} the renderer uses — one recipe, never
15
+ * two implementations that must be kept in agreement.
16
+ *
17
+ * ── What is reconstructed, and the one honest limit ──────────────────────────
18
+ * Five of the six digest components live in the rule file itself (`learning-key`
19
+ * and `confidence` in frontmatter, `learning-id` and `source-session` in the
20
+ * `## Provenance` block, and the `## Evidence` block verbatim). The sixth,
21
+ * `evidence-recorded-at`, is the learning's `created_at` and exists ONLY in
22
+ * learnings.jsonl. For a key that still resolves there it is read across; for a
23
+ * key that does not, it is emitted as the EMPTY STRING. It is never invented:
24
+ * a fabricated timestamp would make the digest a reference masquerading as
25
+ * evidence, which is the exact failure #1101 exists to close.
26
+ *
27
+ * ── Why the digest input is the `## Evidence` block ONLY ─────────────────────
28
+ * Not "everything between the H1 and `## Provenance`". Several rules carry a
29
+ * hand-written `## Extension Review (…)` section appended after the evidence,
30
+ * and a legitimate future hand-annotation must not break the seal. The seal
31
+ * covers the machine-authored evidence and its provenance header — nothing else.
32
+ *
33
+ * Usage:
34
+ * node scripts/backfill-evidence-digest.mjs # dry-run (default)
35
+ * node scripts/backfill-evidence-digest.mjs --apply # write
36
+ * node scripts/backfill-evidence-digest.mjs --verify # re-check every seal
37
+ * node scripts/backfill-evidence-digest.mjs --verify <file.md>...
38
+ *
39
+ * `--verify` reads ONLY the `.md` files handed to it. It never opens
40
+ * learnings.jsonl — that is the whole point, and it is what makes the digest
41
+ * verifiable in a fresh clone.
42
+ *
43
+ * Idempotent: a rule that already carries an `evidence-digest:` frontmatter key
44
+ * is left byte-identical and reported as `skipped (already sealed)`.
45
+ *
46
+ * @module scripts/backfill-evidence-digest
47
+ */
48
+
49
+ import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
50
+ import { join, relative, resolve } from 'node:path';
51
+
52
+ import { computeEvidenceDigest } from './lib/reconcile/renderer.mjs';
53
+
54
+ /** ISO-8601 instant — mirrors the renderer's `EVIDENCE_RECORDED_AT_RE`. */
55
+ const EVIDENCE_RECORDED_AT_RE = /^\d{4}-\d{2}-\d{2}T[0-9:.]+(?:Z|[+-]\d{2}:\d{2})$/;
56
+
57
+ /** Shape of the sealed value — mirrors the renderer's `EVIDENCE_DIGEST_RE`. */
58
+ const EVIDENCE_DIGEST_RE = /^sha256-v1:[0-9a-f]{64}$/;
59
+
60
+ /**
61
+ * Split a rule document into its frontmatter block and its body lines.
62
+ *
63
+ * @param {string} content
64
+ * @returns {{ fmLines: string[], bodyLines: string[] }|null} null when the file
65
+ * has no parseable `--- … ---` frontmatter block.
66
+ */
67
+ function splitDocument(content) {
68
+ const lines = content.split('\n');
69
+ if (lines[0] !== '---') return null;
70
+ const close = lines.indexOf('---', 1);
71
+ if (close === -1) return null;
72
+ return { fmLines: lines.slice(0, close + 1), bodyLines: lines.slice(close + 1) };
73
+ }
74
+
75
+ /**
76
+ * Read a single-line frontmatter scalar's RAW text (no coercion).
77
+ *
78
+ * Raw on purpose: the digest hashes `String(metadata.confidence)`, and the
79
+ * renderer wrote that same string into the frontmatter via template literal —
80
+ * so the bytes on disk ARE the canonical component. Parsing to a Number and
81
+ * re-stringifying would be a second chance to disagree.
82
+ *
83
+ * @param {string[]} fmLines
84
+ * @param {string} key
85
+ * @returns {string|null}
86
+ */
87
+ function fmScalar(fmLines, key) {
88
+ const re = new RegExp(`^${key}:\\s*(.*)$`);
89
+ for (const line of fmLines) {
90
+ const m = re.exec(line);
91
+ if (m) return m[1].trim();
92
+ }
93
+ return null;
94
+ }
95
+
96
+ /**
97
+ * Read a `- <key>: \`<value>\`` line out of the `## Provenance` block.
98
+ *
99
+ * @param {string[]} bodyLines
100
+ * @param {string} key
101
+ * @returns {string|null}
102
+ */
103
+ function provenanceToken(bodyLines, key) {
104
+ const re = new RegExp(`^- ${key}: \`(.*)\`\\s*$`);
105
+ for (const line of bodyLines) {
106
+ const m = re.exec(line);
107
+ if (m) return m[1];
108
+ }
109
+ return null;
110
+ }
111
+
112
+ /**
113
+ * Extract the `## Evidence` block verbatim — the bytes the renderer wrote.
114
+ *
115
+ * Bounded by the next `## ` heading or the next HTML comment
116
+ * (`<!-- untrusted-content:end -->` / the provenance marker), whichever comes
117
+ * first, with trailing blank lines trimmed. Both shapes occur on disk: only 10
118
+ * of the 23 generated rules carry the untrusted envelope; the older ones run
119
+ * straight from `## Evidence` into the next heading.
120
+ *
121
+ * @param {string[]} bodyLines
122
+ * @returns {string|null} null when there is no `## Evidence` heading.
123
+ */
124
+ export function extractEvidenceBlock(bodyLines) {
125
+ const start = bodyLines.findIndex((l) => l.trim() === '## Evidence');
126
+ if (start === -1) return null;
127
+ const out = [];
128
+ for (let i = start + 1; i < bodyLines.length; i++) {
129
+ const line = bodyLines[i];
130
+ if (line.startsWith('## ') || line.startsWith('<!-- ')) break;
131
+ out.push(line);
132
+ }
133
+ while (out.length > 0 && out[out.length - 1].trim() === '') out.pop();
134
+ return out.join('\n');
135
+ }
136
+
137
+ /**
138
+ * Recompute a rule document's digest from ITS OWN BYTES.
139
+ *
140
+ * Opens no other file. This is the offline verification path: five header
141
+ * fields out of `## Provenance` + frontmatter, plus the `## Evidence` block.
142
+ *
143
+ * @param {string} content - the full `.md` document.
144
+ * @returns {{ ok: boolean, reason?: string, stored?: string, computed?: string }}
145
+ */
146
+ export function verifyDocument(content) {
147
+ const split = splitDocument(content);
148
+ if (!split) return { ok: false, reason: 'no parseable frontmatter block' };
149
+ const { fmLines, bodyLines } = split;
150
+
151
+ const stored = fmScalar(fmLines, 'evidence-digest');
152
+ if (!stored) return { ok: false, reason: 'no evidence-digest frontmatter key' };
153
+ if (!EVIDENCE_DIGEST_RE.test(stored)) {
154
+ return { ok: false, reason: `evidence-digest is malformed: ${stored}` };
155
+ }
156
+
157
+ const evidenceBlock = extractEvidenceBlock(bodyLines);
158
+ if (evidenceBlock === null) return { ok: false, reason: 'no ## Evidence block' };
159
+
160
+ const recordedLine = bodyLines.find((l) => l.startsWith('- evidence-recorded-at:'));
161
+ const evidenceRecordedAt = recordedLine
162
+ ? recordedLine.slice('- evidence-recorded-at:'.length).trim()
163
+ : '';
164
+
165
+ const computed = computeEvidenceDigest({
166
+ learningKey: fmScalar(fmLines, 'learning-key') ?? '',
167
+ learningId: provenanceToken(bodyLines, 'learning-id') ?? '',
168
+ sourceSession: provenanceToken(bodyLines, 'source-session') ?? '',
169
+ evidenceRecordedAt,
170
+ confidence: fmScalar(fmLines, 'confidence') ?? '',
171
+ evidenceBlock,
172
+ });
173
+
174
+ return computed === stored
175
+ ? { ok: true, stored, computed }
176
+ : { ok: false, reason: 'digest mismatch — the sealed content changed', stored, computed };
177
+ }
178
+
179
+ /**
180
+ * kebab() as the emitter derives learning keys — mirrors checker.mjs Check 8.
181
+ *
182
+ * @param {string} s
183
+ * @returns {string}
184
+ */
185
+ const kebab = (s) =>
186
+ String(s)
187
+ .toLowerCase()
188
+ .replace(/[^a-z0-9]+/g, '-')
189
+ .replace(/^-+|-+$/g, '');
190
+
191
+ /**
192
+ * Build `learning-key → created_at` from the (gitignored, possibly absent)
193
+ * learnings store.
194
+ *
195
+ * @param {string} repoRoot
196
+ * @returns {Map<string, string>}
197
+ */
198
+ function loadCreatedAtByKey(repoRoot) {
199
+ const map = new Map();
200
+ const p = join(repoRoot, '.orchestrator', 'metrics', 'learnings.jsonl');
201
+ if (!existsSync(p)) return map;
202
+ for (const line of readFileSync(p, 'utf8').split('\n')) {
203
+ const trimmed = line.trim();
204
+ if (!trimmed) continue;
205
+ let entry;
206
+ try {
207
+ entry = JSON.parse(trimmed);
208
+ } catch {
209
+ continue;
210
+ }
211
+ if (!entry || typeof entry !== 'object') continue;
212
+ const type = typeof entry.type === 'string' ? entry.type : '';
213
+ const subjectOrTitle =
214
+ (typeof entry.title === 'string' && entry.title !== '' ? entry.title : '') ||
215
+ (typeof entry.subject === 'string' && entry.subject !== '' ? entry.subject : '');
216
+ if (!type || !subjectOrTitle) continue;
217
+ const createdAt = typeof entry.created_at === 'string' ? entry.created_at : '';
218
+ map.set(`${type}/${kebab(subjectOrTitle)}`, createdAt);
219
+ }
220
+ return map;
221
+ }
222
+
223
+ /**
224
+ * Compute the sealed form of one rule document.
225
+ *
226
+ * @param {string} content
227
+ * @param {Map<string, string>} createdAtByKey
228
+ * @returns {{ status: 'sealed'|'skipped'|'unsealable', content?: string,
229
+ * digest?: string, recordedAt?: string, reason?: string }}
230
+ */
231
+ export function sealDocument(content, createdAtByKey) {
232
+ const split = splitDocument(content);
233
+ if (!split) return { status: 'unsealable', reason: 'no parseable frontmatter block' };
234
+ const { fmLines, bodyLines } = split;
235
+
236
+ if (fmScalar(fmLines, 'evidence-digest') !== null) {
237
+ return { status: 'skipped', reason: 'already sealed' };
238
+ }
239
+
240
+ const learningKey = fmScalar(fmLines, 'learning-key');
241
+ const confidence = fmScalar(fmLines, 'confidence');
242
+ const expiresAtIdx = fmLines.findIndex((l) => l.startsWith('expires-at:'));
243
+ const sourceSessionIdx = bodyLines.findIndex((l) => l.startsWith('- source-session:'));
244
+ const evidenceBlock = extractEvidenceBlock(bodyLines);
245
+
246
+ if (!learningKey) return { status: 'unsealable', reason: 'no learning-key' };
247
+ if (confidence === null) return { status: 'unsealable', reason: 'no confidence' };
248
+ if (expiresAtIdx === -1) return { status: 'unsealable', reason: 'no expires-at line' };
249
+ if (sourceSessionIdx === -1) return { status: 'unsealable', reason: 'no - source-session: line' };
250
+ if (evidenceBlock === null) return { status: 'unsealable', reason: 'no ## Evidence block' };
251
+
252
+ const rawCreatedAt = createdAtByKey.get(learningKey) ?? '';
253
+ // Never invented. A key that no longer resolves, or a `created_at` that is not
254
+ // an ISO instant, contributes the empty string — see the module doc.
255
+ const evidenceRecordedAt = EVIDENCE_RECORDED_AT_RE.test(rawCreatedAt) ? rawCreatedAt : '';
256
+
257
+ const learningId = provenanceToken(bodyLines, 'learning-id') ?? '';
258
+ const sourceSession = provenanceToken(bodyLines, 'source-session') ?? '';
259
+
260
+ const digest = computeEvidenceDigest({
261
+ learningKey,
262
+ learningId,
263
+ sourceSession,
264
+ evidenceRecordedAt,
265
+ confidence,
266
+ evidenceBlock,
267
+ });
268
+
269
+ // Frontmatter: AFTER `expires-at`, so every key before it keeps its byte
270
+ // position (the renderer appends in the same place, for the same reason).
271
+ const nextFm = [...fmLines];
272
+ nextFm.splice(expiresAtIdx + 1, 0, `evidence-digest: ${digest}`);
273
+
274
+ // Body: the same three lines the renderer emits, right after `- source-session:`.
275
+ const nextBody = [...bodyLines];
276
+ nextBody.splice(
277
+ sourceSessionIdx + 1,
278
+ 0,
279
+ `- evidence-digest: \`${digest}\``,
280
+ `- evidence-recorded-at: ${evidenceRecordedAt}`,
281
+ '- evidence-digest-input: learning-key \\n learning-id \\n source-session \\n evidence-recorded-at \\n confidence \\n <the `## Evidence` block above, verbatim UTF-8, LF-joined, no trailing newline>',
282
+ );
283
+
284
+ return {
285
+ status: 'sealed',
286
+ content: [...nextFm, ...nextBody].join('\n'),
287
+ digest,
288
+ recordedAt: evidenceRecordedAt,
289
+ };
290
+ }
291
+
292
+ /**
293
+ * List every `.claude/rules/*.md` carrying `auto-generated: true`.
294
+ *
295
+ * @param {string} repoRoot
296
+ * @returns {string[]} absolute paths
297
+ */
298
+ function generatedRuleFiles(repoRoot) {
299
+ const dir = join(repoRoot, '.claude', 'rules');
300
+ if (!existsSync(dir)) return [];
301
+ return readdirSync(dir)
302
+ .filter((f) => f.endsWith('.md') && !f.startsWith('.'))
303
+ .map((f) => join(dir, f))
304
+ .filter((p) => /^auto-generated:\s*true\s*$/m.test(readFileSync(p, 'utf8')));
305
+ }
306
+
307
+ function main(argv) {
308
+ const apply = argv.includes('--apply');
309
+ const verify = argv.includes('--verify');
310
+ const repoRoot = resolve(process.cwd());
311
+ const explicit = argv.filter((a) => !a.startsWith('--'));
312
+
313
+ if (verify) {
314
+ const files = explicit.length > 0 ? explicit.map((f) => resolve(f)) : generatedRuleFiles(repoRoot);
315
+ let ok = 0;
316
+ let bad = 0;
317
+ for (const abs of files) {
318
+ const r = verifyDocument(readFileSync(abs, 'utf8'));
319
+ const rel = relative(repoRoot, abs);
320
+ if (r.ok) {
321
+ ok++;
322
+ console.log(` VERIFIED ${rel}`);
323
+ } else {
324
+ bad++;
325
+ console.log(` FAILED ${rel} — ${r.reason}`);
326
+ if (r.stored && r.computed) {
327
+ console.log(` stored: ${r.stored}`);
328
+ console.log(` computed: ${r.computed}`);
329
+ }
330
+ }
331
+ }
332
+ console.log(`\nverified ${ok}, failed ${bad}, of ${files.length} file(s) — no learnings.jsonl consulted`);
333
+ process.exit(bad === 0 ? 0 : 1);
334
+ }
335
+
336
+ const createdAtByKey = loadCreatedAtByKey(repoRoot);
337
+ const files = generatedRuleFiles(repoRoot);
338
+ let sealed = 0;
339
+ let skipped = 0;
340
+ let unsealable = 0;
341
+ const noDate = [];
342
+
343
+ for (const abs of files) {
344
+ const rel = relative(repoRoot, abs);
345
+ const before = readFileSync(abs, 'utf8');
346
+ const r = sealDocument(before, createdAtByKey);
347
+ if (r.status === 'skipped') {
348
+ skipped++;
349
+ continue;
350
+ }
351
+ if (r.status === 'unsealable') {
352
+ unsealable++;
353
+ console.log(` UNSEALABLE ${rel} — ${r.reason}`);
354
+ continue;
355
+ }
356
+ sealed++;
357
+ if (r.recordedAt === '') noDate.push(rel);
358
+ if (apply) writeFileSync(abs, r.content, 'utf8');
359
+ console.log(
360
+ ` ${apply ? 'SEALED ' : 'WOULD-SEAL'} ${rel}${r.recordedAt === '' ? ' [evidence-recorded-at: EMPTY — created_at unavailable]' : ''}`,
361
+ );
362
+ }
363
+
364
+ console.log(
365
+ `\n${apply ? 'sealed' : 'would seal'} ${sealed}, skipped ${skipped} (already sealed), unsealable ${unsealable}, of ${files.length} auto-generated rule(s)`,
366
+ );
367
+ if (noDate.length > 0) {
368
+ console.log(`\n${noDate.length} rule(s) sealed WITHOUT an evidence-recorded-at (learning-key no longer resolves;`);
369
+ console.log('the date is emitted as the empty digest component and is NEVER invented):');
370
+ for (const rel of noDate) console.log(` - ${rel}`);
371
+ }
372
+ if (!apply) console.log('\n(dry-run — pass --apply to write)');
373
+ process.exit(unsealable === 0 ? 0 : 1);
374
+ }
375
+
376
+ if (import.meta.url === `file://${process.argv[1]}`) main(process.argv.slice(2));
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * cursor-install.mjs — Install Session Orchestrator Cursor rules into a project.
3
+ * cursor-install.mjs — Install Session Orchestrator Cursor surface into a project.
4
4
  *
5
- * Behaviour-parity port of cursor-install.sh (issue #218).
5
+ * Links rules, slash-command wrappers, skill wrappers, and writes `.cursor/hooks.json`
6
+ * so `/session` is a native Cursor command and hooks fire through the payload bridge.
6
7
  *
7
8
  * Usage:
8
9
  * node cursor-install.mjs [TARGET]
@@ -11,27 +12,26 @@
11
12
  *
12
13
  * Exit codes:
13
14
  * 0 — success
14
- * 1 — source rules not found
15
+ * 1 — source rules not found, or TARGET is not an existing directory
15
16
  */
16
17
 
17
- import { existsSync, mkdirSync, readdirSync, symlinkSync, statSync, lstatSync } from 'node:fs';
18
+ import { existsSync, mkdirSync, readdirSync, symlinkSync, statSync, lstatSync, writeFileSync } from 'node:fs';
18
19
  import path from 'node:path';
19
20
  import { fileURLToPath } from 'node:url';
20
21
 
21
- // ---------------------------------------------------------------------------
22
- // Resolve SO_ROOT (parent of scripts/)
23
- // ---------------------------------------------------------------------------
22
+ import { CURSOR_TO_CANONICAL_EVENT } from './lib/cursor-hook-bridge.mjs';
24
23
 
25
24
  const __filename = fileURLToPath(import.meta.url);
26
25
  const SCRIPT_DIR = path.dirname(__filename);
27
26
  const SO_ROOT = path.dirname(SCRIPT_DIR);
28
27
 
29
- // ---------------------------------------------------------------------------
30
- // Parse arguments — TARGET defaults to cwd (mirrors: TARGET="${1:-$(pwd)}")
31
- // ---------------------------------------------------------------------------
32
-
33
28
  const TARGET = process.argv[2] ?? process.cwd();
34
29
 
30
+ if (!existsSync(TARGET) || !statSync(TARGET).isDirectory()) {
31
+ process.stderr.write(`ERROR: Target directory does not exist: ${TARGET}\n`);
32
+ process.exit(1);
33
+ }
34
+
35
35
  // ---------------------------------------------------------------------------
36
36
  // Print banner
37
37
  // ---------------------------------------------------------------------------
@@ -43,10 +43,6 @@ process.stdout.write(`Source: ${SO_ROOT}/.cursor/rules/\n`);
43
43
  process.stdout.write(`Target: ${TARGET}/.cursor/rules/\n`);
44
44
  process.stdout.write('\n');
45
45
 
46
- // ---------------------------------------------------------------------------
47
- // Validate source
48
- // ---------------------------------------------------------------------------
49
-
50
46
  const SOURCE_RULES_DIR = path.join(SO_ROOT, '.cursor', 'rules');
51
47
 
52
48
  if (!existsSync(SOURCE_RULES_DIR) || !statSync(SOURCE_RULES_DIR).isDirectory()) {
@@ -54,18 +50,6 @@ if (!existsSync(SOURCE_RULES_DIR) || !statSync(SOURCE_RULES_DIR).isDirectory())
54
50
  process.exit(1);
55
51
  }
56
52
 
57
- // ---------------------------------------------------------------------------
58
- // Create target directory
59
- // mkdir -p "$TARGET/.cursor/rules"
60
- // ---------------------------------------------------------------------------
61
-
62
- const TARGET_RULES_DIR = path.join(TARGET, '.cursor', 'rules');
63
- mkdirSync(TARGET_RULES_DIR, { recursive: true });
64
-
65
- // ---------------------------------------------------------------------------
66
- // Symlink each .mdc file (mirrors the for-loop in the .sh script)
67
- // ---------------------------------------------------------------------------
68
-
69
53
  function _isSymlink(p) {
70
54
  try { return lstatSync(p).isSymbolicLink(); } catch { return false; }
71
55
  }
@@ -74,40 +58,97 @@ function _isFile(p) {
74
58
  try { return statSync(p).isFile(); } catch { return false; }
75
59
  }
76
60
 
77
- let count = 0;
61
+ function _isDir(p) {
62
+ try { return statSync(p).isDirectory(); } catch { return false; }
63
+ }
64
+
65
+ function shQuote(value) {
66
+ return `'${String(value).replace(/'/g, `'\\''`)}'`;
67
+ }
68
+
69
+ function linkPath(source, dest, label) {
70
+ mkdirSync(path.dirname(dest), { recursive: true });
71
+ if (_isSymlink(dest)) {
72
+ process.stdout.write(` SKIP: ${label} (symlink exists)\n`);
73
+ return 'skip';
74
+ }
75
+ if (existsSync(dest)) {
76
+ process.stdout.write(` SKIP: ${label} (file exists — not overwriting)\n`);
77
+ return 'skip';
78
+ }
79
+ symlinkSync(source, dest);
80
+ process.stdout.write(` LINK: ${label}\n`);
81
+ return 'link';
82
+ }
83
+
84
+ let ruleLinks = 0;
85
+
86
+ const TARGET_RULES_DIR = path.join(TARGET, '.cursor', 'rules');
87
+ mkdirSync(TARGET_RULES_DIR, { recursive: true });
78
88
 
79
89
  const entries = readdirSync(SOURCE_RULES_DIR);
80
90
  for (const filename of entries) {
81
91
  if (!filename.endsWith('.mdc')) continue;
82
-
83
92
  const mdcFile = path.join(SOURCE_RULES_DIR, filename);
84
-
85
- // Only process regular files (mirrors: [[ ! -f "$mdc_file" ]] && continue)
86
93
  if (!_isFile(mdcFile)) continue;
94
+ const result = linkPath(mdcFile, path.join(TARGET_RULES_DIR, filename), filename);
95
+ if (result === 'link') ruleLinks += 1;
96
+ }
87
97
 
88
- const targetPath = path.join(TARGET_RULES_DIR, filename);
98
+ const SOURCE_COMMANDS_DIR = path.join(SO_ROOT, '.cursor', 'commands');
99
+ const TARGET_COMMANDS_DIR = path.join(TARGET, '.cursor', 'commands');
100
+ let commandLinks = 0;
101
+ if (_isDir(SOURCE_COMMANDS_DIR)) {
102
+ mkdirSync(TARGET_COMMANDS_DIR, { recursive: true });
103
+ for (const filename of readdirSync(SOURCE_COMMANDS_DIR)) {
104
+ if (!filename.endsWith('.md')) continue;
105
+ const source = path.join(SOURCE_COMMANDS_DIR, filename);
106
+ if (!_isFile(source)) continue;
107
+ const result = linkPath(source, path.join(TARGET_COMMANDS_DIR, filename), `commands/${filename}`);
108
+ if (result === 'link') commandLinks += 1;
109
+ }
110
+ }
89
111
 
90
- if (_isSymlink(targetPath)) {
91
- process.stdout.write(` SKIP: ${filename} (symlink exists)\n`);
92
- } else if (existsSync(targetPath)) {
93
- process.stdout.write(` SKIP: ${filename} (file exists — not overwriting)\n`);
94
- } else {
95
- symlinkSync(mdcFile, targetPath);
96
- process.stdout.write(` LINK: ${filename}\n`);
97
- count++;
112
+ const SOURCE_SKILLS_DIR = path.join(SO_ROOT, '.cursor', 'skills');
113
+ const TARGET_SKILLS_DIR = path.join(TARGET, '.cursor', 'skills');
114
+ let skillLinks = 0;
115
+ if (_isDir(SOURCE_SKILLS_DIR)) {
116
+ mkdirSync(TARGET_SKILLS_DIR, { recursive: true });
117
+ for (const name of readdirSync(SOURCE_SKILLS_DIR)) {
118
+ const source = path.join(SOURCE_SKILLS_DIR, name);
119
+ if (!_isDir(source)) continue;
120
+ const result = linkPath(source, path.join(TARGET_SKILLS_DIR, name), `skills/${name}`);
121
+ if (result === 'link') skillLinks += 1;
98
122
  }
99
123
  }
100
124
 
101
- // ---------------------------------------------------------------------------
102
- // Done mirrors the .sh final output exactly
103
- // ---------------------------------------------------------------------------
125
+ function renderHooksJson(soRoot) {
126
+ const runNode = shQuote(path.join(soRoot, 'hooks', 'run-node.sh'));
127
+ const bridge = shQuote(path.join(soRoot, 'scripts', 'lib', 'cursor-hook-bridge.mjs'));
128
+ const hooks = {};
129
+ for (const eventName of Object.keys(CURSOR_TO_CANONICAL_EVENT)) {
130
+ hooks[eventName] = [
131
+ { command: `sh ${runNode} ${bridge} --event ${eventName}` },
132
+ ];
133
+ }
134
+ return `${JSON.stringify({ version: 1, hooks }, null, 2)}\n`;
135
+ }
136
+
137
+ const targetHooksPath = path.join(TARGET, '.cursor', 'hooks.json');
138
+ let hooksWritten = 0;
139
+ if (_isSymlink(targetHooksPath) || existsSync(targetHooksPath)) {
140
+ process.stdout.write(' SKIP: hooks.json (file exists — not overwriting)\n');
141
+ } else {
142
+ mkdirSync(path.dirname(targetHooksPath), { recursive: true });
143
+ writeFileSync(targetHooksPath, renderHooksJson(SO_ROOT), 'utf8');
144
+ process.stdout.write(' WRITE: hooks.json\n');
145
+ hooksWritten = 1;
146
+ }
104
147
 
105
148
  process.stdout.write('\n');
106
- process.stdout.write(`Done! ${count} rules linked.\n`);
149
+ process.stdout.write(`Done! ${ruleLinks} rules linked, ${commandLinks} commands, ${skillLinks} skills, ${hooksWritten} hooks.json written.\n`);
107
150
  process.stdout.write('\n');
108
151
  process.stdout.write('Next steps:\n');
109
152
  process.stdout.write(" 1. Ensure CLAUDE.md (or AGENTS.md on Codex CLI) has a '## Session Config' section\n");
110
- process.stdout.write(' 2. (Optional) Configure hooks in Cursor Settings > Hooks:\n');
111
- process.stdout.write(` - afterFileEdit: ${SO_ROOT}/hooks/enforce-scope.sh\n`);
112
- process.stdout.write(` - beforeShellExecution: ${SO_ROOT}/hooks/enforce-commands.sh\n`);
113
- process.stdout.write(' 3. Open your project in Cursor and type /session to start!\n');
153
+ process.stdout.write(' 2. Reload Cursor (hooks.json is watched; restart if /session is missing)\n');
154
+ process.stdout.write(' 3. Type /session to start\n');
@@ -22,7 +22,8 @@
22
22
  *
23
23
  * Exit codes (per .claude/rules/cli-design.md):
24
24
  * 0 — success (event emitted)
25
- * 1 — user/input error (missing --type, malformed --payload JSON, non-object payload)
25
+ * 1 — user/input error (missing --type, malformed --payload JSON, non-object
26
+ * payload, or a record rejected by validateEventRecord — #1177)
26
27
  * 2 — system error (write failure, unexpected internal error)
27
28
  *
28
29
  * Data → stdout (only with --json). Diagnostics → stderr (always). Related: #611.
@@ -49,7 +50,8 @@ Flags:
49
50
 
50
51
  Exit codes:
51
52
  0 — success
52
- 1 — user/input error (missing --type, malformed --payload, non-object payload)
53
+ 1 — user/input error (missing --type, malformed --payload, non-object payload,
54
+ or an event record rejected by the events schema)
53
55
  2 — system error (write failure / internal error)
54
56
  `;
55
57
 
@@ -147,6 +149,12 @@ try {
147
149
  try {
148
150
  await emitEvent(type, payload, filePath ? { filePath } : {});
149
151
  } catch (err) {
152
+ // A schema-validation rejection is a USER/INPUT error (bad --type or a
153
+ // payload key colliding with the schema) — exit 1, not 2. Nothing was
154
+ // written: emitEvent validates before it touches the filesystem (#1177).
155
+ if (err?.name === 'EventValidationError') {
156
+ fail(`invalid event: ${err.message}`, 1, jsonMode);
157
+ }
150
158
  // Write/IO failures are system errors (exit 2).
151
159
  fail(`failed to emit event: ${err.message}`, 2, jsonMode);
152
160
  }