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

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 (45) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.ja-JP.md +4 -3
  4. package/README.ko-KR.md +4 -3
  5. package/README.md +4 -3
  6. package/README.pt-BR.md +4 -3
  7. package/README.zh-CN.md +4 -3
  8. package/bin/install.js +33 -15
  9. package/commands/gsd/map-codebase.md +4 -4
  10. package/commands/gsd/ns-project.md +2 -1
  11. package/commands/gsd/onboard.md +46 -0
  12. package/gsd-core/bin/gsd-tools.cjs +47 -2
  13. package/gsd-core/bin/lib/capability-registry.cjs +467 -49
  14. package/gsd-core/bin/lib/capability-state.cjs +117 -18
  15. package/gsd-core/bin/lib/capability-writer.cjs +11 -1
  16. package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +136 -0
  17. package/gsd-core/bin/lib/claude-orchestration.cjs +404 -0
  18. package/gsd-core/bin/lib/clusters.cjs +1 -0
  19. package/gsd-core/bin/lib/command-aliases.cjs +8 -0
  20. package/gsd-core/bin/lib/init-command-router.cjs +4 -0
  21. package/gsd-core/bin/lib/init.cjs +25 -69
  22. package/gsd-core/bin/lib/install-engine.cjs +8 -5
  23. package/gsd-core/bin/lib/install-profiles.cjs +2 -0
  24. package/gsd-core/bin/lib/loop-resolver.cjs +7 -1
  25. package/gsd-core/bin/lib/model-resolver.cjs +69 -4
  26. package/gsd-core/bin/lib/onboard-projection.cjs +309 -0
  27. package/gsd-core/bin/lib/runtime-name-policy.cjs +3 -1
  28. package/gsd-core/bin/lib/surface.cjs +51 -30
  29. package/gsd-core/bin/shared/model-catalog.json +5 -0
  30. package/gsd-core/references/gsd-run-resolver.md +8 -0
  31. package/gsd-core/templates/project.md +1 -1
  32. package/gsd-core/workflows/do.md +4 -3
  33. package/gsd-core/workflows/help/modes/brief.md +2 -1
  34. package/gsd-core/workflows/help/modes/default.md +2 -1
  35. package/gsd-core/workflows/help/modes/full.md +11 -1
  36. package/gsd-core/workflows/help/modes/topic.md +1 -1
  37. package/gsd-core/workflows/onboard.md +277 -0
  38. package/gsd-core/workflows/quick.md +20 -0
  39. package/package.json +1 -1
  40. package/scripts/gen-golden-install-parity-zcode.cjs +77 -0
  41. package/scripts/run-tests.cjs +5 -3
  42. package/scripts/sync-runtime-launcher.cjs +20 -2
  43. package/skills/gsd-map-codebase/SKILL.md +3 -3
  44. package/skills/gsd-ns-project/SKILL.md +1 -0
  45. package/skills/gsd-onboard/SKILL.md +46 -0
