session-orchestrator 3.22.0 → 3.23.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/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 +168 -0
- package/README.md +18 -15
- package/agents/AGENTS.md +23 -4
- package/agents/code-implementer.md +2 -1
- package/agents/db-specialist.md +2 -1
- 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 +14 -0
- package/docs/codex-setup.md +64 -0
- package/docs/components.md +6 -6
- package/docs/cursor-setup.md +26 -47
- package/docs/events-schema.md +76 -4
- 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 +26 -4
- package/docs/session-config-template.md +4 -3
- package/docs/telemetry.md +22 -0
- package/hooks/_lib/lock-bootstrap.mjs +8 -4
- package/hooks/_lib/vcs-create-matcher.mjs +397 -38
- package/hooks/enforce-scope.mjs +64 -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 +211 -10
- package/hooks/on-session-start.mjs +214 -11
- package/hooks/on-stop.mjs +48 -9
- package/hooks/post-subagent-discovery-validator.mjs +34 -3
- package/hooks/post-tool-batch-wave-signal.mjs +11 -2
- package/hooks/pre-bash-issue-budget.mjs +117 -4
- 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/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 +5 -0
- package/scripts/backfill-evidence-digest.mjs +376 -0
- package/scripts/cursor-install.mjs +89 -48
- 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/autopilot/worktree-pipeline.mjs +240 -16
- package/scripts/lib/build-live-signals.mjs +24 -5
- package/scripts/lib/ci-status-banner.mjs +158 -11
- package/scripts/lib/command-blocker.mjs +70 -0
- package/scripts/lib/config/reconcile.mjs +79 -4
- package/scripts/lib/config/section-extractor.mjs +235 -36
- package/scripts/lib/config-schema.mjs +9 -1
- package/scripts/lib/config.mjs +57 -6
- 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/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/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 +5 -2
- package/scripts/lib/memory-paths.mjs +15 -6
- package/scripts/lib/mode-selector/scoring.mjs +53 -6
- 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 +281 -13
- 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 +90 -0
- package/scripts/lib/session-close-backfill.mjs +369 -28
- package/scripts/lib/session-discovery.mjs +13 -3
- package/scripts/lib/session-end/phase-skip.mjs +37 -4
- 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 +159 -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-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/sync.mjs +46 -8
- 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 +16 -32
- 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-test-value-bans.mjs +165 -17
- package/scripts/lib/validate/check-unwired-features.mjs +340 -32
- 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-writer.mjs +211 -10
- package/scripts/lib/vault-status/narrative-mirror.mjs +188 -8
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +832 -0
- package/scripts/lib/wave-transcript-tail.mjs +869 -0
- package/scripts/materialize-wave-scope.mjs +209 -12
- package/scripts/mcp-server.sh +11 -2
- package/scripts/parse-config.mjs +65 -0
- package/scripts/token-audit.sh +9 -2
- package/scripts/validate-plugin.mjs +3 -0
- package/scripts/validate-wave-scope.mjs +67 -0
- package/scripts/vault-mirror.mjs +203 -34
- package/skills/_shared/monitor-patterns.md +31 -5
- package/skills/_shared/parallel-aware-auq.md +1 -1
- package/skills/_shared/parallel-aware-preamble.md +4 -2
- 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/dispatcher/SKILL.md +1 -1
- package/skills/journey-audit/SKILL.md +269 -0
- package/skills/peekaboo-driver/SKILL.md +15 -3
- package/skills/persona-panel/SKILL.md +1 -1
- package/skills/reconcile/SKILL.md +41 -1
- 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 +11 -3
- 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 +30 -16
- 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 +4 -2
- package/skills/wave-executor/circuit-breaker.md +34 -9
- package/skills/wave-executor/wave-loop.md +102 -19
- package/templates/_shared/journey-manifest.md +110 -0
- package/templates/_shared/rules/parallel-sessions.md +0 -77
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cursor-hook-bridge.mjs — map Cursor IDE hook events onto existing handlers.
|
|
3
|
+
*
|
|
4
|
+
* Cursor emits native events (`beforeShellExecution`, `preToolUse`, …) with its
|
|
5
|
+
* own stdin JSON and reads `{ permission, user_message, agent_message }` back.
|
|
6
|
+
* Existing Session Orchestrator hooks speak the Claude/Codex contract:
|
|
7
|
+
* `{ hook_event_name, tool_name, tool_input, ... }` plus an exit-0
|
|
8
|
+
* `hookSpecificOutput.permissionDecision` envelope (#906).
|
|
9
|
+
*
|
|
10
|
+
* This bridge is the Cursor counterpart of `pi-hook-bridge.mjs`. It closes the
|
|
11
|
+
* #919 silent-no-op: without it, `enforce-commands.mjs` sees no `tool_name ===
|
|
12
|
+
* "Bash"` on a Cursor payload and writes 0 bytes / exit 0, so the command runs.
|
|
13
|
+
*
|
|
14
|
+
* Usage (`.cursor/hooks.json`):
|
|
15
|
+
* sh hooks/run-node.sh scripts/lib/cursor-hook-bridge.mjs --event beforeShellExecution
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { spawnSync } from 'node:child_process';
|
|
19
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
import { pathToFileURL } from 'node:url';
|
|
22
|
+
|
|
23
|
+
import { matcherMatches, readHookDecision } from './pi-hook-bridge.mjs';
|
|
24
|
+
import { resolvePluginRoot } from './plugin-root.mjs';
|
|
25
|
+
|
|
26
|
+
export const CURSOR_TO_CANONICAL_EVENT = Object.freeze({
|
|
27
|
+
sessionStart: 'SessionStart',
|
|
28
|
+
sessionEnd: 'SessionEnd',
|
|
29
|
+
beforeShellExecution: 'PreToolUse',
|
|
30
|
+
afterShellExecution: 'PostToolUse',
|
|
31
|
+
preToolUse: 'PreToolUse',
|
|
32
|
+
postToolUse: 'PostToolUse',
|
|
33
|
+
afterFileEdit: 'PostToolUse',
|
|
34
|
+
postToolUseFailure: 'PostToolUseFailure',
|
|
35
|
+
stop: 'Stop',
|
|
36
|
+
subagentStart: 'SubagentStart',
|
|
37
|
+
subagentStop: 'SubagentStop',
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export const CURSOR_BLOCKING_EVENTS = Object.freeze([
|
|
41
|
+
'beforeShellExecution',
|
|
42
|
+
'preToolUse',
|
|
43
|
+
'beforeMCPExecution',
|
|
44
|
+
'subagentStart',
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
const TOOL_NAME_MAP = Object.freeze({
|
|
48
|
+
shell: 'Bash',
|
|
49
|
+
bash: 'Bash',
|
|
50
|
+
write: 'Write',
|
|
51
|
+
tabwrite: 'Write',
|
|
52
|
+
strreplace: 'Edit',
|
|
53
|
+
edit: 'Edit',
|
|
54
|
+
multiedit: 'MultiEdit',
|
|
55
|
+
read: 'Read',
|
|
56
|
+
tabread: 'Read',
|
|
57
|
+
delete: 'Write',
|
|
58
|
+
task: 'Agent',
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @param {unknown} value
|
|
63
|
+
* @returns {Record<string, unknown>}
|
|
64
|
+
*/
|
|
65
|
+
function objectOrEmpty(value) {
|
|
66
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @param {string|undefined} toolName
|
|
71
|
+
* @returns {string|undefined}
|
|
72
|
+
*/
|
|
73
|
+
export function mapCursorToolName(toolName) {
|
|
74
|
+
if (typeof toolName !== 'string' || toolName.length === 0) return undefined;
|
|
75
|
+
const key = toolName.toLowerCase();
|
|
76
|
+
return TOOL_NAME_MAP[key] ?? toolName;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @param {unknown} value
|
|
81
|
+
* @returns {string|undefined}
|
|
82
|
+
*/
|
|
83
|
+
function firstString(...values) {
|
|
84
|
+
for (const value of values) {
|
|
85
|
+
if (typeof value === 'string' && value.length > 0) return value;
|
|
86
|
+
}
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* @param {string} cursorEventName
|
|
92
|
+
* @param {Record<string, unknown>} event
|
|
93
|
+
* @returns {string|undefined}
|
|
94
|
+
*/
|
|
95
|
+
function inferCursorToolName(cursorEventName, event) {
|
|
96
|
+
if (cursorEventName === 'beforeShellExecution' || cursorEventName === 'afterShellExecution') {
|
|
97
|
+
return 'Bash';
|
|
98
|
+
}
|
|
99
|
+
if (cursorEventName === 'afterFileEdit') {
|
|
100
|
+
return mapCursorToolName(typeof event.tool_type === 'string' ? event.tool_type : 'Write');
|
|
101
|
+
}
|
|
102
|
+
const raw = firstString(
|
|
103
|
+
typeof event.tool_name === 'string' ? event.tool_name : undefined,
|
|
104
|
+
typeof event.toolName === 'string' ? event.toolName : undefined,
|
|
105
|
+
typeof event.tool_type === 'string' ? event.tool_type : undefined,
|
|
106
|
+
typeof event.toolType === 'string' ? event.toolType : undefined,
|
|
107
|
+
);
|
|
108
|
+
return mapCursorToolName(raw);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* @param {Record<string, unknown>} event
|
|
113
|
+
* @returns {Record<string, unknown>}
|
|
114
|
+
*/
|
|
115
|
+
function collectToolInput(event) {
|
|
116
|
+
const nested = objectOrEmpty(event.tool_input ?? event.arguments ?? event.args ?? event.input);
|
|
117
|
+
const next = { ...nested };
|
|
118
|
+
const command = firstString(
|
|
119
|
+
typeof event.command === 'string' ? event.command : undefined,
|
|
120
|
+
typeof next.command === 'string' ? next.command : undefined,
|
|
121
|
+
);
|
|
122
|
+
if (command) next.command = command;
|
|
123
|
+
|
|
124
|
+
const filePath = firstString(
|
|
125
|
+
typeof next.file_path === 'string' ? next.file_path : undefined,
|
|
126
|
+
typeof event.file_path === 'string' ? event.file_path : undefined,
|
|
127
|
+
typeof event.filePath === 'string' ? event.filePath : undefined,
|
|
128
|
+
typeof event.path === 'string' ? event.path : undefined,
|
|
129
|
+
typeof next.path === 'string' ? next.path : undefined,
|
|
130
|
+
);
|
|
131
|
+
if (filePath) {
|
|
132
|
+
next.file_path = filePath;
|
|
133
|
+
if (typeof next.path !== 'string') next.path = filePath;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (typeof next.old_string !== 'string' && typeof next.oldText === 'string') {
|
|
137
|
+
next.old_string = next.oldText;
|
|
138
|
+
}
|
|
139
|
+
if (typeof next.new_string !== 'string' && typeof next.newText === 'string') {
|
|
140
|
+
next.new_string = next.newText;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return next;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Build the hook stdin payload expected by existing session-orchestrator hooks.
|
|
148
|
+
*
|
|
149
|
+
* @param {string} cursorEventName
|
|
150
|
+
* @param {Record<string, unknown>} [event]
|
|
151
|
+
* @param {Record<string, unknown>} [ctx]
|
|
152
|
+
* @returns {Record<string, unknown>}
|
|
153
|
+
*/
|
|
154
|
+
export function normalizeCursorHookPayload(cursorEventName, event = {}, ctx = {}) {
|
|
155
|
+
const eventObject = objectOrEmpty(event);
|
|
156
|
+
const ctxObject = objectOrEmpty(ctx);
|
|
157
|
+
const toolName = inferCursorToolName(cursorEventName, eventObject);
|
|
158
|
+
const toolInput = collectToolInput(eventObject);
|
|
159
|
+
const cwd = firstString(
|
|
160
|
+
typeof ctxObject.cwd === 'string' ? ctxObject.cwd : undefined,
|
|
161
|
+
typeof eventObject.cwd === 'string' ? eventObject.cwd : undefined,
|
|
162
|
+
process.cwd(),
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
hook_event_name: CURSOR_TO_CANONICAL_EVENT[cursorEventName] ?? cursorEventName,
|
|
167
|
+
cursor_event_name: cursorEventName,
|
|
168
|
+
session_id: eventObject.session_id ?? eventObject.sessionId ?? ctxObject.sessionId,
|
|
169
|
+
cwd,
|
|
170
|
+
reason: eventObject.reason,
|
|
171
|
+
tool_name: toolName,
|
|
172
|
+
tool_input: toolInput,
|
|
173
|
+
tool_call_id: eventObject.tool_call_id ?? eventObject.toolCallId,
|
|
174
|
+
tool_response: eventObject.tool_response ?? eventObject.result ?? eventObject.output,
|
|
175
|
+
is_error: eventObject.is_error ?? eventObject.isError,
|
|
176
|
+
timestamp: eventObject.timestamp ?? new Date().toISOString(),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* @param {string} pluginRoot
|
|
182
|
+
* @returns {Record<string, unknown>}
|
|
183
|
+
*/
|
|
184
|
+
export function loadCursorHookManifest(pluginRoot = resolvePluginRoot()) {
|
|
185
|
+
const manifestPath = path.join(pluginRoot, 'hooks', 'hooks-cursor.json');
|
|
186
|
+
if (!existsSync(manifestPath)) {
|
|
187
|
+
throw new Error(`Cursor hook manifest not found at ${manifestPath}`);
|
|
188
|
+
}
|
|
189
|
+
return JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* @param {string} cursorEventName
|
|
194
|
+
* @param {Record<string, unknown>} event
|
|
195
|
+
* @param {string|undefined} canonicalToolName
|
|
196
|
+
* @returns {string[]}
|
|
197
|
+
*/
|
|
198
|
+
function matcherTargets(cursorEventName, event, canonicalToolName) {
|
|
199
|
+
const rawToolName = firstString(
|
|
200
|
+
typeof event.tool_name === 'string' ? event.tool_name : undefined,
|
|
201
|
+
typeof event.toolName === 'string' ? event.toolName : undefined,
|
|
202
|
+
typeof event.tool_type === 'string' ? event.tool_type : undefined,
|
|
203
|
+
);
|
|
204
|
+
return [
|
|
205
|
+
cursorEventName,
|
|
206
|
+
typeof event.reason === 'string' ? event.reason : '',
|
|
207
|
+
rawToolName ?? '',
|
|
208
|
+
canonicalToolName ?? '',
|
|
209
|
+
cursorEventName === 'beforeShellExecution' || cursorEventName === 'afterShellExecution' ? 'bash' : '',
|
|
210
|
+
cursorEventName === 'beforeShellExecution' || cursorEventName === 'afterShellExecution' ? 'shell' : '',
|
|
211
|
+
].map((v) => v.toLowerCase());
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* @param {Record<string, unknown>} manifest
|
|
216
|
+
* @param {string} cursorEventName
|
|
217
|
+
* @param {Record<string, unknown>} [event]
|
|
218
|
+
* @returns {Array<Record<string, unknown>>}
|
|
219
|
+
*/
|
|
220
|
+
export function selectCursorHooks(manifest, cursorEventName, event = {}) {
|
|
221
|
+
const hooksByEvent = objectOrEmpty(manifest.hooks);
|
|
222
|
+
const entry = hooksByEvent[cursorEventName];
|
|
223
|
+
const eventObject = objectOrEmpty(event);
|
|
224
|
+
const canonicalToolName = inferCursorToolName(cursorEventName, eventObject);
|
|
225
|
+
const targets = matcherTargets(cursorEventName, eventObject, canonicalToolName);
|
|
226
|
+
const selected = [];
|
|
227
|
+
|
|
228
|
+
if (entry && !Array.isArray(entry) && typeof objectOrEmpty(entry).script === 'string') {
|
|
229
|
+
const script = objectOrEmpty(entry).script;
|
|
230
|
+
selected.push({
|
|
231
|
+
type: 'command',
|
|
232
|
+
command: `sh "$CURSOR_PLUGIN_ROOT/hooks/run-node.sh" "$CURSOR_PLUGIN_ROOT/${script}"`,
|
|
233
|
+
});
|
|
234
|
+
return selected;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const matchers = Array.isArray(entry) ? entry : [];
|
|
238
|
+
for (const matcherConfig of matchers) {
|
|
239
|
+
const config = objectOrEmpty(matcherConfig);
|
|
240
|
+
const matcher = typeof config.matcher === 'string' ? config.matcher : '';
|
|
241
|
+
if (!matcherMatches(matcher, targets)) continue;
|
|
242
|
+
const hooks = Array.isArray(config.hooks) ? config.hooks : [];
|
|
243
|
+
for (const hook of hooks) {
|
|
244
|
+
selected.push(objectOrEmpty(hook));
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return selected;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* @param {Record<string, unknown>} hook
|
|
253
|
+
* @param {Record<string, unknown>} payload
|
|
254
|
+
* @param {{ pluginRoot: string, cwd?: string, env?: NodeJS.ProcessEnv }} options
|
|
255
|
+
* @returns {{ command: string, status: number|null, stdout: string, stderr: string, blocked: boolean, reason: string|null }}
|
|
256
|
+
*/
|
|
257
|
+
export function runCursorHookCommand(hook, payload, options) {
|
|
258
|
+
const command = typeof hook.command === 'string' ? hook.command : '';
|
|
259
|
+
if (!command) {
|
|
260
|
+
return { command, status: 0, stdout: '', stderr: '', blocked: false, reason: null };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const timeoutSeconds = Number.isFinite(hook.timeout) ? Number(hook.timeout) : 5;
|
|
264
|
+
const cwd = options.cwd || (typeof payload.cwd === 'string' ? payload.cwd : process.cwd());
|
|
265
|
+
const env = {
|
|
266
|
+
...process.env,
|
|
267
|
+
...options.env,
|
|
268
|
+
};
|
|
269
|
+
delete env.CLAUDE_PLUGIN_ROOT;
|
|
270
|
+
delete env.CODEX_PLUGIN_ROOT;
|
|
271
|
+
delete env.PI_PLUGIN_ROOT;
|
|
272
|
+
delete env.CLAUDE_PROJECT_DIR;
|
|
273
|
+
delete env.CODEX_PROJECT_DIR;
|
|
274
|
+
delete env.PI_PROJECT_DIR;
|
|
275
|
+
env.CURSOR_PLUGIN_ROOT = options.pluginRoot;
|
|
276
|
+
env.CURSOR_RULES_DIR = options.pluginRoot;
|
|
277
|
+
env.PLUGIN_ROOT = options.pluginRoot;
|
|
278
|
+
env.SO_PLATFORM = 'cursor';
|
|
279
|
+
if (typeof payload.cwd === 'string') env.CURSOR_PROJECT_DIR = payload.cwd;
|
|
280
|
+
|
|
281
|
+
// `hook.command` is a shell line from the repo-committed hooks.json, so a
|
|
282
|
+
// shell is the intended interpreter — the same commit-gated trust class as
|
|
283
|
+
// the `*-command` keys run by scripts/lib/quality-gate.mjs (see
|
|
284
|
+
// .claude/rules/security.md § Session Config Command Trust). Nothing here
|
|
285
|
+
// interpolates user or payload input into the command string; the payload
|
|
286
|
+
// travels on stdin.
|
|
287
|
+
// nosemgrep: unsafe-shell-spawn
|
|
288
|
+
const result = spawnSync(command, {
|
|
289
|
+
shell: true,
|
|
290
|
+
cwd,
|
|
291
|
+
env,
|
|
292
|
+
input: JSON.stringify(payload) + '\n',
|
|
293
|
+
encoding: 'utf8',
|
|
294
|
+
timeout: timeoutSeconds * 1000,
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
const stdout = result.stdout ?? '';
|
|
298
|
+
const stderr = result.stderr ?? '';
|
|
299
|
+
const decision = readHookDecision(stdout);
|
|
300
|
+
const isPreToolUse = payload.hook_event_name === 'PreToolUse';
|
|
301
|
+
const infraFailure = Boolean(result.error) ||
|
|
302
|
+
result.status === null ||
|
|
303
|
+
(result.status !== 0 && result.status !== 2) ||
|
|
304
|
+
decision.malformed;
|
|
305
|
+
const blocked = result.status === 2 ||
|
|
306
|
+
decision.deny ||
|
|
307
|
+
(isPreToolUse && infraFailure);
|
|
308
|
+
|
|
309
|
+
let reason = decision.reason;
|
|
310
|
+
if (reason === null && blocked) {
|
|
311
|
+
reason = result.error?.message
|
|
312
|
+
|| (decision.malformed
|
|
313
|
+
? `Hook stdout is not a parseable decision envelope (possible truncated output) — failing closed: ${command}`
|
|
314
|
+
: '')
|
|
315
|
+
|| stdout.trim()
|
|
316
|
+
|| stderr.trim()
|
|
317
|
+
|| `Hook command blocked: ${command}`;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
command,
|
|
322
|
+
status: result.status,
|
|
323
|
+
stdout,
|
|
324
|
+
stderr,
|
|
325
|
+
blocked,
|
|
326
|
+
reason,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* @param {string} cursorEventName
|
|
332
|
+
* @param {Record<string, unknown>} [event]
|
|
333
|
+
* @param {Record<string, unknown>} [ctx]
|
|
334
|
+
* @param {{ pluginRoot?: string, manifest?: Record<string, unknown>, env?: NodeJS.ProcessEnv }} [options]
|
|
335
|
+
* @returns {Promise<{ block: boolean, reason: string|null, payload: Record<string, unknown>, results: Array<object> }>}
|
|
336
|
+
*/
|
|
337
|
+
export async function runCursorHookEvent(cursorEventName, event = {}, ctx = {}, options = {}) {
|
|
338
|
+
const pluginRoot = options.pluginRoot ?? resolvePluginRoot();
|
|
339
|
+
const manifest = options.manifest ?? loadCursorHookManifest(pluginRoot);
|
|
340
|
+
const payload = normalizeCursorHookPayload(cursorEventName, event, ctx);
|
|
341
|
+
const hooks = selectCursorHooks(manifest, cursorEventName, event);
|
|
342
|
+
const results = [];
|
|
343
|
+
|
|
344
|
+
for (const hook of hooks) {
|
|
345
|
+
const result = runCursorHookCommand(hook, payload, {
|
|
346
|
+
pluginRoot,
|
|
347
|
+
cwd: typeof payload.cwd === 'string' ? payload.cwd : process.cwd(),
|
|
348
|
+
env: options.env,
|
|
349
|
+
});
|
|
350
|
+
results.push(result);
|
|
351
|
+
if (result.blocked) {
|
|
352
|
+
return {
|
|
353
|
+
block: true,
|
|
354
|
+
reason: result.reason,
|
|
355
|
+
payload,
|
|
356
|
+
results,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return {
|
|
362
|
+
block: false,
|
|
363
|
+
reason: null,
|
|
364
|
+
payload,
|
|
365
|
+
results,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Map a bridge result onto Cursor's hook stdout contract.
|
|
371
|
+
*
|
|
372
|
+
* @param {string} cursorEventName
|
|
373
|
+
* @param {{ block: boolean, reason: string|null }} result
|
|
374
|
+
* @returns {Record<string, unknown>}
|
|
375
|
+
*/
|
|
376
|
+
export function toCursorHookOutput(cursorEventName, result) {
|
|
377
|
+
const blocking = CURSOR_BLOCKING_EVENTS.includes(cursorEventName);
|
|
378
|
+
if (result.block && blocking) {
|
|
379
|
+
const reason = result.reason || 'Blocked by Session Orchestrator';
|
|
380
|
+
return {
|
|
381
|
+
permission: 'deny',
|
|
382
|
+
user_message: reason,
|
|
383
|
+
agent_message: reason,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
if (result.block && (cursorEventName === 'afterFileEdit' || cursorEventName === 'postToolUse')) {
|
|
387
|
+
return {
|
|
388
|
+
additional_context: result.reason || 'Session Orchestrator post-hoc warning',
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
if (blocking) {
|
|
392
|
+
return { permission: 'allow' };
|
|
393
|
+
}
|
|
394
|
+
return {};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function readStdinSync() {
|
|
398
|
+
try {
|
|
399
|
+
return readFileSync(0, 'utf8');
|
|
400
|
+
} catch {
|
|
401
|
+
return '';
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async function main() {
|
|
406
|
+
const eventIdx = process.argv.indexOf('--event');
|
|
407
|
+
const eventName = eventIdx >= 0 ? process.argv[eventIdx + 1] : '';
|
|
408
|
+
if (!eventName || typeof CURSOR_TO_CANONICAL_EVENT[eventName] !== 'string') {
|
|
409
|
+
process.stderr.write(`cursor-hook-bridge: missing or unknown --event ${eventName || '(none)'}\n`);
|
|
410
|
+
process.stdout.write(`${JSON.stringify({ permission: 'allow' })}\n`);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const raw = readStdinSync();
|
|
415
|
+
let event;
|
|
416
|
+
try {
|
|
417
|
+
event = raw.trim() ? JSON.parse(raw) : {};
|
|
418
|
+
} catch (err) {
|
|
419
|
+
const blocking = CURSOR_BLOCKING_EVENTS.includes(eventName);
|
|
420
|
+
if (blocking) {
|
|
421
|
+
const reason = `Cursor hook payload is not valid JSON: ${err instanceof Error ? err.message : err}`;
|
|
422
|
+
process.stdout.write(`${JSON.stringify({
|
|
423
|
+
permission: 'deny',
|
|
424
|
+
user_message: reason,
|
|
425
|
+
agent_message: reason,
|
|
426
|
+
})}\n`);
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
process.stdout.write('{}\n');
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const result = await runCursorHookEvent(eventName, objectOrEmpty(event), { cwd: process.cwd() });
|
|
434
|
+
process.stdout.write(`${JSON.stringify(toCursorHookOutput(eventName, result))}\n`);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const isMain = process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
438
|
+
if (isMain) {
|
|
439
|
+
main().catch((err) => {
|
|
440
|
+
process.stderr.write(`cursor-hook-bridge: ${err instanceof Error ? err.message : err}\n`);
|
|
441
|
+
process.stdout.write(`${JSON.stringify({ permission: 'allow' })}\n`);
|
|
442
|
+
});
|
|
443
|
+
}
|
|
@@ -99,8 +99,8 @@ export async function runDispatch({ startDir, now, deps } = {}) {
|
|
|
99
99
|
* session-lock.mjs — returns its result VERBATIM so callers (the skill prose,
|
|
100
100
|
* Wave-4 tests) can branch on the exact `{ ok, reason, ... }` contract.
|
|
101
101
|
*
|
|
102
|
-
* On `ok:false` (race lost / busy — reasons: active, stale-
|
|
103
|
-
*
|
|
102
|
+
* On `ok:false` (race lost / busy — reasons: active, stale-heartbeat,
|
|
103
|
+
* fs-error, ...) the dispatcher must exclude that repo and
|
|
104
104
|
* re-rank the remaining free candidates. On `ok:true` the claim is held and the
|
|
105
105
|
* coordinator may launch the chosen entry command.
|
|
106
106
|
*
|