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

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 (116) hide show
  1. package/.claude-plugin/plugin.json +2 -1
  2. package/agents/gsd-planner.md +8 -57
  3. package/agents/gsd-security-auditor.md +37 -18
  4. package/bin/install.js +151 -13
  5. package/gemini-extension.json +1 -1
  6. package/gsd-core/bin/gsd-tools.cjs +35 -3
  7. package/gsd-core/bin/lib/audit-command-router.cjs +52 -14
  8. package/gsd-core/bin/lib/capability-lifecycle.cjs +30 -7
  9. package/gsd-core/bin/lib/capability-registry.cjs +96 -83
  10. package/gsd-core/bin/lib/capability-validator.cjs +22 -0
  11. package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +40 -2
  12. package/gsd-core/bin/lib/command-routing-hub.cjs +10 -3
  13. package/gsd-core/bin/lib/config-schema.cjs +1 -0
  14. package/gsd-core/bin/lib/config.cjs +67 -24
  15. package/gsd-core/bin/lib/coverage.cjs +464 -0
  16. package/gsd-core/bin/lib/graphify-command-router.cjs +53 -36
  17. package/gsd-core/bin/lib/init.cjs +8 -4
  18. package/gsd-core/bin/lib/install-profiles.cjs +6 -3
  19. package/gsd-core/bin/lib/intel-command-router.cjs +79 -60
  20. package/gsd-core/bin/lib/planning-workspace.cjs +157 -13
  21. package/gsd-core/bin/lib/profile-output.cjs +18 -6
  22. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +53 -16
  23. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +21 -3
  24. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +15 -0
  25. package/gsd-core/bin/lib/runtime-name-policy.cjs +47 -2
  26. package/gsd-core/bin/lib/state.cjs +364 -52
  27. package/gsd-core/bin/lib/surface.cjs +42 -10
  28. package/gsd-core/bin/lib/update-context.cjs +2 -2
  29. package/gsd-core/references/planner-guidance.md +66 -0
  30. package/gsd-core/references/planning-config.md +2 -2
  31. package/gsd-core/references/security-asvs-levels.md +27 -0
  32. package/gsd-core/templates/SECURITY.md +6 -4
  33. package/gsd-core/templates/summary-complex.md +4 -0
  34. package/gsd-core/templates/summary-minimal.md +3 -0
  35. package/gsd-core/templates/summary-standard.md +4 -0
  36. package/gsd-core/templates/summary.md +41 -0
  37. package/gsd-core/workflows/execute-plan.md +5 -0
  38. package/gsd-core/workflows/new-project.md +4 -4
  39. package/gsd-core/workflows/plan-phase.md +15 -0
  40. package/gsd-core/workflows/secure-phase.md +13 -7
  41. package/gsd-core/workflows/verify-work.md +30 -1
  42. package/package.json +4 -2
  43. package/scripts/gen-plugin-skills.cjs +117 -0
  44. package/scripts/lint-test-file-count.allowlist.json +2 -1
  45. package/scripts/prompt-injection-scan.sh +4 -0
  46. package/scripts/release-notes/conventional-title.cjs +88 -0
  47. package/scripts/release-notes/format-github-release-notes.cjs +4 -3
  48. package/skills/gsd-add-tests/SKILL.md +38 -0
  49. package/skills/gsd-ai-integration-phase/SKILL.md +37 -0
  50. package/skills/gsd-audit-fix/SKILL.md +33 -0
  51. package/skills/gsd-audit-milestone/SKILL.md +37 -0
  52. package/skills/gsd-audit-uat/SKILL.md +25 -0
  53. package/skills/gsd-autonomous/SKILL.md +51 -0
  54. package/skills/gsd-capture/SKILL.md +67 -0
  55. package/skills/gsd-cleanup/SKILL.md +24 -0
  56. package/skills/gsd-code-review/SKILL.md +59 -0
  57. package/skills/gsd-complete-milestone/SKILL.md +142 -0
  58. package/skills/gsd-config/SKILL.md +56 -0
  59. package/skills/gsd-debug/SKILL.md +53 -0
  60. package/skills/gsd-discuss-phase/SKILL.md +77 -0
  61. package/skills/gsd-docs-update/SKILL.md +49 -0
  62. package/skills/gsd-eval-review/SKILL.md +33 -0
  63. package/skills/gsd-execute-phase/SKILL.md +65 -0
  64. package/skills/gsd-explore/SKILL.md +28 -0
  65. package/skills/gsd-extract-learnings/SKILL.md +22 -0
  66. package/skills/gsd-fast/SKILL.md +31 -0
  67. package/skills/gsd-forensics/SKILL.md +56 -0
  68. package/skills/gsd-graphify/SKILL.md +204 -0
  69. package/skills/gsd-health/SKILL.md +31 -0
  70. package/skills/gsd-help/SKILL.md +29 -0
  71. package/skills/gsd-import/SKILL.md +46 -0
  72. package/skills/gsd-inbox/SKILL.md +39 -0
  73. package/skills/gsd-ingest-docs/SKILL.md +43 -0
  74. package/skills/gsd-manager/SKILL.md +45 -0
  75. package/skills/gsd-map-codebase/SKILL.md +83 -0
  76. package/skills/gsd-mempalace-capture/SKILL.md +71 -0
  77. package/skills/gsd-mempalace-recall/SKILL.md +102 -0
  78. package/skills/gsd-milestone-summary/SKILL.md +51 -0
  79. package/skills/gsd-mvp-phase/SKILL.md +45 -0
  80. package/skills/gsd-new-milestone/SKILL.md +45 -0
  81. package/skills/gsd-new-project/SKILL.md +47 -0
  82. package/skills/gsd-ns-context/SKILL.md +24 -0
  83. package/skills/gsd-ns-ideate/SKILL.md +23 -0
  84. package/skills/gsd-ns-manage/SKILL.md +35 -0
  85. package/skills/gsd-ns-project/SKILL.md +26 -0
  86. package/skills/gsd-ns-review/SKILL.md +28 -0
  87. package/skills/gsd-ns-workflow/SKILL.md +33 -0
  88. package/skills/gsd-pause-work/SKILL.md +43 -0
  89. package/skills/gsd-phase/SKILL.md +57 -0
  90. package/skills/gsd-plan-phase/SKILL.md +63 -0
  91. package/skills/gsd-plan-review-convergence/SKILL.md +60 -0
  92. package/skills/gsd-pr-branch/SKILL.md +26 -0
  93. package/skills/gsd-profile-user/SKILL.md +47 -0
  94. package/skills/gsd-progress/SKILL.md +49 -0
  95. package/skills/gsd-quick/SKILL.md +174 -0
  96. package/skills/gsd-resume-work/SKILL.md +31 -0
  97. package/skills/gsd-review/SKILL.md +42 -0
  98. package/skills/gsd-review-backlog/SKILL.md +63 -0
  99. package/skills/gsd-secure-phase/SKILL.md +36 -0
  100. package/skills/gsd-settings/SKILL.md +29 -0
  101. package/skills/gsd-ship/SKILL.md +24 -0
  102. package/skills/gsd-sketch/SKILL.md +60 -0
  103. package/skills/gsd-spec-phase/SKILL.md +63 -0
  104. package/skills/gsd-spike/SKILL.md +57 -0
  105. package/skills/gsd-stats/SKILL.md +20 -0
  106. package/skills/gsd-surface/SKILL.md +162 -0
  107. package/skills/gsd-thread/SKILL.md +24 -0
  108. package/skills/gsd-ui-phase/SKILL.md +35 -0
  109. package/skills/gsd-ui-review/SKILL.md +33 -0
  110. package/skills/gsd-ultraplan-phase/SKILL.md +34 -0
  111. package/skills/gsd-undo/SKILL.md +35 -0
  112. package/skills/gsd-update/SKILL.md +50 -0
  113. package/skills/gsd-validate-phase/SKILL.md +36 -0
  114. package/skills/gsd-verify-work/SKILL.md +39 -0
  115. package/skills/gsd-workspace/SKILL.md +53 -0
  116. package/skills/gsd-workstreams/SKILL.md +70 -0
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "gsd-core",
3
3
  "displayName": "GSD Core",
