@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
@@ -270,17 +270,49 @@ function applySurface(runtimeConfigDir, layout, manifest, clusterMap, registry)
270
270
  // module's deep seam (ADR-1508 / #1511 Phase 2) — no attribution resolver
271
271
  // needed here (proven: Co-Authored-By never appears in staged content; see
272
272
  // brief PROVEN KEY FACT). No getInstallExports() call required.
273
- for (const kind of layout.kinds) {
274
- const staged = kind.stage(resolved);
275
- if (kind.kind === 'skills') {
276
- runtimeArtifactConversion.rewriteStagedSkillBodies(staged, {
277
- runtime: layout.runtime,
278
- configDir: layout.configDir,
279
- scope: layout.scope ?? 'global',
280
- });
273
+ // #1615 adversarial review (PR #1622): commands kind was previously skipped,
274
+ // leaving raw @~/.claude/... references in Windsurf workflow bodies after a
275
+ // /gsd-surface profile change. Same gap affected any runtime with commands
276
+ // kinds (windsurf, opencode, kilo, cursor, augment, codebuddy, gemini).
277
+ //
278
+ // Asymmetry note: rewriteStagedSkillBodies mutates in place (returns void),
279
+ // but rewriteStagedCommandBodies copies to a fresh mkdtemp dir and returns
280
+ // its path (commands .md files are flat; mutating the staged source would
281
+ // corrupt the package source on full-profile runs). Caller MUST sync from
282
+ // the returned dir and clean it up.
283
+ const tempDirsToClean = [];
284
+ try {
285
+ for (const kind of layout.kinds) {
286
+ let staged = kind.stage(resolved);
287
+ if (kind.kind === 'skills') {
288
+ runtimeArtifactConversion.rewriteStagedSkillBodies(staged, {
289
+ runtime: layout.runtime,
290
+ configDir: layout.configDir,
291
+ scope: layout.scope ?? 'global',
292
+ });
293
+ }
294
+ else if (kind.kind === 'commands') {
295
+ const rewritten = runtimeArtifactConversion.rewriteStagedCommandBodies(staged, {
296
+ runtime: layout.runtime,
297
+ configDir: layout.configDir,
298
+ scope: layout.scope ?? 'global',
299
+ });
300
+ if (rewritten && rewritten !== staged) {
301
+ staged = rewritten;
302
+ tempDirsToClean.push(rewritten);
303
+ }
304
+ }
305
+ const dest = node_path_1.default.join(layout.configDir, kind.destSubpath);
306
+ _syncGsdDir(staged, dest, kind, skillManifest);
307
+ }
308
+ }
309
+ finally {
310
+ for (const dir of tempDirsToClean) {
311
+ try {
312
+ node_fs_1.default.rmSync(dir, { recursive: true, force: true });
313
+ }
314
+ catch { /* best-effort cleanup */ }
281
315
  }
282
- const dest = node_path_1.default.join(layout.configDir, kind.destSubpath);
283
- _syncGsdDir(staged, dest, kind, skillManifest);
284
316
  }
285
317
  return resolved;
286
318
  }
@@ -31,8 +31,8 @@ exports.RUNTIME_DIRS = [
31
31
  ['antigravity', '.gemini/antigravity'],
32
32
  ['antigravity', '.agents'], // local Antigravity install dir canonical (#791; bin/install.js getDirName('antigravity'))
33
33
  ['antigravity', '.agent'], // local Antigravity install dir legacy (#503; backward-compat with pre-#791 installs)
34
- ['windsurf', '.devin'], // local Windsurf/Devin Desktop install dir canonical (#1085; bin/install.js getDirName('windsurf'))
35
- ['windsurf', '.windsurf'], // local Windsurf install dir legacy (#1085; backward-compat with pre-#1085 installs)
34
+ ['windsurf', '.windsurf'], // local Windsurf workflow dir canonical (#1615; bin/install.js getDirName('windsurf'))
35
+ ['windsurf', '.devin'], // local Devin Desktop install dir legacy (#1085; backward-compat)
36
36
  ['gemini', '.gemini'],
37
37
  ['kilo', '.config/kilo'],
38
38
  ['kilo', '.kilo'],
@@ -184,3 +184,69 @@ Execute: `/gsd:execute-phase {phase} --gaps-only`
184
184
  ## Checkpoint Reached / Revision Complete
185
185
 
186
186
  Follow templates in checkpoints and revision_mode sections respectively.
187
+
188
+ ---
189
+
190
+ ## Goal-Backward Worked Example
191
+
192
+ ### Step 2: Derive Observable Truths
193
+
194
+ For "working chat interface":
195
+ - User can see existing messages
196
+ - User can type a new message
197
+ - User can send the message
198
+ - Sent message appears in the list
199
+ - Messages persist across page refresh
200
+
201
+ **Test:** Each truth verifiable by a human using the application.
202
+
203
+ ### Step 3: Derive Required Artifacts
204
+
205
+ "User can see existing messages" requires:
206
+ - Message list component (renders Message[])
207
+ - Messages state (loaded from somewhere)
208
+ - API route or data source (provides messages)
209
+ - Message type definition (shapes the data)
210
+
211
+ **Test:** Each artifact = a specific file or database object.
212
+
213
+ ### Step 4: Derive Required Wiring
214
+
215
+ Message list component wiring:
216
+ - Imports Message type (not using `any`)
217
+ - Receives messages prop or fetches from API
218
+ - Maps over messages to render (not hardcoded)
219
+ - Handles empty state (not just crashes)
220
+
221
+ ### Step 5: Identify Key Links
222
+
223
+ "Where is this most likely to break?" Key links = critical connections where breakage causes cascading failures.
224
+
225
+ ### Must-Haves Output Format
226
+
227
+ ```yaml
228
+ must_haves:
229
+ truths:
230
+ - "User can see existing messages"
231
+ - "User can send a message"
232
+ - "Messages persist across refresh"
233
+ artifacts:
234
+ - path: "src/components/Chat.tsx"
235
+ provides: "Message list rendering"
236
+ min_lines: 30
237
+ - path: "src/app/api/chat/route.ts"
238
+ provides: "Message CRUD operations"
239
+ exports: ["GET", "POST"]
240
+ - path: "prisma/schema.prisma"
241
+ provides: "Message model"
242
+ contains: "model Message"
243
+ key_links:
244
+ - from: "src/components/Chat.tsx"
245
+ to: "src/app/api/chat/route.ts"
246
+ via: "fetch in useEffect — calls /api/chat endpoint"
247
+ pattern: "fetch.*api/chat"
248
+ - from: "src/app/api/chat/route.ts"
249
+ to: "prisma/schema.prisma"
250
+ via: "database query via prisma.message"
251
+ pattern: "prisma\\.message\\.(find|create)"
252
+ ```
@@ -275,8 +275,8 @@ Set via `workflow.*` namespace in config.json (e.g., `"workflow": { "research":
275
275
  | `workflow.code_review_depth` | string | `"standard"` | `"light"`, `"standard"`, `"deep"` | Depth level for code review analysis in the ship workflow |
276
276
  | `workflow._auto_chain_active` | boolean | `false` | `true`, `false` | Internal: tracks whether autonomous chaining is active |
277
277
  | `workflow.security_enforcement` | boolean | `true` | `true`, `false` | Enable threat-model-anchored security verification via `/gsd:secure-phase`. When `false`, security checks are skipped entirely |
278
- | `workflow.security_asvs_level` | number | `1` | `1`, `2`, `3` | OWASP ASVS verification level. Level 1 = opportunistic, Level 2 = standard, Level 3 = comprehensive |
279
- | `workflow.security_block_on` | string | `"high"` | `"high"`, `"medium"`, `"low"` | Minimum severity that blocks phase advancement |
278
+ | `workflow.security_asvs_level` | number | `1` | `1`, `2`, `3` | OWASP ASVS verification level. Level 1 = opportunistic, Level 2 = standard, Level 3 = comprehensive. Scales both planner threat-disposition rigor (which threats must be mitigated vs. accepted) and auditor verification depth (grep-level → boundary-placement check → full data-flow trace). See `gsd-core/references/security-asvs-levels.md`. |
279
+ | `workflow.security_block_on` | string | `"high"` | `"critical"`, `"high"`, `"medium"`, `"low"`, `"none"` | Minimum threat severity that blocks phase advancement. The auditor counts only open threats at or above this severity toward the blocking gate (SECURITY.md `threats_open`); `none` disables severity blocking. |
280
280
  | `workflow.post_planning_gaps` | boolean | `true` | `true`, `false` | Post-planning gap report (#2493). After plans are generated, scans REQUIREMENTS.md and CONTEXT.md `<decisions>` against all PLAN.md files and emits a unified `Source \| Item \| Status` table. Non-blocking. Set to `false` to skip Step 13e of plan-phase. _Alias:_ `post_planning_gaps` is the flat-key form used in `CONFIG_DEFAULTS`; `workflow.post_planning_gaps` is the canonical namespaced form. |
281
281
 
282
282
  ### Ship Fields
@@ -0,0 +1,27 @@
1
+ # Security ASVS Levels
2
+
3
+ GSD threat modeling maps OWASP ASVS levels to planner disposition rigor and auditor verification depth. Higher levels are supersets of lower — L3 includes all L2 and L1 requirements.
4
+
5
+ ## L1 — Opportunistic (default)
6
+
7
+ **Scope:** Cover threats on primary trust boundaries and high-impact components.
8
+
9
+ **Planner disposition:** `mitigate` critical/high-severity threats. `mitigate` medium-severity threats if they occur on a primary trust boundary; otherwise `accept` with documented rationale explaining the specific risk tolerance. `accept` low-risk threats with a rationale statement. `transfer` when threat is third-party responsibility.
10
+
11
+ **Auditor verification depth:** Verify each declared mitigation is PRESENT in the cited file (grep-level check — find the pattern, confirm the call exists).
12
+
13
+ ## L2 — Standard
14
+
15
+ **Scope:** Map ALL applicable STRIDE categories for every in-scope component.
16
+
17
+ **Planner disposition:** `mitigate` medium-severity-and-above threats. Every `accept` MUST have explicit documented rationale explaining why the risk is tolerable for this specific context.
18
+
19
+ **Auditor verification depth:** Verify the mitigation ACTUALLY ADDRESSES the threat vector (not just that some pattern is present) and is placed at the correct trust boundary. A login check in the wrong layer does not close the threat.
20
+
21
+ ## L3 — Comprehensive
22
+
23
+ **Scope:** Exhaustive STRIDE × all components; defense-in-depth for critical threats.
24
+
25
+ **Planner disposition:** `mitigate` all threats except those explicitly accepted with documented sign-off. Defense-in-depth layers required for critical threats (multiple independent controls).
26
+
27
+ **Auditor verification depth:** Deep verification — trace data flow end-to-end, check edge cases and ordering, confirm the mitigation cannot be bypassed via alternate code paths or parameter manipulation.
@@ -2,6 +2,7 @@
2
2
  phase: {N}
3
3
  slug: {phase-slug}
4
4
  status: draft
5
+ # threats_open = count of OPEN threats at or above workflow.security_block_on severity (the blocking gate)
5
6
  threats_open: 0
6
7
  asvs_level: 1
7
8
  created: {date}
@@ -23,11 +24,12 @@ created: {date}
23
24
 
24
25
  ## Threat Register
25
26
 
26
- | Threat ID | Category | Component | Disposition | Mitigation | Status |
27
- |-----------|----------|-----------|-------------|------------|--------|
28
- | T-{N}-01 | {STRIDE category} | {component} | {mitigate / accept / transfer} | {control or reference} | open |
27
+ | Threat ID | Category | Component | Severity | Disposition | Mitigation | Status |
28
+ |-----------|----------|-----------|----------|-------------|------------|--------|
29
+ | T-{N}-01 | {STRIDE category} | {component} | {critical / high / medium / low} | {mitigate / accept / transfer} | {control or reference} | open |
29
30
 
30
- *Status: open · closed*
31
+ *Status: open · closed · open — below {block_on} threshold (non-blocking)*
32
+ *Severity: critical > high > medium > low — only open threats at or above workflow.security_block_on count toward threats_open*
31
33
  *Disposition: mitigate (implementation required) · accept (documented risk) · transfer (third-party)*
32
34
 
33
35
  ---
@@ -19,6 +19,10 @@ key-decisions:
19
19
  - "Decision 1"
20
20
  patterns-established:
21
21
  - "Pattern 1: description"
22
+ # coverage: (#1602) optional per-deliverable UAT-routing block — see templates/summary.md <coverage_guidance>.
23
+ # Add live `coverage:` entries (id/description/verification[]/human_judgment[/rationale]) to enable
24
+ # deterministic UAT routing in verify-work; OMIT for legacy prose-only SUMMARYs. When coverage is
25
+ # uncertain, default human_judgment: true with a rationale — never auto-skip the human.
22
26
  duration: Xmin
23
27
  completed: YYYY-MM-DD
24
28
  status: complete
@@ -13,6 +13,9 @@ key-files:
13
13
  created: [important files created]
14
14
  modified: [important files modified]
15
15
  key-decisions: []
16
+ # coverage: (#1602) optional per-deliverable UAT-routing block — see templates/summary.md <coverage_guidance>.
17
+ # Add live `coverage:` entries to enable deterministic UAT routing in verify-work; OMIT for legacy
18
+ # prose-only SUMMARYs. When coverage is uncertain, default human_judgment: true — never auto-skip the human.
16
19
  duration: Xmin
17
20
  completed: YYYY-MM-DD
18
21
  status: complete
@@ -14,6 +14,10 @@ key-files:
14
14
  modified: [important files modified]
15
15
  key-decisions:
16
16
  - "Decision 1"
17
+ # coverage: (#1602) optional per-deliverable UAT-routing block — see templates/summary.md <coverage_guidance>.
18
+ # Add live `coverage:` entries (id/description/verification[]/human_judgment[/rationale]) to enable
19
+ # deterministic UAT routing in verify-work; OMIT for legacy prose-only SUMMARYs. When coverage is
20
+ # uncertain, default human_judgment: true with a rationale — never auto-skip the human.
17
21
  duration: Xmin
18
22
  completed: YYYY-MM-DD
19
23
  status: complete
@@ -40,6 +40,24 @@ patterns-established:
40
40
 
41
41
  requirements-completed: [] # REQUIRED — Copy ALL requirement IDs from this plan's `requirements` frontmatter field.
42
42
 
43
+ # Coverage metadata (#1602) — one entry per shipped deliverable. Drives DETERMINISTIC UAT routing in verify-work.
44
+ # OMIT this whole block for legacy/prose-only SUMMARYs — verify-work then falls back to the ## Accomplishments bullets
45
+ # (byte-identical behavior for un-migrated phases). See <coverage_guidance> below for the contract.
46
+ coverage:
47
+ - id: D1
48
+ description: "[deliverable in human-readable form — what would have been a prose ## Accomplishments bullet]"
49
+ requirement: "[REQ-ID from this plan's `requirements`, or omit if none]"
50
+ verification:
51
+ - kind: unit # unit | integration | e2e | automated_ui | manual_procedural | other
52
+ ref: "[tests/path.test.ts#test name | playwright:shot.png | command invocation]"
53
+ status: pass # pass | fail | unknown — from the latest run
54
+ human_judgment: false # REQUIRED boolean. false => may auto-pass IF every verification status is `pass`.
55
+ - id: D2
56
+ description: "[a deliverable that needs a human to sign off]"
57
+ verification: []
58
+ human_judgment: true
59
+ rationale: "[REQUIRED when human_judgment: true — why automation is insufficient]"
60
+
43
61
  # Metrics
44
62
  duration: Xmin
45
63
  completed: YYYY-MM-DD
@@ -148,6 +166,29 @@ None - no external service configuration required.
148
166
  **Population:** Frontmatter is populated during summary creation in execute-plan.md. See `<step name="create_summary">` for field-by-field guidance.
149
167
  </frontmatter_guidance>
150
168
 
169
+ <coverage_guidance>
170
+ **Purpose (#1602):** The `coverage:` block is a per-deliverable Requirements Traceability Matrix. It lets `verify-work`'s `extract_tests` step route deliverables DETERMINISTICALLY — auto-passing those proven by passing tests and reserving human UAT for genuine judgment — instead of re-deriving coverage from prose. Consumed via `gsd-tools uat classify-coverage --summary <SUMMARY>`.
171
+
172
+ **Field semantics:**
173
+
174
+ | Field | Purpose |
175
+ |---|---|
176
+ | `id` | Stable identifier (`D1`, `D2`…) for cross-referencing from UAT.md and audit reports. Must be unique within the SUMMARY. |
177
+ | `description` | The deliverable in human-readable form — what would have been a prose bullet. |
178
+ | `requirement` | Links back to a REQUIREMENTS.md REQ-ID (joins `requirements-completed`). Optional. |
179
+ | `verification[].kind` | Enum: `unit \| integration \| e2e \| automated_ui \| manual_procedural \| other`. |
180
+ | `verification[].ref` | Test path + descriptor (`file#test name`), Playwright screenshot ref, or command invocation. Required per entry. |
181
+ | `verification[].status` | `pass \| fail \| unknown` — populated from the latest test run. |
182
+ | `human_judgment` | Explicit boolean; REQUIRED. `true` always routes to a human. |
183
+ | `rationale` | REQUIRED when `human_judgment: true`. The audit trail for why automation is insufficient. |
184
+
185
+ **Deterministic contract (what the classifier does):**
186
+ - A deliverable auto-passes (no human prompt) **only** when `human_judgment: false` AND `verification` is non-empty AND every `verification[].status` is `pass`. This is the narrow, fully-proven case.
187
+ - **Everything else is presented to a human** — `human_judgment: true`, an empty `verification:`, any non-`pass`/`unknown` status, or any schema error. A false-negative is a redundant prompt (the status quo); a false-positive ships a bug UAT existed to catch.
188
+ - **Fail-safe default:** if you cannot determine coverage for a deliverable, you MUST set `human_judgment: true` with `rationale: "Coverage not determined at authoring time — verifier must classify"`. Never leave a deliverable's `human_judgment` empty, and never set it `false` just to skip the prompt — auto-pass additionally requires a passing `verification` entry, so the flag alone cannot skip the human.
189
+ - `coverage: []` means "no deliverables to classify" (the single-confirmation path). OMITTING the block entirely means "legacy" — `verify-work` falls back to prose `## Accomplishments` extraction unchanged.
190
+ </coverage_guidance>
191
+
151
192
  <one_liner_rules>
152
193
  The one-liner MUST be substantive:
153
194
 
@@ -377,6 +377,11 @@ Create `{phase}-{plan}-SUMMARY.md` at `.planning/phases/XX-name/`. Use `~/.claud
377
377
 
378
378
  **Frontmatter:** phase, plan, subsystem, tags | requires/provides/affects | tech-stack.added/patterns | key-files.created/modified | key-decisions | requirements-completed (**MUST** copy `requirements` array from PLAN.md frontmatter verbatim) | duration ($DURATION), completed ($PLAN_END_TIME date).
379
379
 
380
+ **Coverage block (#1602):** Populate the `coverage:` frontmatter block — one entry per shipped deliverable (the structured form of each `## Accomplishments` bullet). For each deliverable, aggregate the task-level `<verify>` results and tests:
381
+ - A task whose `<verify>` command passed or whose matching test passed → a `verification` entry with `kind` + `ref` (`tests/path#name`, Playwright screenshot ref, or command) + `status: pass`, and `human_judgment: false`.
382
+ - A judgment-dependent deliverable (UX adequacy, external/multi-session behavior, anything no test asserts) → `human_judgment: true` with a `rationale`.
383
+ - **Every deliverable MUST be classified.** If you cannot determine coverage, default to `human_judgment: true` with `rationale: "Coverage not determined at authoring time — verifier must classify"`. Never set `human_judgment: false` without a non-empty all-`pass` `verification` — `verify-work` auto-passes (skips the human) ONLY on that proof, so an unproven `false` still routes to the human but loses the audit trail. Omit the whole block only for a genuinely prose-only SUMMARY (verify-work then uses the legacy `## Accomplishments` path). The block is validated downstream by `gsd-tools uat classify-coverage`.
384
+
380
385
  Title: `# Phase [X] Plan [Y]: [Name] Summary`
381
386
 
382
387
  One-liner SUBSTANTIVE: "JWT auth with refresh rotation using jose library" not "Authentication implemented"
@@ -109,9 +109,9 @@ elif [ -n "$OPENCODE_CONFIG_DIR" ] || [ -n "$OPENCODE_CONFIG" ]; then RUNTIME="o
109
109
  else RUNTIME="claude"; fi
110
110
  ```
111
111
 
112
- Set the instruction file variable:
112
+ Set the instruction file variable via the shared runtime-name policy adapter (`gsd-tools query project-instruction-file`, backed by `getProjectInstructionFile` in `runtime-name-policy.cjs` — the single source of truth shared with `profile-output.cjs`):
113
113
  ```bash
114
- if [ "$RUNTIME" = "codex" ]; then INSTRUCTION_FILE="AGENTS.md"; else INSTRUCTION_FILE=".claude/CLAUDE.md"; fi
114
+ INSTRUCTION_FILE=$(gsd_run query project-instruction-file --runtime "$RUNTIME")
115
115
  ```
116
116
 
117
117
  All subsequent references to the project instruction file use `$INSTRUCTION_FILE`.
@@ -1533,7 +1533,7 @@ PHASE1_HAS_UI=$(echo "$PHASE1_SECTION" | grep -qi "UI hint.*yes" && echo "true"
1533
1533
  - `.planning/REQUIREMENTS.md`
1534
1534
  - `.planning/ROADMAP.md`
1535
1535
  - `.planning/STATE.md`
1536
- - `$INSTRUCTION_FILE` (`AGENTS.md` for Codex, `.claude/CLAUDE.md` for all other runtimes)
1536
+ - `$INSTRUCTION_FILE` (runtime-derived via the shared `getProjectInstructionFile` policy: `AGENTS.md` for codex/opencode/kilo/kimi, `.github/copilot-instructions.md` for copilot, `GEMINI.md` for gemini/antigravity, `.claude/CLAUDE.md` for claude)
1537
1537
 
1538
1538
  </output>
1539
1539
 
@@ -1555,7 +1555,7 @@ PHASE1_HAS_UI=$(echo "$PHASE1_SECTION" | grep -qi "UI hint.*yes" && echo "true"
1555
1555
  - [ ] ROADMAP.md created with phases, requirement mappings, success criteria
1556
1556
  - [ ] STATE.md initialized
1557
1557
  - [ ] REQUIREMENTS.md traceability updated
1558
- - [ ] `$INSTRUCTION_FILE` generated with GSD workflow guidance (AGENTS.md for Codex, `.claude/CLAUDE.md` otherwise; an existing hand-crafted file without GSD markers is left untouched unless `--force`)
1558
+ - [ ] `$INSTRUCTION_FILE` generated with GSD workflow guidance (runtime-derived via the shared `getProjectInstructionFile` policy — `AGENTS.md` for codex/opencode/kilo/kimi, `.github/copilot-instructions.md` for copilot, `GEMINI.md` for gemini/antigravity, `.claude/CLAUDE.md` for claude; an existing hand-crafted file without GSD markers is left untouched unless `--force`)
1559
1559
  - [ ] User knows next step is `/gsd:discuss-phase 1`
1560
1560
 
1561
1561
  **Atomic commits:** Each phase commits its artifacts immediately. If context is lost, artifacts persist.
@@ -693,6 +693,21 @@ Also available:
693
693
 
694
694
  **Exit the plan-phase workflow. Do not continue.**
695
695
 
696
+ ## 5.65. Codebase Map Freshness Pre-Check (drift plan:pre gate)
697
+
698
+ If `activeHooks` (from `PLAN_PRE_HOOKS_JSON`, §5.6) has a `kind == "gate"`, `capId == "drift"`,
699
+ `check.query == "verify.codebase-drift"` entry (`workflow.plan_drift_precheck` on), run the same check the
700
+ execute gate uses; otherwise skip to step 6:
701
+
702
+ ```bash
703
+ DRIFT=$(gsd_run verify codebase-drift 2>/dev/null || echo '{"skipped":true}')
704
+ ```
705
+
706
+ This gate is **non-blocking** and **never blocks, never spawns** the mapper at plan time. If `skipped` or
707
+ `action_required` is false, continue silently to step 6. If `action_required` is true, print `message`
708
+ verbatim (it ends with a `/gsd:map-codebase` pointer) and continue — planning proceeds whether or not the
709
+ map is refreshed first. (`drift_action: auto-remap` stays at `execute:wave:post`.)
710
+
696
711
  ## 6. Check Existing Plans
697
712
 
698
713
  ```bash
@@ -27,6 +27,8 @@ Parse: `phase_dir`, `phase_number`, `phase_name`, `phase_slug`, `padded_phase`.
27
27
  ```bash
28
28
  AUDITOR_MODEL=$(gsd_run query resolve-model gsd-security-auditor --raw)
29
29
  VERIFY_POST_HOOKS_JSON=$(gsd_run loop render-hooks verify:post --raw)
30
+ SECURITY_ASVS=$(gsd_run query config-get workflow.security_asvs_level --raw 2>/dev/null || echo "1")
31
+ SECURITY_BLOCK_ON=$(gsd_run query config-get workflow.security_block_on --raw 2>/dev/null || echo "high")
30
32
  ```
31
33
 
32
34
  Resolve active step hooks from `VERIFY_POST_HOOKS_JSON` where `kind == "step"` and `ref.skill == "secure-phase"`.
@@ -51,7 +53,7 @@ SUMMARY_FILES=$(ls "${PHASE_DIR}"/*-SUMMARY.md 2>/dev/null)
51
53
 
52
54
  ### 2a. Read Phase Artifacts
53
55
 
54
- Read PLAN.md — extract `<threat_model>` block: trust boundaries, STRIDE register (`threat_id`, `category`, `component`, `disposition`, `mitigation_plan`).
56
+ Read PLAN.md — extract `<threat_model>` block: trust boundaries, STRIDE register (`threat_id`, `category`, `component`, `severity`, `disposition`, `mitigation_plan`).
55
57
 
56
58
  ### 2b. Read Summary Threat Flags
57
59
 
@@ -59,7 +61,7 @@ Read SUMMARY.md — extract `## Threat Flags` entries.
59
61
 
60
62
  ### 2c. Build Threat Register
61
63
 
62
- Per threat: `{ threat_id, category, component, disposition, mitigation_pattern, files_to_check }`
64
+ Per threat: `{ threat_id, category, component, severity, disposition, mitigation_pattern, files_to_check }`
63
65
 
64
66
  Also set `register_authored_at_plan_time: true` if **at least one** PLAN file contained a parseable `<threat_model>` block; `false` if no PLAN files had any `<threat_model>` block (legacy phase authored before formal threat modelling was standard).
65
67
 
@@ -72,10 +74,11 @@ Classify each threat:
72
74
  | CLOSED | mitigation found OR accepted risk documented in SECURITY.md OR transfer documented |
73
75
  | OPEN | none of the above |
74
76
 
75
- Build: `{ threat_id, category, component, disposition, status, evidence }`
77
+ Build: `{ threat_id, category, component, severity, disposition, status, evidence }`
76
78
 
77
79
  **Short-circuit rule:**
78
- - If `threats_open: 0 AND register_authored_at_plan_time: true` → skip to Step 6 directly. All plan-time threats are verified CLOSED.
80
+ - If `threats_open: 0 AND register_authored_at_plan_time: true AND asvs_level == 1` → skip to Step 6 directly. No open threats at or above the block threshold remain (threats_open: 0); below-threshold open threats may remain and are non-blocking. L1 grep-depth is sufficient; no deeper verification required.
81
+ - If `threats_open: 0 AND register_authored_at_plan_time: true AND asvs_level >= 2` → **do NOT skip**. The preliminary threat classification is grep-level (L1 depth) and is insufficient for L2/L3. Proceed to Step 5 (spawn the auditor) so that L2 boundary-placement checks and L3 end-to-end trace checks are performed. Skipping the auditor here would defeat ASVS level scaling for "clean" phases.
79
82
  - If `threats_open: 0 AND register_authored_at_plan_time: false` → **do NOT skip**. Empty-by-no-planning must not rubber-stamp a clean SECURITY.md. Proceed to Step 5 in **retroactive-STRIDE mode** — the auditor builds a register from implementation files first, then verifies mitigations.
80
83
  - If `threats_open > 0` → proceed to Step 4 (present threat plan to user).
81
84
 
@@ -95,6 +98,8 @@ Call AskUserQuestion with threat table and options:
95
98
  - `register_authored_at_plan_time: true` — **Verify mitigations exist** — do not scan for new threats. The register is complete; verify each threat's mitigation is present in the implementation.
96
99
  - `register_authored_at_plan_time: false` (retroactive-STRIDE mode) — **Retroactive-STRIDE: build a STRIDE register from implementation files first, then verify mitigations.** The phase was authored before formal threat modelling; the auditor must construct the register from scratch before verifying.
97
100
 
101
+ Substitute `{SECURITY_ASVS}` with the value of `$SECURITY_ASVS` and `{SECURITY_BLOCK_ON}` with the value of `$SECURITY_BLOCK_ON` resolved in Step 0 via `config-get`.
102
+
98
103
  Print: `◆ Spawning security auditor... (runs in a subagent — no output until it returns, ~1–5 min; expected, not a freeze)`
99
104
 
100
105
  ```
@@ -141,7 +146,7 @@ Handle return:
141
146
 
142
147
  ```
143
148
  GSD > PHASE {N} SECURITY BLOCKED
144
- {K} threats open — phase advancement blocked until threats_open: 0
149
+ {K} blocking threats open — phase advancement blocked until threats_open: 0
145
150
  ▶ Fix mitigations then re-run: /gsd:secure-phase {N}
146
151
  ▶ Or document accepted risks in SECURITY.md and re-run.
147
152
  ```
@@ -159,7 +164,7 @@ gsd_run query commit "docs(phase-${PHASE}): add/update security threat verificat
159
164
  **Secured (threats_open: 0):**
160
165
  ```
161
166
  GSD > PHASE {N} THREAT-SECURE
162
- threats_open: 0 — all threats have dispositions.
167
+ threats_open: 0 — no blocking threats remain (threats_open: 0).
163
168
  ▶ /gsd:validate-phase {N} validate test coverage
164
169
  ▶ /gsd:verify-work {N} run UAT
165
170
  ```
@@ -173,7 +178,8 @@ Display `/clear` reminder.
173
178
  - [ ] Input state detected (A/B/C) — state C exits cleanly
174
179
  - [ ] PLAN.md threat model parsed, register built
175
180
  - [ ] SUMMARY.md threat flags incorporated
176
- - [ ] threats_open: 0 AND register_authored_at_plan_time: true → skip directly to Step 6
181
+ - [ ] threats_open: 0 AND register_authored_at_plan_time: true AND asvs_level == 1 → skip directly to Step 6 (L1 grep-depth sufficient)
182
+ - [ ] threats_open: 0 AND register_authored_at_plan_time: true AND asvs_level >= 2 → do NOT skip; auditor spawned for L2/L3 deep verification
177
183
  - [ ] threats_open: 0 AND register_authored_at_plan_time: false → retroactive-STRIDE mode (Step 5), not skipped
178
184
  - [ ] User gate with threat table presented
179
185
  - [ ] Auditor spawned with complete context
@@ -178,7 +178,24 @@ fi
178
178
 
179
179
  The verb owns the canonical regex `/^As a .+, I want to .+, so that .+\.$/` and returns slot extractions plus per-error guidance when invalid. Halt UAT generation on failure — never attempt to derive user-flow steps from a non-User-Story goal (low-quality UAT).
180
180
 
181
- **Extract testable deliverables from SUMMARY.md:**
181
+ **Coverage-aware deterministic classification (#1602).** Before deriving checkpoints from prose, classify each SUMMARY's structured `coverage:` block. For each `*-SUMMARY.md`:
182
+
183
+ ```bash
184
+ COVERAGE=$(gsd_run query uat.classify-coverage --summary "$SUMMARY_FILE")
185
+ ```
186
+
187
+ Read the JSON result (`mode`, `total`, `all_auto_covered`, `auto_passed[]`, `present[]`, `errors[]`):
188
+
189
+ - **`mode: legacy`** (no `coverage:` block, OR a malformed block that could not be parsed) → **fall through** to the prose-based extraction below. Behavior is byte-identical to pre-#1602 for un-migrated SUMMARYs; do NOT auto-pass anything. If `errors[]` is non-empty (a `malformed_block`), note the broken coverage block to the user before proceeding so the SUMMARY can be fixed.
190
+ - **`mode: coverage`** →
191
+ - Each `auto_passed[]` entry is recorded in UAT.md as `result: pass`, `source: automated` (see `create_uat_file`) — **do not present it as a checkpoint.** It is deterministically covered by the passing tests in its `verification` refs.
192
+ - Each `present[]` entry becomes a human UAT checkpoint: use its `description` as the test and carry its `rationale` into the checkpoint context. The `reason` (`human_judgment` / `no_verification` / `verification_not_passing` / `validation_failed`) explains why a human is needed.
193
+ - If `all_auto_covered` is `true` (every entry auto-passed, including the `coverage: []` case) → do NOT generate zero checkpoints; present a **single confirmation summary** listing the auto-covered deliverables with their covering tests and ask the user to confirm.
194
+ - Surface any `errors[]` to the user (malformed coverage block) but still treat their entries as human checkpoints — **never drop a deliverable** (fail-safe).
195
+
196
+ The cold-start smoke test injection below still applies in `coverage` mode.
197
+
198
+ **Extract testable deliverables from SUMMARY.md (legacy fallback — used when `mode: legacy`):**
182
199
 
183
200
  Parse for:
184
201
  1. **Accomplishments** - Features/functionality added
@@ -252,6 +269,18 @@ result: [pending]
252
269
 
253
270
  ...
254
271
 
272
+ **Coverage auto-passed entries (#1602):** for each `auto_passed[]` entry from `uat classify-coverage`, write a Tests entry pre-resolved as automated — these are NOT presented to the user:
273
+
274
+ ```
275
+ ### N. [coverage description]
276
+ expected: [coverage description]
277
+ result: pass
278
+ source: automated
279
+ coverage_id: [D-id]
280
+ ```
281
+
282
+ The `source: automated` marker is additive — existing consumers that read only `result:` are unaffected.
283
+
255
284
  ## Summary
256
285
 
257
286
  total: [N]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengsd/gsd-core",
3
- "version": "1.6.0-rc.2",
3
+ "version": "1.6.0-rc.3",
4
4
  "description": "GSD Core is a meta-prompting, context engineering, and spec-driven development system for AI coding agents.",
5
5
  "bin": {
6
6
  "gsd-core": "bin/install.js",
@@ -10,6 +10,7 @@
10
10
  "files": [
11
11
  "bin",
12
12
  "commands",
13
+ "skills",
13
14
  "gsd-core",
14
15
  "assets",
15
16
  "agents",
@@ -78,11 +79,12 @@
78
79
  "check:alias-drift": "node scripts/check-alias-drift.cjs",
79
80
  "check:identity-drift": "node scripts/lint-package-identity-drift.cjs",
80
81
  "check:integrity": "node scripts/check-npm-integrity.cjs",
81
- "build": "npm run generate:identity && npm run build:lib && npm run gen:loop-host-contract && npm run gen:capability-registry && npm run build:hooks",
82
+ "build": "npm run generate:identity && npm run build:lib && npm run gen:plugin-skills && npm run gen:loop-host-contract && npm run gen:capability-registry && npm run build:hooks",
82
83
  "build:hooks": "node scripts/build-hooks.js",
83
84
  "build:lib": "tsc -p tsconfig.build.json",
84
85
  "generate:identity": "node scripts/generate-package-identity.cjs",
85
86
  "gen:loop-host-contract": "node scripts/gen-loop-host-contract.cjs --write",
87
+ "gen:plugin-skills": "node scripts/gen-plugin-skills.cjs --write",
86
88
  "gen:capability-registry": "node scripts/gen-capability-registry.cjs --write",
87
89
  "prepack": "npm run build:lib",
88
90
  "prepare": "npm run build:lib",
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * gen-plugin-skills.cjs — generates skills/gsd-<stem>/SKILL.md from
6
+ * commands/gsd/*.md using convertClaudeCommandToClaudeSkill.
7
+ *
8
+ * Usage:
9
+ * node scripts/gen-plugin-skills.cjs # print summary to stdout
10
+ * node scripts/gen-plugin-skills.cjs --write # write skills/ dir
11
+ * node scripts/gen-plugin-skills.cjs --check # exit 1 if committed skills/ is stale
12
+ *
13
+ * #1596 Phase B-provide. The Claude Code plugin contract discovers skills from
14
+ * a skills/ directory (plugins-reference). GSD's source-of-truth commands live
15
+ * in commands/gsd/*.md (command frontmatter); this script converts each to
16
+ * skill format using the same convertClaudeCommandToClaudeSkill the file-copy
17
+ * installer uses, producing a build-generated skills/ dir that ships in the
18
+ * npm package and serves plugin-only installs.
19
+ *
20
+ * Depends on: gsd-core/bin/lib/runtime-artifact-conversion.cjs (compiled from
21
+ * src/runtime-artifact-conversion.cts by `npm run build:lib`). Must run AFTER
22
+ * build:lib in the build chain.
23
+ */
24
+
25
+ const fs = require('node:fs');
26
+ const path = require('node:path');
27
+ const { ExitError, runMain } = require('./lib/cli-exit.cjs');
28
+
29
+ const ROOT = path.resolve(__dirname, '..');
30
+ const COMMANDS_DIR = path.join(ROOT, 'commands', 'gsd');
31
+ const SKILLS_DIR = path.join(ROOT, 'skills');
32
+ const CONVERSION_MODULE = path.join(ROOT, 'gsd-core', 'bin', 'lib', 'runtime-artifact-conversion.cjs');
33
+ const PREFIX = 'gsd-';
34
+ const RUNTIME = 'claude';
35
+
36
+ function generateSkills(conversion) {
37
+ const cmdNames = conversion.readGsdCommandNames();
38
+ const files = fs.readdirSync(COMMANDS_DIR).filter(f => f.endsWith('.md'));
39
+ const results = [];
40
+ for (const file of files) {
41
+ const stem = file.slice(0, -3);
42
+ const skillName = PREFIX + stem;
43
+ const src = fs.readFileSync(path.join(COMMANDS_DIR, file), 'utf8');
44
+ const converted = conversion.convertClaudeCommandToClaudeSkill(src, skillName, RUNTIME, cmdNames, true);
45
+ results.push({ skillName, content: converted });
46
+ }
47
+ return results;
48
+ }
49
+
50
+ function main() {
51
+ const args = new Set(process.argv.slice(2));
52
+ const WRITE = args.has('--write');
53
+ const CHECK = args.has('--check');
54
+
55
+ if (!fs.existsSync(CONVERSION_MODULE)) {
56
+ throw new ExitError(
57
+ 1,
58
+ `gen-plugin-skills: ${path.relative(ROOT, CONVERSION_MODULE)} not found.\n` +
59
+ 'Run `npm run build:lib` first (this script depends on the compiled converter).'
60
+ );
61
+ }
62
+ const conversion = require(CONVERSION_MODULE);
63
+ const results = generateSkills(conversion);
64
+
65
+ if (WRITE) {
66
+ fs.rmSync(SKILLS_DIR, { recursive: true, force: true });
67
+ fs.mkdirSync(SKILLS_DIR, { recursive: true });
68
+ for (const { skillName, content } of results) {
69
+ const skillDir = path.join(SKILLS_DIR, skillName);
70
+ fs.mkdirSync(skillDir, { recursive: true });
71
+ fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content);
72
+ }
73
+ process.stdout.write(`gen-plugin-skills: wrote ${results.length} skills to ${path.relative(ROOT, SKILLS_DIR)}/\n`);
74
+ return 0;
75
+ }
76
+
77
+ if (CHECK) {
78
+ if (!fs.existsSync(SKILLS_DIR)) {
79
+ throw new ExitError(1, 'gen-plugin-skills: skills/ missing. Run: npm run gen:plugin-skills -- --write');
80
+ }
81
+ let stale = 0;
82
+ const expectedNames = new Set(results.map(r => r.skillName));
83
+ for (const { skillName, content } of results) {
84
+ const skillMd = path.join(SKILLS_DIR, skillName, 'SKILL.md');
85
+ if (!fs.existsSync(skillMd)) {
86
+ process.stderr.write(`gen-plugin-skills: missing ${path.relative(ROOT, skillMd)}\n`);
87
+ stale++;
88
+ continue;
89
+ }
90
+ if (fs.readFileSync(skillMd, 'utf8') !== content) {
91
+ process.stderr.write(`gen-plugin-skills: stale ${path.relative(ROOT, skillMd)}\n`);
92
+ stale++;
93
+ }
94
+ }
95
+ const existingDirs = fs.readdirSync(SKILLS_DIR, { withFileTypes: true })
96
+ .filter(e => e.isDirectory() && e.name.startsWith(PREFIX));
97
+ for (const dir of existingDirs) {
98
+ if (!expectedNames.has(dir.name)) {
99
+ process.stderr.write(`gen-plugin-skills: stale (no source) ${path.relative(ROOT, path.join(SKILLS_DIR, dir.name))}\n`);
100
+ stale++;
101
+ }
102
+ }
103
+ if (stale > 0) {
104
+ throw new ExitError(1, `gen-plugin-skills: ${stale} stale skill(s). Run: npm run gen:plugin-skills -- --write`);
105
+ }
106
+ process.stdout.write(`gen-plugin-skills: ${results.length} skills up to date\n`);
107
+ return 0;
108
+ }
109
+
110
+ process.stdout.write(
111
+ `gen-plugin-skills: would write ${results.length} skills to ${path.relative(ROOT, SKILLS_DIR)}/\n` +
112
+ ' (use --write to generate, --check to verify staleness)\n'
113
+ );
114
+ return 0;
115
+ }
116
+
117
+ runMain(main);