@@ -78,6 +78,61 @@ function warnModelPolicyUnmappable(agentType, policyModel, tier) {
78
78
  function _resetModelPolicyWarningCacheForTests() {
79
79
  _modelPolicyUnmappableWarned.clear();
80
80
  }
81
+ // Dedupe stderr warnings for unmappable model_overrides Claude IDs (#2041).
82
+ const _modelOverrideUnmappableWarned = new Set();
83
+ function warnModelOverrideUnmappable(agentType, overrideValue) {
84
+ const key = `${agentType}::${overrideValue}`;
85
+ if (_modelOverrideUnmappableWarned.has(key))
86
+ return;
87
+ _modelOverrideUnmappableWarned.add(key);
88
+ // Cap emission length so an oversized or secret-shaped value cannot leak in
89
+ // full to stderr/logs (#2041 security review). MUST go to stderr — resolve-
90
+ // model's JSON result is parsed from stdout.
91
+ const safe = overrideValue.length > 64 ? overrideValue.slice(0, 64) + '…' : overrideValue;
92
+ process.stderr.write(`gsd: warning — model_overrides value "${safe}" for ${agentType} ` +
93
+ `has no Claude agent alias; falling through to tier resolution.\n`);
94
+ }
95
+ // Test-only: reset the model_overrides warn-dedupe cache between cases (#2041).
96
+ function _resetModelOverrideWarningCacheForTests() {
97
+ _modelOverrideUnmappableWarned.clear();
98
+ }
99
+ /**
100
+ * #2041 — Map a `model_overrides` value to its Claude Agent-tool alias on the
101
+ * claude runtime, mirroring the `model_policy` path (#1144). Claude Code's
102
+ * Agent tool `model` parameter documents only tier aliases (opus/sonnet/haiku/
103
+ * fable); a full Claude model ID returned verbatim is silently dropped by the
104
+ * spawner. Returns the value to return verbatim, or null to signal "fall
105
+ * through to normal tier/dynamic-routing resolution" (used when a Claude full
106
+ * ID has no alias — matches model_policy's warn-and-fall-through). Non-Claude
107
+ * runtimes and non-Claude values always pass through verbatim.
108
+ *
109
+ * Hardening (code+security review): a `typeof` guard preserves the pre-fix
110
+ * no-crash behavior if a malformed config surfaces a non-string value, and an
111
+ * `Object.hasOwn` lookup defeats `__proto__`/`constructor` lookups on the plain
112
+ * object literal so those reserved keys cannot return a truthy non-string.
113
+ */
114
+ function mapClaudeOverrideForRuntime(override, configRuntime, agentType) {
115
+ // Defensive: model_overrides is typed Record<string,string> but a malformed
116
+ // config could surface a non-string; pass through verbatim (preserving the
117
+ // pre-fix no-crash behaviour) and let the downstream Agent tool reject it.
118
+ if (typeof override !== 'string')
119
+ return override;
120
+ const onClaude = !configRuntime || configRuntime === 'claude';
121
+ if (!onClaude)
122
+ return override;
123
+ // Object.hasOwn guards against __proto__/constructor returning a truthy
124
+ // non-string from the plain object literal (#2041 security review).
125
+ if (Object.hasOwn(CLAUDE_POLICY_ID_TO_ALIAS, override)) {
126
+ return CLAUDE_POLICY_ID_TO_ALIAS[override];
127
+ }
128
+ if (CLAUDE_AGENT_ALIASES.has(override))
129
+ return override;
130
+ if (override.startsWith('claude-')) {
131
+ warnModelOverrideUnmappable(agentType, override);
132
+ return null;
133
+ }
134
+ return override;
135
+ }
81
136
  /**
82
137
  * #49 — Provider-neutral model policy preset resolution.
83
138
  */
@@ -134,11 +189,16 @@ function resolveModelPolicy(policy, tier) {
134
189
  }
135
190
  function resolveModelInternal(cwd, agentType) {
136
191
  const config = loadConfig(cwd);
137
- // 1. Per-agent override
192
+ // 1. Per-agent override (#2041: map Claude full IDs → Agent-tool aliases on
193
+ // the claude runtime, mirroring the model_policy path #1144; non-Claude
194
+ // runtimes and non-Claude values pass through verbatim).
138
195
  const modelOverrides = config['model_overrides'];
139
196
  const override = modelOverrides?.[agentType];
140
197
  if (override) {
141
- return override;
198
+ const mapped = mapClaudeOverrideForRuntime(override, config['runtime'], agentType);
199
+ if (mapped !== null)
200
+ return mapped;
201
+ // Unmappable Claude ID — fall through to tier resolution (matches model_policy).
142
202
  }
143
203
  // 2. Compute the tier
144
204
  // eslint-disable-next-line @typescript-eslint/no-base-to-string
@@ -252,8 +312,12 @@ function resolveModelForTier(cwd, agentType, attempt) {
252
312
  const attemptN = Number.isInteger(attempt) && attempt > 0 ? attempt : 0;
253
313
  const modelOverrides = config['model_overrides'];
254
314
  const override = modelOverrides?.[agentType];
255
- if (override)
256
- return override;
315
+ if (override) {
316
+ const mapped = mapClaudeOverrideForRuntime(override, config['runtime'], agentType);
317
+ if (mapped !== null)
318
+ return mapped;
319
+ // Unmappable Claude ID — fall through to dynamic_routing / model_policy resolution.
320
+ }
257
321
  if (config['model_policy'] && config['runtime'] && config['runtime'] !== 'claude') {
258
322
  return resolveModelInternal(cwd, agentType);
259
323
  }
@@ -454,6 +518,7 @@ module.exports = {
454
518
  resolveModelPolicy,
455
519
  resolveModelInternal,
456
520
  _resetModelPolicyWarningCacheForTests,
521
+ _resetModelOverrideWarningCacheForTests,
457
522
  VALID_GRANULARITIES,
458
523
  resolveGranularityInternal,
459
524
  assertValidGranularityOverride,
@@ -0,0 +1,309 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ const node_fs_1 = __importDefault(require("node:fs"));
6
+ const node_path_1 = __importDefault(require("node:path"));
7
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- core-utils.cjs is an export= CommonJS module
8
+ const coreUtils = require("./core-utils.cjs");
9
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- planning-workspace.cjs is an export= CommonJS module
10
+ const planningWorkspace = require("./planning-workspace.cjs");
11
+ const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
12
+ const { pathExistsInternal, toPosixPath } = coreUtils;
13
+ const { planningDir, planningRoot } = planningWorkspace;
14
+ const CODE_EXTENSIONS = new Set([
15
+ '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.py', '.go', '.rs', '.swift', '.java',
16
+ '.kt', '.kts', '.c', '.cpp', '.cc', '.h', '.hpp', '.cs', '.rb', '.php', '.dart',
17
+ '.m', '.mm', '.scala', '.groovy', '.lua', '.r', '.R', '.zig', '.ex', '.exs', '.clj',
18
+ ]);
19
+ const CODE_SCAN_SKIP_DIRS = new Set([
20
+ 'node_modules', '.git', '.planning', '.claude', '.codex', '__pycache__', 'target',
21
+ 'dist', 'build', '.next', '.nuxt', '.svelte-kit', 'coverage', 'vendor', '.venv', 'venv',
22
+ ]);
23
+ const PACKAGE_FILES = [
24
+ 'package.json', 'requirements.txt', 'pyproject.toml', 'Cargo.toml', 'go.mod',
25
+ 'Package.swift', 'build.gradle', 'build.gradle.kts', 'pom.xml', 'Gemfile',
26
+ 'composer.json', 'pubspec.yaml', 'CMakeLists.txt', 'Makefile', 'build.zig',
27
+ 'mix.exs', 'project.clj',
28
+ ];
29
+ const REQUIRED_CODEBASE_MAP_FILES = [
30
+ 'STACK.md', 'ARCHITECTURE.md', 'STRUCTURE.md', 'CONVENTIONS.md', 'TESTING.md',
31
+ 'INTEGRATIONS.md', 'CONCERNS.md',
32
+ ];
33
+ const FAST_CODEBASE_MAP_FILES = [
34
+ 'STACK.md', 'INTEGRATIONS.md', 'ARCHITECTURE.md', 'STRUCTURE.md',
35
+ ];
36
+ const PLANNING_DOC_SEGMENTS = new Set([
37
+ 'adr', 'adrs', 'prd', 'prds', 'spec', 'specs', 'rfc', 'rfcs',
38
+ ]);
39
+ function hasCodeFilesInternal(dir, depth = 0) {
40
+ if (depth > 3)
41
+ return false;
42
+ let entries;
43
+ try {
44
+ entries = node_fs_1.default.readdirSync(dir, { withFileTypes: true });
45
+ }
46
+ catch {
47
+ return false;
48
+ }
49
+ for (const entry of entries) {
50
+ if (entry.isFile() && CODE_EXTENSIONS.has(node_path_1.default.extname(entry.name)))
51
+ return true;
52
+ if (entry.isDirectory() && !CODE_SCAN_SKIP_DIRS.has(entry.name)) {
53
+ if (hasCodeFilesInternal(node_path_1.default.join(dir, entry.name), depth + 1))
54
+ return true;
55
+ }
56
+ }
57
+ return false;
58
+ }
59
+ function hasPackageFileInternal(cwd) {
60
+ return PACKAGE_FILES.some((file) => pathExistsInternal(cwd, file));
61
+ }
62
+ function listPlanningDocCandidates(cwd) {
63
+ const roots = ['docs', 'adr', 'adrs', 'prd', 'prds', 'spec', 'specs', 'rfc', 'rfcs'];
64
+ const candidates = new Set();
65
+ function isPlanningDocCandidate(rel, name) {
66
+ const upperName = name.toUpperCase();
67
+ const relLower = rel.toLowerCase();
68
+ const pathSegments = relLower.split('/');
69
+ return (/(^|[-_ ])(ADR|PRD|SPEC|RFC)([-_ ]|\.)/i.test(name) ||
70
+ /^\d{4}[-_].+\.md$/i.test(name) ||
71
+ pathSegments.some((segment) => PLANNING_DOC_SEGMENTS.has(segment)) ||
72
+ upperName === 'REQUIREMENTS.MD');
73
+ }
74
+ function addCandidate(rel, name) {
75
+ if (name.toLowerCase().endsWith('.md') && isPlanningDocCandidate(rel, name)) {
76
+ candidates.add(toPosixPath(rel));
77
+ }
78
+ }
79
+ function visit(dir, relDir, depth) {
80
+ if (depth > 3)
81
+ return;
82
+ let entries;
83
+ try {
84
+ entries = node_fs_1.default.readdirSync(dir, { withFileTypes: true });
85
+ }
86
+ catch {
87
+ return;
88
+ }
89
+ for (const entry of entries) {
90
+ const rel = relDir ? `${relDir}/${entry.name}` : entry.name;
91
+ if (entry.isDirectory()) {
92
+ if (!CODE_SCAN_SKIP_DIRS.has(entry.name)) {
93
+ visit(node_path_1.default.join(dir, entry.name), rel, depth + 1);
94
+ }
95
+ continue;
96
+ }
97
+ if (entry.isFile())
98
+ addCandidate(rel, entry.name);
99
+ }
100
+ }
101
+ let rootEntries = [];
102
+ try {
103
+ rootEntries = node_fs_1.default.readdirSync(cwd, { withFileTypes: true });
104
+ }
105
+ catch {
106
+ rootEntries = [];
107
+ }
108
+ for (const entry of rootEntries) {
109
+ if (entry.isFile())
110
+ addCandidate(entry.name, entry.name);
111
+ }
112
+ for (const root of roots) {
113
+ const full = node_path_1.default.join(cwd, root);
114
+ if (node_fs_1.default.existsSync(full))
115
+ visit(full, root, 0);
116
+ }
117
+ return [...candidates].sort();
118
+ }
119
+ function listCodebaseMapFiles(cwd) {
120
+ const codebaseDir = node_path_1.default.join(planningRoot(cwd), 'codebase');
121
+ if (!node_fs_1.default.existsSync(codebaseDir))
122
+ return [];
123
+ return REQUIRED_CODEBASE_MAP_FILES.filter((file) => node_fs_1.default.existsSync(node_path_1.default.join(codebaseDir, file)));
124
+ }
125
+ function getMapReadiness(hasCompleteMap, hasFastMap) {
126
+ if (hasCompleteMap)
127
+ return 'complete';
128
+ if (hasFastMap)
129
+ return 'fast';
130
+ return 'none';
131
+ }
132
+ function mapSummaryStatus(mapReadiness) {
133
+ if (mapReadiness === 'complete')
134
+ return '.planning/codebase/ (complete codebase map)';
135
+ if (mapReadiness === 'fast')
136
+ return '.planning/codebase/ (fast/partial codebase map; complete map still required for project setup)';
137
+ return 'missing';
138
+ }
139
+ function mapFinalStatus(mapReadiness) {
140
+ if (mapReadiness === 'complete')
141
+ return 'complete';
142
+ if (mapReadiness === 'fast')
143
+ return 'fast/partial; complete map still required for project setup';
144
+ return 'missing';
145
+ }
146
+ function planningMissing(projectExists, requirementsExists, roadmapExists, stateExists) {
147
+ const missing = [];
148
+ if (!projectExists)
149
+ missing.push('PROJECT.md');
150
+ if (!requirementsExists)
151
+ missing.push('REQUIREMENTS.md');
152
+ if (!roadmapExists)
153
+ missing.push('ROADMAP.md');
154
+ if (!stateExists)
155
+ missing.push('STATE.md');
156
+ return missing;
157
+ }
158
+ function nextAction(params) {
159
+ if (params.isBrownfield && params.needsOnboardCodebaseMap) {
160
+ return {
161
+ kind: 'map-codebase',
162
+ command: params.fastMode ? params.handoffCommands.map_codebase_fast : params.handoffCommands.map_codebase,
163
+ reason: 'Existing code was detected, but the required .planning/codebase/ map is missing.',
164
+ };
165
+ }
166
+ if (params.hasPlanningArtifacts && params.missingPlanningFiles.length > 0) {
167
+ return {
168
+ kind: 'partial-planning',
169
+ missing: params.missingPlanningFiles,
170
+ reason: 'Project planning exists but required planning files are missing.',
171
+ };
172
+ }
173
+ if (params.fastMode && params.mapReadiness === 'fast' && !params.projectExists) {
174
+ return {
175
+ kind: 'complete-map-before-new-project',
176
+ command: params.handoffCommands.map_codebase,
177
+ reason: 'The fast map is enough for lightweight onboarding, but project setup still requires the complete codebase map.',
178
+ };
179
+ }
180
+ if (params.hasDocsCandidates && !params.projectExists) {
181
+ return {
182
+ kind: 'ingest-docs',
183
+ command: params.handoffCommands.ingest_docs,
184
+ reason: 'Detected existing ADR/PRD/SPEC/RFC document(s) before project setup.',
185
+ };
186
+ }
187
+ if (!params.isBrownfield && !params.projectExists && !params.hasDocsCandidates) {
188
+ return {
189
+ kind: 'new-project',
190
+ command: params.handoffCommands.new_project,
191
+ reason: 'No existing code or planning docs were detected.',
192
+ };
193
+ }
194
+ if (!params.projectExists) {
195
+ return {
196
+ kind: 'new-project',
197
+ command: params.handoffCommands.new_project,
198
+ reason: 'Codebase context is ready for project initialization.',
199
+ };
200
+ }
201
+ if (!params.onboardingSummaryExists) {
202
+ return {
203
+ kind: 'write-summary',
204
+ summary_path: params.onboardingSummaryPath,
205
+ reason: 'Onboarding summary is missing.',
206
+ };
207
+ }
208
+ return {
209
+ kind: 'ready',
210
+ reason: 'Onboarding summary already exists.',
211
+ };
212
+ }
213
+ function buildHandoffCommands(cwd) {
214
+ const runtime = (0, runtime_slash_cjs_1.resolveRuntime)(cwd);
215
+ return {
216
+ ingest_docs: (0, runtime_slash_cjs_1.formatGsdSlash)('ingest-docs', runtime),
217
+ manager: (0, runtime_slash_cjs_1.formatGsdSlash)('manager', runtime),
218
+ map_codebase: (0, runtime_slash_cjs_1.formatGsdSlash)('map-codebase', runtime),
219
+ map_codebase_fast: (0, runtime_slash_cjs_1.formatGsdSlash)('map-codebase --fast', runtime),
220
+ new_project: (0, runtime_slash_cjs_1.formatGsdSlash)('new-project', runtime),
221
+ onboard: (0, runtime_slash_cjs_1.formatGsdSlash)('onboard', runtime),
222
+ };
223
+ }
224
+ function buildOnboardProjection(cwd, options) {
225
+ const handoffCommands = buildHandoffCommands(cwd);
226
+ const codebaseMapFiles = listCodebaseMapFiles(cwd);
227
+ const missingCodebaseMapFiles = REQUIRED_CODEBASE_MAP_FILES.filter((file) => !codebaseMapFiles.includes(file));
228
+ const missingFastCodebaseMapFiles = FAST_CODEBASE_MAP_FILES.filter((file) => !codebaseMapFiles.includes(file));
229
+ const docCandidates = listPlanningDocCandidates(cwd);
230
+ const hasCode = hasCodeFilesInternal(cwd);
231
+ const hasPackageFile = hasPackageFileInternal(cwd);
232
+ const isBrownfield = hasCode || hasPackageFile;
233
+ const hasCodebaseMap = codebaseMapFiles.length === REQUIRED_CODEBASE_MAP_FILES.length;
234
+ const hasFastCodebaseMap = missingFastCodebaseMapFiles.length === 0;
235
+ const mapReadinessValue = getMapReadiness(hasCodebaseMap, hasFastCodebaseMap);
236
+ const needsCodebaseMap = isBrownfield && !hasCodebaseMap;
237
+ const needsFastCodebaseMap = isBrownfield && !hasFastCodebaseMap;
238
+ const needsOnboardCodebaseMap = options.fast ? needsFastCodebaseMap : needsCodebaseMap;
239
+ const projectRootPath = node_path_1.default.join(planningRoot(cwd), 'PROJECT.md');
240
+ const projectScopedPath = node_path_1.default.join(planningDir(cwd), 'PROJECT.md');
241
+ const projectExists = node_fs_1.default.existsSync(projectRootPath) || node_fs_1.default.existsSync(projectScopedPath);
242
+ const requirementsExists = node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'REQUIREMENTS.md'));
243
+ const roadmapExists = node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md'));
244
+ const stateExists = node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'STATE.md'));
245
+ const onboardingSummaryPath = node_path_1.default.join(planningRoot(cwd), 'onboarding', 'SUMMARY.md');
246
+ const onboardingSummaryExists = node_fs_1.default.existsSync(onboardingSummaryPath);
247
+ const hasPlanningArtifacts = projectExists || requirementsExists || roadmapExists || stateExists;
248
+ const missingPlanningFiles = planningMissing(projectExists, requirementsExists, roadmapExists, stateExists);
249
+ return {
250
+ commit_docs: options.commitDocs,
251
+ text_mode: options.textMode,
252
+ project_exists: projectExists,
253
+ planning_exists: node_fs_1.default.existsSync(planningRoot(cwd)),
254
+ requirements_exists: requirementsExists,
255
+ roadmap_exists: roadmapExists,
256
+ state_exists: stateExists,
257
+ config_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningDir(cwd), 'config.json')),
258
+ handoff_commands: handoffCommands,
259
+ has_existing_code: hasCode,
260
+ has_package_file: hasPackageFile,
261
+ is_brownfield: isBrownfield,
262
+ fast_mode: options.fast,
263
+ map_readiness: mapReadinessValue,
264
+ next_action: nextAction({
265
+ fastMode: options.fast,
266
+ isBrownfield,
267
+ needsOnboardCodebaseMap,
268
+ hasDocsCandidates: docCandidates.length > 0,
269
+ projectExists,
270
+ mapReadiness: mapReadinessValue,
271
+ onboardingSummaryExists,
272
+ onboardingSummaryPath: toPosixPath(node_path_1.default.relative(cwd, onboardingSummaryPath)),
273
+ hasPlanningArtifacts,
274
+ missingPlanningFiles,
275
+ handoffCommands,
276
+ }),
277
+ needs_codebase_map: needsCodebaseMap,
278
+ needs_fast_codebase_map: needsFastCodebaseMap,
279
+ has_codebase_map: hasCodebaseMap,
280
+ has_fast_codebase_map: hasFastCodebaseMap,
281
+ codebase_dir_exists: node_fs_1.default.existsSync(node_path_1.default.join(planningRoot(cwd), 'codebase')),
282
+ fast_codebase_map_files_required: FAST_CODEBASE_MAP_FILES,
283
+ codebase_map_files_present: codebaseMapFiles,
284
+ missing_codebase_map_files: missingCodebaseMapFiles,
285
+ missing_fast_codebase_map_files: missingFastCodebaseMapFiles,
286
+ codebase_map_summary_status: mapSummaryStatus(mapReadinessValue),
287
+ codebase_map_final_status: mapFinalStatus(mapReadinessValue),
288
+ has_docs_candidates: docCandidates.length > 0,
289
+ doc_candidate_count: docCandidates.length,
290
+ doc_candidates: docCandidates,
291
+ onboarding_summary_exists: onboardingSummaryExists,
292
+ onboarding_summary_path: toPosixPath(node_path_1.default.relative(cwd, onboardingSummaryPath)),
293
+ project_path: toPosixPath(node_path_1.default.relative(cwd, node_fs_1.default.existsSync(projectRootPath) ? projectRootPath : projectScopedPath)),
294
+ requirements_path: toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd), 'REQUIREMENTS.md'))),
295
+ roadmap_path: toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd), 'ROADMAP.md'))),
296
+ state_path: toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningDir(cwd), 'STATE.md'))),
297
+ codebase_dir: toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningRoot(cwd), 'codebase'))),
298
+ onboarding_dir: toPosixPath(node_path_1.default.relative(cwd, node_path_1.default.join(planningRoot(cwd), 'onboarding'))),
299
+ };
300
+ }
301
+ module.exports = {
302
+ REQUIRED_CODEBASE_MAP_FILES,
303
+ FAST_CODEBASE_MAP_FILES,
304
+ buildOnboardProjection,
305
+ hasCodeFilesInternal,
306
+ hasPackageFileInternal,
307
+ listCodebaseMapFiles,
308
+ listPlanningDocCandidates,
309
+ };
@@ -209,6 +209,7 @@ const RUNTIME_LABELS = {
209
209
  kimi: 'Kimi CLI',
210
210
  codebuddy: 'CodeBuddy',
211
211
  cline: 'Cline',
212
+ zcode: 'ZCode',
212
213
  };
