@opengsd/gsd-core 1.5.0 → 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 (63) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/agents/gsd-plan-checker.md +34 -0
  3. package/agents/gsd-planner.md +2 -0
  4. package/bin/install.js +108 -34
  5. package/gemini-extension.json +1 -1
  6. package/gsd-core/bin/gsd-tools.cjs +677 -2
  7. package/gsd-core/bin/lib/adr-parser.cjs +24 -17
  8. package/gsd-core/bin/lib/audit.cjs +2 -2
  9. package/gsd-core/bin/lib/capability-consent.cjs +763 -0
  10. package/gsd-core/bin/lib/capability-ledger.cjs +831 -0
  11. package/gsd-core/bin/lib/capability-lifecycle.cjs +1551 -0
  12. package/gsd-core/bin/lib/capability-loader.cjs +764 -0
  13. package/gsd-core/bin/lib/capability-lock.cjs +553 -0
  14. package/gsd-core/bin/lib/capability-registry.cjs +198 -4
  15. package/gsd-core/bin/lib/capability-source.cjs +1242 -0
  16. package/gsd-core/bin/lib/capability-state.cjs +9 -6
  17. package/gsd-core/bin/lib/capability-trust.cjs +550 -0
  18. package/gsd-core/bin/lib/capability-validator.cjs +2066 -0
  19. package/gsd-core/bin/lib/capability-writer.cjs +14 -5
  20. package/gsd-core/bin/lib/check-command-router.cjs +69 -18
  21. package/gsd-core/bin/lib/command-aliases.cjs +8 -0
  22. package/gsd-core/bin/lib/config-loader.cjs +92 -84
  23. package/gsd-core/bin/lib/config-schema.cjs +26 -7
  24. package/gsd-core/bin/lib/config.cjs +1 -1
  25. package/gsd-core/bin/lib/decisions.cjs +149 -60
  26. package/gsd-core/bin/lib/gap-checker.cjs +126 -11
  27. package/gsd-core/bin/lib/init.cjs +91 -22
  28. package/gsd-core/bin/lib/legacy-cleanup.cjs +96 -0
  29. package/gsd-core/bin/lib/loop-resolver.cjs +26 -2
  30. package/gsd-core/bin/lib/markdown-sectionizer.cjs +471 -0
  31. package/gsd-core/bin/lib/milestone.cjs +41 -2
  32. package/gsd-core/bin/lib/phase-command-router.cjs +5 -0
  33. package/gsd-core/bin/lib/phase-lifecycle.cjs +14 -5
  34. package/gsd-core/bin/lib/phase.cjs +29 -0
  35. package/gsd-core/bin/lib/project-root.cjs +89 -2
  36. package/gsd-core/bin/lib/resolution.cjs +26 -0
  37. package/gsd-core/bin/lib/roadmap-parser.cjs +44 -98
  38. package/gsd-core/bin/lib/runtime-homes.cjs +53 -1
  39. package/gsd-core/bin/lib/semver-compare.cjs +127 -0
  40. package/gsd-core/bin/lib/state-document.cjs +4 -2
  41. package/gsd-core/bin/lib/state.cjs +317 -161
  42. package/gsd-core/bin/lib/uat-predicate.cjs +7 -47
  43. package/gsd-core/bin/lib/uat.cjs +39 -26
  44. package/gsd-core/bin/lib/verify.cjs +29 -13
  45. package/gsd-core/bin/shared/config-defaults.manifest.json +4 -0
  46. package/gsd-core/bin/shared/config-schema.manifest.json +4 -1
  47. package/gsd-core/references/execute-phase-between-wave-reset.md +43 -0
  48. package/gsd-core/references/execute-phase-wave-guard.md +33 -0
  49. package/gsd-core/references/planner-antipatterns.md +48 -0
  50. package/gsd-core/references/planning-config.md +3 -0
  51. package/gsd-core/references/scout-codebase.md +2 -2
  52. package/gsd-core/workflows/discuss-phase/templates/context.md +1 -1
  53. package/gsd-core/workflows/discuss-phase.md +1 -2
  54. package/gsd-core/workflows/execute-phase.md +4 -6
  55. package/package.json +3 -3
  56. package/scripts/gen-capability-matrix.cjs +284 -0
  57. package/scripts/gen-capability-registry.cjs +96 -1853
  58. package/scripts/lint-regression-test-names.allowlist.json +1 -0
  59. package/scripts/lint-resolution-provenance.allowlist.json +1 -0
  60. package/scripts/lint-resolution-provenance.cjs +192 -0
  61. package/scripts/lint-test-file-count.allowlist.json +9 -0
  62. package/scripts/run-tests.cjs +14 -0
  63. 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",
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",
@@ -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
@@ -7118,18 +7118,23 @@ function _runLegacyInstallMigrations(runtime, configDir, scope = 'global') {
7118
7118
  * @param {'global'|'local'} [scope]
7119
7119
  */
7120
7120
  function _runLegacyUninstallCleanup(runtime, configDir, scope = 'global') {
7121
- // Claude global / Qwen: commands/gsd/ is a legacy location (global Claude
7122
- // uses skills/ now; Qwen always uses skills/). Remove whole directory.
7123
- // Claude local: commands/gsd/ is the primary current location — skip here,
7124
- // 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).
7125
7127
  // #2973 / Codex review (bd1f06c9): preserve user-owned dev-preferences.md
7126
7128
  // before destructive wipe. Migration to skills/gsd-dev-preferences/SKILL.md
7127
7129
  // is deferred and returned so the caller can apply it AFTER layout-driven
7128
7130
  // removal — this prevents the layout's gsd-* prefix removal from wiping the
7129
7131
  // freshly created skill dir (same pattern as _runLegacyInstallMigrations).
7130
7132
  let savedLegacyArtifacts = null;
7131
- // commands/gsd/ is a legacy location for Qwen, Hermes, and Claude-global.
7132
- // 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.
7133
7138
  const isLegacyCommandsGsd = runtime === 'qwen' || runtime === 'hermes' || (runtime === 'claude' && scope === 'global');
7134
7139
  if (isLegacyCommandsGsd) {
7135
7140
  const legacyCommandsGsd = path.join(configDir, 'commands', 'gsd');
@@ -8066,23 +8071,37 @@ function uninstall(isGlobal, runtime = 'claude') {
8066
8071
  } catch { /* best-effort */ }
8067
8072
  }
8068
8073
 
8069
- // 1c. Claude local: remove commands/gsd/ (primary local install location).
8070
- // The layout's _removeGsdEntries uses the 'gsd-' prefix which applies to
8071
- // flat command dirs (OpenCode/Kilo). Claude local files use no prefix inside
8072
- // the namespaced directory, so layout does not remove them. Handle inline.
8073
- // 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.
8074
8076
  if (!isGlobal && runtime === 'claude') {
8075
- const gsdCommandsDir = path.join(targetDir, 'commands', 'gsd');
8076
- if (fs.existsSync(gsdCommandsDir)) {
8077
- const devPrefsPath = path.join(gsdCommandsDir, 'dev-preferences.md');
8078
- const preservedDevPrefs = fs.existsSync(devPrefsPath) ? fs.readFileSync(devPrefsPath, 'utf-8') : null;
8079
- 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 });
8080
8099
  removedCount++;
8081
- console.log(` ${green}✓${reset} Removed commands/gsd/`);
8082
- if (preservedDevPrefs) {
8100
+ console.log(` ${green}✓${reset} Removed legacy commands/gsd/`);
8101
+ if (savedDevPrefs) {
8083
8102
  try {
8084
- fs.mkdirSync(gsdCommandsDir, { recursive: true });
8085
- fs.writeFileSync(devPrefsPath, preservedDevPrefs);
8103
+ fs.mkdirSync(legacyGsdCommandsDir, { recursive: true });
8104
+ fs.writeFileSync(legacyDevPrefsPath, savedDevPrefs);
8086
8105
  console.log(` ${green}✓${reset} Preserved commands/gsd/dev-preferences.md`);
8087
8106
  } catch (err) {
8088
8107
  console.error(` ${red}✗${reset} Failed to restore dev-preferences.md: ${err.message}`);
@@ -8849,7 +8868,11 @@ function writeManifest(configDir, runtime = 'claude', options = {}) {
8849
8868
  const isKimi = runtime === 'kimi';
8850
8869
  const isHermes = runtime === 'hermes';
8851
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.
8852
8874
  const commandsDir = path.join(configDir, 'commands', 'gsd');
8875
+ const flatCommandsDir = path.join(configDir, 'commands');
8853
8876
  const opencodeCommandDir = path.join(configDir, 'command');
8854
8877
  // Hermes nests GSD skills under skills/gsd/ as a single category (#2841).
8855
8878
  // All other runtimes that use the Codex-style skills layout use a flat skills/ root.
@@ -8875,17 +8898,27 @@ function writeManifest(configDir, runtime = 'claude', options = {}) {
8875
8898
  if (USER_OWNED_ARTIFACTS.includes(rel)) continue;
8876
8899
  manifest.files['gsd-core/' + rel] = hash;
8877
8900
  }
8878
- // Record commands/gsd/ for any runtime that emits it (Gemini globally,
8879
- // Claude Code locally — see #2923). Manifest must reflect everything on
8880
- // disk so saveLocalPatches() can detect user edits and so per-runtime
8881
- // assertions about minimal-mode emit can read manifest.files instead of
8882
- // re-walking the dir.
8883
- 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)) {
8884
8907
  const cmdHashes = generateManifest(commandsDir);
8885
8908
  for (const [rel, hash] of Object.entries(cmdHashes)) {
8886
8909
  manifest.files['commands/gsd/' + rel] = hash;
8887
8910
  }
8888
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
+ }
8889
8922
  if ((isOpencode || isKilo) && fs.existsSync(opencodeCommandDir)) {
8890
8923
  for (const file of fs.readdirSync(opencodeCommandDir)) {
8891
8924
  if (file.startsWith('gsd-') && file.endsWith('.md')) {
@@ -9985,18 +10018,59 @@ function install(isGlobal, runtime = 'claude', options = {}) {
9985
10018
  }
9986
10019
  }
9987
10020
  } else {
9988
- // Claude Code local: commands/gsd/ format — Claude Code reads local project
9989
- // 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)
9990
10026
  const commandsDir = path.join(targetDir, 'commands');
9991
10027
  fs.mkdirSync(commandsDir, { recursive: true });
9992
10028
  const gsdSrc = _stageSkills(_commandsDir);
9993
- const gsdDest = path.join(commandsDir, 'gsd');
9994
- copyWithPathReplacement(gsdSrc, gsdDest, pathPrefix, runtime, true, isGlobal);
9995
- if (verifyInstalled(gsdDest, 'commands/gsd')) {
9996
- const count = fs.readdirSync(gsdDest).filter(f => f.endsWith('.md')).length;
9997
- 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)`);
9998
10056
  } else {
9999
- 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
+ }
10000
10074
  }
10001
10075
 
10002
10076
  // Clean up any stale skills/ from a previous local install
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gsd-core",
3
- "version": "1.5.0",
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
  }