@opengsd/gsd-core 1.4.0-rc.1 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/.claude-plugin/plugin.json +23 -0
  2. package/GEMINI.md +53 -0
  3. package/agents/gsd-ai-researcher.md +1 -1
  4. package/agents/gsd-code-fixer.md +1 -1
  5. package/agents/gsd-code-reviewer.md +1 -1
  6. package/agents/gsd-domain-researcher.md +1 -1
  7. package/agents/gsd-eval-auditor.md +1 -1
  8. package/agents/gsd-eval-planner.md +1 -1
  9. package/agents/gsd-framework-selector.md +1 -1
  10. package/agents/gsd-nyquist-auditor.md +1 -1
  11. package/agents/gsd-pattern-mapper.md +1 -1
  12. package/agents/gsd-security-auditor.md +1 -1
  13. package/agents/gsd-ui-auditor.md +1 -1
  14. package/agents/gsd-ui-checker.md +1 -1
  15. package/agents/gsd-ui-researcher.md +1 -1
  16. package/agents/gsd-user-profiler.md +1 -1
  17. package/bin/install.js +1911 -354
  18. package/commands/gsd/autonomous.md +2 -0
  19. package/commands/gsd/execute-phase.md +2 -0
  20. package/commands/gsd/plan-phase.md +2 -0
  21. package/commands/gsd/progress.md +1 -0
  22. package/commands/gsd/stats.md +1 -0
  23. package/commands/gsd/update.md +3 -2
  24. package/gemini-extension.json +6 -0
  25. package/gsd-core/bin/check-latest-version.cjs +58 -4
  26. package/gsd-core/bin/lib/install-profiles.cjs +58 -0
  27. package/gsd-core/bin/lib/installer-migration-report.cjs +3 -0
  28. package/gsd-core/bin/lib/installer-migrations/000-first-time-baseline.cjs +1 -1
  29. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +67 -9
  30. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +56 -0
  31. package/gsd-core/bin/lib/runtime-homes.cjs +32 -11
  32. package/gsd-core/bin/lib/shell-command-projection.cjs +6 -0
  33. package/gsd-core/bin/lib/surface.cjs +54 -11
  34. package/gsd-core/workflows/help/modes/full.md +2 -1
  35. package/gsd-core/workflows/review.md +2 -2
  36. package/gsd-core/workflows/update.md +32 -5
  37. package/hooks/dist/gsd-config-reload.js +133 -0
  38. package/hooks/dist/gsd-cursor-post-tool.js +75 -0
  39. package/hooks/dist/gsd-cursor-session-start.js +52 -0
  40. package/hooks/dist/managed-hooks-registry.cjs +3 -0
  41. package/hooks/gsd-config-reload.js +133 -0
  42. package/hooks/gsd-cursor-post-tool.js +75 -0
  43. package/hooks/gsd-cursor-session-start.js +52 -0
  44. package/hooks/hooks.json +69 -0
  45. package/hooks/managed-hooks-registry.cjs +3 -0
  46. package/package.json +5 -1
  47. package/scripts/build-hooks.js +7 -0
  48. package/scripts/changeset/cli.cjs +53 -10
  49. package/scripts/ci-test-scope.cjs +120 -18
  50. package/scripts/issue-dedupe.cjs +278 -0
  51. package/scripts/lint-test-file-count.allowlist.json +1 -0
  52. package/scripts/release-notes/discord-release-summary.cjs +373 -0
  53. package/scripts/research-profiles.cjs +3 -3
  54. package/scripts/sync-manifest-versions.cjs +119 -0
@@ -2,6 +2,8 @@
2
2
  name: gsd:autonomous
3
3
  description: Run all remaining phases autonomously — discuss→plan→execute per phase
4
4
  argument-hint: "[--from N] [--to N] [--only N] [--interactive]"
5
+ context: fork
6
+ effort: xhigh
5
7
  allowed-tools:
6
8
  - Read
7
9
  - Write
@@ -2,6 +2,8 @@
2
2
  name: gsd:execute-phase
3
3
  description: Execute all plans in a phase with wave-based parallelization
4
4
  argument-hint: "<phase-number> [--wave N] [--gaps-only] [--interactive] [--tdd]"
5
+ context: fork
6
+ effort: xhigh
5
7
  allowed-tools:
6
8
  - Read
7
9
  - Write
@@ -2,6 +2,8 @@
2
2
  name: gsd:plan-phase
3
3
  description: Create detailed phase plan (PLAN.md) with verification loop
4
4
  argument-hint: "[phase] [--auto] [--research] [--skip-research] [--research-phase <N>] [--view] [--gaps] [--skip-verify] [--prd <file>] [--ingest <path-or-glob>] [--ingest-format <auto|nygard|madr|narrative>] [--reviews] [--text] [--tdd] [--mvp]"
