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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor/commands/autopilot-multi.md +14 -0
- package/.cursor/commands/autopilot.md +14 -0
- package/.cursor/commands/bootstrap.md +14 -0
- package/.cursor/commands/brainstorm.md +14 -0
- package/.cursor/commands/close.md +13 -0
- package/.cursor/commands/contract-version-bump.md +14 -0
- package/.cursor/commands/debug.md +14 -0
- package/.cursor/commands/discovery.md +14 -0
- package/.cursor/commands/dispatcher.md +14 -0
- package/.cursor/commands/eli5.md +14 -0
- package/.cursor/commands/eval.md +14 -0
- package/.cursor/commands/evolve.md +14 -0
- package/.cursor/commands/go.md +14 -0
- package/.cursor/commands/grill.md +14 -0
- package/.cursor/commands/harness-audit.md +13 -0
- package/.cursor/commands/journey-audit.md +14 -0
- package/.cursor/commands/memory-cleanup.md +14 -0
- package/.cursor/commands/persona-panel.md +14 -0
- package/.cursor/commands/plan.md +14 -0
- package/.cursor/commands/portfolio.md +14 -0
- package/.cursor/commands/reconcile.md +14 -0
- package/.cursor/commands/release.md +14 -0
- package/.cursor/commands/repo-audit.md +13 -0
- package/.cursor/commands/session.md +14 -0
- package/.cursor/commands/spinout.md +14 -0
- package/.cursor/commands/sunset-review.md +14 -0
- package/.cursor/commands/templates-ack.md +14 -0
- package/.cursor/commands/test.md +14 -0
- package/.cursor/hooks.json +60 -0
- package/.cursor/rules/000-session-orchestrator.mdc +8 -0
- package/.cursor/rules/010-session-workflow.mdc +9 -1
- package/.cursor/rules/020-quality-gates.mdc +1 -1
- package/.cursor/rules/030-wave-execution.mdc +1 -1
- package/.cursor/rules/050-plan.mdc +2 -2
- package/.cursor/rules/070-gitlab-ops.mdc +73 -57
- package/.cursor/rules/080-ecosystem-health.mdc +7 -7
- package/.cursor/skills/architecture/SKILL.md +13 -0
- package/.cursor/skills/autopilot/SKILL.md +12 -0
- package/.cursor/skills/bootstrap/SKILL.md +12 -0
- package/.cursor/skills/brainstorm/SKILL.md +13 -0
- package/.cursor/skills/claude-md-drift-check/SKILL.md +13 -0
- package/.cursor/skills/contract-version-bump/SKILL.md +12 -0
- package/.cursor/skills/convergence-monitoring/SKILL.md +12 -0
- package/.cursor/skills/daily/SKILL.md +12 -0
- package/.cursor/skills/debug/SKILL.md +13 -0
- package/.cursor/skills/discovery/SKILL.md +13 -0
- package/.cursor/skills/dispatcher/SKILL.md +13 -0
- package/.cursor/skills/docs-orchestrator/SKILL.md +13 -0
- package/.cursor/skills/domain-model/SKILL.md +13 -0
- package/.cursor/skills/ecosystem-health/SKILL.md +13 -0
- package/.cursor/skills/eli5/SKILL.md +13 -0
- package/.cursor/skills/eval/SKILL.md +12 -0
- package/.cursor/skills/evolve/SKILL.md +13 -0
- package/.cursor/skills/frontmatter-guard/SKILL.md +13 -0
- package/.cursor/skills/gitlab-ops/SKILL.md +13 -0
- package/.cursor/skills/gitlab-portfolio/SKILL.md +13 -0
- package/.cursor/skills/grill/SKILL.md +13 -0
- package/.cursor/skills/hook-development/SKILL.md +13 -0
- package/.cursor/skills/journey-audit/SKILL.md +13 -0
- package/.cursor/skills/mcp-builder/SKILL.md +13 -0
- package/.cursor/skills/memory-cleanup/SKILL.md +12 -0
- package/.cursor/skills/mode-selector/SKILL.md +13 -0
- package/.cursor/skills/npm-publish/SKILL.md +12 -0
- package/.cursor/skills/peekaboo-driver/SKILL.md +13 -0
- package/.cursor/skills/persona-panel/SKILL.md +12 -0
- package/.cursor/skills/plan/SKILL.md +13 -0
- package/.cursor/skills/playwright-driver/SKILL.md +13 -0
- package/.cursor/skills/quality-gates/SKILL.md +13 -0
- package/.cursor/skills/reconcile/SKILL.md +12 -0
- package/.cursor/skills/remote-offload/SKILL.md +13 -0
- package/.cursor/skills/repo-audit/SKILL.md +13 -0
- package/.cursor/skills/session-end/SKILL.md +13 -0
- package/.cursor/skills/session-plan/SKILL.md +13 -0
- package/.cursor/skills/session-start/SKILL.md +13 -0
- package/.cursor/skills/skill-creator/SKILL.md +13 -0
- package/.cursor/skills/spinout/SKILL.md +12 -0
- package/.cursor/skills/sunset-review/SKILL.md +13 -0
- package/.cursor/skills/test-runner/SKILL.md +13 -0
- package/.cursor/skills/tmux-layout/SKILL.md +13 -0
- package/.cursor/skills/ubiquitous-language/SKILL.md +13 -0
- package/.cursor/skills/using-orchestrator/SKILL.md +13 -0
- package/.cursor/skills/vault-mirror/SKILL.md +13 -0
- package/.cursor/skills/vault-sync/SKILL.md +13 -0
- package/.cursor/skills/wave-executor/SKILL.md +13 -0
- package/.cursor/skills/write-executable-plan/SKILL.md +13 -0
- package/.mcp.json +4 -1
- package/CHANGELOG.md +446 -0
- package/README.md +22 -17
- package/agents/AGENTS.md +23 -4
- package/agents/code-implementer.md +2 -1
- package/agents/db-specialist.md +2 -2
- package/agents/docs-writer.md +3 -1
- package/agents/eval-judge.md +1 -1
- package/agents/session-reviewer.md +7 -1
- package/agents/test-writer.md +2 -1
- package/agents/ui-developer.md +2 -1
- package/commands/bootstrap.md +2 -2
- package/commands/close.md +3 -1
- package/commands/go.md +1 -1
- package/commands/journey-audit.md +43 -0
- package/docs/USER-GUIDE.md +2 -2
- package/docs/ci-setup.md +194 -25
- package/docs/codex-setup.md +64 -0
- package/docs/components.md +7 -7
- package/docs/cursor-setup.md +26 -47
- package/docs/events-schema.md +120 -10
- package/docs/github-mirror-protection.md +197 -0
- package/docs/pi-setup.md +2 -0
- package/docs/rule-authoring.md +3 -1
- package/docs/scope-collision-guard.md +49 -2
- package/docs/session-config-reference.md +89 -9
- package/docs/session-config-template.md +38 -7
- package/docs/telemetry/telemetry-claims.md +11 -10
- package/docs/telemetry.md +52 -1
- package/hooks/_lib/atomic-json.mjs +111 -0
- package/hooks/_lib/lock-bootstrap.mjs +8 -4
- package/hooks/_lib/subagent-paths.mjs +143 -0
- package/hooks/_lib/vcs-create-matcher.mjs +397 -38
- package/hooks/cwd-change-restore.mjs +9 -29
- package/hooks/enforce-scope.mjs +93 -0
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +201 -20
- package/hooks/hooks-pi.json +1 -1
- package/hooks/hooks.json +2 -2
- package/hooks/on-session-end.mjs +486 -19
- package/hooks/on-session-start.mjs +263 -12
- package/hooks/on-stop.mjs +392 -24
- package/hooks/post-bash-write-verify.mjs +104 -4
- package/hooks/post-subagent-discovery-validator.mjs +182 -21
- package/hooks/post-tool-batch-wave-signal.mjs +165 -42
- package/hooks/post-tool-failure-corrective-context.mjs +9 -32
- package/hooks/pre-bash-issue-budget.mjs +117 -4
- package/hooks/pre-bash-memory-propose-audit.mjs +13 -7
- package/hooks/pre-bash-sessions-ledger-guard.mjs +159 -0
- package/hooks/pre-bash-staging-fence.mjs +4 -0
- package/hooks/pre-task-scope-disjoint.mjs +368 -35
- package/hooks/skill-invocation-telemetry.mjs +21 -10
- package/hooks/subagent-telemetry.mjs +11 -26
- package/monitors/monitors.json +6 -0
- package/package.json +1 -1
- package/pi/prompts/journey-audit.md +12 -0
- package/rules/_index.md +9 -1
- package/rules/always-on/ask-via-tool.md +62 -0
- package/rules/always-on/bash-harness-pitfalls.md +168 -0
- package/rules/always-on/build-value.md +47 -0
- package/rules/always-on/cross-session-messaging.md +59 -0
- package/rules/always-on/loop-and-monitor.md +221 -0
- package/rules/always-on/parallel-sessions.md +142 -12
- package/rules/always-on/receiving-review.md +108 -0
- package/rules/always-on/test-value.md +40 -0
- package/rules/always-on/verification-before-completion.md +77 -0
- package/scripts/archive-closed-prds.mjs +258 -18
- package/scripts/autopilot.mjs +31 -12
- package/scripts/backfill-abandoned-sessions.mjs +80 -11
- package/scripts/backfill-evidence-digest.mjs +376 -0
- package/scripts/cursor-install.mjs +89 -48
- package/scripts/emit-event.mjs +10 -2
- package/scripts/export-hw-learnings.mjs +143 -2
- package/scripts/express-path.mjs +299 -0
- package/scripts/generate-cursor-adapter.mjs +253 -0
- package/scripts/github-protection-audit.mjs +358 -0
- package/scripts/lib/auq/parse.mjs +5 -29
- package/scripts/lib/auto-dialectic.mjs +68 -0
- package/scripts/lib/autopilot/worktree-pipeline.mjs +318 -18
- package/scripts/lib/build-live-signals.mjs +49 -27
- package/scripts/lib/ci-status-banner.mjs +158 -11
- package/scripts/lib/cold-start-detector.mjs +23 -14
- package/scripts/lib/command-blocker.mjs +70 -0
- package/scripts/lib/config/block-header.mjs +55 -0
- package/scripts/lib/config/discovery-validator.mjs +7 -2
- package/scripts/lib/config/health-endpoints.mjs +383 -0
- package/scripts/lib/config/reconcile.mjs +79 -4
- package/scripts/lib/config/remote-hosts.mjs +233 -0
- package/scripts/lib/config/section-extractor.mjs +235 -36
- package/scripts/lib/config-schema.mjs +9 -1
- package/scripts/lib/config.mjs +87 -8
- package/scripts/lib/convergence-monitor.mjs +13 -2
- package/scripts/lib/cursor-hook-bridge.mjs +443 -0
- package/scripts/lib/dispatcher/cli.mjs +2 -2
- package/scripts/lib/dispatcher/enumerate.mjs +2 -17
- package/scripts/lib/events-schema.mjs +48 -0
- package/scripts/lib/events.mjs +238 -5
- package/scripts/lib/evolve/autonomy-verdict.mjs +9 -4
- package/scripts/lib/evolve/autopilot-effectiveness.mjs +18 -1
- package/scripts/lib/express-path.mjs +327 -0
- package/scripts/lib/file-lock.mjs +22 -4
- package/scripts/lib/gates/gate-full.mjs +81 -8
- package/scripts/lib/gates/gate-helpers.mjs +76 -15
- package/scripts/lib/git-config-drift.mjs +134 -5
- package/scripts/lib/gitlab-portfolio/cli.mjs +3 -15
- package/scripts/lib/harness-audit/categories/category1.mjs +17 -6
- package/scripts/lib/host-identity.mjs +247 -2
- package/scripts/lib/instruction-budget-guard.mjs +31 -1
- package/scripts/lib/issue-budget.mjs +229 -30
- package/scripts/lib/learnings/io.mjs +55 -10
- package/scripts/lib/learnings/schema.mjs +95 -28
- package/scripts/lib/lock-reaper.mjs +7 -1
- package/scripts/lib/locks/staging-fence-lock.mjs +5 -1
- package/scripts/lib/locks/state-md-lock.mjs +8 -1
- package/scripts/lib/memory-banner.mjs +25 -10
- package/scripts/lib/memory-paths.mjs +15 -6
- package/scripts/lib/mode-selector/scoring.mjs +53 -6
- package/scripts/lib/peer-discovery.mjs +20 -2
- package/scripts/lib/platform.mjs +72 -9
- package/scripts/lib/plugin-root.mjs +143 -19
- package/scripts/lib/project-hygiene.mjs +43 -3
- package/scripts/lib/quality-gate.mjs +271 -13
- package/scripts/lib/reconcile/emitter.mjs +87 -19
- package/scripts/lib/reconcile/engine.mjs +517 -18
- package/scripts/lib/reconcile/idempotency.mjs +102 -1
- package/scripts/lib/reconcile/renderer.mjs +148 -3
- package/scripts/lib/reconcile/sanitize.mjs +40 -17
- package/scripts/lib/reconcile/writer.mjs +415 -84
- package/scripts/lib/rule-loader.mjs +37 -2
- package/scripts/lib/rules-sync.mjs +51 -8
- package/scripts/lib/scope-gate.mjs +126 -0
- package/scripts/lib/session-close-backfill.mjs +427 -37
- package/scripts/lib/session-discovery.mjs +69 -5
- package/scripts/lib/session-end/phase-skip.mjs +38 -5
- package/scripts/lib/session-end/worktree-cleanup.mjs +154 -7
- package/scripts/lib/session-id.mjs +30 -14
- package/scripts/lib/session-identity/own-session.mjs +220 -0
- package/scripts/lib/session-lock.mjs +85 -30
- package/scripts/lib/session-schema/normalizer.mjs +70 -3
- package/scripts/lib/session-schema/validator.mjs +40 -0
- package/scripts/lib/session-start-probes.mjs +608 -0
- package/scripts/lib/session-transition.mjs +277 -0
- package/scripts/lib/sessions-canonical.mjs +446 -0
- package/scripts/lib/sessions-staleness-banner.mjs +124 -57
- package/scripts/lib/spiral-carryover.mjs +90 -9
- package/scripts/lib/state-md/frontmatter-mutators.mjs +41 -8
- package/scripts/lib/state-md/mission-status.mjs +350 -52
- package/scripts/lib/state-md/yaml-parser.mjs +145 -16
- package/scripts/lib/state-md.mjs +12 -2
- package/scripts/lib/telemetry/schema.mjs +74 -8
- package/scripts/lib/telemetry/sync.mjs +91 -16
- package/scripts/lib/tmux-layout/telemetry.mjs +14 -2
- package/scripts/lib/validate/check-agents.mjs +66 -0
- package/scripts/lib/validate/check-cursor-adapter.mjs +102 -0
- package/scripts/lib/validate/check-dead-bridge.mjs +24 -2
- package/scripts/lib/validate/check-doc-cli-commands.mjs +25 -65
- package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +370 -0
- package/scripts/lib/validate/check-hooks-symmetry.mjs +29 -63
- package/scripts/lib/validate/check-playwright-mcp-canary.mjs +13 -22
- package/scripts/lib/validate/check-plugin-monitors.mjs +10 -4
- package/scripts/lib/validate/check-skill-script-paths.mjs +436 -0
- package/scripts/lib/validate/check-test-value-bans.mjs +165 -17
- package/scripts/lib/validate/check-untracked-test-deps.mjs +10 -0
- package/scripts/lib/validate/check-unwired-features.mjs +333 -32
- package/scripts/lib/validate/check-validator-registration.mjs +248 -0
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +6 -28
- package/scripts/lib/validate/markdown-fences.mjs +196 -0
- package/scripts/lib/validate/repo-files.mjs +275 -0
- package/scripts/lib/validate-vendored-rules.mjs +229 -7
- package/scripts/lib/vault-mirror/process.mjs +99 -43
- package/scripts/lib/vault-mirror/telemetry.mjs +210 -0
- package/scripts/lib/vault-staleness-banner.mjs +76 -6
- package/scripts/lib/vault-status/board-lock.mjs +185 -0
- package/scripts/lib/vault-status/board-writer.mjs +381 -141
- package/scripts/lib/vault-status/narrative-mirror.mjs +190 -27
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +832 -0
- package/scripts/lib/wave-executor/remote-dispatch.mjs +504 -0
- package/scripts/lib/wave-resource-gate.mjs +127 -7
- package/scripts/lib/wave-transcript-tail.mjs +889 -0
- package/scripts/materialize-wave-scope.mjs +228 -15
- package/scripts/mcp-server.sh +11 -2
- package/scripts/memory-propose.mjs +132 -8
- package/scripts/parse-config.mjs +65 -0
- package/scripts/promote-vault-strict.mjs +4 -15
- package/scripts/site-numbers.mjs +36 -4
- package/scripts/token-audit.sh +9 -2
- package/scripts/validate-plugin.mjs +29 -0
- package/scripts/validate-wave-scope.mjs +67 -0
- package/scripts/vault-consolidate.mjs +3 -11
- package/scripts/vault-integration-watcher.mjs +2 -4
- package/scripts/vault-mirror.mjs +305 -51
- package/skills/_shared/monitor-patterns.md +31 -5
- package/skills/_shared/parallel-aware-auq.md +31 -2
- package/skills/_shared/parallel-aware-preamble.md +19 -4
- package/skills/_shared/platform-tools.md +11 -5
- package/skills/_shared/state-ownership.md +29 -2
- package/skills/autopilot/SKILL.md +5 -1
- package/skills/bootstrap/SKILL.md +3 -3
- package/skills/bootstrap/_shared-template.md +18 -10
- package/skills/bootstrap/deep-template.md +10 -6
- package/skills/bootstrap/fast-template.md +15 -8
- package/skills/bootstrap/standard-template.md +10 -6
- package/skills/claude-md-drift-check/checker.mjs +39 -11
- package/skills/contract-version-bump/SKILL.md +1 -1
- package/skills/dispatcher/SKILL.md +1 -1
- package/skills/ecosystem-health/SKILL.md +4 -1
- package/skills/ecosystem-health/wizard.md +5 -0
- package/skills/evolve/SKILL.md +38 -1
- package/skills/journey-audit/SKILL.md +270 -0
- package/skills/peekaboo-driver/SKILL.md +15 -3
- package/skills/persona-panel/SKILL.md +1 -1
- package/skills/reconcile/SKILL.md +46 -3
- package/skills/remote-offload/SKILL.md +89 -0
- package/skills/session-end/SKILL.md +17 -4
- package/skills/session-end/metrics-collection.md +7 -4
- package/skills/session-end/phase-3-6-tail.md +20 -9
- package/skills/session-end/phase-3-7a-recommendations.md +16 -2
- package/skills/session-plan/SKILL.md +6 -1
- package/skills/session-plan/wave-template.md +1 -0
- package/skills/session-start/SKILL.md +54 -17
- package/skills/session-start/phase-7-5-mode-selector.md +15 -3
- package/skills/session-start/phase-8-5-express-path.md +77 -12
- package/skills/vault-sync/validator.mjs +31 -0
- package/skills/wave-executor/SKILL.md +5 -3
- package/skills/wave-executor/circuit-breaker.md +34 -9
- package/skills/wave-executor/wave-loop.md +143 -22
- package/templates/_shared/journey-manifest.md +110 -0
- 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
|
|
3
|
+
* cursor-install.mjs — Install Session Orchestrator Cursor surface into a project.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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! ${
|
|
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.
|
|
111
|
-
process.stdout.write(
|
|
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');
|
package/scripts/emit-event.mjs
CHANGED
|
@@ -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
|
|
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
|
}
|