@opengsd/gsd-core 1.5.0-rc.5 → 1.6.0-rc.1

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 (87) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/LICENSE +1 -1
  3. package/agents/gsd-executor.md +22 -0
  4. package/agents/gsd-plan-checker.md +34 -0
  5. package/agents/gsd-planner.md +2 -0
  6. package/bin/install.js +161 -133
  7. package/commands/gsd/autonomous.md +1 -1
  8. package/commands/gsd/execute-phase.md +1 -1
  9. package/commands/gsd/plan-phase.md +1 -1
  10. package/gemini-extension.json +1 -1
  11. package/gsd-core/bin/gsd-tools.cjs +713 -7
  12. package/gsd-core/bin/lib/adr-parser.cjs +24 -17
  13. package/gsd-core/bin/lib/audit.cjs +2 -2
  14. package/gsd-core/bin/lib/capability-consent.cjs +763 -0
  15. package/gsd-core/bin/lib/capability-ledger.cjs +831 -0
  16. package/gsd-core/bin/lib/capability-lifecycle.cjs +1551 -0
  17. package/gsd-core/bin/lib/capability-loader.cjs +764 -0
  18. package/gsd-core/bin/lib/capability-lock.cjs +553 -0
  19. package/gsd-core/bin/lib/capability-registry.cjs +198 -4
  20. package/gsd-core/bin/lib/capability-source.cjs +1242 -0
  21. package/gsd-core/bin/lib/capability-state.cjs +9 -6
  22. package/gsd-core/bin/lib/capability-trust.cjs +550 -0
  23. package/gsd-core/bin/lib/capability-validator.cjs +2066 -0
  24. package/gsd-core/bin/lib/capability-writer.cjs +14 -5
  25. package/gsd-core/bin/lib/check-command-router.cjs +69 -18
  26. package/gsd-core/bin/lib/command-aliases.cjs +8 -0
  27. package/gsd-core/bin/lib/config-loader.cjs +92 -84
  28. package/gsd-core/bin/lib/config-schema.cjs +26 -7
  29. package/gsd-core/bin/lib/config.cjs +1 -1
  30. package/gsd-core/bin/lib/decisions.cjs +154 -47
  31. package/gsd-core/bin/lib/gap-checker.cjs +126 -11
  32. package/gsd-core/bin/lib/init.cjs +91 -22
  33. package/gsd-core/bin/lib/legacy-cleanup.cjs +96 -0
  34. package/gsd-core/bin/lib/loop-resolver.cjs +26 -2
  35. package/gsd-core/bin/lib/markdown-sectionizer.cjs +471 -0
  36. package/gsd-core/bin/lib/milestone.cjs +41 -2
  37. package/gsd-core/bin/lib/phase-command-router.cjs +5 -0
  38. package/gsd-core/bin/lib/phase-id.cjs +1 -1
  39. package/gsd-core/bin/lib/phase-lifecycle.cjs +14 -5
  40. package/gsd-core/bin/lib/phase.cjs +70 -6
  41. package/gsd-core/bin/lib/probe-core.cjs +7 -0
  42. package/gsd-core/bin/lib/prohibition-enforcement.cjs +201 -26
  43. package/gsd-core/bin/lib/project-root.cjs +89 -2
  44. package/gsd-core/bin/lib/resolution.cjs +26 -0
  45. package/gsd-core/bin/lib/roadmap-parser.cjs +69 -118
  46. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +4 -1
  47. package/gsd-core/bin/lib/runtime-homes.cjs +53 -1
  48. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +24 -3
  49. package/gsd-core/bin/lib/semver-compare.cjs +127 -0
  50. package/gsd-core/bin/lib/state-document.cjs +4 -2
  51. package/gsd-core/bin/lib/state.cjs +379 -174
  52. package/gsd-core/bin/lib/teams-status.cjs +74 -0
  53. package/gsd-core/bin/lib/uat-predicate.cjs +7 -47
  54. package/gsd-core/bin/lib/uat.cjs +75 -6
  55. package/gsd-core/bin/lib/verify.cjs +29 -13
  56. package/gsd-core/bin/lib/worktree-safety.cjs +7 -1
  57. package/gsd-core/bin/shared/config-defaults.manifest.json +4 -0
  58. package/gsd-core/bin/shared/config-schema.manifest.json +4 -1
  59. package/gsd-core/references/execute-phase-between-wave-reset.md +43 -0
  60. package/gsd-core/references/execute-phase-wave-guard.md +33 -0
  61. package/gsd-core/references/planner-antipatterns.md +48 -0
  62. package/gsd-core/references/planning-config.md +3 -0
  63. package/gsd-core/references/prohibition-probe.md +48 -19
  64. package/gsd-core/references/scout-codebase.md +2 -2
  65. package/gsd-core/references/worktree-branch-check.md +11 -5
  66. package/gsd-core/workflows/discuss-phase/templates/context.md +1 -1
  67. package/gsd-core/workflows/discuss-phase.md +1 -2
  68. package/gsd-core/workflows/docs-update.md +23 -31
  69. package/gsd-core/workflows/execute-phase.md +6 -7
  70. package/gsd-core/workflows/map-codebase.md +8 -10
  71. package/gsd-core/workflows/plan-phase.md +6 -0
  72. package/gsd-core/workflows/quick.md +23 -2
  73. package/gsd-core/workflows/spec-phase.md +11 -6
  74. package/gsd-core/workflows/verify-phase.md +3 -3
  75. package/hooks/dist/gsd-worktree-path-guard.js +34 -18
  76. package/hooks/gsd-worktree-path-guard.js +34 -18
  77. package/package.json +4 -4
  78. package/scripts/ci-prepare-test-scope.cjs +56 -14
  79. package/scripts/diff-touches-shipped-paths.cjs +5 -11
  80. package/scripts/gen-capability-matrix.cjs +284 -0
  81. package/scripts/gen-capability-registry.cjs +96 -1853
  82. package/scripts/lint-regression-test-names.allowlist.json +1 -0
  83. package/scripts/lint-resolution-provenance.allowlist.json +1 -0
  84. package/scripts/lint-resolution-provenance.cjs +192 -0
  85. package/scripts/lint-test-file-count.allowlist.json +9 -0
  86. package/scripts/run-tests.cjs +15 -0
  87. package/scripts/sync-manifest-versions.cjs +77 -5
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "gsd-core",
3
3
  "displayName": "GSD Core",