5
+ context: fork
6
+ effort: xhigh
5
7
  allowed-tools:
6
8
  - Read
7
9
  - Write
@@ -2,6 +2,7 @@
2
2
  name: gsd:progress
3
3
  description: Check progress, advance workflow, or dispatch freeform intent — the unified GSD situational command
4
4
  argument-hint: "[--forensic | --next | --do \"task description\"]"
5
+ effort: low
5
6
  allowed-tools:
6
7
  - Read
7
8
  - Bash
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: gsd:stats
3
3
  description: Display project statistics — phases, plans, requirements, git metrics, and timeline
4
+ effort: low
4
5
  allowed-tools:
5
6
  - Read
6
7
  - Bash
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: gsd:update
3
3
  description: Update GSD to latest version with changelog display
4
- argument-hint: "[--sync | --reapply]"
4
+ argument-hint: "[--sync | --reapply | --next | --rc]"
5
5
  allowed-tools:
6
6
  - Read
7
7
  - Write
@@ -31,6 +31,7 @@ Routes to the update workflow which handles:
31
31
  <flags>
32
32
  - **--sync**: Sync managed GSD skills across runtime roots so multi-runtime users stay aligned after an update. Runs the sync-skills workflow (--from, --to, --dry-run, --apply flags supported).
33
33
  - **--reapply**: Reapply local modifications after a GSD update. Uses three-way comparison (pristine baseline, user-modified backup, newly installed version) to merge user customizations back. Runs the reapply-patches workflow.
34
+ - **--next** (alias **--rc**): Target the `@next` RC dist-tag instead of `@latest` so you can install or refresh a release candidate (e.g. `1.4.0-rc.1`) through the normal update flow — scope/runtime detection, changelog preview, custom-file backup, and cache clearing all still apply. Omitting it keeps targeting `@latest` (no change). See ADR #660 for the RC channel.
34
35
  - **(no flag)**: Standard update — check for new version, show changelog, install.
35
36
  </flags>
36
37
 
@@ -38,7 +39,7 @@ Routes to the update workflow which handles:
38
39
  Parse the first token of $ARGUMENTS:
39
40
  - If it is `--sync`: strip the flag, execute the sync-skills workflow (passing remaining args for --from/--to/--dry-run/--apply).
40
41
  - If it is `--reapply`: strip the flag, execute the reapply-patches workflow.
41
- - Otherwise: execute the update workflow end-to-end.
42
+ - Otherwise (including `--next` / `--rc`): execute the update workflow end-to-end, passing `$ARGUMENTS` through so the workflow's parse_update_channel step can select the release channel.
42
43
 
43
44
  </process>
