@opengsd/gsd-core 1.3.1 → 1.4.0-rc.2

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 (136) hide show
  1. package/.claude-plugin/plugin.json +23 -0
  2. package/GEMINI.md +53 -0
  3. package/agents/gsd-advisor-researcher.md +1 -20
  4. package/agents/gsd-ai-researcher.md +2 -21
  5. package/agents/gsd-code-fixer.md +1 -1
  6. package/agents/gsd-code-reviewer.md +1 -1
  7. package/agents/gsd-domain-researcher.md +2 -21
  8. package/agents/gsd-eval-auditor.md +1 -1
  9. package/agents/gsd-eval-planner.md +1 -1
  10. package/agents/gsd-executor.md +1 -1
  11. package/agents/gsd-framework-selector.md +1 -1
  12. package/agents/gsd-nyquist-auditor.md +1 -1
  13. package/agents/gsd-pattern-mapper.md +1 -1
  14. package/agents/gsd-phase-researcher.md +92 -166
  15. package/agents/gsd-planner.md +9 -36
  16. package/agents/gsd-project-researcher.md +62 -141
  17. package/agents/gsd-security-auditor.md +1 -1
  18. package/agents/gsd-ui-auditor.md +1 -1
  19. package/agents/gsd-ui-checker.md +1 -1
  20. package/agents/gsd-ui-researcher.md +3 -22
  21. package/agents/gsd-user-profiler.md +1 -1
  22. package/agents/gsd-verifier.md +8 -2
  23. package/bin/install.js +1977 -339
  24. package/commands/gsd/autonomous.md +2 -0
  25. package/commands/gsd/execute-phase.md +2 -0
  26. package/commands/gsd/graphify.md +11 -6
  27. package/commands/gsd/import.md +6 -2
  28. package/commands/gsd/plan-phase.md +4 -2
  29. package/commands/gsd/progress.md +1 -0
  30. package/commands/gsd/stats.md +1 -0
  31. package/commands/gsd/update.md +3 -2
  32. package/gemini-extension.json +6 -0
  33. package/gsd-core/bin/check-latest-version.cjs +61 -6
  34. package/gsd-core/bin/gsd-tools.cjs +238 -32
  35. package/gsd-core/bin/lib/check-command-router.cjs +1 -0
  36. package/gsd-core/bin/lib/cli-exit.cjs +42 -0
  37. package/gsd-core/bin/lib/command-routing-hub.cjs +1 -1
  38. package/gsd-core/bin/lib/commands.cjs +5 -4
  39. package/gsd-core/bin/lib/config.cjs +28 -4
  40. package/gsd-core/bin/lib/core.cjs +72 -28
  41. package/gsd-core/bin/lib/graphify.cjs +2 -2
  42. package/gsd-core/bin/lib/init-command-router.cjs +2 -2
  43. package/gsd-core/bin/lib/init.cjs +19 -3
  44. package/gsd-core/bin/lib/install-profiles.cjs +58 -0
  45. package/gsd-core/bin/lib/installer-migration-report.cjs +3 -0
  46. package/gsd-core/bin/lib/installer-migrations/000-first-time-baseline.cjs +1 -1
  47. package/gsd-core/bin/lib/intel.cjs +3 -20
  48. package/gsd-core/bin/lib/package-legitimacy.cjs +368 -0
  49. package/gsd-core/bin/lib/phase.cjs +3 -3
  50. package/gsd-core/bin/lib/research-provider.cjs +137 -0
  51. package/gsd-core/bin/lib/research-store.cjs +167 -0
  52. package/gsd-core/bin/lib/roadmap-upgrade.cjs +4 -19
  53. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +67 -9
  54. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +56 -0
  55. package/gsd-core/bin/lib/runtime-homes.cjs +32 -11
  56. package/gsd-core/bin/lib/security.cjs +73 -0
  57. package/gsd-core/bin/lib/shell-command-projection.cjs +9 -0
  58. package/gsd-core/bin/lib/surface.cjs +54 -11
  59. package/gsd-core/bin/lib/validate.cjs +2 -2
  60. package/gsd-core/bin/lib/verification-command-router.cjs +31 -0
  61. package/gsd-core/bin/lib/verification.cjs +193 -0
  62. package/gsd-core/bin/lib/verify.cjs +2 -2
  63. package/gsd-core/bin/lib/workstream-inventory.cjs +1 -1
  64. package/gsd-core/bin/lib/worktree-base-ref.cjs +325 -0
  65. package/gsd-core/bin/lib/worktree-safety.cjs +31 -0
  66. package/gsd-core/bin/shared/config-schema.manifest.json +2 -1
  67. package/gsd-core/bin/verify-reapply-patches.cjs +8 -11
  68. package/gsd-core/references/planner-load-graph-context.md +36 -0
  69. package/gsd-core/references/planning-config.md +3 -1
  70. package/gsd-core/references/research-documentation-lookup.md +29 -0
  71. package/gsd-core/references/research-philosophy.md +29 -0
  72. package/gsd-core/references/research-verification-protocol.md +27 -0
  73. package/gsd-core/workflows/execute-phase.md +19 -8
  74. package/gsd-core/workflows/help/modes/full.md +4 -3
  75. package/gsd-core/workflows/ingest-docs.md +3 -2
  76. package/gsd-core/workflows/plan-phase.md +14 -10
  77. package/gsd-core/workflows/plan-review-convergence.md +3 -3
  78. package/gsd-core/workflows/review.md +24 -7
  79. package/gsd-core/workflows/ship.md +5 -8
  80. package/gsd-core/workflows/spec-phase.md +2 -1
  81. package/gsd-core/workflows/update.md +34 -6
  82. package/hooks/dist/gsd-config-reload.js +133 -0
  83. package/hooks/dist/gsd-context-monitor.js +1 -1
  84. package/hooks/dist/gsd-cursor-post-tool.js +75 -0
  85. package/hooks/dist/gsd-cursor-session-start.js +52 -0
  86. package/hooks/dist/gsd-workflow-guard.js +1 -0
  87. package/hooks/dist/gsd-worktree-path-guard.js +1 -1
  88. package/hooks/dist/managed-hooks-registry.cjs +3 -0
  89. package/hooks/gsd-config-reload.js +133 -0
  90. package/hooks/gsd-context-monitor.js +1 -1
  91. package/hooks/gsd-cursor-post-tool.js +75 -0
  92. package/hooks/gsd-cursor-session-start.js +52 -0
  93. package/hooks/gsd-workflow-guard.js +1 -0
  94. package/hooks/gsd-worktree-path-guard.js +1 -1
  95. package/hooks/hooks.json +69 -0
  96. package/hooks/managed-hooks-registry.cjs +3 -0
  97. package/package.json +8 -1
  98. package/scripts/affected-tests-lib.cjs +3 -2
  99. package/scripts/build-hooks.js +7 -0
  100. package/scripts/changeset/cli.cjs +226 -28
  101. package/scripts/changeset/lint.cjs +5 -4
  102. package/scripts/changeset/new.cjs +4 -4
  103. package/scripts/check-alias-drift.cjs +77 -71
  104. package/scripts/check-env.cjs +185 -179
  105. package/scripts/check-npm-integrity.cjs +115 -109
  106. package/scripts/ci-guard-runner.cjs +11 -5
  107. package/scripts/ci-prepare-test-scope.cjs +27 -22
  108. package/scripts/ci-rebase-check.cjs +46 -45
  109. package/scripts/ci-test-scope.cjs +126 -22
  110. package/scripts/diff-touches-shipped-paths.cjs +52 -44
  111. package/scripts/gen-inventory-manifest.cjs +38 -32
  112. package/scripts/gen-research-agents.cjs +276 -0
  113. package/scripts/issue-dedupe.cjs +278 -0
  114. package/scripts/lib/cli-exit.cjs +56 -0
  115. package/scripts/lint-command-contract.cjs +28 -22
  116. package/scripts/lint-descriptions.cjs +32 -28
  117. package/scripts/lint-docs-required.cjs +4 -4
  118. package/scripts/lint-legacy-dir-name.cjs +56 -52
  119. package/scripts/lint-pr-check-project-dir.cjs +3 -1
  120. package/scripts/lint-shell-command-projection-drift.cjs +27 -22
  121. package/scripts/lint-skill-deps.cjs +31 -26
  122. package/scripts/lint-test-file-count.allowlist.json +2 -0
  123. package/scripts/lint-test-file-count.cjs +5 -4
  124. package/scripts/mutation-matrix.cjs +6 -3
  125. package/scripts/prompt-injection-scan.sh +1 -1
  126. package/scripts/release-notes/discord-release-summary.cjs +373 -0
  127. package/scripts/release-notes/format-github-release-notes.cjs +8 -3
  128. package/scripts/release-tarball-smoke.cjs +6 -4
  129. package/scripts/research-profiles.cjs +149 -0
  130. package/scripts/run-affected-tests.cjs +2 -1
  131. package/scripts/run-cross-platform-tests.cjs +11 -7
  132. package/scripts/run-tests.cjs +8 -7
  133. package/scripts/strip-prose-atrefs.cjs +1 -1
  134. package/scripts/sync-manifest-versions.cjs +119 -0
  135. package/scripts/sync-runtime-launcher.cjs +0 -3
  136. package/scripts/verify-npm-publish.cjs +14 -26