4
- "version": "1.6.0-rc.2",
4
+ "version": "1.6.0-rc.3",
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",
@@ -19,5 +19,6 @@
19
19
  "gsd"
20
20
  ],
21
21
  "commands": "./commands/gsd/",
22
+ "skills": "./skills/",
22
23
  "hooks": "./hooks/hooks.json"
23
24
  }
@@ -380,11 +380,11 @@ Output: [Artifacts created]
380
380
 
381
381
  ## STRIDE Threat Register
382
382
 
383
- | Threat ID | Category | Component | Disposition | Mitigation Plan |
384
- |-----------|----------|-----------|-------------|-----------------|
385
- | T-{phase}-01 | {S/T/R/I/D/E} | {function/endpoint/file} | mitigate | {specific: e.g., "validate input with zod at route entry"} |
386
- | T-{phase}-02 | {category} | {component} | accept | {rationale: e.g., "no PII, low-value target"} |
387
- | T-{phase}-SC | Tampering | npm/pip/cargo installs | mitigate | slopcheck + blocking human checkpoint for [ASSUMED]/[SUS] |
383
+ | Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
384
+ |-----------|----------|-----------|----------|-------------|-----------------|
385
+ | T-{phase}-01 | {S/T/R/I/D/E} | {function/endpoint/file} | {critical\|high\|medium\|low} | mitigate | {specific mitigation action} |
386
+ | T-{phase}-02 | {category} | {component} | low | accept | {rationale for acceptance} |
387
+ | T-{phase}-SC | Tampering | npm/pip/cargo installs | high | mitigate | slopcheck + blocking human checkpoint for [ASSUMED]/[SUS] |
388
388
  </threat_model>