213
214
  /**
214
215
  * Map a canonical runtime id to its short display label for the
@@ -255,6 +256,7 @@ const GLOBAL_CONFIG_HOME_FRAGMENTS = {
255
256
  codebuddy: "'.codebuddy'",
256
257
  cline: "'.cline'",
257
258
  kimi: "'.config', 'agents'",
259
+ zcode: "'.zcode'",
258
260
  };
259
261
  /**
260
262
  * Return the global config-home path-fragment source snippet for a runtime
@@ -277,7 +279,7 @@ function getGlobalConfigHomeFragment(runtime) {
277
279
  */
278
280
  const RUNTIME_FLAG_IDS = Object.freeze([
279
281
  'opencode', 'kilo', 'codex', 'copilot', 'antigravity', 'cursor',
280
- 'windsurf', 'augment', 'trae', 'qwen', 'hermes', 'codebuddy', 'cline', 'kimi',
282
+ 'windsurf', 'augment', 'trae', 'qwen', 'hermes', 'codebuddy', 'cline', 'kimi', 'zcode',
281
283
  ]);
282
284
  /**
283
285
  * Return a frozen map of `is<Runtime>` boolean predicates for the given runtime
@@ -32,6 +32,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
32
32
  return (mod && mod.__esModule) ? mod : { "default": mod };
33
33
  };
34
34
  const node_fs_1 = __importDefault(require("node:fs"));
35
+ const node_os_1 = __importDefault(require("node:os"));
35
36
  const node_path_1 = __importDefault(require("node:path"));
36
37
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
37
38
  // eslint-disable-next-line @typescript-eslint/no-require-imports
@@ -261,32 +262,52 @@ function resolveSurface(runtimeConfigDir, manifest, clusterMap, registry) {
261
262
  * Re-stage the active surface using the resolved layout.
262
263
  * Iterates layout.kinds and syncs each artifact kind to its destination.
263
264
  */