4
- "version": "1.5.0-rc.5",
4
+ "version": "1.6.0-rc.1",
5
5
  "description": "GSD Core is a meta-prompting, context engineering, and spec-driven development system for AI coding agents.",
6
6
  "author": {
7
7
  "name": "open-gsd",
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 Lex Christopherson
3
+ Copyright (c) 2026 Open GSD
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -103,6 +103,24 @@ PLAN_START_EPOCH=$(date +%s)
103
103
  ```
104
104
  </step>
105
105
 
106
+ <worktree_metadata_capture>
107
+ If running inside a git worktree, capture authoritative worktree identity before
108
+ any task commit changes HEAD. The execute-phase orchestrator consumes this from
109
+ your final `<worktree_metadata>` return block to build the wave cleanup manifest
110
+ without relying on runtime harness metadata (#1297).
111
+
112
+ ```bash
113
+ GSD_WORKTREE_PATH=""
114
+ GSD_WORKTREE_BRANCH=""
115
+ GSD_WORKTREE_EXPECTED_BASE=""
116
+ if [ -f .git ]; then
117
+ GSD_WORKTREE_PATH=$(git rev-parse --show-toplevel)
118
+ GSD_WORKTREE_BRANCH=$(git rev-parse --abbrev-ref HEAD)
119
+ GSD_WORKTREE_EXPECTED_BASE=$(git rev-parse HEAD)
120
+ fi
121
+ ```
122
+ </worktree_metadata_capture>
123
+
106
124
  <step name="determine_execution_pattern">
107
125
  ```bash
108
126
  grep -n "type=\"checkpoint" [plan-path]
@@ -761,6 +779,10 @@ into the user's project history.
761
779
  **Tasks:** {completed}/{total}
762
780
  **SUMMARY:** {path to SUMMARY.md}
763
781
 
782
+ <worktree_metadata>
783
+ {"agent_id":"{phase}-{plan}","worktree_path":"${GSD_WORKTREE_PATH:-}","branch":"${GSD_WORKTREE_BRANCH:-}","expected_base":"${GSD_WORKTREE_EXPECTED_BASE:-}"}
784
+ </worktree_metadata>
785
+
764
786
  **Commits:**
765
787
  - {hash}: {message}
766
788
  - {hash}: {message}
@@ -647,6 +647,40 @@ issue:
647
647
  fix_hint: "Add auth middleware pattern from PATTERNS.md ## Shared Patterns to plan"
648
648
  ```
649
649
 
650
+ ## Dimension: Verify Command Format Sanity (#1478, #1479)
651
+
652
+ **Question:** Do `<verify>` commands use patterns that can actually match the tool's output? Are numeric counts measured? Are errors suppressed into comparison-feeding defaults?
653
+
654
+ **Red flags — BLOCKER:**
655
+ - `pnpm ls … | grep -E '^package'` — `^` anchor on tree-formatted package manager output (never matches tree-prefixed lines)
656
+ - Any verify block with `VAR=$(cmd 2>/dev/null || echo "0"); [ "$VAR" = ... ]` — swallowed error feeds passing comparison
657
+ - `|| true` or `|| :` as right-hand side of assignments that feed comparisons
658
+
659
+ **Red flags — WARNING:**
660
+ - Hard-coded count assertion (`grep '52 test files'`, `grep '714 passed'`) with no measurement provenance in the plan
661
+
662
+ **Process:**
663
+ 1. For each `<automated>` block piping a package-manager list command into grep with a `^` anchor: BLOCKER.
664
+ 2. For each `<automated>` block containing `2>/dev/null || echo` where the result feeds a `[ "$VAR" = ... ]` comparison: BLOCKER.
665
+ 3. For each `<automated>` block asserting a specific numeric count not cited as measured in this plan: WARNING.
666
+
667
+ ## Dimension: Numeric/Factual Claim Authority (#1480)
668
+
669
+ **Rule:** RESEARCH.md is produced at research time and may be stale. Numeric claims (test counts, file counts, version numbers) and factual state claims ("feature X is implemented") in RESEARCH.md may not reflect the current codebase. The plan may be more current. RESEARCH.md is authoritative for architectural decisions and constraints — not for measurements.
670
+
671
+ **Process when a plan's numeric/factual claim conflicts with RESEARCH.md:**
672
+
673
+ 1. **Attempt live measurement first** with a targeted read-only command (e.g., `find . -name '*.test.*' | wc -l`). Run it. Use the result as ground truth:
674
+ - Measurement confirms plan → WARNING: RESEARCH.md is stale; recommend updating it.
675
+ - Measurement contradicts plan → BLOCKER: plan value is wrong; prescribe the measured value.
676
+
677
+ 2. **If live measurement is not possible** (external system, future state): report the discrepancy WITHOUT prescribing which value is correct:
678
+ > Discrepancy: plan asserts X, RESEARCH.md asserts Y. Cannot determine ground truth without live measurement. Verify manually and update the stale artifact.
679
+
680
+ **NEVER** prescribe a specific value by assuming RESEARCH.md is authoritative for a numeric/factual claim.
681
+
682
+ **Note:** A targeted read-only shell command (counting files, reading a schema, checking a version file) is NOT "running the application" — it is live measurement. Such commands are permitted under this dimension even when the anti-pattern block says "DO NOT run the application."
683
+
650
684
  </verification_dimensions>
651
685
 
652
686
  <verification_process>
@@ -200,6 +200,8 @@ Full rules + worked examples: @gsd-core/references/planner-antipatterns.md ("Com
200
200
 
201
201
  <region_scoped_negative_gate>
202
202
  **Region-scoped negative gates (WARN, #968):** Region-scope a file-wide negative grep when a sibling task needs that construct elsewhere in the same file; `validate_plan` WARNS. See: @gsd-core/references/planner-antipatterns.md ("Region-Scoped Negative Gates").
203
+
204
+ **Verify-gate hygiene (#1478/#1479):** See @gsd-core/references/planner-antipatterns.md.
203
205
  </region_scoped_negative_gate>
204
206
 
205
207
  **<done>:** Acceptance criteria - measurable state of completion.
package/bin/install.js CHANGED
@@ -1796,6 +1796,10 @@ function skillFrontmatterName(skillDirName) {
1796
1796
  return skillDirName;
1797
1797
  }
1798
1798
 
1799
+ function normalizeClaudeSkillEffort(effort) {
1800
+ return effort === 'xhigh' ? 'max' : effort;
1801
+ }
1802
+
1799
1803
  /**
1800
1804
  * Qwen Code skills accept an optional numeric `priority` frontmatter field.
1801
1805
  * Per the Qwen skills spec (qwen-code/docs/users/features/skills.md, verified
@@ -1892,7 +1896,7 @@ function convertClaudeCommandToClaudeSkill(content, skillName, runtime = null, c
1892
1896
  // token-budget tier). Fields are Claude-specific; unknown frontmatter
1893
1897
  // fields are silently ignored by other runtimes (backward-compatible).
1894
1898
  if (context) fm += `context: ${context}\n`;
1895
- if (effort) fm += `effort: ${effort}\n`;
1899
+ if (effort) fm += `effort: ${normalizeClaudeSkillEffort(effort)}\n`;
1896
1900
  if (toolsBlock) fm += toolsBlock;
1897
1901
  fm += '---';
1898
1902
 
@@ -3194,110 +3198,55 @@ function generateCodexAgentToml(agentName, agentContent, modelOverrides = null,
3194
3198
  }
3195
3199
 
3196
3200
  /**
3197
- * Generate the agents/openai.yaml TUI chip metadata content for a Codex skill.
3201
+ * Remove stale agents/openai.yaml sidecar files from GSD-managed Codex skill dirs.
3198
3202
  *
3199
- * This file is written alongside SKILL.md as <skill-dir>/agents/openai.yaml.
3200
- * Codex loads it as a SkillMetadataFile (codex-rs/core-skills/src/loader.rs),
3201
- * making the skill discoverable in the /skills TUI popup with a display name
3202
- * and short description. If the file is absent, Codex silently skips it (fails open).
3203
+ * Prior to #1326, GSD's Codex install path wrote an agents/openai.yaml file
3204
+ * alongside each gsd-* SKILL.md. Recent Codex builds index BOTH SKILL.md and
3205
+ * the sidecar, causing each GSD skill to appear twice in autocomplete. This
3206
+ * function removes those stale sidecars and — if the agents/ subdirectory is
3207
+ * now empty — prunes it too.
3203
3208
  *
3204
- * Schema (interface section):
3205
- * display_name: short human-readable skill name (strip gsd- prefix)
3206
- * short_description: 1-2 sentence description for TUI chip, ≤180 chars
3207
- *
3208
- * @param {string} skillName - Full skill name e.g. "gsd-plan-phase"
3209
- * @param {string} shortDescription - Description text (already truncated by caller)
3210
- * @returns {string} YAML content for agents/openai.yaml
3211
- */
3212
- function generateCodexSkillMetadataYaml(skillName, shortDescription) {
3213
- // Display name: strip "gsd-" prefix and convert hyphens to spaces for readability.
3214
- const displayName = skillName.replace(/^gsd-/, '').replace(/-/g, ' ');
3215
- // yamlQuote (= JSON.stringify) handles all YAML-unsafe chars: backslashes,
3216
- // quotes, newlines, control characters, and Unicode escapes.
3217
- return [
3218
- 'interface:',
3219
- ` display_name: ${yamlQuote(displayName)}`,
3220
- ` short_description: ${yamlQuote(shortDescription)}`,
3221
- '',
3222
- ].join('\n');
3223
- }
3224
-
3225
- /**
3226
- * Write agents/openai.yaml TUI chip metadata for each gsd-* skill directory.
3227
- *
3228
- * Called after layout-driven skill install for Codex. Iterates every gsd-*
3229
- * skill directory in skillsDir, reads the SKILL.md frontmatter to extract the
3230
- * short-description already emitted by convertClaudeCommandToCodexSkill, then
3231
- * writes <skill-dir>/agents/openai.yaml using generateCodexSkillMetadataYaml.
3232
- *
3233
- * Fails open: individual skill directories that cannot be processed are silently
3234
- * skipped so a single malformed SKILL.md cannot block the whole install.
3235
- *
3236
- * User-owned skill directories (e.g. gsd-dev-preferences) are explicitly
3237
- * skipped so existing user-authored agents/openai.yaml files are never
3238
- * overwritten. These dirs are listed in the same USER_OWNED_SKILL_DIRS
3239
- * constant used by installOpencodeFamilySkills.
3240
- *
3241
- * The YAML-quoted description value is unescaped before embedding so that
3242
- * YAML escape sequences (e.g. \" in a double-quoted scalar) become the
3243
- * literal characters they represent rather than being double-escaped in the
3244
- * output.
3209
+ * Behaviour:
3210
+ * - Returns immediately if skillsDir does not exist (fails open).
3211
+ * - Only touches directories whose names start with "gsd-".
3212
+ * - Skips user-owned dirs (gsd-dev-preferences) — their agents/ content is
3213
+ * never modified, mirroring the same USER_OWNED_SKILL_DIRS guard used by
3214
+ * installOpencodeFamilySkills.
3215
+ * - For each managed gsd-* dir, if agents/openai.yaml exists, deletes it.
3216
+ * - If agents/ is now empty, removes the directory; if it still contains
3217
+ * other files (e.g. user-added content), leaves it in place.
3218
+ * - Non-gsd-* dirs and their agents/ content are never touched.
3219
+ * - Individual failures are caught and swallowed so a single bad dir cannot
3220
+ * block the install (fail-open, matching the original design).
3245
3221
  *
3246
3222
  * @param {string} skillsDir - Path to the skills/ directory (e.g. ~/.codex/skills)
3247
3223
  */
3248
- function writeCodexSkillMetadataFiles(skillsDir) {
3224
+ function cleanupCodexSkillMetadataSidecars(skillsDir) {
3249
3225
  if (!fs.existsSync(skillsDir)) return;
3250
3226
  // Mirror the user-owned list from installOpencodeFamilySkills (#2973).
3251
3227
  // We MUST skip these dirs — their contents are user-generated and must
3252
- // never be overwritten by GSD's install path.
3228
+ // never be modified by GSD's install path.
3253
3229
  const _userOwnedSkillDirs = new Set(['gsd-dev-preferences']);
3254
3230
  for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
3255
3231
  if (!entry.isDirectory() || !entry.name.startsWith('gsd-')) continue;
3256
3232
  if (_userOwnedSkillDirs.has(entry.name)) continue; // preserve user content
3257
- const skillDir = path.join(skillsDir, entry.name);
3258
- const skillMdPath = path.join(skillDir, 'SKILL.md');
3233
+ const agentsSubdir = path.join(skillsDir, entry.name, 'agents');
3234
+ const sidecarPath = path.join(agentsSubdir, 'openai.yaml');
3259
3235
  try {
3260
- const content = fs.readFileSync(skillMdPath, 'utf8');
3261
- const { frontmatter } = extractFrontmatterAndBody(content);
3262
- // Prefer the short-description field emitted by convertClaudeCommandToCodexSkill;
3263
- // fall back to description, then a synthetic label from the skill name.
3264
- let shortDesc = '';
3265
- if (frontmatter) {
3266
- // SKILL.md uses YAML frontmatter with a nested metadata.short-description key.
3267
- // extractFrontmatterField handles only top-level keys; parse the metadata block
3268
- // by looking for " short-description:" directly.
3269
- const metaMatch = frontmatter.match(/^[ \t]*metadata\s*:\s*\n((?:[ \t]+.*\n?)*)/m);
3270
- if (metaMatch) {
3271
- const metaBlock = metaMatch[1];
3272
- const sdMatch = metaBlock.match(/^[ \t]+short-description\s*:\s*(.+)$/m);
3273
- if (sdMatch) {
3274
- // Unescape YAML double-quoted scalar escapes before embedding.
3275
- // convertClaudeCommandToCodexSkill always emits a double-quoted
3276
- // value (via yamlQuote) so only double-quote unescaping is needed.
3277
- let raw = sdMatch[1].trim();
3278
- if (raw.startsWith('"') && raw.endsWith('"')) {
3279
- // Strip outer double-quotes and decode \" → " and \\ → \
3280
- raw = raw.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\');
3281
- } else {
3282
- // Single-quoted or unquoted: strip surrounding quotes/whitespace
3283
- raw = raw.replace(/^["']|["']$/g, '');
3284
- }
3285
- shortDesc = raw;
3286
- }
3287
- }
3288
- if (!shortDesc) {
3289
- shortDesc = extractFrontmatterField(frontmatter, 'description') || '';
3290
- }
3291
- }
3292
- if (!shortDesc) {
3293
- shortDesc = `Run GSD workflow ${entry.name}.`;
3236
+ // Symlink guard: if agents/ is a symlink pointing outside the skills tree,
3237
+ // deleting through it could escape the tree. Skip this dir entirely.
3238
+ let agentsStat;
3239
+ try { agentsStat = fs.lstatSync(agentsSubdir); } catch (_e) { continue; }
3240
+ if (agentsStat.isSymbolicLink()) continue;
3241
+ if (fs.existsSync(sidecarPath)) {
3242
+ fs.rmSync(sidecarPath);
3243
+ }
3244
+ // Prune the agents/ dir only if it is now empty (leave it if other files remain).
3245
+ if (fs.existsSync(agentsSubdir) && fs.readdirSync(agentsSubdir).length === 0) {
3246
+ fs.rmdirSync(agentsSubdir);
3294
3247
  }
3295
- const yamlContent = generateCodexSkillMetadataYaml(entry.name, shortDesc);
3296
- const agentsSubdir = path.join(skillDir, 'agents');
3297
- fs.mkdirSync(agentsSubdir, { recursive: true });
3298
- fs.writeFileSync(path.join(agentsSubdir, 'openai.yaml'), yamlContent);
3299
3248
  } catch (_err) {
3300
- // Fail open — missing or unreadable SKILL.md must not block the install.
3249
+ // Fail open — a single bad dir must not block the install.
3301
3250
  }
3302
3251
  }
3303
3252
  }
@@ -6843,6 +6792,12 @@ function _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal = false) {
6843
6792
  content = content.replace(/~\/\.claude\//g, pathPrefix);
6844
6793
  content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
6845
6794
  content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
6795
+ // Bare forms (no trailing slash) — use (?![\w-]) instead of \b so that
6796
+ // .claude-plugin / .claudeignore are NOT corrupted (the \b word-boundary
6797
+ // fires between 'e' and '-', which rewrites .claude-plugin → .cursor-plugin).
6798
+ content = content.replace(/~\/\.claude(?![\w-])/g, normalizedPathPrefix);
6799
+ content = content.replace(/\$HOME\/\.claude(?![\w-])/g, normalizedPathPrefix);
6800
+ content = content.replace(/\.\/\.claude(?![\w-])/g, `./${dirName}`);
6846
6801
  content = content.replace(/~\/\.cursor\//g, pathPrefix);
6847
6802
  content = processAttribution(content, getCommitAttribution(runtime));
6848
6803
  break;
@@ -7163,18 +7118,23 @@ function _runLegacyInstallMigrations(runtime, configDir, scope = 'global') {
7163
7118
  * @param {'global'|'local'} [scope]
7164
7119
  */
7165
7120
  function _runLegacyUninstallCleanup(runtime, configDir, scope = 'global') {
7166
- // Claude global / Qwen: commands/gsd/ is a legacy location (global Claude
7167
- // uses skills/ now; Qwen always uses skills/). Remove whole directory.
7168
- // Claude local: commands/gsd/ is the primary current location — skip here,
7169
- // let layout's _removeGsdEntries handle gsd-prefixed file removal.
7121
+ // commands/gsd/ is a legacy location for Qwen, Hermes, and all Claude installs.
7122
+ // Prior to #1367 fix, Claude-local used commands/gsd/<cmd>.md (colon-namespaced).
7123
+ // After #1367, Claude-local uses flat commands/gsd-<cmd>.md. The inline uninstall
7124
+ // block (1c) handles removal of flat files; this function handles the legacy
7125
+ // commands/gsd/ directory for all Claude scopes (global was already included,
7126
+ // local is now added since that layout is also legacy post-#1367).
7170
7127
  // #2973 / Codex review (bd1f06c9): preserve user-owned dev-preferences.md
7171
7128
  // before destructive wipe. Migration to skills/gsd-dev-preferences/SKILL.md
7172
7129
  // is deferred and returned so the caller can apply it AFTER layout-driven
7173
7130
  // removal — this prevents the layout's gsd-* prefix removal from wiping the
7174
7131
  // freshly created skill dir (same pattern as _runLegacyInstallMigrations).
7175
7132
  let savedLegacyArtifacts = null;
7176
- // commands/gsd/ is a legacy location for Qwen, Hermes, and Claude-global.
7177
- // Claude-local commands/gsd/ is the primary current location — skip here.
7133
+ // commands/gsd/ is a legacy location for Qwen, Hermes, and Claude global.
7134
+ // Claude local is intentionally excluded: the inline uninstall block (1c) handles
7135
+ // commands/gsd/ for claude local, preserving dev-preferences.md by restoring it
7136
+ // to the same location (#1423). Using migrateLegacyDevPreferencesToSkill here
7137
+ // (which would redirect to skills/) conflicts with the test contract for local installs.
7178
7138
  const isLegacyCommandsGsd = runtime === 'qwen' || runtime === 'hermes' || (runtime === 'claude' && scope === 'global');
7179
7139
  if (isLegacyCommandsGsd) {
7180
7140
  const legacyCommandsGsd = path.join(configDir, 'commands', 'gsd');
@@ -8111,23 +8071,37 @@ function uninstall(isGlobal, runtime = 'claude') {
8111
8071
  } catch { /* best-effort */ }
8112
8072
  }
8113
8073
 
8114
- // 1c. Claude local: remove commands/gsd/ (primary local install location).
8115
- // The layout's _removeGsdEntries uses the 'gsd-' prefix which applies to
8116
- // flat command dirs (OpenCode/Kilo). Claude local files use no prefix inside
8117
- // the namespaced directory, so layout does not remove them. Handle inline.
8118
- // Preserve dev-preferences.md across the wipe (#1423).
8074
+ // 1c. Claude local: remove flat gsd-*.md commands from commands/ (current layout,
8075
+ // #1367 fix). Also remove legacy commands/gsd/ subdirectory from prior installs.
8119
8076
  if (!isGlobal && runtime === 'claude') {
8120
- const gsdCommandsDir = path.join(targetDir, 'commands', 'gsd');
8121
- if (fs.existsSync(gsdCommandsDir)) {
8122
- const devPrefsPath = path.join(gsdCommandsDir, 'dev-preferences.md');
8123
- const preservedDevPrefs = fs.existsSync(devPrefsPath) ? fs.readFileSync(devPrefsPath, 'utf-8') : null;
8124
- fs.rmSync(gsdCommandsDir, { recursive: true });
8077
+ const commandsDir = path.join(targetDir, 'commands');
8078
+ // Remove flat gsd-*.md files (current layout after #1367 fix)
8079
+ if (fs.existsSync(commandsDir)) {
8080
+ let removed = 0;
8081
+ for (const f of fs.readdirSync(commandsDir)) {
8082
+ if (f.startsWith('gsd-') && f.endsWith('.md')) {
8083
+ fs.rmSync(path.join(commandsDir, f), { force: true });
8084
+ removed++;
8085
+ }
8086
+ }
8087
+ if (removed > 0) {
8088
+ removedCount++;
8089
+ console.log(` ${green}✓${reset} Removed ${removed} flat gsd-*.md commands from commands/`);
8090
+ }
8091
+ }
8092
+ // Remove legacy commands/gsd/ subdirectory if it still exists (pre-#1367 layout).
8093
+ // Preserve user-owned dev-preferences.md if present (#1423 parity).
8094
+ const legacyGsdCommandsDir = path.join(targetDir, 'commands', 'gsd');
8095
+ if (fs.existsSync(legacyGsdCommandsDir)) {
8096
+ const legacyDevPrefsPath = path.join(legacyGsdCommandsDir, 'dev-preferences.md');
8097
+ const savedDevPrefs = fs.existsSync(legacyDevPrefsPath) ? fs.readFileSync(legacyDevPrefsPath, 'utf-8') : null;
8098
+ fs.rmSync(legacyGsdCommandsDir, { recursive: true });
8125
8099
  removedCount++;
8126
- console.log(` ${green}✓${reset} Removed commands/gsd/`);
8127
- if (preservedDevPrefs) {
8100
+ console.log(` ${green}✓${reset} Removed legacy commands/gsd/`);
8101
+ if (savedDevPrefs) {
8128
8102
  try {
8129
- fs.mkdirSync(gsdCommandsDir, { recursive: true });
8130
- fs.writeFileSync(devPrefsPath, preservedDevPrefs);
8103
+ fs.mkdirSync(legacyGsdCommandsDir, { recursive: true });
8104
+ fs.writeFileSync(legacyDevPrefsPath, savedDevPrefs);
8131
8105
  console.log(` ${green}✓${reset} Preserved commands/gsd/dev-preferences.md`);
8132
8106
  } catch (err) {
8133
8107
  console.error(` ${red}✗${reset} Failed to restore dev-preferences.md: ${err.message}`);
@@ -8894,7 +8868,11 @@ function writeManifest(configDir, runtime = 'claude', options = {}) {
8894
8868
  const isKimi = runtime === 'kimi';
8895
8869
  const isHermes = runtime === 'hermes';
8896
8870
  const gsdDir = path.join(configDir, 'gsd-core');
8871
+ // #1367: Claude local now writes flat gsd-*.md files at commands/ (not commands/gsd/).
8872
+ // commandsDir points to the old location for Gemini (which still uses commands/gsd/).
8873
+ // Claude local uses flatCommandsDir instead for manifest recording.
8897
8874
  const commandsDir = path.join(configDir, 'commands', 'gsd');
8875
+ const flatCommandsDir = path.join(configDir, 'commands');
8898
8876
  const opencodeCommandDir = path.join(configDir, 'command');
8899
8877
  // Hermes nests GSD skills under skills/gsd/ as a single category (#2841).
8900
8878
  // All other runtimes that use the Codex-style skills layout use a flat skills/ root.
@@ -8920,17 +8898,27 @@ function writeManifest(configDir, runtime = 'claude', options = {}) {
8920
8898
  if (USER_OWNED_ARTIFACTS.includes(rel)) continue;
8921
8899
  manifest.files['gsd-core/' + rel] = hash;
8922
8900
  }
8923
- // Record commands/gsd/ for any runtime that emits it (Gemini globally,
8924
- // Claude Code locally — see #2923). Manifest must reflect everything on
8925
- // disk so saveLocalPatches() can detect user edits and so per-runtime
8926
- // assertions about minimal-mode emit can read manifest.files instead of
8927
- // re-walking the dir.
8928
- if (fs.existsSync(commandsDir)) {
8901
+ // Record commands surface for runtimes that emit it:
8902
+ // Gemini: commands/gsd/<cmd>.toml (nested, colon-namespaced)
8903
+ // Claude local (#1367 fix): flat gsd-<cmd>.md at commands/ level
8904
+ // Manifest must reflect everything on disk so saveLocalPatches() can detect
8905
+ // user edits and per-runtime minimal-mode assertions can read manifest.files.
8906
+ if (isGemini && fs.existsSync(commandsDir)) {
8929
8907
  const cmdHashes = generateManifest(commandsDir);
8930
8908
  for (const [rel, hash] of Object.entries(cmdHashes)) {
8931
8909
  manifest.files['commands/gsd/' + rel] = hash;
8932
8910
  }
8933
8911
  }
8912
+ // Claude local (#1367): flat gsd-*.md files at commands/ level.
8913
+ // Only claude local writes gsd-*.md here; global installs don't emit commands,
8914
+ // so this branch is a no-op for global (no matching files to find).
8915
+ if (runtime === 'claude' && fs.existsSync(flatCommandsDir)) {
8916
+ for (const file of fs.readdirSync(flatCommandsDir)) {
8917
+ if (file.startsWith('gsd-') && file.endsWith('.md')) {
8918
+ manifest.files['commands/' + file] = fileHash(path.join(flatCommandsDir, file));
8919
+ }
8920
+ }
8921
+ }
8934
8922
  if ((isOpencode || isKilo) && fs.existsSync(opencodeCommandDir)) {
8935
8923
  for (const file of fs.readdirSync(opencodeCommandDir)) {
8936
8924
  if (file.startsWith('gsd-') && file.endsWith('.md')) {
@@ -9837,14 +9825,14 @@ function install(isGlobal, runtime = 'claude', options = {}) {
9837
9825
  const scope = isGlobal ? 'global' : 'local';
9838
9826
  installRuntimeArtifacts(runtime, targetDir, scope, _resolvedProfile);
9839
9827
 
9840
- // #774 — Codex only: write agents/openai.yaml TUI chip metadata alongside each
9841
- // installed skill so the /skills popup shows name + description for each gsd-* skill.
9842
- // The SkillMetadataFile is loaded by codex-rs/core-skills/src/loader.rs from
9843
- // <skill-dir>/agents/openai.yaml; absence is silently tolerated (fails open).
9844
- // We parse the SKILL.md frontmatter to extract short-description already emitted
9845
- // by convertClaudeCommandToCodexSkill and use it as the TUI chip description.
9828
+ // #1326 — Codex only: remove stale agents/openai.yaml sidecars from managed
9829
+ // gsd-* skill dirs. Prior installs wrote these files so Codex would show a
9830
+ // display name and description in the /skills TUI popup. Recent Codex builds
9831
+ // index BOTH SKILL.md and the sidecar, causing each GSD skill to appear twice
9832
+ // in autocomplete. Cleaning them up fixes the duplication; SKILL.md alone is
9833
+ // sufficient for Codex discovery. User-owned dirs are never touched.
9846
9834
  if (isCodex) {
9847
- writeCodexSkillMetadataFiles(path.join(targetDir, 'skills'));
9835
+ cleanupCodexSkillMetadataSidecars(path.join(targetDir, 'skills'));
9848
9836
  }
9849
9837
 
9850
9838
  // Hermes only: write DESCRIPTION.md for the gsd/ category after layout install
@@ -10030,18 +10018,59 @@ function install(isGlobal, runtime = 'claude', options = {}) {
10030
10018
  }
10031
10019
  }
10032
10020
  } else {
10033
- // Claude Code local: commands/gsd/ format — Claude Code reads local project
10034
- // commands from .claude/commands/gsd/, not .claude/skills/
10021
+ // Claude Code local: flat gsd-<cmd>.md layout — Claude Code registers
10022
+ // commands from .claude/commands/ using the filename stem as the command
10023
+ // name, so gsd-<cmd>.md produces the /gsd-<cmd> hyphen form used everywhere
10024
+ // in the framework. The old commands/gsd/<cmd>.md subdirectory layout caused
10025
+ // Claude Code to namespace commands as /gsd:<cmd> (colon form). (#1367)
10035
10026
  const commandsDir = path.join(targetDir, 'commands');
10036
10027
  fs.mkdirSync(commandsDir, { recursive: true });
10037
10028
  const gsdSrc = _stageSkills(_commandsDir);
10038
- const gsdDest = path.join(commandsDir, 'gsd');
10039
- copyWithPathReplacement(gsdSrc, gsdDest, pathPrefix, runtime, true, isGlobal);
10040
- if (verifyInstalled(gsdDest, 'commands/gsd')) {
10041
- const count = fs.readdirSync(gsdDest).filter(f => f.endsWith('.md')).length;
10042
- console.log(` ${green}✓${reset} Installed ${count} commands to commands/gsd/`);
10029
+ const cmdNames = readGsdCommandNames();
10030
+
10031
+ // Remove stale gsd-*.md files before writing new ones (clean install)
10032
+ if (fs.existsSync(commandsDir)) {
10033
+ for (const f of fs.readdirSync(commandsDir)) {
10034
+ if (f.startsWith('gsd-') && f.endsWith('.md')) {
10035
+ fs.unlinkSync(path.join(commandsDir, f));
10036
+ }
10037
+ }
10038
+ }
10039
+
10040
+ // Write each command as gsd-<stem>.md (flat, hyphen-prefixed)
10041
+ let cmdCount = 0;
10042
+ if (fs.existsSync(gsdSrc)) {
10043
+ for (const entry of fs.readdirSync(gsdSrc, { withFileTypes: true })) {
10044
+ if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
10045
+ const stem = entry.name.slice(0, -3);
10046
+ let content = fs.readFileSync(path.join(gsdSrc, entry.name), 'utf8');
10047
+ content = _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal);
10048
+ content = normalizeAgentBodyForRuntime(content, runtime, cmdNames);
10049
+ fs.writeFileSync(path.join(commandsDir, `gsd-${stem}.md`), content);
10050
+ cmdCount++;
10051
+ }
10052
+ }
10053
+
10054
+ if (cmdCount > 0) {
10055
+ console.log(` ${green}✓${reset} Installed ${cmdCount} commands to commands/ (gsd-<cmd>.md flat form)`);
10043
10056
  } else {
10044
- failures.push('commands/gsd');
10057
+ failures.push('commands/gsd-*');
10058
+ }
10059
+
10060
+ // Legacy cleanup: remove old commands/gsd/ subdirectory from prior installs
10061
+ // that used the namespaced layout (wrote bare-name files under commands/gsd/).
10062
+ const legacyGsdDir = path.join(commandsDir, 'gsd');
10063
+ if (fs.existsSync(legacyGsdDir)) {
10064
+ // Preserve user-owned dev-preferences.md before wiping
10065
+ const devPrefsPath = path.join(legacyGsdDir, 'dev-preferences.md');
10066
+ const preservedDevPrefs = fs.existsSync(devPrefsPath) ? fs.readFileSync(devPrefsPath, 'utf-8') : null;
10067
+ fs.rmSync(legacyGsdDir, { recursive: true });
10068
+ console.log(` ${green}✓${reset} Removed legacy commands/gsd/ (migrated to flat gsd-<cmd>.md layout)`);
10069
+ if (preservedDevPrefs) {
10070
+ // Migrate dev-preferences to the new flat form
10071
+ fs.writeFileSync(path.join(commandsDir, 'gsd-dev-preferences.md'), preservedDevPrefs);
10072
+ console.log(` ${green}✓${reset} Migrated dev-preferences.md to commands/gsd-dev-preferences.md`);
10073
+ }
10045
10074
  }
10046
10075
 
10047
10076
  // Clean up any stale skills/ from a previous local install
@@ -12126,8 +12155,7 @@ module.exports = {
12126
12155
  convertClaudeToGeminiAgent,
12127
12156
  convertClaudeAgentToCodexAgent,
12128
12157
  generateCodexAgentToml,
12129
- generateCodexSkillMetadataYaml,
12130
- writeCodexSkillMetadataFiles,
12158
+ cleanupCodexSkillMetadataSidecars,
12131
12159
  generateCodexConfigBlock,
12132
12160
  stripGsdFromCodexConfig,
12133
12161
  migrateCodexHooksMapFormat,
@@ -2,7 +2,7 @@
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] [--converge]"
5
- effort: xhigh
5
+ effort: max
6
6
  allowed-tools:
7
7
  - Read
8
8
  - Write
@@ -2,7 +2,7 @@
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
- effort: xhigh
5
+ effort: max
6
6
  allowed-tools:
7
7
  - Read
8
8
  - Write
@@ -2,7 +2,7 @@
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
- effort: xhigh
5
+ effort: max
6
6
  allowed-tools:
7
7
  - Read
8
8
  - Write
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gsd-core",
3
- "version": "1.5.0-rc.5",
3
+ "version": "1.6.0-rc.1",
4
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
5
  "contextFileName": "GEMINI.md"
6
6
  }