@@ -0,0 +1,167 @@
1
+ "use strict";
2
+ /**
3
+ * Research Store Module
4
+ *
5
+ * Provides deterministic cache key generation, TTL policy, path resolution,
6
+ * and JSON-backed put/get operations for research entries.
7
+ *
8
+ * ADR-457 build-at-publish: authored as TypeScript .cts → emits .cjs via tsc.
9
+ */
10
+ var __importDefault = (this && this.__importDefault) || function (mod) {
11
+ return (mod && mod.__esModule) ? mod : { "default": mod };
12
+ };
13
+ const node_fs_1 = __importDefault(require("node:fs"));
14
+ const node_os_1 = __importDefault(require("node:os"));
15
+ const node_path_1 = __importDefault(require("node:path"));
16
+ const node_crypto_1 = __importDefault(require("node:crypto"));
17
+ const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
18
+ // ---------------------------------------------------------------------------
19
+ // Constants
20
+ // ---------------------------------------------------------------------------
21
+ const DAY_MS = 86_400_000;
22
+ // ---------------------------------------------------------------------------
23
+ // researchKey
24
+ // ---------------------------------------------------------------------------
25
+ function normalize(x) {
26
+ if (x === null || x === undefined)
27
+ return '';
28
+ if (typeof x === 'object')
29
+ return JSON.stringify(x).trim().toLowerCase();
30
+ // After excluding null, undefined, and object, x can only be a primitive —
31
+ // cast through number | string | boolean to avoid no-base-to-string on unknown.
32
+ return `${x}`.trim().toLowerCase();
33
+ }
34
+ function researchKey(input) {
35
+ const parts = {
36
+ ecosystem: normalize(input.ecosystem),
37
+ library: normalize(input.library),
38
+ version: normalize(input.version),
39
+ query: normalize(input.query),
40
+ kind: normalize(input.kind),
41
+ };
42
+ const serialized = JSON.stringify(parts);
43
+ return node_crypto_1.default.createHash('sha256').update(serialized).digest('hex');
44
+ }
45
+ // ---------------------------------------------------------------------------
46
+ // ttlForSource
47
+ // ---------------------------------------------------------------------------
48
+ function ttlForSource(source, confidence) {
49
+ if (source === 'curated' && confidence === 'HIGH')
50
+ return 30 * DAY_MS;
51
+ if (source === 'curated' && confidence === 'MEDIUM')
52
+ return 7 * DAY_MS;
53
+ return DAY_MS;
54
+ }
55
+ // ---------------------------------------------------------------------------
56
+ // tierForSource / resolveStorePath
57
+ // ---------------------------------------------------------------------------
58
+ const CURATED_SOURCES = new Set(['curated']);
59
+ function tierForSource(source) {
60
+ return CURATED_SOURCES.has(source) ? 'user' : 'project';
61
+ }
62
+ function resolveStorePath(cwd, source, { homeDir = node_os_1.default.homedir() } = {}) {
63
+ if (tierForSource(source) === 'user') {
64
+ return node_path_1.default.join(homeDir, '.gsd', 'research-cache');
65
+ }
66
+ return node_path_1.default.join(cwd, '.planning', 'research', '.cache');
67
+ }
68
+ // ---------------------------------------------------------------------------
69
+ // isValidResearchKey
70
+ // ---------------------------------------------------------------------------
71
+ /**
72
+ * Returns true iff key is a valid 64-character lowercase hexadecimal SHA-256
73
+ * string (the exact shape produced by researchKey). Any other shape —
74
+ * including path-traversal sequences — is rejected.
75
+ */
76
+ function isValidResearchKey(key) {
77
+ return typeof key === 'string' && /^[0-9a-f]{64}$/.test(key);
78
+ }
79
+ // ---------------------------------------------------------------------------
80
+ // putResearch
81
+ // ---------------------------------------------------------------------------
82
+ function putResearch(cwd, key, payload, { clock = Date, homeDir = node_os_1.default.homedir() } = {}) {
83
+ // Defense-in-depth: reject any key that is not a 64-char sha256 hex string.
84
+ if (!isValidResearchKey(key)) {
85
+ throw new Error('invalid research key');
86
+ }
87
+ const { content, source, provider, confidence, kind, version } = payload;
88
+ let ttl = ttlForSource(source, confidence);
89
+ // Cap TTL when version is blank/missing — a versionless curated entry must not
90
+ // get the long 30-day window since we can't know if it's still current.
91
+ if (!version) {
92
+ ttl = Math.min(ttl, DAY_MS);
93
+ }
94
+ const fetched_at = new Date(clock.now()).toISOString();
95
+ const entry = { content, source, provider, confidence, fetched_at, ttl, kind };
96
+ const dir = resolveStorePath(cwd, source, { homeDir });
97
+ // Belt-and-suspenders: ensure the resolved file path stays inside the store dir.
98
+ const resolvedDir = node_path_1.default.resolve(dir);
99
+ const filePath = node_path_1.default.join(dir, `${key}.json`);
100
+ const resolvedFile = node_path_1.default.resolve(filePath);
101
+ if (!resolvedFile.startsWith(resolvedDir + node_path_1.default.sep)) {
102
+ throw new Error('invalid research key');
103
+ }
104
+ node_fs_1.default.mkdirSync(dir, { recursive: true });
105
+ (0, shell_command_projection_cjs_1.platformWriteSync)(filePath, JSON.stringify(entry));
106
+ return entry;
107
+ }
108
+ // ---------------------------------------------------------------------------
109
+ // getResearch
110
+ // ---------------------------------------------------------------------------
111
+ function getResearch(cwd, key, { clock = Date, homeDir = node_os_1.default.homedir() } = {}) {
112
+ // Defense-in-depth: reject any key that is not a 64-char sha256 hex string.
113
+ if (!isValidResearchKey(key)) {
114
+ return { hit: false, stale: false, entry: null };
115
+ }
116
+ try {
117
+ // Search both physical tiers: user (curated) and project (web/etc.)
118
+ const userDir = node_path_1.default.join(homeDir, '.gsd', 'research-cache');
119
+ const projectDir = node_path_1.default.join(cwd, '.planning', 'research', '.cache');
120
+ const tierDirs = [userDir, projectDir];
121
+ const candidates = [];
122
+ for (const dir of tierDirs) {
123
+ const resolvedDir = node_path_1.default.resolve(dir);
124
+ const filePath = node_path_1.default.join(dir, `${key}.json`);
125
+ // Belt-and-suspenders: ensure path stays inside tier dir
126
+ if (!node_path_1.default.resolve(filePath).startsWith(resolvedDir + node_path_1.default.sep))
127
+ continue;
128
+ if (!node_fs_1.default.existsSync(filePath))
129
+ continue;
130
+ let entry;
131
+ try {
132
+ entry = JSON.parse(node_fs_1.default.readFileSync(filePath, 'utf8'));
133
+ }
134
+ catch {
135
+ // Corrupt file in this tier — skip it
136
+ continue;
137
+ }
138
+ // Finding 3: validate entry metadata shape before accepting as a candidate.
139
+ // An entry with missing/invalid fetched_at or ttl must be treated as a miss.
140
+ const parsedFetchedAt = Date.parse(entry.fetched_at);
141
+ if (!Number.isFinite(parsedFetchedAt))
142
+ continue;
143
+ if (typeof entry.ttl !== 'number' ||
144
+ !Number.isFinite(entry.ttl) ||
145
+ entry.ttl <= 0)
146
+ continue;
147
+ const age = clock.now() - parsedFetchedAt;
148
+ const stale = age > entry.ttl;
149
+ candidates.push({ entry, stale, age });
150
+ }
151
+ if (candidates.length === 0) {
152
+ return { hit: false, stale: false, entry: null };
153
+ }
154
+ // Prefer: non-stale over stale; among same-staleness, lowest age (most recent)
155
+ candidates.sort((a, b) => {
156
+ if (a.stale !== b.stale)
157
+ return a.stale ? 1 : -1; // non-stale first
158
+ return a.age - b.age; // lower age (more recent) first
159
+ });
160
+ const best = candidates[0];
161
+ return { hit: true, stale: best.stale, entry: best.entry };
162
+ }
163
+ catch {
164
+ return { hit: false, stale: false, entry: null };
165
+ }
166
+ }
167
+ module.exports = { isValidResearchKey, researchKey, ttlForSource, tierForSource, resolveStorePath, putResearch, getResearch };
@@ -25,8 +25,6 @@ const MIGRATED_PHASE_HEADING_RE = /^#{2,4}\s*(?:\[[^\]]+\]\s*)?Phase\s+\d+-\d{2}
25
25
  // Matches milestone section headings: ## v1.0, ## Roadmap v2.0, ## ✅ v1.0, ## [GSD] v1.0, etc.
