@opengsd/gsd-core 1.6.0-rc.2 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/.claude-plugin/plugin.json +2 -1
  2. package/agents/gsd-advisor-researcher.md +2 -0
  3. package/agents/gsd-ai-researcher.md +2 -0
  4. package/agents/gsd-assumptions-analyzer.md +2 -0
  5. package/agents/gsd-doc-classifier.md +2 -0
  6. package/agents/gsd-doc-synthesizer.md +2 -0
  7. package/agents/gsd-domain-researcher.md +2 -0
  8. package/agents/gsd-eval-auditor.md +6 -9
  9. package/agents/gsd-phase-researcher.md +2 -0
  10. package/agents/gsd-planner.md +8 -57
  11. package/agents/gsd-project-researcher.md +2 -0
  12. package/agents/gsd-research-synthesizer.md +2 -0
  13. package/agents/gsd-security-auditor.md +37 -18
  14. package/agents/gsd-ui-researcher.md +2 -0
  15. package/bin/install.js +370 -18
  16. package/gemini-extension.json +1 -1
  17. package/gsd-core/bin/gsd-tools.cjs +46 -4
  18. package/gsd-core/bin/lib/audit-command-router.cjs +52 -14
  19. package/gsd-core/bin/lib/capability-lifecycle.cjs +30 -7
  20. package/gsd-core/bin/lib/capability-registry.cjs +96 -83
  21. package/gsd-core/bin/lib/capability-validator.cjs +22 -0
  22. package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +40 -2
  23. package/gsd-core/bin/lib/command-aliases.cjs +10 -1
  24. package/gsd-core/bin/lib/command-routing-hub.cjs +10 -3
  25. package/gsd-core/bin/lib/config-schema.cjs +1 -0
  26. package/gsd-core/bin/lib/config.cjs +67 -24
  27. package/gsd-core/bin/lib/coverage.cjs +464 -0
  28. package/gsd-core/bin/lib/decisions.cjs +27 -0
  29. package/gsd-core/bin/lib/eval-command-router.cjs +21 -0
  30. package/gsd-core/bin/lib/eval.cjs +60 -0
  31. package/gsd-core/bin/lib/frontmatter.cjs +132 -13
  32. package/gsd-core/bin/lib/graphify-command-router.cjs +53 -36
  33. package/gsd-core/bin/lib/init.cjs +139 -30
  34. package/gsd-core/bin/lib/install-profiles.cjs +6 -3
  35. package/gsd-core/bin/lib/intel-command-router.cjs +79 -60
  36. package/gsd-core/bin/lib/io.cjs +1 -0
  37. package/gsd-core/bin/lib/phase.cjs +16 -2
  38. package/gsd-core/bin/lib/plan-scan.cjs +2 -2
  39. package/gsd-core/bin/lib/planning-workspace.cjs +157 -13
  40. package/gsd-core/bin/lib/profile-output.cjs +18 -6
  41. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +53 -16
  42. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +21 -3
  43. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +15 -0
  44. package/gsd-core/bin/lib/runtime-name-policy.cjs +47 -2
  45. package/gsd-core/bin/lib/shell-command-projection.cjs +10 -0
  46. package/gsd-core/bin/lib/state.cjs +398 -60
  47. package/gsd-core/bin/lib/surface.cjs +42 -10
  48. package/gsd-core/bin/lib/uat-predicate.cjs +13 -6
  49. package/gsd-core/bin/lib/update-context.cjs +2 -2
  50. package/gsd-core/bin/lib/verification.cjs +67 -6
  51. package/gsd-core/bin/lib/verify.cjs +8 -1
  52. package/gsd-core/bin/shared/config-defaults.manifest.json +3 -0
  53. package/gsd-core/bin/shared/config-schema.manifest.json +2 -1
  54. package/gsd-core/references/planner-guidance.md +66 -0
  55. package/gsd-core/references/planning-config.md +2 -2
  56. package/gsd-core/references/security-asvs-levels.md +27 -0
  57. package/gsd-core/references/untrusted-input-boundary.md +13 -0
  58. package/gsd-core/templates/SECURITY.md +6 -4
  59. package/gsd-core/templates/summary-complex.md +4 -0
  60. package/gsd-core/templates/summary-minimal.md +3 -0
  61. package/gsd-core/templates/summary-standard.md +4 -0
  62. package/gsd-core/templates/summary.md +41 -0
  63. package/gsd-core/workflows/autonomous.md +53 -46
  64. package/gsd-core/workflows/complete-milestone.md +27 -8
  65. package/gsd-core/workflows/execute-phase.md +1 -1
  66. package/gsd-core/workflows/execute-plan.md +5 -0
  67. package/gsd-core/workflows/manager.md +17 -7
  68. package/gsd-core/workflows/new-project.md +82 -16
  69. package/gsd-core/workflows/plan-phase.md +15 -0
  70. package/gsd-core/workflows/profile-user.md +6 -2
  71. package/gsd-core/workflows/progress.md +37 -4
  72. package/gsd-core/workflows/quick.md +3 -1
  73. package/gsd-core/workflows/secure-phase.md +13 -7
  74. package/gsd-core/workflows/ship.md +3 -1
  75. package/gsd-core/workflows/spec-phase.md +3 -1
  76. package/gsd-core/workflows/transition.md +14 -12
  77. package/gsd-core/workflows/ui-review.md +2 -6
  78. package/gsd-core/workflows/verify-work.md +74 -1
  79. package/hooks/dist/gsd-read-injection-scanner.js +49 -25
  80. package/hooks/gsd-read-injection-scanner.js +49 -25
  81. package/hooks/hooks.json +1 -1
  82. package/package.json +4 -2
  83. package/scripts/check-alias-drift.cjs +5 -0
  84. package/scripts/gen-plugin-skills.cjs +117 -0
  85. package/scripts/lint-test-file-count.allowlist.json +2 -1
  86. package/scripts/prompt-injection-scan.sh +9 -0
  87. package/scripts/release-notes/conventional-title.cjs +88 -0
  88. package/scripts/release-notes/format-github-release-notes.cjs +4 -3
  89. package/skills/gsd-add-tests/SKILL.md +38 -0
  90. package/skills/gsd-ai-integration-phase/SKILL.md +37 -0
  91. package/skills/gsd-audit-fix/SKILL.md +33 -0
  92. package/skills/gsd-audit-milestone/SKILL.md +37 -0
  93. package/skills/gsd-audit-uat/SKILL.md +25 -0
  94. package/skills/gsd-autonomous/SKILL.md +51 -0
  95. package/skills/gsd-capture/SKILL.md +67 -0
  96. package/skills/gsd-cleanup/SKILL.md +24 -0
  97. package/skills/gsd-code-review/SKILL.md +59 -0
  98. package/skills/gsd-complete-milestone/SKILL.md +142 -0
  99. package/skills/gsd-config/SKILL.md +56 -0
  100. package/skills/gsd-debug/SKILL.md +53 -0
  101. package/skills/gsd-discuss-phase/SKILL.md +77 -0
  102. package/skills/gsd-docs-update/SKILL.md +49 -0
  103. package/skills/gsd-eval-review/SKILL.md +33 -0
  104. package/skills/gsd-execute-phase/SKILL.md +65 -0
  105. package/skills/gsd-explore/SKILL.md +28 -0
  106. package/skills/gsd-extract-learnings/SKILL.md +22 -0
  107. package/skills/gsd-fast/SKILL.md +31 -0
  108. package/skills/gsd-forensics/SKILL.md +56 -0
  109. package/skills/gsd-graphify/SKILL.md +204 -0
  110. package/skills/gsd-health/SKILL.md +31 -0
  111. package/skills/gsd-help/SKILL.md +29 -0
  112. package/skills/gsd-import/SKILL.md +46 -0
  113. package/skills/gsd-inbox/SKILL.md +39 -0
  114. package/skills/gsd-ingest-docs/SKILL.md +43 -0
  115. package/skills/gsd-manager/SKILL.md +45 -0
  116. package/skills/gsd-map-codebase/SKILL.md +83 -0
  117. package/skills/gsd-mempalace-capture/SKILL.md +71 -0
  118. package/skills/gsd-mempalace-recall/SKILL.md +102 -0
  119. package/skills/gsd-milestone-summary/SKILL.md +51 -0
  120. package/skills/gsd-mvp-phase/SKILL.md +45 -0
  121. package/skills/gsd-new-milestone/SKILL.md +45 -0
  122. package/skills/gsd-new-project/SKILL.md +47 -0
  123. package/skills/gsd-ns-context/SKILL.md +24 -0
  124. package/skills/gsd-ns-ideate/SKILL.md +23 -0
  125. package/skills/gsd-ns-manage/SKILL.md +35 -0
  126. package/skills/gsd-ns-project/SKILL.md +26 -0
  127. package/skills/gsd-ns-review/SKILL.md +28 -0
  128. package/skills/gsd-ns-workflow/SKILL.md +33 -0
  129. package/skills/gsd-pause-work/SKILL.md +43 -0
  130. package/skills/gsd-phase/SKILL.md +57 -0
  131. package/skills/gsd-plan-phase/SKILL.md +63 -0
  132. package/skills/gsd-plan-review-convergence/SKILL.md +60 -0
  133. package/skills/gsd-pr-branch/SKILL.md +26 -0
  134. package/skills/gsd-profile-user/SKILL.md +47 -0
  135. package/skills/gsd-progress/SKILL.md +49 -0
  136. package/skills/gsd-quick/SKILL.md +174 -0
  137. package/skills/gsd-resume-work/SKILL.md +31 -0
  138. package/skills/gsd-review/SKILL.md +42 -0
  139. package/skills/gsd-review-backlog/SKILL.md +63 -0
  140. package/skills/gsd-secure-phase/SKILL.md +36 -0
  141. package/skills/gsd-settings/SKILL.md +29 -0
  142. package/skills/gsd-ship/SKILL.md +24 -0
  143. package/skills/gsd-sketch/SKILL.md +60 -0
  144. package/skills/gsd-spec-phase/SKILL.md +63 -0
  145. package/skills/gsd-spike/SKILL.md +57 -0
  146. package/skills/gsd-stats/SKILL.md +20 -0
  147. package/skills/gsd-surface/SKILL.md +162 -0
  148. package/skills/gsd-thread/SKILL.md +24 -0
  149. package/skills/gsd-ui-phase/SKILL.md +35 -0
  150. package/skills/gsd-ui-review/SKILL.md +33 -0
  151. package/skills/gsd-ultraplan-phase/SKILL.md +34 -0
  152. package/skills/gsd-undo/SKILL.md +35 -0
  153. package/skills/gsd-update/SKILL.md +50 -0
  154. package/skills/gsd-validate-phase/SKILL.md +36 -0
  155. package/skills/gsd-verify-work/SKILL.md +39 -0
  156. package/skills/gsd-workspace/SKILL.md +53 -0
  157. package/skills/gsd-workstreams/SKILL.md +70 -0