389
389
 
390
390
  <verification>
@@ -459,7 +459,7 @@ Only include what Claude literally cannot do.
459
459
  **Step 0: Extract Requirement IDs**
460
460
  Read ROADMAP.md `**Requirements:**` line for this phase. Strip brackets if present (e.g., `[AUTH-01, AUTH-02]` → `AUTH-01, AUTH-02`). Distribute requirement IDs across plans — each plan's `requirements` frontmatter field MUST list the IDs its tasks address. **CRITICAL:** Every requirement ID MUST appear in at least one plan. Plans with an empty `requirements` field are invalid.
461
461
 
462
- **Security (when `security_enforcement` enabled — absent = enabled):** Identify trust boundaries in this phase's scope. Map STRIDE categories to applicable tech stack from RESEARCH.md security domain. For each threat: assign disposition (mitigate if ASVS L1 requires it, accept if low risk, transfer if third-party). Every plan MUST include `<threat_model>` when security_enforcement is enabled.
462
+ **Security (when `security_enforcement` enabled — absent = enabled):** Identify trust boundaries in this phase's scope. Map STRIDE categories to applicable tech stack from RESEARCH.md security domain. For each threat: assign a **severity** (critical|high|medium|low) based on impact × likelihood, and a disposition (`mitigate`/`accept`/`transfer`) per the configured OWASP ASVS level — see @~/.claude/gsd-core/references/security-asvs-levels.md. Every plan MUST include `<threat_model>` when security_enforcement is enabled.
463
463
 
464
464
  **Package legitimacy gate (npm/pip/cargo only):**
465
465
  - Require RESEARCH.md `## Package Legitimacy Audit` before package-manager install tasks.
@@ -477,66 +477,16 @@ Take phase goal from ROADMAP.md. Must be outcome-shaped, not task-shaped.
477
477
  **Step 2: Derive Observable Truths**
478
478
  "What must be TRUE for this goal to be achieved?" List 3-7 truths from USER's perspective.
479
479
 
480
- For "working chat interface":
481
- - User can see existing messages
482
- - User can type a new message
483
- - User can send the message
484
- - Sent message appears in the list
485
- - Messages persist across page refresh
486
-
487
- **Test:** Each truth verifiable by a human using the application.
488
-
489
480
  **Step 3: Derive Required Artifacts**
490
481
  For each truth: "What must EXIST for this to be true?"