26
26
  // The optional bracket-token prefix (e.g., [GSD]) must be tested before the emoji group.
27
27
  const MILESTONE_HEADING_RE = /^##\s+(?:\[[^\]]+\]\s+|Roadmap\s+|[✅🚧]\s*)?v(\d+)\.(\d+)(?:\s|:)/iu;
28
- // Matches checklist phase references: - [ ] **Phase N:** or - [x] **Phase N:** (also decimal)
29
- const CHECKLIST_PHASE_RE = /^(\s*-\s*\[[ x]\]\s*\*{0,2})Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:/gi;
30
28
  // ─── Pure computation helpers ─────────────────────────────────────────────────
31
29
  /**
32
30
  * Parse the ROADMAP.md content and build a list of phase entries with their
@@ -126,19 +124,6 @@ function buildNewDirName(oldDirName, newId, projectCode) {
126
124
  const newBase = slug ? `${paddedMilestone}-${subStr}-${slug}` : `${paddedMilestone}-${subStr}`;
127
125
  return projectCode ? `${projectCode}-${newBase}` : newBase;
128
126
  }
129
- /**
130
- * Read project_code from config.json if present.
131
- */
132
- function readProjectCode(configPath) {
133
- try {
134
- const raw = node_fs_1.default.readFileSync(configPath, 'utf8');
135
- const parsed = JSON.parse(raw);
136
- return typeof parsed['project_code'] === 'string' ? parsed['project_code'] : null;
137
- }
138
- catch {
139
- return null;
140
- }
141
- }
142
127
  // ─── computeMigrationPlan ─────────────────────────────────────────────────────