package/bin/install.js CHANGED
@@ -1517,11 +1517,17 @@ function convertGeminiToolName(claudeTool) {
1517
1517
  // Task/Agent: exclude — agents are auto-registered as callable tools.
1518
1518
  // AskUserQuestion: exclude — Gemini CLI does not expose an ask_user tool;
1519
1519
  // emitting it causes frontmatter validation errors (#3362).
1520
+ // Skill/SlashCommand: exclude — Gemini CLI has no 'skill' built-in tool;
1521
+ // the lowercase fallback would emit an invalid 'skill'/'slashcommand' name
1522
+ // that fails frontmatter validation (tools.N: Invalid tool name) and aborts
1523
+ // the entire agent load (#1394).
1520
1524
  if (
1521
1525
  claudeTool === 'Task' ||
1522
1526
  claudeTool === 'Agent' ||
1523
1527
  claudeTool === 'AskUserQuestion' ||
1524
- claudeTool === 'ask_user'
1528
+ claudeTool === 'ask_user' ||
1529
+ claudeTool === 'Skill' ||
1530
+ claudeTool === 'SlashCommand'
1525
1531
  ) {
1526
1532
  return null;
1527
1533
  }
@@ -2451,20 +2457,18 @@ function convertClaudeToWindsurfMarkdown(content) {
2451
2457
  // Replace subagent_type from Claude to Windsurf format
2452
2458
  converted = converted.replace(/subagent_type="general-purpose"/g, 'subagent_type="generalPurpose"');
2453
2459
  converted = converted.replace(/\$ARGUMENTS\b/g, '{{GSD_ARGS}}');
2454
- // Replace project-level Claude conventions with Windsurf/Devin equivalents
2455
- // Workspace skills install to .devin/ (Devin Desktop preferred dir, #1085).
2456
- // Legacy .windsurf/ is still recognized on read but new installs use .devin/.
2457
- converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.devin/rules`');
2458
- converted = converted.replace(/\.\/CLAUDE\.md/g, '.devin/rules');
2459
- converted = converted.replace(/`CLAUDE\.md`/g, '`.devin/rules`');
2460
- converted = converted.replace(/\bCLAUDE\.md\b/g, '.devin/rules');
2461
- converted = converted.replace(/\.claude\/skills\//g, '.devin/skills/');
2462
- converted = converted.replace(/\.\/\.claude\//g, './.devin/');
2463
- converted = converted.replace(/\.claude\//g, '.devin/');
2460
+ // Replace project-level Claude conventions with Windsurf equivalents.
2461
+ converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.windsurf/rules`');
2462
+ converted = converted.replace(/\.\/CLAUDE\.md/g, '.windsurf/rules');
2463
+ converted = converted.replace(/`CLAUDE\.md`/g, '`.windsurf/rules`');
2464
+ converted = converted.replace(/\bCLAUDE\.md\b/g, '.windsurf/rules');
2465
+ converted = converted.replace(/\.claude\/skills\//g, '.windsurf/skills/');
2466
+ converted = converted.replace(/\.\/\.claude\//g, './.windsurf/');
2467
+ converted = converted.replace(/\.claude\//g, '.windsurf/');
2464
2468
  // Bare forms (no trailing slash) — after slash forms to avoid double-rewrite.
2465
2469
  // Use negative lookahead (?![\w-]) to preserve .claude-plugin and .claudeignore.
2466
- converted = converted.replace(/~\/\.claude(?![\w-])/g, '~/.devin');
2467
- converted = converted.replace(/\$HOME\/\.claude(?![\w-])/g, '$HOME/.devin');
2470
+ converted = converted.replace(/~\/\.claude(?![\w-])/g, '~/.windsurf');
2471
+ converted = converted.replace(/\$HOME\/\.claude(?![\w-])/g, '$HOME/.windsurf');
2468
2472
  // Environment variable name rewrite
2469
2473
  converted = converted.replace(/\bCLAUDE_CONFIG_DIR\b/g, 'WINDSURF_CONFIG_DIR');
2470
2474
  // Remove Claude Code-specific bug workarounds before brand replacement
@@ -2518,6 +2522,33 @@ function convertClaudeCommandToWindsurfSkill(content, skillName) {
2518
2522
  return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`;
2519
2523
  }
2520
2524
 
2525
+ function convertClaudeCommandToWindsurfWorkflow(content, commandName) {
2526
+ // #1615 security: commandName flows unsanitized into a markdown body that
2527
+ // Windsurf loads as an LLM-readable workflow. Validate at entry to prevent
2528
+ // (a) prompt injection via newlines / markdown structure in the filename,
2529
+ // (b) path-component injection via .., /, \ in stem → @-reference target.
2530
+ // Pattern: optional gsd- prefix + lowercase alphanumeric + dashes; rejects
2531
+ // everything else. See DEFECT.PROMPT-INJECTION-SCAN-COLLISION and the
2532
+ // PR #1622 security review.
2533
+ if (typeof commandName !== 'string' || !/^(?:gsd-)?[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(commandName)) {
2534
+ const preview = typeof commandName === 'string' ? JSON.stringify(commandName.slice(0, 60)) : String(commandName);
2535
+ throw new Error(
2536
+ `convertClaudeCommandToWindsurfWorkflow: rejected commandName ${preview}; ` +
2537
+ 'must match /^(?:gsd-)?[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/ (no slashes, backslashes, spaces, dots, trailing dash, or control chars — prevents prompt injection and path-component injection into the workflow body)'
2538
+ );
2539
+ }
2540
+ const converted = convertClaudeToWindsurfMarkdown(content);
2541
+ const { frontmatter } = extractFrontmatterAndBody(converted);
2542
+ const description = frontmatter ? extractFrontmatterField(frontmatter, 'description') : '';
2543
+ const stem = commandName.startsWith('gsd-') ? commandName.slice(4) : commandName;
2544
+ const workflow = `# ${commandName}\n\n${toSingleLine(description || `Run ${commandName}.`)}\n\nRead and execute the GSD command at @~/.claude/gsd-core/commands/gsd/${stem}.md end-to-end. Treat the user's message after /${commandName} as the command arguments.`;
2545
+ const byteLength = Buffer.byteLength(workflow, 'utf8');
2546
+ if (byteLength > 12000) {
2547
+ throw new Error(`Windsurf workflow ${commandName} exceeds 12000 bytes (${byteLength}); extract references before installing`);
2548
+ }
2549
+ return workflow;
2550
+ }
2551
+
2521
2552
  /**
2522
2553
  * Convert Claude Code agent markdown to Windsurf agent format.
2523
2554
  * Strips frontmatter fields Windsurf doesn't support (color, skills),
@@ -3223,6 +3254,66 @@ function cleanupCodexSkillMetadataSidecars(skillsDir) {
3223
3254
  }
3224
3255
  }
3225
3256
 
3257
+ /**
3258
+ * Remove legacy Windsurf skill artifacts from .devin/skills/gsd- directories.
3259
+ *
3260
+ * Pre-#1615 Windsurf installs wrote skills under .devin/ (Devin Desktop
3261
+ * preferred dir, #1085). #1615 moved Windsurf to .windsurf/workflows/.
3262
+ * Old .devin/skills/gsd- dirs linger on disk indefinitely and confuse
3263
+ * users who see two GSD trees.
3264
+ *
3265
+ * Preserves user-owned content:
3266
+ * - non-gsd-* dirs under .devin/skills/ (user-authored skills)
3267
+ * - gsd-dev-preferences/ (user-owned per #2973)
3268
+ * - any files (not dirs) under .devin/skills/
3269
+ *
3270
+ * @param {string} workspaceDir - workspace root (process.cwd() for local installs)
3271
+ * @returns {number} count of removed legacy gsd-* skill directories
3272
+ */
3273
+ function cleanupWindsurfLegacyDevinSkills(workspaceDir) {
3274
+ const legacySkillsDir = path.join(workspaceDir, '.devin', 'skills');
3275
+ if (!fs.existsSync(legacySkillsDir)) return 0;
3276
+
3277
+ // Mirror the user-owned list from cleanupCodexSkillMetadataSidecars (#2973).
3278
+ const _userOwnedSkillDirs = new Set(['gsd-dev-preferences']);
3279
+ let removed = 0;
3280
+
3281
+ for (const entry of fs.readdirSync(legacySkillsDir, { withFileTypes: true })) {
3282
+ if (!entry.isDirectory() || !entry.name.startsWith('gsd-')) continue;
3283
+ if (_userOwnedSkillDirs.has(entry.name)) continue;
3284
+
3285
+ const dirToRemove = path.join(legacySkillsDir, entry.name);
3286
+ try {
3287
+ // Symlink guard: if the gsd-* dir is itself a symlink pointing outside
3288
+ // the .devin tree, deleting through it could escape the tree. Skip.
3289
+ const stat = fs.lstatSync(dirToRemove);
3290
+ if (stat.isSymbolicLink()) continue;
3291
+
3292
+ fs.rmSync(dirToRemove, { recursive: true, force: true });
3293
+ removed++;
3294
+ } catch (_err) {
3295
+ // Fail open — a single bad dir must not block the install.
3296
+ }
3297
+ }
3298
+
3299
+ // If .devin/skills/ is now empty, prune it. If .devin/ itself is then empty,
3300
+ // prune that too — leaves the workspace clean for the new .windsurf/ layout.
3301
+ // Never remove non-empty containers (user may have other Devin content).
3302
+ try {
3303
+ if (fs.existsSync(legacySkillsDir) && fs.readdirSync(legacySkillsDir).length === 0) {
3304
+ fs.rmdirSync(legacySkillsDir);
3305
+ const devinDir = path.join(workspaceDir, '.devin');
3306
+ if (fs.existsSync(devinDir) && fs.readdirSync(devinDir).length === 0) {
3307
+ fs.rmdirSync(devinDir);
3308
+ }
3309
+ }
3310
+ } catch (_err) {
3311
+ // best-effort container cleanup
3312
+ }
3313
+
3314
+ return removed;
3315
+ }
3316
+
3226
3317
  /**
3227
3318
  * Generate the GSD config block for Codex config.toml.
3228
3319
  * @param {Array<{name: string, description: string}>} agents
@@ -9592,6 +9683,17 @@ function install(isGlobal, runtime = 'claude', options = {}) {
9592
9683
  cleanupCodexSkillMetadataSidecars(path.join(targetDir, 'skills'));
9593
9684
  }
9594
9685
 
9686
+ // #1629 Finding B: Windsurf local only — remove legacy .devin/skills/gsd-*
9687
+ // dirs from pre-#1615 installs. #1615 moved Windsurf to .windsurf/workflows/
9688
+ // but never cleaned up the old .devin/skills/ layout (#1085). User-owned
9689
+ // content is preserved (non-gsd- dirs, gsd-dev-preferences, symlinks).
9690
+ if (isWindsurf && !isGlobal) {
9691
+ const removedCount = cleanupWindsurfLegacyDevinSkills(process.cwd());
9692
+ if (removedCount > 0) {
9693
+ console.log(` ${green}✓${reset} Removed ${removedCount} legacy .devin/skills/gsd-* dir(s) (pre-#1615 Windsurf layout)`);
9694
+ }
9695
+ }
9696
+
9595
9697
  // Hermes only: write DESCRIPTION.md for the gsd/ category after layout install
9596
9698
  if (isHermes) {
9597
9699
  writeHermesCategoryDescription(path.join(targetDir, 'skills', 'gsd'));
@@ -9632,6 +9734,23 @@ function install(isGlobal, runtime = 'claude', options = {}) {
9632
9734
  } else {
9633
9735
  failures.push('agents/gsd.yaml');
9634
9736
  }
9737
+ } else if (isWindsurf) {
9738
+ if (isGlobal) {
9739
+ console.log(` ${green}✓${reset} Windsurf global install skipped workflow artifacts (workspace-only)`);
9740
+ } else {
9741
+ const workflowsDir = path.join(targetDir, 'workflows');
9742
+ if (fs.existsSync(workflowsDir)) {
9743
+ const workflowCount = fs.readdirSync(workflowsDir)
9744
+ .filter(f => f.startsWith('gsd-') && f.endsWith('.md')).length;
9745
+ if (workflowCount > 0) {
9746
+ console.log(` ${green}✓${reset} Installed ${workflowCount} workflows to workflows/`);
9747
+ } else {
9748
+ failures.push('workflows/gsd-*');
9749
+ }
9750
+ } else {
9751
+ failures.push('workflows/gsd-*');
9752
+ }
9753
+ }
9635
9754
  } else {
9636
9755
  const skillsDir = path.join(targetDir, 'skills');
9637
9756
  if (fs.existsSync(skillsDir)) {
@@ -9857,6 +9976,23 @@ function install(isGlobal, runtime = 'claude', options = {}) {
9857
9976
  failures.push('gsd-core');
9858
9977
  }
9859
9978
 
9979
+ // #1629 critical fix: Windsurf workflow wrappers (convertClaudeCommandToWindsurfWorkflow)
9980
+ // delegate to command bodies at <targetDir>/gsd-core/commands/gsd/${stem}.md via a
9981
+ // hardcoded @~/.claude/gsd-core/commands/gsd/ path that _applyRuntimeRewrites rewrites
9982
+ // to the install target. The source gsd-core/ dir does NOT ship with commands/ —
9983
+ // the canonical command source lives at the package root (commands/gsd/). Without
9984
+ // this copy, every /gsd-* workflow in Cascade references a missing file and the LLM
9985
+ // cannot execute the command body. Surfaced by the #1629 regression test after the
9986
+ // original adversarial review of #1622 missed it.
9987
+ if (isWindsurf && !isGlobal) {
9988
+ const commandsSrc = path.join(src, 'commands', 'gsd');
9989
+ const commandsDest = path.join(skillDest, 'commands', 'gsd');
9990
+ if (fs.existsSync(commandsSrc)) {
9991
+ copyWithPathReplacement(commandsSrc, commandsDest, pathPrefix, runtime, true, isGlobal);
9992
+ console.log(` ${green}✓${reset} Installed command bodies to gsd-core/commands/gsd/ (workflow delegation targets)`);
9993
+ }
9994
+ }
9995
+
9860
9996
  // Copy shared manifests into the gsd-core payload
9861
9997
  // at the co-located path that CJS modules resolve first:
9862
9998
  // gsd-core/bin/shared/*.json
@@ -11171,10 +11307,14 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
11171
11307
  configureKiloPermissions(isGlobal, configDir);
11172
11308
  }
11173
11309
 
11174
- // For non-Claude runtimes, set resolve_model_ids: "omit" in ~/.gsd/defaults.json
11175
- // so resolveModelInternal() returns '' instead of Claude aliases (opus/sonnet/haiku)
11176
- // that the runtime can't resolve. Users can still use model_overrides for explicit IDs.
11177
- // See #1156. Guard matches the #130-class pattern on configureOpencodePermissions above.
11310
+ // For non-Claude runtimes, DEFAULT resolve_model_ids to "omit" in ~/.gsd/defaults.json
11311
+ // when it is absent or falsy, so resolveModelInternal() returns '' instead of Claude
11312
+ // aliases (opus/sonnet/haiku) the runtime can't resolve. An explicit `true` opt-in
11313
+ // (resolveModelInternal returns full materialized model IDs) MUST be preserved —
11314
+ // rewriting it to "omit" would make generated agent manifests inherit the active
11315
+ // chat model instead of pinning the resolved model. See #1156 (default-to-omit
11316
+ // intent) and #1569 (preserve explicit true). Guard matches the #130-class pattern
11317
+ // on configureOpencodePermissions above.
11178
11318
  if (runtime !== 'claude' && !process.env.GSD_TEST_MODE) {
11179
11319
  const gsdDir = path.join(os.homedir(), '.gsd');
11180
11320
  const defaultsPath = path.join(gsdDir, 'defaults.json');
@@ -11182,7 +11322,22 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
11182
11322
  fs.mkdirSync(gsdDir, { recursive: true });
11183
11323
  let defaults = {};
11184
11324
  try { defaults = JSON.parse(fs.readFileSync(defaultsPath, 'utf8')); } catch { /* new file */ }
11185
- if (defaults.resolve_model_ids !== 'omit') {
11325
+ // Recover a malformed (valid-JSON-but-non-object) defaults.json to a fresh object so
11326
+ // the write below succeeds and the file is no longer broken. Without this, `null` /
11327
+ // `[]` / a number / a string bypass the parse catch and either throw a TypeError on
11328
+ // property access (swallowed by the outer try/catch, leaving the file broken) or get
11329
+ // a property set that won't round-trip through JSON.stringify. (#1657)
11330
+ if (defaults === null || typeof defaults !== 'object' || Array.isArray(defaults)) {
11331
+ defaults = {};
11332
+ }
11333
+ // Three-valued domain: false/absent → aliases; true → full IDs; "omit" → ''.
11334
+ // Honor ONLY an explicit canonical `true` opt-in (full model IDs) and an existing
11335
+ // "omit"; default everything else — absent, falsy, OR any non-canonical value — to
11336
+ // "omit", the safe non-Claude default. Allowlist-based so malformed values
11337
+ // (0, "", "yes", {}, …) don't leak Claude aliases the runtime can't resolve (#1569).
11338
+ const existing = defaults.resolve_model_ids;
11339
+ const shouldDefaultToOmit = existing !== true && existing !== 'omit';
11340
+ if (shouldDefaultToOmit) {
11186
11341
  defaults.resolve_model_ids = 'omit';
11187
11342
  fs.writeFileSync(defaultsPath, JSON.stringify(defaults, null, 2) + '\n');
11188
11343
  console.log(` ${green}✓${reset} Set resolve_model_ids: "omit" in ~/.gsd/defaults.json`);
@@ -11638,6 +11793,189 @@ function homePathCoveredByRc(globalBin, homeDir, rcFileNames) {
11638
11793
  return false;
11639
11794
  }
11640
11795
 
11796
+ /**
11797
+ * Decode fish's universal-variable value escaping (the inverse of fish's
11798
+ * `full_escape`). fish serializes every non-`[A-Za-z0-9/_]` byte in
11799
+ * `fish_variables` — e.g. space -> `\x20`, hyphen -> `\x2d`, dot -> `\x2e` —
11800
+ * and joins list elements with the literal 4-char token `\x1e` (NOT a raw
11801
+ * 0x1e byte). Callers split on `\x1e` first, then decode each element here.
11802
+ *
11803
+ * Pure and total: any unrecognised `\`-sequence is passed through verbatim,
11804
+ * so `decode(fishEscape(p)) === p` holds for every path string. Exported for
11805
+ * a fast-check round-trip property test (#323).
11806
+ *
11807
+ * @param {string} s A single (already `\x1e`-split) escaped value.
11808
+ * @returns {string} The decoded literal.
11809
+ */
11810
+ function decodeFishUniversalValue(s) {
11811
+ let out = '';
11812
+ for (let i = 0; i < s.length; i++) {
11813
+ const c = s[i];
11814
+ if (c !== '\\') { out += c; continue; }
11815
+ const n = s[i + 1];
11816
+ if (n === 'n') { out += '\n'; i += 1; }
11817
+ else if (n === 'r') { out += '\r'; i += 1; }
11818
+ else if (n === 't') { out += '\t'; i += 1; }
11819
+ else if (n === '\\') { out += '\\'; i += 1; }
11820
+ else if (n === 'x' || n === 'X') {
11821
+ const hex = s.slice(i + 2, i + 4);
11822
+ if (/^[0-9a-fA-F]{2}$/.test(hex)) { out += String.fromCharCode(parseInt(hex, 16)); i += 3; }
11823
+ else { out += c; }
11824
+ } else if (n === 'u') {
11825
+ const hex = s.slice(i + 2, i + 6);
11826
+ if (/^[0-9a-fA-F]{4}$/.test(hex)) { out += String.fromCharCode(parseInt(hex, 16)); i += 5; }
11827
+ else { out += c; }
11828
+ } else if (n === 'U') {
11829
+ const hex = s.slice(i + 2, i + 10);
11830
+ if (/^[0-9a-fA-F]{8}$/.test(hex)) { out += String.fromCodePoint(parseInt(hex, 16)); i += 9; }
11831
+ else { out += c; }
11832
+ } else { out += c; }
11833
+ }
11834
+ return out;
11835
+ }
11836
+
11837
+ /**
11838
+ * Check whether fish's configuration already places `globalBin` on PATH (#323).
11839
+ *
11840
+ * fish does not use the sh-style `export PATH=` rc files that
11841
+ * `homePathCoveredByRc()` parses, so a fish user whose `fish_user_paths`
11842
+ * already covers the global bin would otherwise see a false-positive
11843
+ * "not on your PATH" warning on every install. Two detection routes,
11844
+ * mirroring how `fish_add_path` actually persists:
11845
+ *
11846
+ * 1. The universal-variable store `fish_variables` — a
11847
+ * `SETUVAR fish_user_paths:<a>\x1e<b>…` line whose `\x1e`-separated
11848
+ * entries are absolute paths (fish does not HOME-expand them here).
11849
+ * 2. `config.fish` — explicit `fish_add_path …`, `set -gx PATH …`, or
11850
+ * `set -Ux fish_user_paths …` lines that name the directory after
11851
+ * HOME expansion.
11852
+ *
11853
+ * Best-effort and side-effect-free: any unreadable / missing file is ignored
11854
+ * (no fish subprocess is spawned). Honours `$XDG_CONFIG_HOME` and always also
11855
+ * checks `~/.config/fish`. Pass `fishConfigDir` to override the lookup
11856
+ * directory (tests).
11857
+ *
11858
+ * @param {string} globalBin Absolute path to npm's global bin directory.
11859
+ * @param {string} homeDir Absolute path used to substitute HOME / ~.
11860
+ * @param {string} [fishConfigDir] Override the fish config directory.
11861
+ * @returns {boolean} true iff fish config adds globalBin to PATH.
11862
+ */
11863
+ function homePathCoveredByFishConfig(globalBin, homeDir, fishConfigDir) {
11864
+ if (!globalBin || !homeDir) return false;
11865
+ const path = require('path');
11866
+ const fs = require('fs');
11867
+
11868
+ const normalise = (p) => {
11869
+ if (!p) return '';
11870
+ let n = p.replace(/[\\/]+$/g, '');
11871
+ if (n === '') n = p.startsWith('/') ? '/' : p;
11872
+ return n;
11873
+ };
11874
+
11875
+ const targetAbs = normalise(path.resolve(globalBin));
11876
+ const homeAbs = path.resolve(homeDir);
11877
+
11878
+ const baseDirs = [];
11879
+ if (fishConfigDir) {
11880
+ baseDirs.push(fishConfigDir);
11881
+ } else {
11882
+ if (process.env.XDG_CONFIG_HOME) {
11883
+ baseDirs.push(path.join(process.env.XDG_CONFIG_HOME, 'fish'));
11884
+ }
11885
+ baseDirs.push(path.join(homeAbs, '.config', 'fish'));
11886
+ }
11887
+
11888
+ const expandHome = (segment) => {
11889
+ let s = segment;
11890
+ s = s.replace(/\$\{HOME\}/g, homeAbs).replace(/\$HOME/g, homeAbs);
11891
+ if (s.startsWith('~/') || s === '~') {
11892
+ s = s === '~' ? homeAbs : path.join(homeAbs, s.slice(2));
11893
+ }
11894
+ return s;
11895
+ };
11896
+
11897
+ // Compare an already-resolved absolute literal (a decoded fish_user_paths
11898
+ // entry — fish stores these resolved, never as `$VAR`/`~`). A literal `$`
11899
+ // here is part of the directory name, so it must NOT be treated as an
11900
+ // unexpanded variable.
11901
+ const matchesLiteral = (segment) => {
11902
+ if (!segment || !path.isAbsolute(segment)) return false;
11903
+ try {
11904
+ return normalise(path.resolve(segment)) === targetAbs;
11905
+ } catch {
11906
+ return false;
11907
+ }
11908
+ };
11909
+
11910
+ // Compare a config.fish shell token: strip surrounding quotes, expand the
11911
+ // common HOME forms, and skip anything still holding a `$` (an unexpanded
11912
+ // variable such as `$PATH` / `$fish_user_paths`) or still relative.
11913
+ const matchesTarget = (rawSegment) => {
11914
+ if (!rawSegment) return false;
11915
+ let seg = rawSegment.trim();
11916
+ if ((seg.startsWith('"') && seg.endsWith('"')) ||
11917
+ (seg.startsWith("'") && seg.endsWith("'"))) {
11918
+ seg = seg.slice(1, -1);
11919
+ }
11920
+ const expanded = expandHome(seg);
11921
+ if (expanded.includes('$')) return false;
11922
+ return matchesLiteral(expanded);
11923
+ };
11924
+
11925
+ const readLines = (filePath) => {
11926
+ try {
11927
+ return fs.readFileSync(filePath, 'utf8').split(/\r?\n/);
11928
+ } catch {
11929
+ return null;
11930
+ }
11931
+ };
11932
+
11933
+ for (const baseDir of baseDirs) {
11934
+ // Route 1: universal variable store.
11935
+ const uvarLines = readLines(path.join(baseDir, 'fish_variables'));
11936
+ if (uvarLines) {
11937
+ for (const rawLine of uvarLines) {
11938
+ const m = /^SETUVAR(?:\s+--\S+)*\s+fish_user_paths:(.*)$/.exec(rawLine);
11939
+ if (!m) continue;
11940
+ // Elements are joined by the literal `\x1e` token; decode each. The
11941
+ // decoded entry is an absolute literal — compare it directly.
11942
+ for (const entry of m[1].split('\\x1e')) {
11943
+ if (matchesLiteral(decodeFishUniversalValue(entry))) return true;
11944
+ }
11945
+ }
11946
+ }
11947
+
11948
+ // Route 2: config.fish explicit PATH mutations.
11949
+ const configLines = readLines(path.join(baseDir, 'config.fish'));
11950
+ if (configLines) {
11951
+ for (const rawLine of configLines) {
11952
+ const line = rawLine.replace(/^\s+/, '');
11953
+ if (line.startsWith('#')) continue;
11954
+
11955
+ let rest = null;
11956
+ let m;
11957
+ if ((m = /^fish_add_path\s+(.+)$/.exec(line))) {
11958
+ rest = m[1];
11959
+ } else if ((m = /^set\s+(?:-\S+\s+)*PATH\s+(.+)$/.exec(line))) {
11960
+ rest = m[1];
11961
+ } else if ((m = /^set\s+(?:-\S+\s+)*fish_user_paths\s+(.+)$/.exec(line))) {
11962
+ rest = m[1];
11963
+ }
11964
+ if (rest === null) continue;
11965
+
11966
+ // Tokens are whitespace-separated; flag tokens (`-g`, `--path`) and
11967
+ // variable references are skipped by matchesTarget / the `-` guard.
11968
+ for (const tok of rest.split(/\s+/)) {
11969
+ if (!tok || tok.startsWith('-')) continue;
11970
+ if (matchesTarget(tok)) return true;
11971
+ }
11972
+ }
11973
+ }
11974
+ }
11975
+
11976
+ return false;
11977
+ }
11978
+
11641
11979
  /**
11642
11980
  * Emit a PATH-export suggestion if globalBin is not already on PATH AND
11643
11981
  * the user's shell rc files do not already cover it via a HOME-relative
@@ -11680,6 +12018,16 @@ function maybeSuggestPathExport(globalBin, homeDir) {
11680
12018
  return;
11681
12019
  }
11682
12020
 
12021
+ // Same idea for fish users: fish_user_paths / config.fish already covers the
12022
+ // dir, the current session just predates it. fish has no sh-style rc file so
12023
+ // homePathCoveredByRc never sees it — check the fish config explicitly (#323).
12024
+ if (homePathCoveredByFishConfig(globalBin, homeDir)) {
12025
+ console.log('');
12026
+ console.log(` ${yellow}⚠${reset} ${bold}${globalBin}${reset}'s directory is already on your PATH via fish's universal variables — open a new fish session (or run ${cyan}exec fish${reset}).`);
12027
+ console.log('');
12028
+ return;
12029
+ }
12030
+
11683
12031
  console.log('');
11684
12032
  console.log(` ${yellow}⚠${reset} ${bold}${globalBin}${reset} is not on your PATH.`);
11685
12033
  console.log(` Add it with one of:`);
@@ -11913,6 +12261,7 @@ module.exports = {
11913
12261
  convertClaudeAgentToCodexAgent,
11914
12262
  generateCodexAgentToml,
11915
12263
  cleanupCodexSkillMetadataSidecars,
12264
+ cleanupWindsurfLegacyDevinSkills,
11916
12265
  generateCodexConfigBlock,
11917
12266
  stripGsdFromCodexConfig,
11918
12267
  migrateCodexHooksMapFormat,
@@ -11974,6 +12323,7 @@ module.exports = {
11974
12323
  skillFrontmatterName,
11975
12324
  convertClaudeToWindsurfMarkdown,
11976
12325
  convertClaudeCommandToWindsurfSkill,
12326
+ convertClaudeCommandToWindsurfWorkflow,
11977
12327
  convertClaudeAgentToWindsurfAgent,
11978
12328
  convertClaudeToAugmentMarkdown,
11979
12329
  convertClaudeCommandToAugmentSkill,
@@ -12015,6 +12365,8 @@ module.exports = {
12015
12365
  USER_OWNED_ARTIFACTS,
12016
12366
  finishInstall,
12017
12367
  homePathCoveredByRc,
12368
+ homePathCoveredByFishConfig,
12369
+ decodeFishUniversalValue,
12018
12370
  maybeSuggestPathExport,
12019
12371
  runtimeMap,
12020
12372
  allRuntimes,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gsd-core",
3
- "version": "1.6.0-rc.2",
3
+ "version": "1.6.0",
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
  }
@@ -85,6 +85,7 @@
85
85
  * UAT Audit:
86
86
  * audit-uat Scan all phases for unresolved UAT/verification items
87
87
  * uat render-checkpoint --file <path> Render the current UAT checkpoint block
88
+ * uat classify-coverage --summary <path> Classify a SUMMARY coverage block into auto-passed vs human-UAT (#1602)
88
89
  *
89
90
  * Open Artifact Audit:
90
91
  * audit-open [--json] Scan all .planning/ artifact types for unresolved items
@@ -221,6 +222,8 @@ const learnings = require('./lib/learnings.cjs');
221
222
  const gapChecker = require('./lib/gap-checker.cjs');
222
223
  const { routeStateCommand } = require('./lib/state-command-router.cjs');
223
224
  const { routeVerifyCommand } = require('./lib/verify-command-router.cjs');
225
+ const { routeEvalCommand } = require('./lib/eval-command-router.cjs');
226
+ const evalMod = require('./lib/eval.cjs');
224
227
  const { routeVerificationCommand } = require('./lib/verification-command-router.cjs');
225
228
  const verification = require('./lib/verification.cjs');
226
229
  const { routeInitCommand } = require('./lib/init-command-router.cjs');
@@ -638,8 +641,8 @@ async function main() {
638
641
  'from-gsd2, frontmatter, gap-analysis, generate-claude-md, generate-claude-profile, ' +
639
642
  'generate-dev-preferences, generate-slug, graphify, history-digest, init, intel, ' +
640
643
  'capability, classify-confidence, git, learnings, list-seeds, list-todos, loop, milestone, package-legitimacy, phase, phase-plan-index, phases, profile-questionnaire, ' +
641
- 'profile-sample, progress, prompt-budget, requirements, research-plan, research-store, resolve-granularity, resolve-model, roadmap, scaffold, state, ' +
642
- 'task, template, user-story, validate, verify, verify-path-exists, verify-summary, workstream, worktree\n\n' +
644
+ 'profile-sample, progress, project-instruction-file, prompt-budget, requirements, research-plan, research-store, resolve-granularity, resolve-model, roadmap, scaffold, state, ' +
645
+ 'task, template, user-story, validate, verify, verify-path-exists, verify-summary, eval, workstream, worktree\n\n' +
643
646
  'Global flags:\n' +
644
647
  ' --raw Emit raw output without post-processing\n' +
645
648
  ' --pick <field> Extract a single field from JSON output (dot/bracket notation)\n' +
@@ -689,6 +692,13 @@ async function main() {
689
692
  'worktree', 'prompt-budget',
690
693
  'research-store', 'research-plan', 'package-legitimacy', 'classify-confidence',
691
694
  'user-story', // pure string validation — no .planning/ access needed
695
+ // #1529: pure runtime→filename projection via getProjectInstructionFile; no
696
+ // .planning/ access needed, and resolving project root would break workflow
697
+ // invocations that run before .planning/ exists (new-project Step 1).
698
+ 'project-instruction-file',
699
+ // #1579: eval.score is pure arithmetic (covered/total + infra weights); it
700
+ // needs no .planning/ access, so skip the findProjectRoot traversal.
701
+ 'eval',
692
702
  ]);
693
703
  if (!SKIP_ROOT_RESOLUTION.has(command)) {
694
704
  cwd = findProjectRoot(cwd);
@@ -1057,6 +1067,11 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1057
1067
  break;
1058
1068
  }
1059
1069
 
1070
+ case 'eval': {
1071
+ routeEvalCommand({ evalMod, args, cwd, raw, error });
1072
+ break;
1073
+ }
1074
+
1060
1075
  // ─── Verification Status ───────────────────────────────────────────────
1061
1076
  //
1062
1077
  // verification status <phaseDir>
@@ -1103,6 +1118,29 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1103
1118
  break;
1104
1119
  }
1105
1120
 
1121
+ case 'project-instruction-file': {
1122
+ // #1529: pure runtime→filename projection. Backs the
1123
+ // `gsd_run query project-instruction-file --runtime <r>` call in
1124
+ // new-project.md so the bash workflow and profile-output.cjs share one
1125
+ // source of truth (getProjectInstructionFile in runtime-name-policy.cjs).
1126
+ // No SDK bridge — pure local lookup, runs before .planning/ exists.
1127
+ const { getProjectInstructionFile } = require('./lib/runtime-name-policy.cjs');
1128
+ // Parse --runtime <value> (space or = form); default to empty so the
1129
+ // safe AGENTS.md cross-agent default applies.
1130
+ const pifArgs = args.slice(1);
1131
+ let pifRuntime = '';
1132
+ for (let i = 0; i < pifArgs.length; i++) {
1133
+ const a = pifArgs[i];
1134
+ if (a === '--runtime' && pifArgs[i + 1] !== undefined) { pifRuntime = pifArgs[++i]; continue; }
1135
+ if (a.startsWith('--runtime=')) { pifRuntime = a.slice('--runtime='.length); continue; }
1136
+ // First positional that isn't a flag also works (lenient); otherwise ignore unknown flags.
1137
+ if (!a.startsWith('-') && !pifRuntime) { pifRuntime = a; }
1138
+ }
1139
+ const filename = getProjectInstructionFile(pifRuntime);
1140
+ process.stdout.write(filename + '\n');
1141
+ break;
1142
+ }
1143
+
1106
1144
  case 'list-todos': {
1107
1145
  commands.cmdListTodos(cwd, args[1], raw);
1108
1146
  break;
@@ -1333,12 +1371,16 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1333
1371
 
1334
1372
  case 'uat': {
1335
1373
  const subcommand = args[1];
1336
- const uat = require('./lib/uat.cjs');
1337
1374
  if (subcommand === 'render-checkpoint') {
1375
+ const uat = require('./lib/uat.cjs');
1338
1376
  const options = parseNamedArgs(args, ['file']);
1339
1377
  uat.cmdRenderCheckpoint(cwd, options, raw);
1378
+ } else if (subcommand === 'classify-coverage') {
1379
+ const coverage = require('./lib/coverage.cjs');
1380
+ const options = parseNamedArgs(args, ['summary', 'file']);
1381
+ coverage.cmdClassify(cwd, options, raw);
1340
1382
  } else {
1341
- error('Unknown uat subcommand. Available: render-checkpoint', ERROR_REASON.SDK_UNKNOWN_COMMAND);
1383
+ error('Unknown uat subcommand. Available: render-checkpoint, classify-coverage', ERROR_REASON.SDK_UNKNOWN_COMMAND);
1342
1384
  }
1343
1385
  break;
1344
1386
  }