491
482
 
492
- "User can see existing messages" requires:
493
- - Message list component (renders Message[])
494
- - Messages state (loaded from somewhere)
495
- - API route or data source (provides messages)
496
- - Message type definition (shapes the data)
497
-
498
- **Test:** Each artifact = a specific file or database object.
499
-
500
483
  **Step 4: Derive Required Wiring**
501
484
  For each artifact: "What must be CONNECTED for this to function?"
502
485
 
503
- Message list component wiring:
504
- - Imports Message type (not using `any`)
505
- - Receives messages prop or fetches from API
506
- - Maps over messages to render (not hardcoded)
507
- - Handles empty state (not just crashes)
508
-
509
486
  **Step 5: Identify Key Links**
510
487
  "Where is this most likely to break?" Key links = critical connections where breakage causes cascading failures.
511
488
 
512
- ## Must-Haves Output Format
513
-
514
- ```yaml
515
- must_haves:
516
- truths:
517
- - "User can see existing messages"
518
- - "User can send a message"
519
- - "Messages persist across refresh"
520
- artifacts:
521
- - path: "src/components/Chat.tsx"
522
- provides: "Message list rendering"
523
- min_lines: 30
524
- - path: "src/app/api/chat/route.ts"
525
- provides: "Message CRUD operations"
526
- exports: ["GET", "POST"]
527
- - path: "prisma/schema.prisma"
528
- provides: "Message model"
529
- contains: "model Message"
530
- key_links:
531
- - from: "src/components/Chat.tsx"
532
- to: "src/app/api/chat/route.ts"
533
- via: "fetch in useEffect — calls /api/chat endpoint"
534
- pattern: "fetch.*api/chat"
535
- - from: "src/app/api/chat/route.ts"
536
- to: "prisma/schema.prisma"
537
- via: "database query via prisma.message"
538
- pattern: "prisma\\.message\\.(find|create)"
539
- ```
489
+ See @~/.claude/gsd-core/references/planner-guidance.md for a worked example and the `must_haves` YAML format.
540
490
 
541
491
  </goal_backward>
542
492
 
@@ -1037,6 +987,7 @@ Phase planning complete when:
1037
987
  - [ ] User knows next steps and wave structure
1038
988
  - [ ] `<threat_model>` present with STRIDE register (when `security_enforcement` enabled)
1039
989
  - [ ] Every threat has a disposition (mitigate / accept / transfer)
990
+ - [ ] Every threat has a Severity (critical|high|medium|low)
1040
991
  - [ ] Mitigations reference specific implementation (not generic advice)
1041
992
 
1042
993
  ## Gap Closure Mode
@@ -33,18 +33,19 @@ Does NOT scan blindly for new vulnerabilities. Verifies each threat in `<threat_
33
33
  - Marking CLOSED based on code structure ("looks like it validates input") without finding the actual validation call
34
34
 
35
35
  **Required finding classification:**
36
- - **BLOCKER** — `OPEN_THREATS`: a declared mitigation is absent in implemented code; phase must not ship
36
+ - **BLOCKER** — `OPEN_THREATS`: a declared mitigation is absent in implemented code AND the threat's severity ≥ `block_on` threshold; phase must not ship until resolved
37
+ - **OPEN — non-blocking** — mitigation absent BUT the threat's severity is below the `block_on` threshold; tracked in SECURITY.md, does NOT count toward `threats_open`, does not block ship
37
38
  - **WARNING** — `unregistered_flag`: new attack surface appeared during implementation with no threat mapping
38
- Every threat must resolve to CLOSED, OPEN (BLOCKER), or documented accepted risk.
39
+ Every threat must resolve to CLOSED, OPEN-blocking (severity ≥ block_on), OPEN-non-blocking (severity below block_on), or documented accepted risk.
39
40
  </adversarial_stance>
40
41
 
41
42
  <execution_flow>
42
43
 
43
44
  <step name="load_context">
44
45
  Read ALL files from `<required_reading>`. Extract:
45
- - PLAN.md `<threat_model>` block: full threat register with IDs, categories, dispositions, mitigation plans
46
+ - PLAN.md `<threat_model>` block: full threat register with IDs, categories, severities, dispositions, mitigation plans
46
47
  - SUMMARY.md `## Threat Flags` section: new attack surface detected by executor during implementation
