@opengsd/gsd-core 1.7.0-rc.4 → 1.7.0-rc.6

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 (113) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.opencode/plugins/gsd-core.js +20 -0
  4. package/agents/gsd-doc-classifier.md +105 -0
  5. package/agents/gsd-doc-synthesizer.md +61 -0
  6. package/agents/gsd-ui-checker.md +30 -0
  7. package/agents/gsd-ui-researcher.md +1 -0
  8. package/bin/install.js +1568 -622
  9. package/gsd-core/bin/gsd-tools.cjs +40 -1
  10. package/gsd-core/bin/lib/api-coverage.cjs +466 -0
  11. package/gsd-core/bin/lib/audit.cjs +6 -3
  12. package/gsd-core/bin/lib/capability-loader.cjs +11 -9
  13. package/gsd-core/bin/lib/capability-registry.cjs +761 -84
  14. package/gsd-core/bin/lib/capability-validator.cjs +56 -18
  15. package/gsd-core/bin/lib/capability-writer.cjs +10 -1
  16. package/gsd-core/bin/lib/check-command-router.cjs +242 -3
  17. package/gsd-core/bin/lib/commands.cjs +7 -5
  18. package/gsd-core/bin/lib/config-loader.cjs +1 -0
  19. package/gsd-core/bin/lib/config.cjs +96 -0
  20. package/gsd-core/bin/lib/core-utils.cjs +4 -1
  21. package/gsd-core/bin/lib/host-integration-adapters/cline-sdk-binding.cjs +234 -0
  22. package/gsd-core/bin/lib/host-integration-adapters/imperative-hook-bus.cjs +145 -0
  23. package/gsd-core/bin/lib/host-integration.cjs +45 -4
  24. package/gsd-core/bin/lib/init.cjs +76 -39
  25. package/gsd-core/bin/lib/install-effort-resolver.cjs +213 -0
  26. package/gsd-core/bin/lib/install-engine.cjs +228 -18
  27. package/gsd-core/bin/lib/installer-migration-report.cjs +7 -0
  28. package/gsd-core/bin/lib/loop-resolver.cjs +68 -17
  29. package/gsd-core/bin/lib/markdown-sectionizer.cjs +50 -11
  30. package/gsd-core/bin/lib/mcp-server.cjs +18 -7
  31. package/gsd-core/bin/lib/milestone.cjs +3 -3
  32. package/gsd-core/bin/lib/normalize-test-command.cjs +187 -0
  33. package/gsd-core/bin/lib/phase-id.cjs +132 -3
  34. package/gsd-core/bin/lib/phase.cjs +78 -16
  35. package/gsd-core/bin/lib/planning-workspace.cjs +17 -0
  36. package/gsd-core/bin/lib/review-reviewer-selection.cjs +24 -7
  37. package/gsd-core/bin/lib/roadmap-command-router.cjs +5 -4
  38. package/gsd-core/bin/lib/roadmap-parser.cjs +21 -30
  39. package/gsd-core/bin/lib/roadmap-upgrade.cjs +9 -9
  40. package/gsd-core/bin/lib/roadmap.cjs +42 -56
  41. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +248 -44
  42. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +2 -2
  43. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +39 -23
  44. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +19 -5
  45. package/gsd-core/bin/lib/runtime-homes.cjs +30 -0
  46. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +534 -37
  47. package/gsd-core/bin/lib/runtime-name-policy.cjs +63 -5
  48. package/gsd-core/bin/lib/security.cjs +6 -36
  49. package/gsd-core/bin/lib/shell-command-projection.cjs +115 -2
  50. package/gsd-core/bin/lib/spec-section.cjs +111 -0
  51. package/gsd-core/bin/lib/stale-bake-guard.cjs +30 -10
  52. package/gsd-core/bin/lib/state-transition.cjs +1 -1
  53. package/gsd-core/bin/lib/state.cjs +24 -24
  54. package/gsd-core/bin/lib/surface.cjs +40 -6
  55. package/gsd-core/bin/lib/uat.cjs +4 -1
  56. package/gsd-core/bin/lib/ui-consideration-probe.cjs +249 -0
  57. package/gsd-core/bin/lib/validate.cjs +15 -6
  58. package/gsd-core/bin/lib/verify.cjs +33 -37
  59. package/gsd-core/bin/shared/config-schema.manifest.json +2 -0
  60. package/gsd-core/bin/shared/model-catalog.json +14 -9
  61. package/gsd-core/references/api-coverage.md +104 -0
  62. package/gsd-core/references/model-profiles.md +2 -2
  63. package/gsd-core/references/planning-config.md +2 -0
  64. package/gsd-core/references/specless-probe-fallback.md +172 -0
  65. package/gsd-core/references/ui-consideration-probe.md +73 -0
  66. package/gsd-core/templates/UI-SPEC.md +25 -0
  67. package/gsd-core/templates/VALIDATION.md +2 -0
  68. package/gsd-core/templates/config.json +2 -1
  69. package/gsd-core/workflows/audit-fix.md +9 -1
  70. package/gsd-core/workflows/audit-milestone.md +7 -4
  71. package/gsd-core/workflows/code-review-fix.md +7 -3
  72. package/gsd-core/workflows/code-review.md +4 -1
  73. package/gsd-core/workflows/discuss-phase-assumptions.md +4 -1
  74. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +8 -4
  75. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +42 -0
  76. package/gsd-core/workflows/execute-phase.md +1 -25
  77. package/gsd-core/workflows/plan-phase.md +37 -2
  78. package/gsd-core/workflows/quick.md +2 -2
  79. package/gsd-core/workflows/review.md +59 -13
  80. package/gsd-core/workflows/settings-advanced.md +12 -9
  81. package/gsd-core/workflows/settings.md +2 -2
  82. package/gsd-core/workflows/ui-phase.md +146 -1
  83. package/gsd-core/workflows/validate-phase.md +2 -2
  84. package/gsd-core/workflows/verify-phase.md +3 -2
  85. package/gsd-core/workflows/verify-work.md +38 -0
  86. package/hooks/dist/gsd-cursor-pre-tool.js +76 -0
  87. package/hooks/dist/gsd-cursor-stop.js +48 -0
  88. package/hooks/dist/gsd-cursor-subagent-start.js +50 -0
  89. package/hooks/dist/gsd-cursor-subagent-stop.js +40 -0
  90. package/hooks/dist/gsd-windsurf-pre-command.js +275 -0
  91. package/hooks/dist/gsd-windsurf-pre-write.js +132 -0
  92. package/hooks/dist/managed-hooks-registry.cjs +6 -0
  93. package/hooks/gsd-cursor-pre-tool.js +76 -0
  94. package/hooks/gsd-cursor-stop.js +48 -0
  95. package/hooks/gsd-cursor-subagent-start.js +50 -0
  96. package/hooks/gsd-cursor-subagent-stop.js +40 -0
  97. package/hooks/gsd-windsurf-pre-command.js +275 -0
  98. package/hooks/gsd-windsurf-pre-write.js +132 -0
  99. package/hooks/managed-hooks-registry.cjs +6 -0
  100. package/package.json +9 -4
  101. package/pi/gsd.cjs +354 -0
  102. package/scripts/build-hooks.js +8 -1
  103. package/scripts/gen-golden-install-parity-zcode.cjs +11 -1
  104. package/scripts/gen-registry.cjs +128 -0
  105. package/scripts/lint-phase-id-drift.cjs +150 -0
  106. package/scripts/lint-test-file-count.allowlist.json +2 -1
  107. package/scripts/registry-schema.cjs +565 -0
  108. package/scripts/run-tests.cjs +21 -1
  109. package/scripts/validate-registry.cjs +117 -0
  110. package/vscode/browser.js +197 -0
  111. package/vscode/extension.js +383 -0
  112. package/vscode/host-binding.js +113 -0
  113. package/vscode/package.json +96 -0
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Cline SDK binding — AgentPlugin + createAgentModel adapters
3
+ * (ADR-1239 Phase D / #2090).
4
+ *
5
+ * Two Context7-verified UPGRADES the file-convention projection ignored, now
6
+ * delivered through the negotiated `hookBus: host` + `modelMode: active`
7
+ * interface points:
8
+ *
9
+ * UPGRADE 1 — `AgentPlugin.hooks.beforeTool` planning-artifact guard.
10
+ * Re-implements the `.clinerules/hooks/PreToolUse` file-convention hook
11
+ * (issue #787) as a real Cline SDK AgentPlugin. Guard semantics are
12
+ * preserved EXACTLY: fail-open, cancel (skip) write-class calls targeting
13
+ * `.planning/`, pass through everything else. The SDK maps the file hook's
14
+ * `{cancel:true, errorMessage}` to `{skip:true, reason}` (beforeTool
15
+ * contract).
16
+ * Cite: https://github.com/cline/cline/blob/main/docs/sdk/plugins.mdx
17
+ * https://github.com/cline/cline/blob/main/sdk/packages/agents/README.md
18
+ *
19
+ * UPGRADE 2 — `DefaultGateway.createAgentModel({providerId, modelId})`.
20
+ * Resolves GSD's per-subagent `model_overrides` / `model_profile_overrides`
21
+ * (already used for OpenCode/Codex passive hosts) into the createAgentModel
22
+ * call params for cline's active model mode. The host gateway owns the
23
+ * actual model instantiation; this binding resolves WHICH model an
24
+ * overridden subagent should use.
25
+ * Cite: https://github.com/cline/cline/blob/main/docs/sdk/reference/gateway.mdx
26
+ * https://github.com/cline/cline/blob/main/sdk/packages/llms/README.md
27
+ *
28
+ * This module is PURE (no I/O, no SDK import): the real `@cline/sdk` is a
29
+ * fast-moving package set not linked at build/test time, so the binding exposes
30
+ * the decision functions a host plugin/gateway would call. Tests drive payloads
31
+ * through them directly (same mock-the-SDK pattern as the VS Code reference
32
+ * binding, tests/fixtures/vscode-host-binding.cjs).
33
+ */
34
+ 'use strict';
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.DEFAULT_CLINE_PROVIDER_ID = exports.clineGsdPlugin = exports.PLANNING_GUARD_REASON = exports.PLANNING_PATH_PATTERN = exports.WRITE_TOOL_PATTERN = void 0;
37
+ exports.evaluateBeforeTool = evaluateBeforeTool;
38
+ exports.inferProviderId = inferProviderId;
39
+ exports.resolveClineAgentModelParams = resolveClineAgentModelParams;
40
+ // ---------------------------------------------------------------------------
41
+ // UPGRADE 1 — beforeTool planning-artifact guard
42
+ // ---------------------------------------------------------------------------
43
+ /**
44
+ * Write-class tool-verb detector. Matches the SAME regex as the #787
45
+ * PreToolUse file-convention hook so the guard behaves identically.
46
+ * Case-insensitive (the SDK delivers tool names in varying case).
47
+ */
48
+ exports.WRITE_TOOL_PATTERN = /write|edit|replace|create|delete|remove|append|apply|patch|insert|mkdir/i;
49
+ /**
50
+ * `.planning/` path detector. Matches `.planning` preceded by start-of-string
51
+ * or a path separator (posix `/` or windows `\`) and followed by a separator or
52
+ * end-of-string — so `.planning-readme.txt` is NOT falsely matched. Mirrors the
53
+ * PreToolUse hook's `(^|[\\/])\.planning([\\/]|$)` exactly.
54
+ */
55
+ exports.PLANNING_PATH_PATTERN = /(^|[\\/])\.planning([\\/]|$)/;
56
+ /**
57
+ * The user-visible reason returned when a `.planning/` write is blocked.
58
+ * Preserves the PreToolUse hook's errorMessage text so the guard behaves
59
+ * identically to the user (cancel→skip, errorMessage→reason).
60
+ */
61
+ exports.PLANNING_GUARD_REASON = Object.freeze('GSD: .planning/ artifacts are managed by GSD workflows. Edit them only through a /gsd-* command, not directly.');
62
+ /**
63
+ * Path-bearing field-name detector. Only PATH-keyed field values are inspected,
64
+ * so a document that merely mentions ".planning/" in its body content is never
65
+ * falsely blocked. Mirrors the PreToolUse hook's PATH_KEY regex exactly.
66
+ */
67
+ const PATH_KEY_PATTERN = /^(path|file|file_?path|filepath|target_?path|target|dir|directory|uri|filename)$/i;
68
+ /**
69
+ * Collect PATH-bearing string field values from an object tree, mirroring the
70
+ * PreToolUse hook's bounded walk. Pure; never throws.
71
+ */
72
+ function collectPathValues(root) {
73
+ const paths = [];
74
+ const walk = (v, depth) => {
75
+ if (depth > 5 || paths.length > 64)
76
+ return;
77
+ if (Array.isArray(v)) {
78
+ for (const x of v)
79
+ walk(x, depth + 1);
80
+ return;
81
+ }
82
+ if (v && typeof v === 'object') {
83
+ const obj = v;
84
+ for (const k of Object.keys(obj)) {
85
+ const val = obj[k];
86
+ if (typeof val === 'string' && PATH_KEY_PATTERN.test(k)) {
87
+ paths.push(val);
88
+ }
89
+ else {
90
+ walk(val, depth + 1);
91
+ }
92
+ }
93
+ }
94
+ };
95
+ walk(root, 0);
96
+ return paths;
97
+ }
98
+ /**
99
+ * Resolve a tool name from a beforeTool payload's `tool` field, which may be a
100
+ * string or an object with a `name` property. Returns '' when absent (treated
101
+ * as non-write-class → allow, fail-open).
102
+ */
103
+ function resolveToolName(tool) {
104
+ if (!tool)
105
+ return '';
106
+ if (typeof tool === 'string')
107
+ return tool;
108
+ const name = tool.name;
109
+ return typeof name === 'string' ? name : '';
110
+ }
111
+ /**
112
+ * The pure guard decision: given a beforeTool payload, decide skip (cancel) or
113
+ * allow. Fail-OPEN — any malformed input, missing tool, or thrown error returns
114
+ * 'allow' (the guard never blocks on a defect, mirroring the PreToolUse hook).
115
+ *
116
+ * @returns `{decision:'skip', reason}` for a write-class call targeting
117
+ * `.planning/`; `{decision:'allow'}` for everything else.
118
+ */
119
+ function evaluateBeforeTool(payload) {
120
+ try {
121
+ if (!payload)
122
+ return { decision: 'allow' };
123
+ const toolName = resolveToolName(payload.tool);
124
+ if (!toolName)
125
+ return { decision: 'allow' };
126
+ const isWrite = exports.WRITE_TOOL_PATTERN.test(toolName);
127
+ if (!isWrite)
128
+ return { decision: 'allow' };
129
+ const paths = collectPathValues(payload.input);
130
+ const isPlanningPath = (s) => exports.PLANNING_PATH_PATTERN.test(s);
131
+ if (paths.some(isPlanningPath)) {
132
+ return { decision: 'skip', reason: exports.PLANNING_GUARD_REASON };
133
+ }
134
+ return { decision: 'allow' };
135
+ }
136
+ catch {
137
+ // Fail-open: a defect in the guard never blocks the user's operation.
138
+ return { decision: 'allow' };
139
+ }
140
+ }
141
+ /**
142
+ * The Cline `AgentPlugin` shape (Context7 /cline/cline). A plugin implements
143
+ * `setup({agentId})` returning `{hooks, tools}`. The `beforeTool` hook returns
144
+ * `{skip:true, reason}` to block or `undefined` to allow.
145
+ *
146
+ * This object is the portable plugin a host loads from `~/.cline/plugins/`
147
+ * (analogous to `.opencode/plugins/gsd-core.js`). Its `beforeTool` delegates to
148
+ * the pure `evaluateBeforeTool` so the decision logic is testable without the
149
+ * SDK linked.
150
+ */
151
+ exports.clineGsdPlugin = Object.freeze({
152
+ name: 'gsd-planning-guard',
153
+ setup(_ctx) {
154
+ return {
155
+ hooks: {
156
+ beforeTool(payload) {
157
+ const decision = evaluateBeforeTool(payload);
158
+ return decision.decision === 'skip' ? { skip: true, reason: decision.reason } : undefined;
159
+ },
160
+ },
161
+ };
162
+ },
163
+ });
164
+ // ---------------------------------------------------------------------------
165
+ // UPGRADE 2 — createAgentModel model-override resolution
166
+ // ---------------------------------------------------------------------------
167
+ /**
168
+ * The fallback provider id when a model id does not match a known provider
169
+ * family. Anthropic is cline's most common default; the host gateway retains
170
+ * the final say over provider resolution.
171
+ */
172
+ exports.DEFAULT_CLINE_PROVIDER_ID = 'anthropic';
173
+ /**
174
+ * Infer a `providerId` (the createAgentModel first arg) from a model id by
175
+ * matching known provider families. Returns DEFAULT_CLINE_PROVIDER_ID for an
176
+ * unrecognized or empty id (fail-safe — the gateway applies its own default).
177
+ *
178
+ * Pure string-prefix classification; does not validate the id is a real model.
179
+ */
180
+ function inferProviderId(modelId) {
181
+ if (typeof modelId !== 'string' || modelId.length === 0)
182
+ return exports.DEFAULT_CLINE_PROVIDER_ID;
183
+ const lower = modelId.toLowerCase();
184
+ if (lower.startsWith('claude'))
185
+ return 'anthropic';
186
+ if (lower.startsWith('gpt') || lower.startsWith('o1') || lower.startsWith('o3') || lower.startsWith('o4'))
187
+ return 'openai';
188
+ if (lower.startsWith('gemini'))
189
+ return 'google';
190
+ if (lower.startsWith('deepseek'))
191
+ return 'deepseek';
192
+ return exports.DEFAULT_CLINE_PROVIDER_ID;
193
+ }
194
+ /**
195
+ * Resolve the createAgentModel call params for a cline subagent from GSD's model
196
+ * override config. Mirrors the precedence OpenCode/Codex use (passive hosts
197
+ * embed the resolved model into agent frontmatter); for cline's active model
198
+ * mode the same resolution flows to `gateway.createAgentModel(params)`.
199
+ *
200
+ * Precedence (matches GSD's model_overrides > model_profile_overrides contract):
201
+ * 1. `modelOverrides[agentType]` — direct per-agent override
202
+ * 2. `modelProfileOverrides[profile][agentType]` — profile-scoped override
203
+ * 3. null — no override; the host gateway applies its own default
204
+ *
205
+ * Pure; never throws. Non-string / empty override values are ignored (fail-safe).
206
+ *
207
+ * @returns the `{providerId, modelId}` for createAgentModel, or null when no
208
+ * override is configured (the gateway default applies — GSD does NOT
209
+ * call createAgentModel in that case).
210
+ */
211
+ function resolveClineAgentModelParams(args) {
212
+ const { agentType, modelOverrides, modelProfileOverrides, profile } = args;
213
+ if (!agentType || typeof agentType !== 'string')
214
+ return null;
215
+ // 1. Direct per-agent override wins.
216
+ if (modelOverrides && typeof modelOverrides === 'object') {
217
+ const direct = modelOverrides[agentType];
218
+ if (typeof direct === 'string' && direct.length > 0) {
219
+ return { providerId: inferProviderId(direct), modelId: direct };
220
+ }
221
+ }
222
+ // 2. Profile-scoped override.
223
+ if (modelProfileOverrides && typeof modelProfileOverrides === 'object' && profile) {
224
+ const profileEntry = modelProfileOverrides[profile];
225
+ if (profileEntry && typeof profileEntry === 'object') {
226
+ const profileModel = profileEntry[agentType];
227
+ if (typeof profileModel === 'string' && profileModel.length > 0) {
228
+ return { providerId: inferProviderId(profileModel), modelId: profileModel };
229
+ }
230
+ }
231
+ }
232
+ // 3. No override — gateway default applies.
233
+ return null;
234
+ }
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Imperative hook-bus adapter — descriptor-driven hooks.json binding
3
+ * (ADR-1239 Phase D / #2089).
4
+ *
5
+ * Generalizes the Cursor-specific `writeCursorHooksJson`/`removeCursorHooksJson`
6
+ * into a descriptor-driven hook-bus binding that reads the negotiated `hookBus`
7
+ * axis + the host's documented hook-event list (from
8
+ * `runtime.hostBehaviors.managedHookEvents`), NOT a hardcoded
9
+ * `sessionStart`/`postToolUse` pair.
10
+ *
11
+ * This module is PURE (no I/O): it resolves the event→script mapping and builds
12
+ * the hooks.json entry manifest. The actual file I/O (copying scripts, writing
13
+ * hooks.json) stays in `runtime-hooks-surface.cts`, which calls into the pure
14
+ * functions exported here. This separation makes the binding testable without a
15
+ * filesystem.
16
+ *
17
+ * Cursor hook-event universe (closed vocabulary per ADR-1239,
18
+ * https://cursor.com/docs/hooks):
19
+ * sessionStart, sessionEnd, preToolUse, postToolUse, subagentStart,
20
+ * subagentStop, beforeShellExecution, afterShellExecution,
21
+ * afterMCPExecution, afterFileEdit, preCompact, stop,
22
+ * beforeTabFileRead, afterTabFileEdit, workspaceOpen
23
+ *
24
+ * GSD registers for the 6 events in the portable floor + subagent lifecycle
25
+ * (AC4a upgrade, #2089):
26
+ * sessionStart, postToolUse, preToolUse, stop, subagentStart, subagentStop
27
+ */
28
+ 'use strict';
29
+ Object.defineProperty(exports, "__esModule", { value: true });
30
+ exports.GSD_HOOK_MARKER = exports.CURSOR_EVENT_SCRIPT_MAP = exports.CURSOR_HOOK_EVENTS = void 0;
31
+ exports.resolveManagedHookEvents = resolveManagedHookEvents;
32
+ exports.resolveHookScripts = resolveHookScripts;
33
+ exports.buildHookBusEntries = buildHookBusEntries;
34
+ /**
35
+ * The full set of Cursor hook events GSD can register for.
36
+ * Frozen closed vocabulary — adding an event requires updating both this set
37
+ * and the event→script mapping below.
38
+ */
39
+ exports.CURSOR_HOOK_EVENTS = Object.freeze([
40
+ 'sessionStart',
41
+ 'postToolUse',
42
+ 'preToolUse',
43
+ 'stop',
44
+ 'subagentStart',
45
+ 'subagentStop',
46
+ ]);
47
+ /**
48
+ * Event → hook-script mapping. Each event maps to a standalone `.js` script
49
+ * under `hooks/` that Cursor invokes via `hooks.json`.
50
+ *
51
+ * Convention: `gsd-cursor-<kebab-event>.js`. The script files are authored in
52
+ * `hooks/` and copied to `<configDir>/hooks/` during install by
53
+ * `runtime-hooks-surface.cts`.
54
+ */
55
+ exports.CURSOR_EVENT_SCRIPT_MAP = Object.freeze({
56
+ sessionStart: 'gsd-cursor-session-start.js',
57
+ postToolUse: 'gsd-cursor-post-tool.js',
58
+ preToolUse: 'gsd-cursor-pre-tool.js',
59
+ stop: 'gsd-cursor-stop.js',
60
+ subagentStart: 'gsd-cursor-subagent-start.js',
61
+ subagentStop: 'gsd-cursor-subagent-stop.js',
62
+ });
63
+ /**
64
+ * The GSD-managed marker written into each hooks.json entry so the
65
+ * reconcile pass can distinguish GSD-owned entries from user-owned ones.
66
+ */
67
+ exports.GSD_HOOK_MARKER = 'gsd-managed';
68
+ /**
69
+ * Resolve the managed hook events from a runtime descriptor's
70
+ * `hostBehaviors.managedHookEvents` list. Falls back to the full
71
+ * `CURSOR_HOOK_EVENTS` set when the descriptor does not declare the list
72
+ * (backward-compat for descriptors predating #2089).
73
+ *
74
+ * Pure: no I/O, never throws. Unknown event names are silently filtered
75
+ * (fail-closed — an unrecognized event is never registered). Falls back to
76
+ * the full CURSOR_HOOK_EVENTS set when the descriptor is absent or all entries
77
+ * are unrecognized (ensures the portable-event floor is always covered).
78
+ *
79
+ * @param managedHookEvents - the descriptor's `hostBehaviors.managedHookEvents` array
80
+ * @returns a deduplicated, validated array of event names
81
+ */
82
+ function resolveManagedHookEvents(managedHookEvents) {
83
+ if (!Array.isArray(managedHookEvents) || managedHookEvents.length === 0) {
84
+ return exports.CURSOR_HOOK_EVENTS;
85
+ }
86
+ const valid = new Set(exports.CURSOR_HOOK_EVENTS);
87
+ const seen = new Set();
88
+ const result = [];
89
+ for (const ev of managedHookEvents) {
90
+ if (typeof ev === 'string' && valid.has(ev) && !seen.has(ev)) {
91
+ seen.add(ev);
92
+ result.push(ev);
93
+ }
94
+ }
95
+ return result.length > 0 ? result : exports.CURSOR_HOOK_EVENTS;
96
+ }
97
+ /**
98
+ * Build the list of hook script files that need to be copied for the given
99
+ * managed events. Each event maps to a script via `CURSOR_EVENT_SCRIPT_MAP`.
100
+ *
101
+ * Pure: returns a deduplicated array of script filenames.
102
+ *
103
+ * @param events - the managed event names (validated by `resolveManagedHookEvents`)
104
+ * @returns array of script filenames (e.g. `['gsd-cursor-session-start.js', ...]`)
105
+ */
106
+ function resolveHookScripts(events) {
107
+ const scripts = [];
108
+ const seen = new Set();
109
+ for (const ev of events) {
110
+ const script = exports.CURSOR_EVENT_SCRIPT_MAP[ev];
111
+ if (script && !seen.has(script)) {
112
+ seen.add(script);
113
+ scripts.push(script);
114
+ }
115
+ }
116
+ return scripts;
117
+ }
118
+ /**
119
+ * Build the hooks.json managed-entry manifest for the given events.
120
+ * Each entry is `{ type: 'command', command: <cmd>, [GSD_HOOK_MARKER]: true }`.
121
+ *
122
+ * The `command` string is built by the caller (it requires platform-specific
123
+ * node-runner resolution from `runtime-hooks-surface.cts`). This function
124
+ * receives a pre-built `event → command` map and attaches the marker.
125
+ *
126
+ * Pure: no I/O.
127
+ *
128
+ * @param events - the managed event names
129
+ * @param commands - a map of event → command string (built by the caller)
130
+ * @returns a map of event → managed entry, ready for hooks.json reconciliation
131
+ */
132
+ function buildHookBusEntries(events, commands) {
133
+ const entries = {};
134
+ for (const ev of events) {
135
+ const cmd = commands[ev];
136
+ if (cmd) {
137
+ entries[ev] = {
138
+ type: 'command',
139
+ command: cmd,
140
+ [exports.GSD_HOOK_MARKER]: true,
141
+ };
142
+ }
143
+ }
144
+ return entries;
145
+ }
@@ -430,12 +430,53 @@ function hookEventSurfaceFor(hookEvents) {
430
430
  // events (session/tool/file/permission); pi ~30 fine-grained extension events;
431
431
  // 'none' = the host exposes no extension surface and the engine owns the bus
432
432
  // (VS Code). Declarative hosts (no plugin API) do not set `extensionEvents`.
433
+ // OpenCode's plugin event surface (ADR-1239 §research; ~25 documented events,
434
+ // GSD binds this subset). Hoisted to a named const — rather than duplicated
435
+ // object literals — so the `kilo` dialect below (#2093) can reuse the IDENTICAL
436
+ // array instead of a copy-pasted one that could silently drift out of sync.
437
+ const OPENCODE_EXTENSION_EVENTS = Object.freeze([
438
+ 'session.created', 'session.idle', 'experimental.session.compacting',
439
+ 'tool.execute.before', 'tool.execute.after', 'file.edited',
440
+ // #2087 — additional documented plugin events GSD binds (opencode.ai/docs/plugins):
441
+ // permission decisions + session error surface.
442
+ 'permission.asked', 'permission.replied', 'session.error',
443
+ ]);
433
444
  const EXTENSION_EVENT_SURFACES = Object.freeze({
434
- opencode: Object.freeze([
435
- 'session.created', 'session.idle', 'experimental.session.compacting',
436
- 'tool.execute.before', 'tool.execute.after', 'file.edited',
445
+ opencode: OPENCODE_EXTENSION_EVENTS,
446
+ // #2093 — Kilo Code is an OpenCode fork sharing the same plugin/extension
447
+ // event bus (host hook bus, UPGRADE 1): reuses OPENCODE_EXTENSION_EVENTS
448
+ // verbatim (not a re-derivation), so the two dialects stay pinned together
449
+ // by construction. See .kilo/plugins/gsd-core.js (copied verbatim from
450
+ // .opencode/plugins/gsd-core.js).
451
+ kilo: OPENCODE_EXTENSION_EVENTS,
452
+ // #2091 — Hermes Agent real plugin hook vocabulary (13 events).
453
+ // Cite: https://github.com/nousresearch/hermes-agent/blob/main/website/docs/user-guide/features/hooks.md
454
+ // Replaces the borrowed `hookEvents: "claude"` 6-event surface that silently
455
+ // never fired on Hermes.
456
+ hermes: Object.freeze([
457
+ 'pre_tool_call', 'post_tool_call',
458
+ 'pre_llm_call', 'post_llm_call',
459
+ 'on_session_start', 'on_session_end',
460
+ 'on_session_finalize', 'on_session_reset',
461
+ 'subagent_start', 'subagent_stop',
462
+ 'pre_gateway_dispatch', 'pre_approval_request',
463
+ 'transform_tool_result',
464
+ ]),
465
+ // #2102 Stage 2 — pi's real ExtensionAPI event vocabulary (~30 fine-grained
466
+ // extension events; documentation-sourced, ADR-1239 §research). Replaces the
467
+ // placeholder single-event ['tool_call'] surface — the Stage 1 value only
468
+ // covered the one event pi/gsd.cjs happened to bind at the time, not the
469
+ // full declared surface.
470
+ pi: Object.freeze([
471
+ 'session_start', 'project_trust', 'resources_discover', 'input',
472
+ 'before_agent_start', 'agent_start', 'message_start', 'message_update',
473
+ 'message_end', 'turn_start', 'context', 'before_provider_request',
474
+ 'after_provider_response', 'tool_execution_start', 'tool_execution_update',
475
+ 'tool_execution_end', 'tool_call', 'tool_result', 'turn_end', 'agent_end',
476
+ 'session_before_switch', 'session_shutdown', 'session_before_fork',
477
+ 'session_info_changed', 'session_before_compact', 'session_compact',
478
+ 'session_before_tree', 'session_tree', 'thinking_level_select', 'model_select',
437
479
  ]),
438
- pi: Object.freeze(['tool_call']),
439
480
  none: Object.freeze([]),
440
481
  });
441
482
  /**