44
45
 
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "gsd-core",
3
+ "version": "1.4.0",
4
+ "description": "GSD Core — a meta-prompting, context engineering, and spec-driven development system for AI coding agents. Loads gsd's operating context into every Gemini CLI session.",
5
+ "contextFileName": "GEMINI.md"
6
+ }
@@ -37,18 +37,64 @@ const CHECK_REASON = Object.freeze({
37
37
 
38
38
  const SEMVER_RE = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
39
39
 
40
+ // #815: the one RC channel ADR #660 sanctions, plus the stable default.
41
+ // An allowlist (not a free string) keeps a typo from silently resolving
42
+ // `npm view` to an empty or foreign dist-tag.
43
+ const ALLOWED_TAGS = Object.freeze(['latest', 'next']);
44
+
45
+ /**
46
+ * Build the `npm view` args for a dist-tag. `latest` keeps the bare package
47
+ * spec so the default invocation is byte-for-byte identical to before tag
48
+ * support existed (#815); any other allowlisted tag appends `@<tag>` so
49
+ * `npm view @opengsd/gsd-core@next version` resolves the RC channel (#660).
50
+ */
51
+ function buildViewArgs(tag = 'latest') {
52
+ if (!ALLOWED_TAGS.includes(tag)) {
53
+ throw new RangeError(`invalid dist-tag '${tag}'; allowed: ${ALLOWED_TAGS.join(', ')}`);
54
+ }
55
+ const spec = tag === 'latest' ? PACKAGE_NAME : `${PACKAGE_NAME}@${tag}`;
56
+ return ['view', spec, 'version'];
57
+ }
58
+
59
+ /**
60
+ * Resolve the requested dist-tag from argv. Defaults to `latest` (no flag =>
61
+ * no behavior change). Restricted to ALLOWED_TAGS so a typo can't silently
62
+ * resolve to an empty/foreign tag (#815 alternative 1).
63
+ */
64
+ function resolveTag(argv) {
65
+ let val;
66
+ const eq = argv.find((a) => typeof a === 'string' && a.startsWith('--tag='));
67
+ if (eq !== undefined) {
68
+ val = eq.slice('--tag='.length);
69
+ } else {
70
+ const i = argv.indexOf('--tag');
71
+ if (i === -1) return 'latest';
72
+ val = argv[i + 1];
73
+ }
74
+ if (!val || !ALLOWED_TAGS.includes(val)) {
75
+ throw new RangeError(
76
+ `invalid --tag '${val || ''}'; allowed: ${ALLOWED_TAGS.join(', ')}`,
77
+ );
78
+ }
79
+ return val;
80
+ }
81
+
40
82
  /**
41
83
  * Pure-ish: takes an injected spawn function so tests don't actually run npm.
42
84
  * In production, defaults to execNpm() from the shell-projection seam.
43
85
  */
44
86
  function checkLatestVersion(opts = {}) {
87
+ const tag = opts.tag || 'latest';
88
+ if (!ALLOWED_TAGS.includes(tag)) {
89
+ throw new RangeError(`invalid dist-tag '${tag}'; allowed: ${ALLOWED_TAGS.join(', ')}`);
90
+ }
45
91
  // Default path routes through the shell-projection seam (execNpm owns the
46
92
  // Windows shell-flag policy and timeout default). The injection point
47
93
  // remains spawnSync-shaped for test compatibility — the adapter below
48
94
  // translates { exitCode } → { status } so the consumer logic is unchanged.
49
95
  // Bounded at 15s so a hung registry doesn't block /gsd-update (#2993 CR).
50
96
  const defaultSpawn = () => {
51
- const r = execNpm(['view', PACKAGE_NAME, 'version'], { timeout: 15_000 });
97
+ const r = execNpm(buildViewArgs(tag), { timeout: 15_000 });
52
98
  return {
53
99
  status: r.exitCode,
54
100
  stdout: r.stdout,
@@ -90,8 +136,16 @@ function checkLatestVersion(opts = {}) {
90
136
  }
91
137
 
92
138
  function main() {
93
- const json = process.argv.includes('--json');
94
- const r = checkLatestVersion();
139
+ const argv = process.argv.slice(2);
140
+ const json = argv.includes('--json');
141
+ let tag;
142
+ try {
143
+ tag = resolveTag(argv);
144
+ } catch (e) {
145
+ process.stderr.write(`check-latest-version: ${e.message}\n`);
146
+ return 2;
147
+ }
148
+ const r = checkLatestVersion({ tag });
95
149
  if (json) {
96
150
  process.stdout.write(JSON.stringify(r) + '\n');
97
151
  } else if (r.ok) {
@@ -104,4 +158,4 @@ function main() {
104
158
 
105
159
  if (require.main === module) runMain(main);
106
160
 
107
- module.exports = { checkLatestVersion, CHECK_REASON, PACKAGE_NAME };
161
+ module.exports = { checkLatestVersion, CHECK_REASON, PACKAGE_NAME, ALLOWED_TAGS, buildViewArgs, resolveTag };
@@ -351,6 +351,63 @@ function stageSkillsForRuntimeAsSkills(srcCommandsDir, resolvedProfile, converte
351
351
  ensureExitCleanup();
352
352
  return stageDir;
353
353
  }
354
+ /**
355
+ * Stage converted command files as flat `.md` files.
356
+ *
357
+ * Analogous to `stageSkillsForRuntimeAsSkills` but for runtimes that use a
358
+ * flat commands directory (e.g. Cursor's `.cursor/commands/<name>.md`).
359
+ * Each source `.md` is passed through `converter` and written as a single flat
360
+ * `${stem}.md` file in the staging directory (no subdirectory, no prefix).
361
+ *
362
+ * The `_copyStaged` commands branch in install.js will add the prefix when
363
+ * copying staged files to the destination directory, so staged files must be
364
+ * named with just the stem (e.g. `help.md` not `gsd-help.md`).
365
+ *
366
+ * The `converter` receives `(content, ${prefix}${stem})` so it can embed the
367
+ * full command name (e.g. 'gsd-help') into the document body if needed.
368
+ *
369
+ * Used by the `convertedCommandsKind` layout descriptor in
370
+ * runtime-artifact-layout.cts (#785 — Cursor 1.6 slash commands).
371
+ *
372
+ * @param srcCommandsDir source commands directory (e.g. commands/gsd/)
373
+ * @param resolvedProfile profile filter — '*' for all, Set for subset
374
+ * @param converter (content, commandName) → string pure converter
375
+ * @param prefix command name prefix (for converter arg), e.g. 'gsd-'
376
+ */
377
+ function stageCommandsForRuntimeFlat(srcCommandsDir, resolvedProfile, converter, prefix) {
378
+ if (!node_fs_1.default.existsSync(srcCommandsDir))
379
+ return srcCommandsDir;
380
+ const stageDir = node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), 'gsd-profile-runtime-commands-'));
381
+ try {
382
+ const entries = node_fs_1.default.readdirSync(srcCommandsDir, { withFileTypes: true });
383
+ for (const entry of entries) {
384
+ if (!entry.isFile())
385
+ continue;
386
+ if (!entry.name.endsWith('.md'))
387
+ continue;
388
+ const stem = entry.name.slice(0, -3);
389
+ if (resolvedProfile.skills !== '*' && !(resolvedProfile.skills).has(stem))
390
+ continue;
391
+ const content = node_fs_1.default.readFileSync(node_path_1.default.join(srcCommandsDir, entry.name), 'utf8');
392
+ // Pass the full command name (with prefix) to the converter so it can
393
+ // reference the installed command name in the body (e.g. for descriptions).
394
+ // The staged file itself is named without the prefix; _copyStaged adds it.
395
+ const commandName = `${prefix}${stem}`;
396
+ const converted = converter(content, commandName);
397
+ node_fs_1.default.writeFileSync(node_path_1.default.join(stageDir, `${stem}.md`), converted);
398
+ }
399
+ }
400
+ catch (err) {
401
+ try {
402
+ node_fs_1.default.rmSync(stageDir, { recursive: true, force: true });
403
+ }
404
+ catch { /* best-effort */ }
405
+ throw err;
406
+ }
407
+ STAGED_DIRS.add(stageDir);
408
+ ensureExitCleanup();
409
+ return stageDir;
410
+ }
354
411
  // ---------------------------------------------------------------------------
355
412
  // Profile marker persistence
356
413
  // ---------------------------------------------------------------------------
@@ -513,6 +570,7 @@ module.exports = {
513
570
  stageSkillsForProfile,
514
571
  stageAgentsForProfile,
515
572
  stageSkillsForRuntimeAsSkills,
573
+ stageCommandsForRuntimeFlat,
516
574
  STAGED_DIRS,
517
575
  readActiveProfile,
518
576
  writeActiveProfile,
@@ -34,7 +34,10 @@ const VALID_CHOICES = ['keep', 'remove'];
34
34
  exports.BUNDLED_GSD_HOOK_FILES = Object.freeze(new Set([
35
35
  'hooks/gsd-check-update-worker.js',
36
36
  'hooks/gsd-check-update.js',
37
+ 'hooks/gsd-config-reload.js',
37
38
  'hooks/gsd-context-monitor.js',
39
+ 'hooks/gsd-cursor-post-tool.js',
40
+ 'hooks/gsd-cursor-session-start.js',
38
41
  'hooks/gsd-graphify-update.sh',
39
42
  'hooks/gsd-phase-boundary.sh',
40
43
  'hooks/gsd-prompt-guard.js',
@@ -24,7 +24,7 @@ const RUNTIME_SURFACES = {
24
24
  kilo: ['gsd-core', 'command', 'skills', 'agents'],
25
25
  copilot: ['gsd-core', 'skills', 'agents'],
26
26
  antigravity: ['gsd-core', 'skills', 'agents'],
27
- cursor: ['gsd-core', 'skills', 'agents'],
27
+ cursor: ['gsd-core', 'skills', 'agents', 'hooks', 'hooks.json'],
28
28
  windsurf: ['gsd-core', 'skills', 'agents', 'rules'],
29
29
  augment: ['gsd-core', 'skills', 'agents'],
30
30
  trae: ['gsd-core', 'skills', 'agents', 'rules'],
@@ -18,7 +18,7 @@ const node_path_1 = __importDefault(require("node:path"));
18
18
  const node_fs_1 = __importDefault(require("node:fs"));
19
19
  // eslint-disable-next-line @typescript-eslint/no-require-imports
20
20
  const installProfiles = require("./install-profiles.cjs");
21
- const { stageSkillsForProfile, stageAgentsForProfile, stageSkillsForRuntimeAsSkills, } = installProfiles;
21
+ const { stageSkillsForProfile, stageAgentsForProfile, stageSkillsForRuntimeAsSkills, stageCommandsForRuntimeFlat, } = installProfiles;
22
22
  // In .cts (CommonJS output) files, `require` is available as a global.
23
23
  const _require = require;
24
24
  /**
@@ -175,6 +175,34 @@ function skillsKind(destSubpath, prefix, converterName, runtime, configDir) {
175
175
  },
176
176
  };
177
177
  }
178
+ /**
179
+ * Build a converted-commands kind descriptor for runtimes that use a flat
180
+ * commands directory with per-file conversion (e.g. Cursor 1.6 slash commands).
181
+ *
182
+ * Unlike `commandsKind` (which passes raw source files through), this kind
183
+ * applies `converterName` from bin/install.js exports to each file during
184
+ * staging, writing flat `${prefix}${stem}.md` files to the staged directory.
185
+ *
186
+ * The staged files are then written by `_copyStaged` (commands branch) which
187
+ * handles prefix logic via the existing layout machinery.
188
+ *
189
+ * @param destSubpath destination subpath within configDir (e.g. 'commands')
190
+ * @param prefix filename prefix, e.g. 'gsd-'
191
+ * @param converterName name of converter function in bin/install.js exports
192
+ * @param configDir runtime config dir (for .gsd-source marker resolution)
193
+ */
194
+ function convertedCommandsKind(destSubpath, prefix, converterName, configDir) {
195
+ return {
196
+ kind: 'commands',
197
+ destSubpath,
198
+ prefix,
199
+ stage: (resolved) => {
200
+ const installExports = getInstallExports();
201
+ const converter = installExports[converterName];
202
+ return stageCommandsForRuntimeFlat(findInstallSourceRoot(configDir), resolved, converter, prefix);
203
+ },
204
+ };
205
+ }
178
206
  // ---------------------------------------------------------------------------
179
207
  // Public API
180
208
  // ---------------------------------------------------------------------------
@@ -205,7 +233,14 @@ function resolveRuntimeArtifactLayout(runtime, configDir, scope = 'global') {
205
233
  }
206
234
  break;
207
235
  case 'cursor':
208
- kinds = [skillsKind('skills', 'gsd-', 'convertClaudeCommandToCursorSkill', 'cursor', configDir)];
236
+ // Cursor 1.6+ supports two artifact surfaces:
237
+ // 1. skills/gsd-<name>/SKILL.md — rich skills with frontmatter + adapter header
238
+ // 2. commands/gsd-<name>.md — plain markdown slash commands (no frontmatter)
239
+ // accessed via '/' in the Agent input (#785)
240
+ kinds = [
241
+ skillsKind('skills', 'gsd-', 'convertClaudeCommandToCursorSkill', 'cursor', configDir),
242
+ convertedCommandsKind('commands', 'gsd-', 'convertClaudeCommandToCursorCommand', configDir),
243
+ ];
209
244
  break;
210
245
  case 'gemini':
211
246
  kinds = [commandsKind('commands/gsd', 'gsd-', configDir)];
@@ -223,7 +258,10 @@ function resolveRuntimeArtifactLayout(runtime, configDir, scope = 'global') {
223
258
  kinds = [skillsKind('skills', 'gsd-', 'convertClaudeCommandToWindsurfSkill', 'windsurf', configDir)];
224
259
  break;
225
260
  case 'augment':
226
- kinds = [skillsKind('skills', 'gsd-', 'convertClaudeCommandToAugmentSkill', 'augment', configDir)];
261
+ kinds = [
262
+ commandsKind('commands', 'gsd-', configDir),
263
+ skillsKind('skills', 'gsd-', 'convertClaudeCommandToAugmentSkill', 'augment', configDir),
264
+ ];
227
265
  break;
228
266
  case 'trae':
229
267
  kinds = [skillsKind('skills', 'gsd-', 'convertClaudeCommandToTraeSkill', 'trae', configDir)];
@@ -235,20 +273,40 @@ function resolveRuntimeArtifactLayout(runtime, configDir, scope = 'global') {
235
273
  kinds = [skillsKind('skills/gsd', '', 'convertClaudeCommandToClaudeSkill', 'hermes', configDir)];
236
274
  break;
237
275
  case 'codebuddy':
238
- kinds = [skillsKind('skills', 'gsd-', 'convertClaudeCommandToCodebuddySkill', 'codebuddy', configDir)];
276
+ // CodeBuddy (Tencent) reads two user-level surfaces (codebuddy.ai/docs/cli):
277
+ // 1. commands/gsd-<name>.md — slash commands shown in the '/' menu (#789)
278
+ // 2. skills/gsd-<name>/SKILL.md — model-invocable skills, emitted with
279
+ // user-invocable:false so they stay OUT of '/' (the commands surface is
280
+ // the sole '/' entry point) — avoids a duplicated /gsd-* per workflow.
281
+ // Subagents (~/.codebuddy/agents/) are already emitted by the generic agents
282
+ // block in bin/install.js; MCP is excluded (gsd ships no MCP server).
283
+ kinds = [
284
+ convertedCommandsKind('commands', 'gsd-', 'convertClaudeCommandToCodebuddyCommand', configDir),
285
+ skillsKind('skills', 'gsd-', 'convertClaudeCommandToCodebuddySkill', 'codebuddy', configDir),
286
+ ];
239
287
  break;
240
288
  case 'cline':
241
- kinds = [];
289
+ kinds = scope === 'global' ? [skillsKind('skills', 'gsd-', 'convertClaudeCommandToClineSkill', 'cline', configDir)] : [];
242
290
  break;
243
291
  case 'opencode':
244
- kinds = [commandsKind('command', 'gsd-', configDir)];
292
+ // OpenCode reads flat slash commands from command/ and on-demand skills
293
+ // from skills/<name>/SKILL.md (https://opencode.ai/docs/skills). Emit both.
294
+ kinds = [
295
+ commandsKind('command', 'gsd-', configDir),
296
+ skillsKind('skills', 'gsd-', 'convertClaudeCommandToOpencodeSkill', 'opencode', configDir),
297
+ ];
245
298
  break;
246
299
  case 'kilo':
247
- kinds = [commandsKind('command', 'gsd-', configDir)];
300
+ // Kilo derives from OpenCode and shares the skills/<name>/SKILL.md layout
301
+ // (https://kilo.ai/docs/customize/skills). Emit flat commands + skills.
302
+ kinds = [
303
+ commandsKind('command', 'gsd-', configDir),
304
+ skillsKind('skills', 'gsd-', 'convertClaudeCommandToKiloSkill', 'kilo', configDir),
305
+ ];
248
306
  break;
249
307
  default:
250
308
  throw new TypeError(`Unknown runtime: '${runtime}' — add to runtime-artifact-layout.cjs table`);
251
309
  }
252
- return { runtime, configDir, kinds };
310
+ return { runtime, configDir, scope, kinds };
253
311
  }
254
- module.exports = { resolveRuntimeArtifactLayout, findInstallSourceRoot };
312
+ module.exports = { resolveRuntimeArtifactLayout, findInstallSourceRoot, getInstallExports };
@@ -0,0 +1,56 @@
1
+ 'use strict';
2
+ // ---------------------------------------------------------------------------
3
+ // Registry
4
+ // ---------------------------------------------------------------------------
5
+ const REGISTRY = Object.freeze({
6
+ claude: Object.freeze({ installSurface: 'settings-json', writesSharedSettings: true, finishPermissionWriter: null }),
7
+ gemini: Object.freeze({ installSurface: 'settings-json', writesSharedSettings: true, finishPermissionWriter: null }),
8
+ antigravity: Object.freeze({ installSurface: 'settings-json', writesSharedSettings: true, finishPermissionWriter: null }),
9
+ augment: Object.freeze({ installSurface: 'settings-json', writesSharedSettings: true, finishPermissionWriter: null }),
10
+ qwen: Object.freeze({ installSurface: 'settings-json', writesSharedSettings: true, finishPermissionWriter: null }),
11
+ hermes: Object.freeze({ installSurface: 'settings-json', writesSharedSettings: true, finishPermissionWriter: null }),
12
+ codebuddy: Object.freeze({ installSurface: 'settings-json', writesSharedSettings: true, finishPermissionWriter: null }),
13
+ opencode: Object.freeze({ installSurface: 'settings-json', writesSharedSettings: true, finishPermissionWriter: 'opencode' }),
14
+ kilo: Object.freeze({ installSurface: 'settings-json', writesSharedSettings: false, finishPermissionWriter: 'kilo' }),
15
+ codex: Object.freeze({ installSurface: 'codex-toml', writesSharedSettings: false, finishPermissionWriter: null }),
16
+ copilot: Object.freeze({ installSurface: 'copilot-instructions', writesSharedSettings: false, finishPermissionWriter: null }),
17
+ cline: Object.freeze({ installSurface: 'cline-rules', writesSharedSettings: false, finishPermissionWriter: null }),
18
+ cursor: Object.freeze({ installSurface: 'cursor-hooks-json', writesSharedSettings: false, finishPermissionWriter: null }),
19
+ windsurf: Object.freeze({ installSurface: 'profile-marker-only', writesSharedSettings: false, finishPermissionWriter: null }),
20
+ trae: Object.freeze({ installSurface: 'profile-marker-only', writesSharedSettings: false, finishPermissionWriter: null }),
21
+ });
22
+ // ---------------------------------------------------------------------------
23
+ // Exports
24
+ // ---------------------------------------------------------------------------
25
+ /** The complete set of 15 supported runtimes for config-adapter dispatch. */
26
+ const ALLOWED_CONFIG_RUNTIMES = new Set(Object.keys(REGISTRY));
27
+ /** All valid installSurface values. */
28
+ const INSTALL_SURFACES = Object.freeze([
29
+ 'settings-json',
30
+ 'codex-toml',
31
+ 'copilot-instructions',
32
+ 'cline-rules',
33
+ 'cursor-hooks-json',
34
+ 'profile-marker-only',
35
+ ]);
36
+ /**
37
+ * Resolve the config adapter intent for a given runtime.
38
+ *
39
+ * Returns a fresh object each call so callers cannot poison the registry by
40
+ * mutating the returned value.
41
+ *
42
+ * @throws {TypeError} if runtime is not a known supported runtime.
43
+ */
44
+ function resolveRuntimeConfigIntent(runtime) {
45
+ if (!Object.hasOwn(REGISTRY, runtime)) {
46
+ throw new TypeError(`Unknown runtime for config adapter: ${runtime}`);
47
+ }
48
+ const entry = REGISTRY[runtime];
49
+ return {
50
+ runtime,
51
+ installSurface: entry.installSurface,
52
+ writesSharedSettings: entry.writesSharedSettings,
53
+ finishPermissionWriter: entry.finishPermissionWriter,
54
+ };
55
+ }
56
+ module.exports = { resolveRuntimeConfigIntent, ALLOWED_CONFIG_RUNTIMES, INSTALL_SURFACES };
@@ -12,9 +12,9 @@
12
12
  * Runtime-specific notes:
13
13
  * hermes — GSD skills nest under skills/gsd/<skillName>/ (not the flat
14
14
  * skills/<skillName>/ layout used by all other runtimes).
15
- * cline — Rules-based; commands are embedded in .clinerules. Cline does
16
- * not use a skills/ directory. getGlobalSkillDir() returns null
17
- * for cline so the caller can emit an appropriate warning.
15
+ * cline — Skills-capable since v3.48.0 (#782). SKILL.md files live at
16
+ * ~/.cline/skills/<skillName>/SKILL.md (same flat layout as cursor/codex).
17
+ * .clinerules is also emitted (rules-based compatibility layer).
18
18
  */
19
19
  var __importDefault = (this && this.__importDefault) || function (mod) {
20
20
  return (mod && mod.__esModule) ? mod : { "default": mod };
@@ -62,8 +62,15 @@ function resolveAntigravityGlobalDir(opts = {}) {
62
62
  /**
63
63
  * Return the global config base directory for the given runtime.
64
64
  * Respects the same env-var overrides as bin/install.js getGlobalDir().
65
+ *
66
+ * @param runtime - The runtime identifier (e.g. 'claude', 'opencode').
67
+ * @param explicitDir - If provided and non-empty, returned immediately after
68
+ * tilde-expansion, overriding all env-var and default logic. This matches
69
+ * the behaviour of bin/install.js getGlobalDir(runtime, explicitDir).
65
70
  */
66
- function getGlobalConfigDir(runtime) {
71
+ function getGlobalConfigDir(runtime, explicitDir) {
72
+ if (explicitDir)
73
+ return expandTilde(explicitDir);
67
74
  const home = node_os_1.default.homedir();
68
75
  const env = process.env;
69
76
  switch (runtime) {
@@ -84,7 +91,11 @@ function getGlobalConfigDir(runtime) {
84
91
  return env['GROK_AGENTS_HOME'] ? expandTilde(env['GROK_AGENTS_HOME']) : node_path_1.default.join(home, '.agents');
85
92
  // ── Copilot (VS Code) ────────────────────────────────────────────────────
86
93
  case 'copilot':
87
- return env['COPILOT_CONFIG_DIR'] ? expandTilde(env['COPILOT_CONFIG_DIR']) : node_path_1.default.join(home, '.copilot');
94
+ if (env['COPILOT_CONFIG_DIR'])
95
+ return expandTilde(env['COPILOT_CONFIG_DIR']);
96
+ if (env['COPILOT_HOME'])
97
+ return expandTilde(env['COPILOT_HOME']);
98
+ return node_path_1.default.join(home, '.copilot');
88
99
  // ── Antigravity ──────────────────────────────────────────────────────────
89
100
  case 'antigravity':
90
101
  return resolveAntigravityGlobalDir({ env, home });
@@ -115,6 +126,8 @@ function getGlobalConfigDir(runtime) {
115
126
  case 'opencode': {
116
127
  if (env['OPENCODE_CONFIG_DIR'])
117
128
  return expandTilde(env['OPENCODE_CONFIG_DIR']);
129
+ if (env['OPENCODE_CONFIG'])
130
+ return node_path_1.default.dirname(expandTilde(env['OPENCODE_CONFIG']));
118
131
  if (env['XDG_CONFIG_HOME'])
119
132
  return node_path_1.default.join(expandTilde(env['XDG_CONFIG_HOME']), 'opencode');
120
133
  return node_path_1.default.join(home, '.config', 'opencode');
@@ -123,6 +136,8 @@ function getGlobalConfigDir(runtime) {
123
136
  case 'kilo': {
124
137
  if (env['KILO_CONFIG_DIR'])
125
138
  return expandTilde(env['KILO_CONFIG_DIR']);
139
+ if (env['KILO_CONFIG'])
140
+ return node_path_1.default.dirname(expandTilde(env['KILO_CONFIG']));
126
141
  if (env['XDG_CONFIG_HOME'])
127
142
  return node_path_1.default.join(expandTilde(env['XDG_CONFIG_HOME']), 'kilo');
128
143
  return node_path_1.default.join(home, '.config', 'kilo');
@@ -136,19 +151,25 @@ function getGlobalConfigDir(runtime) {
136
151
  * Return the global skills base directory for the given runtime.
137
152
  * Most runtimes: <configDir>/skills
138
153
  * Hermes: <configDir>/skills/gsd (nested category layout — #2841)
139
- * Cline: null (rules-based, no skills directory)
154
+ * Cline ≥ v3.48.0: <configDir>/skills (SKILL.md-based global skills — #782)
140
155
  */
141
156
  function getGlobalSkillsBase(runtime) {
142
- if (runtime === 'cline')
143
- return null;
144
- const configDir = getGlobalConfigDir(runtime);
145
- if (runtime === 'hermes')
157
+ if (runtime === 'hermes') {
158
+ const configDir = getGlobalConfigDir(runtime);
146
159
  return node_path_1.default.join(configDir, 'skills', 'gsd');
160
+ }
161
+ // Kilo Code discovers global skills from ~/.kilo/skills/ (HOME-relative),
162
+ // independent of the XDG-based config dir (~/.config/kilo) used for commands.
163
+ // See: https://kilo.ai/docs/customize/skills
164
+ // "Global skills are located in the `.kilo` directory within your Home
165
+ // directory: ~/.kilo/skills/"
166
+ if (runtime === 'kilo')
167
+ return node_path_1.default.join(node_os_1.default.homedir(), '.kilo', 'skills');
168
+ const configDir = getGlobalConfigDir(runtime);
147
169
  return node_path_1.default.join(configDir, 'skills');
148
170
  }
149
171
  /**
150
172
  * Return the full path to a specific skill's directory for the given runtime.
151
- * Returns null for runtimes that don't use a skills directory (cline).
152
173
  */
153
174
  function getGlobalSkillDir(runtime, skillName) {
154
175
  const base = getGlobalSkillsBase(runtime);
@@ -153,6 +153,7 @@ function projectManagedHookCommand({ absoluteRunner, scriptPath, runtime = 'gene
153
153
  const MANAGED_HOOK_BASENAMES_BY_SURFACE = {
154
154
  'settings-json': new Set([
155
155
  'gsd-check-update.js',
156
+ 'gsd-config-reload.js',
156
157
  'gsd-statusline.js',
157
158
  'gsd-context-monitor.js',
158
159
  'gsd-prompt-guard.js',
@@ -168,6 +169,7 @@ const MANAGED_HOOK_BASENAMES_BY_SURFACE = {
168
169
  const MANAGED_HOOK_COMMAND_BASENAMES_BY_SURFACE = {
169
170
  'settings-json': new Set([
170
171
  'gsd-check-update.js',
172
+ 'gsd-config-reload.js',
171
173
  'gsd-statusline.js',
172
174
  'gsd-context-monitor.js',
173
175
  'gsd-prompt-guard.js',
@@ -188,6 +190,10 @@ const MANAGED_HOOK_COMMAND_BASENAMES_BY_SURFACE = {
188
190
  // reconcileCodexHooksJsonSessionStart can replace stale node-runner commands
189
191
  // with the .cmd shim on reinstall (and vice-versa on cross-platform moves).
190
192
  'gsd-check-update.cmd',
193
+ // #772: context-monitor is now registered for Codex SubagentStart/Stop/PostToolUse.
194
+ 'gsd-context-monitor.js',
195
+ // #772: Windows .cmd shim for gsd-context-monitor — same #3426 pattern.
196
+ 'gsd-context-monitor.cmd',
191
197
  ]),
192
198
  };
193
199
  const LEGACY_MANAGED_HOOK_ALIASES_BY_SURFACE = {