264
- function applySurface(runtimeConfigDir, layout, manifest, clusterMap, registry) {
265
+ function applySurface(runtimeConfigDir, layout, manifest, clusterMap, registry, opts) {
265
266
  if (node_path_1.default.resolve(runtimeConfigDir) !== node_path_1.default.resolve(layout.configDir)) {
266
267
  throw new TypeError('applySurface runtimeConfigDir must match layout.configDir');
267
268
  }
268
269
  const skillManifest = normalizeSkillManifest(layout.configDir, manifest);
269
270
  const resolved = resolveSurface(layout.configDir, skillManifest, clusterMap, registry);
270
- // Mirror installRuntimeArtifacts: skills kinds get per-runtime path rewrites
271
- // so SKILL.md bodies reference the install target (pathPrefix), not the
272
- // converter's default ~/.claude paths (#813). Delegated to the conversion
273
- // module's deep seam (ADR-1508 / #1511 Phase 2) — no attribution resolver
274
- // needed here (proven: Co-Authored-By never appears in staged content; see
275
- // brief PROVEN KEY FACT). No getInstallExports() call required.
276
- // #1615 adversarial review (PR #1622): commands kind was previously skipped,
277
- // leaving raw @~/.claude/... references in Windsurf workflow bodies after a
278
- // /gsd-surface profile change. Same gap affected any runtime with commands
279
- // kinds (windsurf, opencode, kilo, cursor, augment, codebuddy, gemini).
280
- //
281
- // Asymmetry note: rewriteStagedSkillBodies mutates in place (returns void),
282
- // but rewriteStagedCommandBodies copies to a fresh mkdtemp dir and returns
283
- // its path (commands .md files are flat; mutating the staged source would
284
- // corrupt the package source on full-profile runs). Caller MUST sync from
285
- // the returned dir and clean it up.
271
+ // #1575: agents kind now mirrors createRuntimeArtifactInstallPlan — build
272
+ // agentCtx (pathPrefix + attribution) and pass it to kind.stage() so
273
+ // stageAgentsForRuntimeWithConverter applies the full inline-loop pipeline
274
+ // (pathRewrites -> attribution -> converter -> normalize). Without this,
275
+ // surface-path agents lack path-prefix rewrites and Co-Authored-By trailers,
276
+ // diverging from a fresh install.
277
+ const _homedirFn = opts?.homedir ?? (() => node_os_1.default.homedir());
278
+ const _resolvedTarget = node_path_1.default.resolve(layout.configDir).replace(/\\/g, '/');
279
+ const _homeDir = _homedirFn().replace(/\\/g, '/');
280
+ const _isGlobal = (layout.scope ?? 'global') === 'global';
281
+ const _isOpencode = layout.runtime === 'opencode';
282
+ const _isWindowsHost = (opts?.platform ?? process.platform) === 'win32';
283
+ const _pathPrefix = runtimeArtifactConversion._computePathPrefix({ isGlobal: _isGlobal, isOpencode: _isOpencode, isWindowsHost: _isWindowsHost, resolvedTarget: _resolvedTarget, homeDir: _homeDir });
284
+ const _attribution = opts?.resolveAttribution ? opts.resolveAttribution(layout.runtime) : undefined;
285
+ const agentCtx = { runtime: layout.runtime, pathPrefix: _pathPrefix, attribution: _attribution };
286
286
  const tempDirsToClean = [];
287
+ // #1575: When the surface has no state modifications AND the base profile is
288
+ // 'full', pass the '*' sentinel for agents staging so ALL agents are staged —
289
+ // matching the install path which uses { skills: '*' }. Without this, agents
290
+ // not referenced by any skill's _calls_agents_ manifest entry would be silently
291
+ // dropped from the surface path. For tiered profiles (core/standard) or when
292
+ // surface mods exist, pass the resolved set so only the filtered subset stages.
293
+ const _surfaceState = readSurface(layout.configDir);
294
+ const _baseProfileName = (_surfaceState && _surfaceState.baseProfile)
295
+ ? _surfaceState.baseProfile
296
+ : (readActiveProfile(layout.configDir) || 'full');
297
+ const _hasSurfaceMods = !!_surfaceState && (_surfaceState.disabledClusters.length > 0 ||
298
+ _surfaceState.explicitAdds.length > 0 ||
299
+ _surfaceState.explicitRemoves.length > 0);
300
+ const _isUnmodifiedFull = _baseProfileName === 'full' && !_hasSurfaceMods;
287
301
  try {
288
302
  for (const kind of layout.kinds) {
289
- let staged = kind.stage(resolved);
303
+ let staged;
304
+ if (kind.kind === 'agents') {
305
+ const agentProfile = _isUnmodifiedFull ? { ...resolved, skills: '*' } : resolved;
306
+ staged = kind.stage(agentProfile, agentCtx);
307
+ }
308
+ else {
309
+ staged = kind.stage(resolved);
310
+ }
290
311
  if (kind.kind === 'skills') {
291
312
  runtimeArtifactConversion.rewriteStagedSkillBodies(staged, {
292
313
  runtime: layout.runtime,
@@ -306,7 +327,7 @@ function applySurface(runtimeConfigDir, layout, manifest, clusterMap, registry)
306
327
  }
307
328
  }
308
329
  const dest = assertDestWithinConfigHome(layout.configDir, kind.destSubpath);
309
- _syncGsdDir(staged, dest, kind, skillManifest);
330
+ _syncGsdDir(staged, dest, kind, skillManifest, layout.runtime);
310
331
  }
311
332
  }
312
333
  finally {
@@ -414,13 +435,17 @@ function pruneSkillDirs(skillsDir, retainedNames, prefix, manifest) {
414
435
  * user-owned dirs. GSD-owned = stem in manifest; removal targets = in manifest AND
415
436
  * not in staged set. User-owned (not in manifest) are always preserved.
416
437
  */
417
- function _syncGsdDir(stagedDir, destDir, kind, manifest) {
438
+ function _syncGsdDir(stagedDir, destDir, kind, manifest, runtime) {
418
439
  if (!node_fs_1.default.existsSync(stagedDir))
419
440
  return;
420
441
  node_fs_1.default.mkdirSync(destDir, { recursive: true });
421
442
  // Normalize: allow legacy string context for backward-compat with internal callers
422
443
  const kindName = (typeof kind === 'string') ? kind : kind.kind;
423
444
  const kindPrefix = (typeof kind === 'object' && kind !== null) ? kind.prefix : 'gsd-';
445
+ // #1575: copilot agents are renamed .md -> .agent.md at copy time, mirroring
446
+ // the inline agent loop in bin/install.js (line ~9118). Other runtimes keep
447
+ // the staged filename verbatim.
448
+ const isCopilotAgents = runtime === 'copilot' && kindName === 'agents';
424
449
  if (kindName === 'skills') {
425
450
  // Skills kind: work with directories, not files.
426
451
  // Each staged entry is a directory named ${prefix}${stem}.
@@ -455,23 +480,19 @@ function _syncGsdDir(stagedDir, destDir, kind, manifest) {
455
480
  const stagedFiles = node_fs_1.default.readdirSync(stagedDir).filter(f => f.endsWith('.md'));
456
481
  const stagedDestNames = new Set();
457
482
  for (const file of stagedFiles) {
458
- const destName = (kindName === 'agents' || namespacedByDir)
459
- ? file
460
- : `${kindPrefix}${file.slice(0, -3)}.md`;
483
+ const destName = isCopilotAgents
484
+ ? file.replace(/\.md$/, '.agent.md')
485
+ : (kindName === 'agents' || namespacedByDir)
486
+ ? file
487
+ : `${kindPrefix}${file.slice(0, -3)}.md`;
461
488
  node_fs_1.default.copyFileSync(node_path_1.default.join(stagedDir, file), node_path_1.default.join(destDir, destName));
462
489
  stagedDestNames.add(destName);
463
490
  }
464
491
  // Prune stale GSD-owned files not in the staged set, preserving user-owned files
465
492
  // (mirrors install's prefix-scoped _removeGsdEntries):
466
- // - agents: only gsd-* are GSD-owned
493
+ // - agents: only gsd-* are GSD-owned (copilot: gsd-*.agent.md)
467
494
  // - flat command dirs: only `${kindPrefix}`-prefixed are GSD-owned
468
495
  // - namespaced command dirs: the whole dir is GSD-owned
469
- //
470
- // Manifest gate (#2018): when the manifest is empty/absent (e.g. an unresolvable
471
- // install source root yields an empty staged dir), the staged set is untrustworthy.
472
- // Skills are guarded by pruneSkillDirs' manifest-membership check; agents must be
473
- // guarded here — skip the prune loop entirely so an empty manifest never deletes
474
- // every gsd-* agent. Copying (above) still runs so genuinely new agents are added.
475
496
  const shouldPruneAgents = !(kindName === 'agents' && (!manifest || manifest.size === 0));
476
497
  if (shouldPruneAgents) {
477
498
  for (const file of node_fs_1.default.readdirSync(destDir).filter(f => f.endsWith('.md'))) {
@@ -62,6 +62,11 @@
62
62
  "sonnet": null,
63
63
  "haiku": null
64
64
  },
65
+ "zcode": {
66
+ "opus": null,
67
+ "sonnet": null,
68
+ "haiku": null
69
+ },
65
70
  "augment": {
66
71
  "opus": null,
67
72
  "sonnet": null,
@@ -0,0 +1,8 @@
1
+ # Standard `gsd_run` Resolver
2
+
3
+ Use this bootstrap block when a workflow must call `gsd-tools` from the active worktree/runtime.
4
+ Keep this resolver centralized; workflows should reference this file instead of copying the shell probe.
5
+
6
+ ```bash
7
+ _GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd-tools is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi
8
+ ```
@@ -166,7 +166,7 @@ and implemented by workflows/transition.md and workflows/complete-milestone.md.
166
166
 
167
167
  For existing codebases:
168
168
 
169
- 1. **Map codebase first** via `/gsd:map-codebase`
169
+ 1. **Onboard or map codebase first** via `/gsd:onboard` (recommended first-time path) or `/gsd:map-codebase`
170
170
 
171
171
  2. **Infer Validated requirements** from existing code:
172
172
  - What does the codebase actually do?
@@ -40,8 +40,9 @@ Evaluate `$ARGUMENTS` against these routing rules. Apply the **first matching**
40
40
 
41
41
  | If the text describes... | Route to | Why |
42
42
  |--------------------------|----------|-----|
43
- | Starting a new project, "set up", "initialize" | `/gsd:new-project` | Needs full project initialization |
44
- | Mapping or analyzing an existing codebase | `/gsd:map-codebase` | Codebase discovery |
43
+ | Starting a new greenfield project, "set up", "initialize" | `/gsd:new-project` | Needs full project initialization |
44
+ | First-time setup for an existing codebase, brownfield onboarding | `/gsd:onboard` | Safe map → docs ingest → project setup sequence |
45
+ | Mapping or analyzing an existing codebase map | `/gsd:map-codebase` | Codebase discovery or refresh |
45
46
  | A bug, error, crash, failure, or something broken | `/gsd:debug` | Needs systematic investigation |
46
47
  | Spiking, "test if", "will this work", "experiment", "prove this out", validate feasibility | `/gsd:spike` | Throwaway experiment to validate feasibility |
47
48
  | Sketching, "mockup", "what would this look like", "prototype the UI", "design this", explore visual direction | `/gsd:sketch` | Throwaway HTML mockups to explore design |
@@ -61,7 +62,7 @@ Evaluate `$ARGUMENTS` against these routing rules. Apply the **first matching**
61
62
  | Completing a milestone, shipping, releasing | `/gsd:complete-milestone` | Milestone lifecycle |
62
63
  | A specific, actionable, small task (add feature, fix typo, update config) | `/gsd:quick` | Self-contained, single executor |
63
64
 
64
- **Requires `.planning/` directory:** All routes except `/gsd:new-project`, `/gsd:map-codebase`, `/gsd:spike`, `/gsd:sketch`, and `/gsd:help`. If the project doesn't exist and the route requires it, suggest `/gsd:new-project` first.
65
+ **Requires `.planning/` directory:** All routes except `/gsd:new-project`, `/gsd:onboard`, `/gsd:map-codebase`, `/gsd:spike`, `/gsd:sketch`, and `/gsd:help`. If the project doesn't exist and the route requires it, suggest `/gsd:onboard` for existing codebases or `/gsd:new-project` for greenfield projects.
65
66
 
66
67
  **Ambiguity handling:** If the text could reasonably match multiple routes, ask the user via AskUserQuestion with the top 2-3 options. For example:
67
68