143
128
  /**
144
129
  * Compute a migration plan without touching the filesystem.
@@ -390,7 +375,7 @@ function applyMigration(cwd, plan, options = {}) {
390
375
  // ── Real run: verify clean working tree ───────────────────────────────────
391
376
  let gitStatus;
392
377
  try {
393
- gitStatus = (0, node_child_process_1.execSync)('git status --porcelain', { cwd, encoding: 'utf8' });
378
+ gitStatus = (0, node_child_process_1.execSync)('git status --porcelain', { cwd, encoding: 'utf8', windowsHide: true });
394
379
  }
395
380
  catch (err) {
396
381
  throw new Error(`git status failed: ${err.message}`);
@@ -401,7 +386,7 @@ function applyMigration(cwd, plan, options = {}) {
401
386
  // Capture HEAD sha for rollback
402
387
  let headSha;
403
388
  try {
404
- headSha = (0, node_child_process_1.execSync)('git rev-parse HEAD', { cwd, encoding: 'utf8' }).trim();
389
+ headSha = (0, node_child_process_1.execSync)('git rev-parse HEAD', { cwd, encoding: 'utf8', windowsHide: true }).trim();
405
390
  }
406
391
  catch (err) {
407
392
  throw new Error(`git rev-parse HEAD failed: ${err.message}`);
@@ -475,8 +460,8 @@ function applyMigration(cwd, plan, options = {}) {
475
460
  catch (err) {
476
461
  // Rollback via git reset --hard + git clean
477
462
  try {
478
- (0, node_child_process_1.execSync)(`git reset --hard ${headSha}`, { cwd, stdio: 'pipe' });
479
- (0, node_child_process_1.execSync)('git clean -fd .planning/phases/', { cwd, stdio: 'pipe' });
463
+ (0, node_child_process_1.execSync)(`git reset --hard ${headSha}`, { cwd, stdio: 'pipe', windowsHide: true });
464
+ (0, node_child_process_1.execSync)('git clean -fd .planning/phases/', { cwd, stdio: 'pipe', windowsHide: true });
480
465
  }
481
466
  catch {
482
467
  // Swallow rollback errors — surface original error
@@ -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);
@@ -24,6 +24,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
24
24
  Object.defineProperty(exports, "__esModule", { value: true });
25
25
  exports.MARKDOWN_LINK_PATTERNS = exports.INJECTION_PATTERNS = void 0;
26
26
  exports.validatePath = validatePath;
27
+ exports.loadTrustedGlobalRoots = loadTrustedGlobalRoots;
27
28
  exports.requireSafePath = requireSafePath;
28
29
  exports.scanForInjection = scanForInjection;
29
30
  exports.sanitizeForPrompt = sanitizeForPrompt;
@@ -35,6 +36,7 @@ exports.validateFieldName = validateFieldName;
35
36
  exports.validatePromptStructure = validatePromptStructure;
36
37
  exports.scanEntropyAnomalies = scanEntropyAnomalies;
37
38
  const node_fs_1 = __importDefault(require("node:fs"));
39
+ const node_os_1 = __importDefault(require("node:os"));
38
40
  const node_path_1 = __importDefault(require("node:path"));
39
41
  // ─── Path Traversal Prevention ──────────────────────────────────────────────
40
42
  /**
@@ -92,6 +94,77 @@ function validatePath(filePath, baseDir, opts = {}) {
92
94
  }
93
95
  return { safe: true, resolved: resolvedPath };
94
96
  }
97
+ /**
98
+ * Load the opt-in trusted global roots allowlist from config.
99
+ *
100
+ * Reads `config.agent_skills_security.trusted_global_roots` (an array of
101
+ * path strings). Each entry is canonicalized via realpathSync: non-strings
102
+ * are dropped, leading `~/` is expanded to `os.homedir()`, entries that are
103
+ * not absolute after expansion are dropped (project-relative paths are
104
+ * rejected as a security boundary), and entries that do not exist on disk are
105
+ * dropped (a non-existent root is not trustworthy). The canonical realpath is
106
+ * used for all subsequent checks and as the stored value — this closes the
107
+ * case-insensitive bypass on macOS APFS (`/users/alice` vs `/Users/alice`)
108
+ * and ensures trust doesn't drift across re-invocations if a root is
109
+ * re-created at a different target. Results are de-duplicated by canonical path.
110
+ */
111
+ function loadTrustedGlobalRoots(config) {
112
+ const roots = config?.['agent_skills_security'];
113
+ const raw = roots?.['trusted_global_roots'];
114
+ if (!Array.isArray(raw))
115
+ return [];
116
+ // Compute canonical homedir once for case-insensitive-safe comparison.
117
+ let realHome;
118
+ try {
119
+ realHome = node_fs_1.default.realpathSync(node_os_1.default.homedir());
120
+ }
121
+ catch {
122
+ realHome = node_os_1.default.homedir();
123
+ }
124
+ const seen = new Set();
125
+ const result = [];
126
+ for (const entry of raw) {
127
+ if (typeof entry !== 'string')
128
+ continue;
129
+ let expanded;
130
+ if (entry === '~') {
131
+ expanded = node_os_1.default.homedir();
132
+ }
133
+ else if (entry.startsWith('~/')) {
134
+ expanded = node_path_1.default.join(node_os_1.default.homedir(), entry.slice(2));
135
+ }
136
+ else {
137
+ expanded = entry;
138
+ }
139
+ if (!node_path_1.default.isAbsolute(expanded))
140
+ continue; // reject project-relative
141
+ // Canonicalize: resolve symlinks and normalise case. If the path doesn't
142
+ // exist or can't be read, skip it — a non-existent root is not trustworthy.
143
+ let real;
144
+ try {
145
+ real = node_fs_1.default.realpathSync(expanded);
146
+ }
147
+ catch {
148
+ continue; // non-existent or unreadable — skip
149
+ }
150
+ // Reject dangerously broad roots: filesystem root (e.g. '/' or 'C:\' or UNC '\\server\share').
151
+ // Normalize both sides by stripping trailing path separators before comparing so that
152
+ // Windows UNC shares (where path.parse().root includes a trailing separator) are caught.
153
+ const stripTrailingSep = (p) => p.replace(/[\\/]+$/, '');
154
+ if (stripTrailingSep(node_path_1.default.parse(real).root) === stripTrailingSep(real))
155
+ continue;
156
+ // Reject homedir itself (canonical compare closes case-insensitive bypass).
157
+ // Apply stripTrailingSep for robustness on platforms where realpathSync may
158
+ // or may not include a trailing separator on the homedir path.
159
+ if (stripTrailingSep(real) === stripTrailingSep(realHome))
160
+ continue;
161
+ if (seen.has(real))
162
+ continue;
163
+ seen.add(real);
164
+ result.push(real);
165
+ }
166
+ return result;
167
+ }
95
168
  /**
96
169
  * Validate a file path and throw on traversal attempt.
97
170
  * Convenience wrapper around validatePath for use in CLI commands.
@@ -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 = {
@@ -381,6 +387,7 @@ function execGit(args, opts = {}) {
381
387
  encoding: 'utf-8',
382
388
  stdio: 'pipe',
383
389
  timeout: opts.timeout ?? 10_000,
390
+ windowsHide: true,
384
391
  });
385
392
  return _spawnResult(result, 'git');
386
393
  }
@@ -391,6 +398,7 @@ function execNpm(args, opts = {}) {
391
398
  encoding: 'utf-8',
392
399
  stdio: ['ignore', 'pipe', 'pipe'],
393
400
  timeout: opts.timeout ?? 15_000,
401
+ windowsHide: true,
394
402
  });
395
403
  return _spawnResult(result, 'npm');
396
404
  }
@@ -401,6 +409,7 @@ function execTool(program, args, opts = {}) {
401
409
  encoding: 'utf-8',
402
410
  stdio: 'pipe',
403
411
  timeout: opts.timeout ?? 30_000,
412
+ windowsHide: true,
404
413
  });
405
414
  return _spawnResult(result, program);
406
415
  }