47
- - `<config>` block: `asvs_level` (1/2/3), `block_on` (open / unregistered / none)
48
+ - `<config>` block: `asvs_level` (1/2/3), `block_on` (critical | high | medium | low | none) — severity ordering: critical > high > medium > low; none = never block
48
49
  - Implementation files: exports, auth patterns, input handling, data flows
49
50
 
50
51
  **Context budget:** Load project skills first (lightweight). Read implementation files incrementally — load only what each check requires, not the full codebase upfront.
@@ -60,7 +61,7 @@ This ensures project-specific patterns, conventions, and best practices are appl
60
61
  </step>
61
62
 
62
63
  <step name="analyze_threats">
63
- For each threat in `<threat_model>`, determine verification method by disposition:
64
+ For each threat in `<threat_model>`, read its `severity` field (critical|high|medium|low). If building the register retroactively (no `<threat_model>` in PLAN.md), assign a severity to each threat you construct based on impact × likelihood. Determine verification method by disposition:
64
65
 
65
66
  | Disposition | Verification Method |
66
67
  |-------------|---------------------|
@@ -69,16 +70,27 @@ For each threat in `<threat_model>`, determine verification method by dispositio
69
70
  | `transfer` | Verify transfer documentation present (insurance, vendor SLA, etc.) |
70
71
 
71
72
  Classify each threat before verification. Record classification for every threat — no threat skipped.
73
+
74
+ **Verification depth scales with `asvs_level`** (see @~/.claude/gsd-core/references/security-asvs-levels.md for full definitions):
75
+ - L1: verify mitigation is PRESENT in the cited file (grep-level — pattern exists).
76
+ - L2: verify the mitigation ADDRESSES the threat vector and is placed at the correct boundary (a check in the wrong layer does not close the threat).
77
+ - L3: deep trace — follow the data flow end-to-end, check edge cases and ordering, confirm no bypass path exists.
72
78
  </step>
73
79
 
74
80
  <step name="verify_and_write">
75
- For each `mitigate` threat: grep for declared mitigation pattern in cited files → found = `CLOSED`, not found = `OPEN`.
81
+ For each `mitigate` threat: grep for declared mitigation pattern in cited files → found = `CLOSED`, not found = `OPEN`. Apply depth per `asvs_level` (see analyze_threats step).
76
82
  For `accept` threats: check SECURITY.md accepted risks log → entry present = `CLOSED`, absent = `OPEN`.
77
83
  For `transfer` threats: check for transfer documentation → present = `CLOSED`, absent = `OPEN`.
78
84
 
79
85
  For each `threat_flag` in SUMMARY.md `## Threat Flags`: if maps to existing threat ID → informational. If no mapping → log as `unregistered_flag` in SECURITY.md (not a blocker).
80
86
 
81
- Write SECURITY.md. Set `threats_open` count. Return structured result.
87
+ **Severity-aware `threats_open` computation (severity order: critical > high > medium > low):**
88
+ `threats_open` (the SECURITY.md frontmatter gate field) = the count of threats whose status is OPEN AND whose severity rank ≥ the `block_on` rank. `block_on: none` ⇒ 0 (nothing ever blocks). `block_on: low` ⇒ all open threats block. `block_on: high` (default) ⇒ only high and critical open threats block.
89
+ Open threats BELOW the block threshold are recorded in SECURITY.md as **open — below {block_on} threshold (non-blocking)** and MUST NOT be counted in `threats_open`.
90
+
91
+ **Fail-closed for missing severity:** if an OPEN threat has no severity or an unparseable severity (e.g. a legacy register predating the Severity column), treat it as `critical` for this computation — it COUNTS toward `threats_open` (blocking). Never silently drop an unranked open threat.
92
+
93
+ Write SECURITY.md. Set `threats_open` to the severity-filtered count. Return structured result.
82
94
  </step>
83
95
 
84
96
  </execution_flow>
@@ -95,9 +107,9 @@ Write SECURITY.md. Set `threats_open` count. Return structured result.
95
107
  **ASVS Level:** {1/2/3}
96
108
 
97
109
  ### Threat Verification
98
- | Threat ID | Category | Disposition | Evidence |
99
- |-----------|----------|-------------|----------|
100
- | {id} | {category} | {mitigate/accept/transfer} | {file:line or doc reference} |
110
+ | Threat ID | Category | Severity | Disposition | Evidence |
111
+ |-----------|----------|----------|-------------|----------|
112
+ | {id} | {category} | {critical\|high\|medium\|low} | {mitigate/accept/transfer} | {file:line or doc reference} |
101
113
 
102
114
  ### Unregistered Flags
103
115
  {none / list from SUMMARY.md ## Threat Flags with no threat mapping}
@@ -115,14 +127,21 @@ SECURITY.md: {path}
115
127
  **ASVS Level:** {1/2/3}
116
128
 
117
129
  ### Closed
118
- | Threat ID | Category | Disposition | Evidence |
119
- |-----------|----------|-------------|----------|
120
- | {id} | {category} | {disposition} | {evidence} |
121
-
122
- ### Open
123
- | Threat ID | Category | Mitigation Expected | Files Searched |
124
- |-----------|----------|---------------------|----------------|
125
- | {id} | {category} | {pattern not found} | {file paths} |
130
+ | Threat ID | Category | Severity | Disposition | Evidence |
131
+ |-----------|----------|----------|-------------|----------|
132
+ | {id} | {category} | {critical\|high\|medium\|low} | {disposition} | {evidence} |
133
+
134
+ ### Open (blocking — severity ≥ block_on threshold)
135
+ | Threat ID | Category | Severity | Mitigation Expected | Files Searched |
136
+ |-----------|----------|----------|---------------------|----------------|
137
+ | {id} | {category} | {critical\|high\|medium\|low} | {pattern not found} | {file paths} |
138
+
139
+ ### Open (non-blocking — severity below block_on threshold)
140
+ | Threat ID | Category | Severity | Mitigation Expected | Files Searched |
141
+ |-----------|----------|----------|---------------------|----------------|
142
+ | {id} | {category} | {critical\|high\|medium\|low} | {pattern not found} | {file paths} |
143
+
144
+ *Only blocking-open threats count toward `threats_open` in SECURITY.md frontmatter.*
126
145
 
127
146
  Next: Implement mitigations or document as accepted in SECURITY.md accepted risks log, then re-run /gsd:secure-phase.
128
147
 
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
@@ -11913,6 +12049,7 @@ module.exports = {
11913
12049
  convertClaudeAgentToCodexAgent,
11914
12050
  generateCodexAgentToml,
11915
12051
  cleanupCodexSkillMetadataSidecars,
12052
+ cleanupWindsurfLegacyDevinSkills,
11916
12053
  generateCodexConfigBlock,
11917
12054
  stripGsdFromCodexConfig,
11918
12055
  migrateCodexHooksMapFormat,
@@ -11974,6 +12111,7 @@ module.exports = {
11974
12111
  skillFrontmatterName,
11975
12112
  convertClaudeToWindsurfMarkdown,
11976
12113
  convertClaudeCommandToWindsurfSkill,
12114
+ convertClaudeCommandToWindsurfWorkflow,
11977
12115
  convertClaudeAgentToWindsurfAgent,
11978
12116
  convertClaudeToAugmentMarkdown,
11979
12117
  convertClaudeCommandToAugmentSkill,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gsd-core",
3
- "version": "1.6.0-rc.2",
3
+ "version": "1.6.0-rc.3",
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
@@ -638,7 +639,7 @@ async function main() {
638
639
  'from-gsd2, frontmatter, gap-analysis, generate-claude-md, generate-claude-profile, ' +
639
640
  'generate-dev-preferences, generate-slug, graphify, history-digest, init, intel, ' +
640
641
  '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
+ 'profile-sample, progress, project-instruction-file, prompt-budget, requirements, research-plan, research-store, resolve-granularity, resolve-model, roadmap, scaffold, state, ' +
642
643
  'task, template, user-story, validate, verify, verify-path-exists, verify-summary, workstream, worktree\n\n' +
643
644
  'Global flags:\n' +
644
645
  ' --raw Emit raw output without post-processing\n' +
@@ -689,6 +690,10 @@ async function main() {
689
690
  'worktree', 'prompt-budget',
690
691
  'research-store', 'research-plan', 'package-legitimacy', 'classify-confidence',
691
692
  'user-story', // pure string validation — no .planning/ access needed
693
+ // #1529: pure runtime→filename projection via getProjectInstructionFile; no
694
+ // .planning/ access needed, and resolving project root would break workflow
695
+ // invocations that run before .planning/ exists (new-project Step 1).
696
+ 'project-instruction-file',
692
697
  ]);
693
698
  if (!SKIP_ROOT_RESOLUTION.has(command)) {
694
699
  cwd = findProjectRoot(cwd);
@@ -1103,6 +1108,29 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1103
1108
  break;
1104
1109
  }
1105
1110
 
1111
+ case 'project-instruction-file': {
1112
+ // #1529: pure runtime→filename projection. Backs the
1113
+ // `gsd_run query project-instruction-file --runtime <r>` call in
1114
+ // new-project.md so the bash workflow and profile-output.cjs share one
1115
+ // source of truth (getProjectInstructionFile in runtime-name-policy.cjs).
1116
+ // No SDK bridge — pure local lookup, runs before .planning/ exists.
1117
+ const { getProjectInstructionFile } = require('./lib/runtime-name-policy.cjs');
1118
+ // Parse --runtime <value> (space or = form); default to empty so the
1119
+ // safe AGENTS.md cross-agent default applies.
1120
+ const pifArgs = args.slice(1);
1121
+ let pifRuntime = '';
1122
+ for (let i = 0; i < pifArgs.length; i++) {
1123
+ const a = pifArgs[i];
1124
+ if (a === '--runtime' && pifArgs[i + 1] !== undefined) { pifRuntime = pifArgs[++i]; continue; }
1125
+ if (a.startsWith('--runtime=')) { pifRuntime = a.slice('--runtime='.length); continue; }
1126
+ // First positional that isn't a flag also works (lenient); otherwise ignore unknown flags.
1127
+ if (!a.startsWith('-') && !pifRuntime) { pifRuntime = a; }
1128
+ }
1129
+ const filename = getProjectInstructionFile(pifRuntime);
1130
+ process.stdout.write(filename + '\n');
1131
+ break;
1132
+ }
1133
+
1106
1134
  case 'list-todos': {
1107
1135
  commands.cmdListTodos(cwd, args[1], raw);
1108
1136
  break;
@@ -1333,12 +1361,16 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
1333
1361
 
1334
1362
  case 'uat': {
1335
1363
  const subcommand = args[1];
1336
- const uat = require('./lib/uat.cjs');
1337
1364
  if (subcommand === 'render-checkpoint') {
1365
+ const uat = require('./lib/uat.cjs');
1338
1366
  const options = parseNamedArgs(args, ['file']);
1339
1367
  uat.cmdRenderCheckpoint(cwd, options, raw);
1368
+ } else if (subcommand === 'classify-coverage') {
1369
+ const coverage = require('./lib/coverage.cjs');
1370
+ const options = parseNamedArgs(args, ['summary', 'file']);
1371
+ coverage.cmdClassify(cwd, options, raw);
1340
1372
  } else {
1341
- error('Unknown uat subcommand. Available: render-checkpoint', ERROR_REASON.SDK_UNKNOWN_COMMAND);
1373
+ error('Unknown uat subcommand. Available: render-checkpoint, classify-coverage', ERROR_REASON.SDK_UNKNOWN_COMMAND);
1342
1374
  }
1343
1375
  break;
1344
1376
  }
@@ -25,6 +25,10 @@
25
25
  */
26
26
  // eslint-disable-next-line @typescript-eslint/no-require-imports
27
27
  const io = require("./io.cjs");
28
+ // Phase 2 (#1646): route through the Hub per ADR-959 §III(B) line 75.
29
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
30
+ const cjsCommandRouterAdapter = require("./cjs-command-router-adapter.cjs");
31
+ const { routeHubCommandFamily } = cjsCommandRouterAdapter;
28
32
  // ─── routeAuditUat ────────────────────────────────────────────────────────────
29
33
  function routeAuditUat({ args, cwd, raw, error, _uat }) {
30
34
  // Suppress unused-variable warnings for args/error — this command has no
@@ -33,27 +37,61 @@ function routeAuditUat({ args, cwd, raw, error, _uat }) {
33
37
  void error;
34
38
  // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
35
39
  const u = _uat ?? require('./uat.cjs');
36
- u.cmdAuditUat(cwd, raw);
40
+ // Phase 2 (#1646): routes through the Hub for uniform observability and
41
+ // HandlerFailure taxonomy. audit-uat has no subcommands — a synthetic 'run'
42
+ // defaultSubcommand gives the Hub a single-handler manifest. The dispatch is
43
+ // trivial but the observability seam (DispatchEvent, GSD_AUDIT=1 trace) is
44
+ // now consistent with graphify/intel/host routers.
45
+ routeHubCommandFamily({
46
+ family: 'audit-uat',
47
+ args,
48
+ subcommands: ['run'],
49
+ defaultSubcommand: 'run',
50
+ handlers: {
51
+ run: () => u.cmdAuditUat(cwd, raw),
52
+ },
53
+ unknownMessage: (subcommand) => `Unknown audit-uat subcommand: "${subcommand}". audit-uat takes no subcommands.`,
54
+ error,
55
+ cwd,
56
+ raw,
57
+ });
37
58
  }
38
59
  // ─── routeAuditOpen ──────────────────────────────────────────────────────────
39
60
  function routeAuditOpen({ args, cwd, raw, error, _audit, _core }) {
40
- // Suppress unused-variable warning for error — audit-open has no subcommand
41
- // dispatch that would call error(); only flag parsing occurs here.
42
- void error;
43
61
  // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
44
62
  const a = _audit ?? require('./audit.cjs');
45
63
  const c = _core ?? io;
64
+ // Phase 2 (#1646): routes through the Hub for uniform observability.
65
+ // `--json` is a flag, not a subcommand — capture it in the closure and strip
66
+ // it from args before Hub dispatch so it isn't mistaken for a subcommand by
67
+ // the manifest check. The handler then branches on wantJson for the two
68
+ // output shapes (JSON object vs human-readable formatted report).
46
69
  const wantJson = args.includes('--json');
47
- const result = a.auditOpenArtifacts(cwd);
48
- if (wantJson) {
49
- // io.output JSON-stringifies its first arg; pass the object directly.
50
- c.output(result, raw);
51
- }
52
- else {
53
- // Human-readable report must bypass JSON encoding — use the rawValue
54
- // form (third arg) which io.output emits verbatim.
55
- c.output(null, true, a.formatAuditReport(result));
56
- }
70
+ const hubArgs = wantJson ? args.filter((arg) => arg !== '--json') : args;
71
+ routeHubCommandFamily({
72
+ family: 'audit-open',
73
+ args: hubArgs,
74
+ subcommands: ['run'],
75
+ defaultSubcommand: 'run',
76
+ handlers: {
77
+ run: () => {
78
+ const result = a.auditOpenArtifacts(cwd);
79
+ if (wantJson) {
80
+ // io.output JSON-stringifies its first arg; pass the object directly.
81
+ c.output(result, raw);
82
+ }
83
+ else {
84
+ // Human-readable report must bypass JSON encoding — use the rawValue
85
+ // form (third arg) which io.output emits verbatim.
86
+ c.output(null, true, a.formatAuditReport(result));
87
+ }
88
+ },
89
+ },
90
+ unknownMessage: (subcommand) => `Unknown audit-open subcommand: "${subcommand}". audit-open takes no subcommands (use --json for JSON output).`,
91
+ error,
92
+ cwd,
93
+ raw,
94
+ });
57
95
  }
58
96
  module.exports = {
59
97
  routeAuditUat,