@opengsd/gsd-core 1.5.0-rc.4 → 1.5.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 (89) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/LICENSE +1 -1
  3. package/agents/gsd-executor.md +22 -0
  4. package/agents/gsd-phase-researcher.md +1 -1
  5. package/agents/gsd-planner.md +4 -0
  6. package/agents/gsd-project-researcher.md +1 -1
  7. package/agents/gsd-verifier.md +45 -13
  8. package/bin/install.js +56 -100
  9. package/commands/gsd/autonomous.md +1 -1
  10. package/commands/gsd/execute-phase.md +1 -1
  11. package/commands/gsd/plan-phase.md +1 -1
  12. package/gemini-extension.json +1 -1
  13. package/gsd-core/bin/gsd-tools.cjs +85 -53
  14. package/gsd-core/bin/lib/agent-command-router.cjs +2 -2
  15. package/gsd-core/bin/lib/agent-install-check.cjs +143 -0
  16. package/gsd-core/bin/lib/audit-command-router.cjs +4 -4
  17. package/gsd-core/bin/lib/capability-activation.cjs +37 -10
  18. package/gsd-core/bin/lib/capability-registry.cjs +2 -0
  19. package/gsd-core/bin/lib/capability-state.cjs +80 -30
  20. package/gsd-core/bin/lib/capability-writer.cjs +5 -4
  21. package/gsd-core/bin/lib/check-command-router.cjs +50 -51
  22. package/gsd-core/bin/lib/commands.cjs +20 -2
  23. package/gsd-core/bin/lib/config-loader.cjs +3 -4
  24. package/gsd-core/bin/lib/config-schema.cjs +1 -1
  25. package/gsd-core/bin/lib/config-types.cjs +2 -1
  26. package/gsd-core/bin/lib/config.cjs +5 -2
  27. package/gsd-core/bin/lib/decisions.cjs +19 -1
  28. package/gsd-core/bin/lib/docs.cjs +14 -2
  29. package/gsd-core/bin/lib/frontmatter.cjs +2 -2
  30. package/gsd-core/bin/lib/gap-checker.cjs +5 -2
  31. package/gsd-core/bin/lib/git-base-branch.cjs +27 -1
  32. package/gsd-core/bin/lib/graphify-command-router.cjs +6 -8
  33. package/gsd-core/bin/lib/graphify.cjs +7 -31
  34. package/gsd-core/bin/lib/gsd2-import.cjs +2 -2
  35. package/gsd-core/bin/lib/init.cjs +30 -4
  36. package/gsd-core/bin/lib/intel-command-router.cjs +6 -3
  37. package/gsd-core/bin/lib/intel.cjs +28 -34
  38. package/gsd-core/bin/lib/io.cjs +2 -4
  39. package/gsd-core/bin/lib/learnings.cjs +2 -2
  40. package/gsd-core/bin/lib/loop-resolver.cjs +45 -167
  41. package/gsd-core/bin/lib/milestone.cjs +13 -5
  42. package/gsd-core/bin/lib/model-resolver.cjs +3 -4
  43. package/gsd-core/bin/lib/phase-id.cjs +3 -5
  44. package/gsd-core/bin/lib/phase-locator.cjs +3 -6
  45. package/gsd-core/bin/lib/phase.cjs +59 -13
  46. package/gsd-core/bin/lib/probe-core.cjs +40 -11
  47. package/gsd-core/bin/lib/profile-output.cjs +5 -2
  48. package/gsd-core/bin/lib/prohibition-enforcement.cjs +660 -0
  49. package/gsd-core/bin/lib/roadmap-command-router.cjs +2 -2
  50. package/gsd-core/bin/lib/roadmap-parser.cjs +28 -25
  51. package/gsd-core/bin/lib/roadmap.cjs +9 -4
  52. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +4 -1
  53. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +24 -3
  54. package/gsd-core/bin/lib/state.cjs +75 -17
  55. package/gsd-core/bin/lib/task-command-router.cjs +2 -2
  56. package/gsd-core/bin/lib/teams-status.cjs +74 -0
  57. package/gsd-core/bin/lib/template.cjs +11 -2
  58. package/gsd-core/bin/lib/uat.cjs +64 -2
  59. package/gsd-core/bin/lib/verification.cjs +8 -5
  60. package/gsd-core/bin/lib/verify.cjs +311 -4
  61. package/gsd-core/bin/lib/workstream-inventory.cjs +2 -2
  62. package/gsd-core/bin/lib/workstream.cjs +8 -2
  63. package/gsd-core/bin/lib/worktree-safety.cjs +44 -3
  64. package/gsd-core/bin/shared/config-schema.manifest.json +2 -0
  65. package/gsd-core/references/planner-antipatterns.md +46 -0
  66. package/gsd-core/references/planning-config.md +5 -1
  67. package/gsd-core/references/prohibition-probe.md +80 -2
  68. package/gsd-core/references/worktree-branch-check.md +11 -5
  69. package/gsd-core/templates/verification-report.md +16 -3
  70. package/gsd-core/workflows/docs-update.md +23 -31
  71. package/gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md +9 -0
  72. package/gsd-core/workflows/execute-phase.md +7 -2
  73. package/gsd-core/workflows/map-codebase.md +8 -10
  74. package/gsd-core/workflows/plan-phase.md +6 -0
  75. package/gsd-core/workflows/quick.md +26 -2
  76. package/gsd-core/workflows/settings-advanced.md +5 -5
  77. package/gsd-core/workflows/settings-integrations.md +5 -5
  78. package/gsd-core/workflows/spec-phase.md +30 -2
  79. package/gsd-core/workflows/verify-phase.md +22 -7
  80. package/hooks/dist/gsd-worktree-path-guard.js +34 -18
  81. package/hooks/gsd-worktree-path-guard.js +34 -18
  82. package/package.json +3 -3
  83. package/scripts/ci-prepare-test-scope.cjs +56 -14
  84. package/scripts/diff-touches-shipped-paths.cjs +5 -11
  85. package/scripts/gen-capability-registry.cjs +27 -1
  86. package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -2
  87. package/scripts/research-profiles.cjs +2 -2
  88. package/scripts/run-tests.cjs +1 -0
  89. package/gsd-core/bin/lib/core.cjs +0 -345
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "gsd-core",
3
3
  "displayName": "GSD Core",
4
- "version": "1.5.0-rc.4",
4
+ "version": "1.5.0",
5
5
  "description": "GSD Core is a meta-prompting, context engineering, and spec-driven development system for AI coding agents.",
6
6
  "author": {
7
7
  "name": "open-gsd",
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 Lex Christopherson
3
+ Copyright (c) 2026 Open GSD
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -103,6 +103,24 @@ PLAN_START_EPOCH=$(date +%s)
103
103
  ```
104
104
  </step>
105
105
 
106
+ <worktree_metadata_capture>
107
+ If running inside a git worktree, capture authoritative worktree identity before
108
+ any task commit changes HEAD. The execute-phase orchestrator consumes this from
109
+ your final `<worktree_metadata>` return block to build the wave cleanup manifest
110
+ without relying on runtime harness metadata (#1297).
111
+
112
+ ```bash
113
+ GSD_WORKTREE_PATH=""
114
+ GSD_WORKTREE_BRANCH=""
115
+ GSD_WORKTREE_EXPECTED_BASE=""
116
+ if [ -f .git ]; then
117
+ GSD_WORKTREE_PATH=$(git rev-parse --show-toplevel)
118
+ GSD_WORKTREE_BRANCH=$(git rev-parse --abbrev-ref HEAD)
119
+ GSD_WORKTREE_EXPECTED_BASE=$(git rev-parse HEAD)
120
+ fi
121
+ ```
122
+ </worktree_metadata_capture>
123
+
106
124
  <step name="determine_execution_pattern">
107
125
  ```bash
108
126
  grep -n "type=\"checkpoint" [plan-path]
@@ -761,6 +779,10 @@ into the user's project history.
761
779
  **Tasks:** {completed}/{total}
762
780
  **SUMMARY:** {path to SUMMARY.md}
763
781
 
782
+ <worktree_metadata>
783
+ {"agent_id":"{phase}-{plan}","worktree_path":"${GSD_WORKTREE_PATH:-}","branch":"${GSD_WORKTREE_BRANCH:-}","expected_base":"${GSD_WORKTREE_EXPECTED_BASE:-}"}
784
+ </worktree_metadata>
785
+
764
786
  **Commits:**
765
787
  - {hash}: {message}
766
788
  - {hash}: {message}
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: gsd-phase-researcher
3
3
  description: Researches how to implement a phase before planning. Produces RESEARCH.md consumed by gsd-planner. Spawned by /gsd:plan-phase orchestrator.
4
- tools: Read, Write, Edit, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*
4
+ tools: Read, Write, Edit, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*, mcp__perplexity__*
5
5
  color: cyan
6
6
  # hooks:
7
7
  # PostToolUse:
@@ -198,6 +198,10 @@ Every task has four required fields:
198
198
  Full rules + worked examples: @gsd-core/references/planner-antipatterns.md ("Comment-Text Discipline").
199
199
  </comment_text_discipline>
200
200
 
201
+ <region_scoped_negative_gate>
202
+ **Region-scoped negative gates (WARN, #968):** Region-scope a file-wide negative grep when a sibling task needs that construct elsewhere in the same file; `validate_plan` WARNS. See: @gsd-core/references/planner-antipatterns.md ("Region-Scoped Negative Gates").
203
+ </region_scoped_negative_gate>
204
+
201
205
  **<done>:** Acceptance criteria - measurable state of completion.
202
206
  - Good: "Valid credentials return 200 + JWT cookie, invalid credentials return 401"
203
207
  - Bad: "Authentication is complete"
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: gsd-project-researcher
3
3
  description: Researches domain ecosystem before roadmap creation. Produces files in .planning/research/ consumed during roadmap creation. Spawned by /gsd:new-project or /gsd:new-milestone orchestrators.
4
- tools: Read, Write, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*
4
+ tools: Read, Write, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*, mcp__perplexity__*
5
5
  color: cyan
6
6
  # hooks:
7
7
  # PostToolUse:
@@ -180,21 +180,28 @@ For each truth, determine if codebase enables it.
180
180
 
181
181
  **Verification status:**
182
182
 
183
- - ✓ VERIFIED: All supporting artifacts pass all checks
183
+ - ✓ VERIFIED: All supporting artifacts pass all checks — and, for a behavior-dependent truth, a behavioral test exercises the asserted behavior (see below)
184
+ - ⚠️ PRESENT_BEHAVIOR_UNVERIFIED: Supporting artifacts are present and wired, but the truth asserts runtime behavior that no test exercises — present, not behaviorally proven. Routes to human verification (Step 8) and does NOT count toward the verified score (Step 9).
184
185
  - ✗ FAILED: One or more artifacts missing, stub, or unwired
185
186
  - ? UNCERTAIN: Can't verify programmatically (needs human)
186
187
 
188
+ **Behavior-dependent truths.** A truth is *behavior-dependent* when its correctness hinges on runtime behavior grep/presence checks cannot see — a **state transition** or a **cancellation / cleanup / ordering invariant** (e.g. "cancels the in-flight task and bumps the generation counter", "resets the busy flag on abort", "rolls back on failure"). For these, symbol presence + wiring is *necessary but not sufficient*: the code can be present and wired yet still leak state on the very path the invariant covers.
189
+
187
190
  For each truth:
188
191
 
189
192
  1. Identify supporting artifacts
190
193
  2. Check artifact status (Step 4)
191
194
  3. Check wiring status (Step 5)
192
- 4. **Before marking FAIL:** Check for override (Step 3b)
193
- 5. Determine truth status
195
+ 4. **Before marking FAIL or PRESENT_BEHAVIOR_UNVERIFIED:** Check for override (Step 3b)
196
+ 5. **Classify behavior-dependence.** If the truth asserts a state transition or a cancellation/cleanup/ordering invariant, its status cannot be VERIFIED on presence alone:
197
+ - A pre-existing test exercises the transition/invariant and passes (confirm via Step 7b's single-named-test path) → ✓ VERIFIED.
198
+ - No such test exists, or it can't run without a server/state mutation → ⚠️ PRESENT_BEHAVIOR_UNVERIFIED. Emit a human-verification item (Step 8) and do not count it toward the verified score (Step 9).
199
+ - An accepted override (Step 3b) carries the truth as PASSED (override), exactly as it does for a FAILED truth.
200
+ 6. Determine truth status
194
201
 
195
202
  ## Step 3b: Check Verification Overrides
196
203
 
197
- Before marking any must-have as FAILED, check the VERIFICATION.md frontmatter for an `overrides:` entry that matches this must-have.
204
+ Before marking any must-have as FAILED or ⚠️ PRESENT_BEHAVIOR_UNVERIFIED, check the VERIFICATION.md frontmatter for an `overrides:` entry that matches this must-have.
198
205
 
199
206
  **Override check procedure:**
200
207
 
@@ -204,12 +211,12 @@ Before marking any must-have as FAILED, check the VERIFICATION.md frontmatter fo
204
211
  4. Key technical terms (file paths, component names, API endpoints) have higher weight
205
212
 
206
213
  **If override found:**
207
- - Mark as `PASSED (override)` instead of FAIL
214
+ - Mark as `PASSED (override)` instead of FAIL/PRESENT_BEHAVIOR_UNVERIFIED
208
215
  - Evidence: `Override: {reason} — accepted by {accepted_by} on {accepted_at}`
209
- - Count toward passing score, not failing score
216
+ - Count toward passing score (`verified_truths`), not failing score
210
217
 
211
218
  **If no override found:**
212
- - Mark as FAILED as normal
219
+ - Mark as FAILED (or ⚠️ PRESENT_BEHAVIOR_UNVERIFIED, per Step 3 step 5) as normal
213
220
  - Consider suggesting an override if the failure looks intentional (alternative implementation exists)
214
221
 
215
222
  **Suggesting overrides:** When a must-have FAILs but evidence shows an alternative implementation that achieves the same intent, include an override suggestion in the report:
@@ -461,6 +468,8 @@ Anti-pattern scanning (Step 7) checks for code smells. Behavioral spot-checks go
461
468
 
462
469
  **When to run:** For phases that produce runnable code (APIs, CLI tools, build scripts, data pipelines). Skip for documentation-only or config-only phases.
463
470
 
471
+ **Behavioral evidence for behavior-dependent truths (Step 3).** When a truth asserts a state transition or a cancellation/cleanup/ordering invariant, the single named test below is what upgrades it from ⚠️ PRESENT_BEHAVIOR_UNVERIFIED to ✓ VERIFIED. Run only the one named test that exercises the transition/invariant — never the full suite (per #25/#753). If no such test exists, leave the truth ⚠️ PRESENT_BEHAVIOR_UNVERIFIED and route it to human verification (Step 8); do not mark it VERIFIED on presence.
472
+
464
473
  **How:**
465
474
 
466
475
  1. **Identify checkable behaviors** from must-haves truths. Select 2-4 that can be tested with a single command:
@@ -548,6 +557,8 @@ done
548
557
 
549
558
  **Needs human if uncertain:** Complex wiring grep can't trace, dynamic state behavior, edge cases.
550
559
 
560
+ **Behavior-unverified truths (Step 3):** Every truth left ⚠️ PRESENT_BEHAVIOR_UNVERIFIED is recorded in the `behavior_unverified_items` frontmatter list (emitted whenever the count > 0, regardless of overall status, so it survives a gaps_found phase) and surfaces for human verification; when the overall status is human_needed it also appears in the human_verification section. Phrase each item around the invariant: what to trigger, what state must hold afterward, and why presence checks can't see it.
561
+
551
562
  **Harvest deferred items from PLAN.md (#3309 / `workflow.human_verify_mode = end-of-phase`):** Scan every PLAN file in the phase for `<verify><human-check>` blocks on `auto` tasks. These are verification items the planner deliberately deferred from `checkpoint:human-verify` to end-of-phase to avoid the executor cold-start cost. Each block has the same shape used by the planner:
552
563
 
553
564
  ```xml
@@ -579,18 +590,30 @@ Classify status using this decision tree IN ORDER (most restrictive first):
579
590
  1. IF any truth FAILED, artifact MISSING/STUB, key link NOT_WIRED, or blocker anti-pattern found:
580
591
  → **status: gaps_found**
581
592
 
582
- 2. IF Step 8 produced ANY human verification items (section is non-empty):
593
+ 2. IF Step 8 produced ANY human verification items (section is non-empty) — this includes every ⚠️ PRESENT_BEHAVIOR_UNVERIFIED truth from Step 3:
583
594
  → **status: human_needed**
584
- (Even if all truths are VERIFIED and score is N/N — human items take priority)
595
+ (Even if all other truths are VERIFIED — human items take priority)
585
596
 
586
597
  3. IF all truths VERIFIED, all artifacts pass, all links WIRED, no blockers, AND no human verification items:
587
598
  → **status: passed**
588
599
 
589
- **passed is ONLY valid when the human verification section is empty.** If you identified items requiring human testing in Step 8, status MUST be human_needed.
600
+ **passed is ONLY valid when the human verification section is empty.** If Step 8 produced any items — including any truth left ⚠️ PRESENT_BEHAVIOR_UNVERIFIED — the status is not `passed`: it is `human_needed`, or `gaps_found` when rule 1 also fires (the ordered tree keeps gaps_found's precedence).
601
+
602
+ **A ⚠️ PRESENT_BEHAVIOR_UNVERIFIED truth is never FAILED and never VERIFIED.** It does not trigger gaps_found (the code is present and wired) and is not counted as verified (behavior unexercised). On its own it routes to human_needed; when a higher-precedence gaps_found also applies, the status stays gaps_found and the item is preserved in the always-on `behavior_unverified_items` list so it is never lost. Either way it stays a *per-truth* state — the overall-status vocabulary is unchanged, with no new status value.
590
603
 
591
604
  > **Shared status seam**: the status vocabulary (`passed`, `gaps_found`, `human_needed`) and the per-status routing (next action and next command for each value) are owned by `src/verification.cts` via `gsd_run query verification.status`. This agent is the single emitter of the frontmatter status field; consumers (ship.md, execute-phase.md) read routing from that query instead of re-deriving it.
592
605
 
593
- **Score:** `verified_truths / total_truths`
606
+ **Score (presence- vs behavior-verified split):**
607
+
608
+ - `verified_truths` counts ✓ VERIFIED truths plus PASSED (override) truths (Step 3b). For a behavior-dependent truth, VERIFIED means a behavioral test passed, not just that symbols are present.
609
+ - ⚠️ PRESENT_BEHAVIOR_UNVERIFIED truths are the *only* ones excluded from `verified_truths`; they are reported separately as `behavior_unverified`.
610
+
611
+ ```text
612
+ score: verified_truths / total_truths # e.g. 6/7
613
+ behavior_unverified: P # truths present + wired but behavior not exercised
614
+ ```
615
+
616
+ A headline N/N therefore certifies that every behavior-dependent truth had behavioral evidence — a clean score can no longer be reached on symbol presence alone.
594
617
 
595
618
  ## Step 9b: Filter Deferred Items
596
619
 
@@ -699,6 +722,7 @@ phase: XX-name
699
722
  verified: YYYY-MM-DDTHH:MM:SSZ
700
723
  status: passed | gaps_found | human_needed
701
724
  score: N/M must-haves verified
725
+ behavior_unverified: 0 # Count of ⚠️ PRESENT_BEHAVIOR_UNVERIFIED truths (present + wired, behavior not exercised); each is detailed in behavior_unverified_items below (and in human_verification when status is human_needed)
702
726
  overrides_applied: 0 # Count of PASSED (override) items included in score
703
727
  overrides: # Only if overrides exist — carried forward or newly added
704
728
  - must_have: "Must-have text that was overridden"
@@ -725,6 +749,11 @@ deferred: # Only if deferred items exist (Step 9b)
725
749
  - truth: "Observable truth addressed in a later phase"
726
750
  addressed_in: "Phase N"
727
751
  evidence: "Matching goal or success criteria text"
752
+ behavior_unverified_items: # Only if behavior_unverified > 0 — emitted regardless of overall status, so these survive a gaps_found phase
753
+ - truth: "Observable truth whose state transition or cancellation/cleanup/ordering invariant no test exercises"
754
+ test: "What to trigger"
755
+ expected: "What state must hold afterward"
756
+ why_human: "Why presence checks can't see it"
728
757
  human_verification: # Only if status: human_needed
729
758
  - test: "What to do"
730
759
  expected: "What should happen"
@@ -746,8 +775,9 @@ human_verification: # Only if status: human_needed
746
775
  | --- | ------- | ---------- | -------------- |
747
776
  | 1 | {truth} | ✓ VERIFIED | {evidence} |
748
777
  | 2 | {truth} | ✗ FAILED | {what's wrong} |
778
+ | 3 | {truth} | ⚠️ PRESENT_BEHAVIOR_UNVERIFIED | {present + wired; no test exercises the transition/invariant — see Human Verification} |
749
779
 
750
- **Score:** {N}/{M} truths verified
780
+ **Score:** {N}/{M} truths verified ({P} present, behavior-unverified)
751
781
 
752
782
  ### Deferred Items
753
783
 
@@ -834,7 +864,7 @@ Structured gaps in VERIFICATION.md frontmatter for `/gsd:plan-phase --gaps`.
834
864
 
835
865
  {If human_needed:}
836
866
  ### Human Verification Required
837
- {N} items need human testing:
867
+ {N} items need human testing (including {P} present-but-behavior-unverified truths — code wired, transition/invariant not exercised by a test):
838
868
  1. **{Test name}** — {what to do}
839
869
  - Expected: {what should happen}
840
870
 
@@ -857,6 +887,8 @@ Automated checks passed. Awaiting human verification.
857
887
 
858
888
  **Keep verification fast.** Use grep/file checks, not running the app.
859
889
 
890
+ **Presence is not behavior.** Grep/file checks prove a symbol is present and wired — they do not prove a state transition or a cancellation/cleanup/ordering invariant holds at runtime. For a behavior-dependent truth, require a passing behavioral test (Step 7b's single named test) or mark it ⚠️ PRESENT_BEHAVIOR_UNVERIFIED and route to human verification. Never let symbol presence alone produce a VERIFIED on a behavior-dependent truth.
891
+
860
892
  **DO NOT commit.** Leave committing to the orchestrator.
861
893
 
862
894
  </critical_rules>
package/bin/install.js CHANGED
@@ -265,9 +265,11 @@ const _gsdLibDir = path.join(__dirname, '..', 'gsd-core', 'bin', 'lib');
265
265
  const { MODEL_PROFILES: GSD_MODEL_PROFILES } = require(path.join(_gsdLibDir, 'model-profiles.cjs'));
266
266
  const {
267
267
  RUNTIME_PROFILE_MAP: GSD_RUNTIME_PROFILE_MAP,
268
+ } = require(path.join(_gsdLibDir, 'model-catalog.cjs'));
269
+ const {
268
270
  resolveTierEntry: gsdResolveTierEntry,
269
271
  EFFORT_SET: GSD_EFFORT_SET,
270
- } = require(path.join(_gsdLibDir, 'core.cjs'));
272
+ } = require(path.join(_gsdLibDir, 'model-resolver.cjs'));
271
273
 
272
274
  // #443 — model-catalog and config-defaults.manifest.json exports needed only
273
275
  // by effort-resolution code paths (resolveInstallTimeEffort /
@@ -1794,6 +1796,10 @@ function skillFrontmatterName(skillDirName) {
1794
1796
  return skillDirName;
1795
1797
  }
1796
1798
 
1799
+ function normalizeClaudeSkillEffort(effort) {
1800
+ return effort === 'xhigh' ? 'max' : effort;
1801
+ }
1802
+
1797
1803
  /**
1798
1804
  * Qwen Code skills accept an optional numeric `priority` frontmatter field.
1799
1805
  * Per the Qwen skills spec (qwen-code/docs/users/features/skills.md, verified
@@ -1890,7 +1896,7 @@ function convertClaudeCommandToClaudeSkill(content, skillName, runtime = null, c
1890
1896
  // token-budget tier). Fields are Claude-specific; unknown frontmatter
1891
1897
  // fields are silently ignored by other runtimes (backward-compatible).
1892
1898
  if (context) fm += `context: ${context}\n`;
1893
- if (effort) fm += `effort: ${effort}\n`;
1899
+ if (effort) fm += `effort: ${normalizeClaudeSkillEffort(effort)}\n`;
1894
1900
  if (toolsBlock) fm += toolsBlock;
1895
1901
  fm += '---';
1896
1902
 
@@ -3192,110 +3198,55 @@ function generateCodexAgentToml(agentName, agentContent, modelOverrides = null,
3192
3198
  }
3193
3199
 
3194
3200
  /**
3195
- * Generate the agents/openai.yaml TUI chip metadata content for a Codex skill.
3196
- *
3197
- * This file is written alongside SKILL.md as <skill-dir>/agents/openai.yaml.
3198
- * Codex loads it as a SkillMetadataFile (codex-rs/core-skills/src/loader.rs),
3199
- * making the skill discoverable in the /skills TUI popup with a display name
3200
- * and short description. If the file is absent, Codex silently skips it (fails open).
3201
- *
3202
- * Schema (interface section):
3203
- * display_name: short human-readable skill name (strip gsd- prefix)
3204
- * short_description: 1-2 sentence description for TUI chip, ≤180 chars
3205
- *
3206
- * @param {string} skillName - Full skill name e.g. "gsd-plan-phase"
3207
- * @param {string} shortDescription - Description text (already truncated by caller)
3208
- * @returns {string} YAML content for agents/openai.yaml
3209
- */
3210
- function generateCodexSkillMetadataYaml(skillName, shortDescription) {
3211
- // Display name: strip "gsd-" prefix and convert hyphens to spaces for readability.
3212
- const displayName = skillName.replace(/^gsd-/, '').replace(/-/g, ' ');
3213
- // yamlQuote (= JSON.stringify) handles all YAML-unsafe chars: backslashes,
3214
- // quotes, newlines, control characters, and Unicode escapes.
3215
- return [
3216
- 'interface:',
3217
- ` display_name: ${yamlQuote(displayName)}`,
3218
- ` short_description: ${yamlQuote(shortDescription)}`,
3219
- '',
3220
- ].join('\n');
3221
- }
3222
-
3223
- /**
3224
- * Write agents/openai.yaml TUI chip metadata for each gsd-* skill directory.
3225
- *
3226
- * Called after layout-driven skill install for Codex. Iterates every gsd-*
3227
- * skill directory in skillsDir, reads the SKILL.md frontmatter to extract the
3228
- * short-description already emitted by convertClaudeCommandToCodexSkill, then
3229
- * writes <skill-dir>/agents/openai.yaml using generateCodexSkillMetadataYaml.
3201
+ * Remove stale agents/openai.yaml sidecar files from GSD-managed Codex skill dirs.
3230
3202
  *
3231
- * Fails open: individual skill directories that cannot be processed are silently
3232
- * skipped so a single malformed SKILL.md cannot block the whole install.
3203
+ * Prior to #1326, GSD's Codex install path wrote an agents/openai.yaml file
3204
+ * alongside each gsd-* SKILL.md. Recent Codex builds index BOTH SKILL.md and
3205
+ * the sidecar, causing each GSD skill to appear twice in autocomplete. This
3206
+ * function removes those stale sidecars and — if the agents/ subdirectory is
3207
+ * now empty — prunes it too.
3233
3208
  *
3234
- * User-owned skill directories (e.g. gsd-dev-preferences) are explicitly
3235
- * skipped so existing user-authored agents/openai.yaml files are never
3236
- * overwritten. These dirs are listed in the same USER_OWNED_SKILL_DIRS
3237
- * constant used by installOpencodeFamilySkills.
3238
- *
3239
- * The YAML-quoted description value is unescaped before embedding so that
3240
- * YAML escape sequences (e.g. \" in a double-quoted scalar) become the
3241
- * literal characters they represent rather than being double-escaped in the
3242
- * output.
3209
+ * Behaviour:
3210
+ * - Returns immediately if skillsDir does not exist (fails open).
3211
+ * - Only touches directories whose names start with "gsd-".
3212
+ * - Skips user-owned dirs (gsd-dev-preferences) — their agents/ content is
3213
+ * never modified, mirroring the same USER_OWNED_SKILL_DIRS guard used by
3214
+ * installOpencodeFamilySkills.
3215
+ * - For each managed gsd-* dir, if agents/openai.yaml exists, deletes it.
3216
+ * - If agents/ is now empty, removes the directory; if it still contains
3217
+ * other files (e.g. user-added content), leaves it in place.
3218
+ * - Non-gsd-* dirs and their agents/ content are never touched.
3219
+ * - Individual failures are caught and swallowed so a single bad dir cannot
3220
+ * block the install (fail-open, matching the original design).
3243
3221
  *
3244
3222
  * @param {string} skillsDir - Path to the skills/ directory (e.g. ~/.codex/skills)
3245
3223
  */
3246
- function writeCodexSkillMetadataFiles(skillsDir) {
3224
+ function cleanupCodexSkillMetadataSidecars(skillsDir) {
3247
3225
  if (!fs.existsSync(skillsDir)) return;
3248
3226
  // Mirror the user-owned list from installOpencodeFamilySkills (#2973).
3249
3227
  // We MUST skip these dirs — their contents are user-generated and must
3250
- // never be overwritten by GSD's install path.
3228
+ // never be modified by GSD's install path.
3251
3229
  const _userOwnedSkillDirs = new Set(['gsd-dev-preferences']);
3252
3230
  for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
3253
3231
  if (!entry.isDirectory() || !entry.name.startsWith('gsd-')) continue;
3254
3232
  if (_userOwnedSkillDirs.has(entry.name)) continue; // preserve user content
3255
- const skillDir = path.join(skillsDir, entry.name);
3256
- const skillMdPath = path.join(skillDir, 'SKILL.md');
3233
+ const agentsSubdir = path.join(skillsDir, entry.name, 'agents');
3234
+ const sidecarPath = path.join(agentsSubdir, 'openai.yaml');
3257
3235
  try {
3258
- const content = fs.readFileSync(skillMdPath, 'utf8');
3259
- const { frontmatter } = extractFrontmatterAndBody(content);
3260
- // Prefer the short-description field emitted by convertClaudeCommandToCodexSkill;
3261
- // fall back to description, then a synthetic label from the skill name.
3262
- let shortDesc = '';
3263
- if (frontmatter) {
3264
- // SKILL.md uses YAML frontmatter with a nested metadata.short-description key.
3265
- // extractFrontmatterField handles only top-level keys; parse the metadata block
3266
- // by looking for " short-description:" directly.
3267
- const metaMatch = frontmatter.match(/^[ \t]*metadata\s*:\s*\n((?:[ \t]+.*\n?)*)/m);
3268
- if (metaMatch) {
3269
- const metaBlock = metaMatch[1];
3270
- const sdMatch = metaBlock.match(/^[ \t]+short-description\s*:\s*(.+)$/m);
3271
- if (sdMatch) {
3272
- // Unescape YAML double-quoted scalar escapes before embedding.
3273
- // convertClaudeCommandToCodexSkill always emits a double-quoted
3274
- // value (via yamlQuote) so only double-quote unescaping is needed.
3275
- let raw = sdMatch[1].trim();
3276
- if (raw.startsWith('"') && raw.endsWith('"')) {
3277
- // Strip outer double-quotes and decode \" → " and \\ → \
3278
- raw = raw.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\');
3279
- } else {
3280
- // Single-quoted or unquoted: strip surrounding quotes/whitespace
3281
- raw = raw.replace(/^["']|["']$/g, '');
3282
- }
3283
- shortDesc = raw;
3284
- }
3285
- }
3286
- if (!shortDesc) {
3287
- shortDesc = extractFrontmatterField(frontmatter, 'description') || '';
3288
- }
3236
+ // Symlink guard: if agents/ is a symlink pointing outside the skills tree,
3237
+ // deleting through it could escape the tree. Skip this dir entirely.
3238
+ let agentsStat;
3239
+ try { agentsStat = fs.lstatSync(agentsSubdir); } catch (_e) { continue; }
3240
+ if (agentsStat.isSymbolicLink()) continue;
3241
+ if (fs.existsSync(sidecarPath)) {
3242
+ fs.rmSync(sidecarPath);
3243
+ }
3244
+ // Prune the agents/ dir only if it is now empty (leave it if other files remain).
3245
+ if (fs.existsSync(agentsSubdir) && fs.readdirSync(agentsSubdir).length === 0) {
3246
+ fs.rmdirSync(agentsSubdir);
3289
3247
  }
3290
- if (!shortDesc) {
3291
- shortDesc = `Run GSD workflow ${entry.name}.`;
3292
- }
3293
- const yamlContent = generateCodexSkillMetadataYaml(entry.name, shortDesc);
3294
- const agentsSubdir = path.join(skillDir, 'agents');
3295
- fs.mkdirSync(agentsSubdir, { recursive: true });
3296
- fs.writeFileSync(path.join(agentsSubdir, 'openai.yaml'), yamlContent);
3297
3248
  } catch (_err) {
3298
- // Fail open — missing or unreadable SKILL.md must not block the install.
3249
+ // Fail open — a single bad dir must not block the install.
3299
3250
  }
3300
3251
  }
3301
3252
  }
@@ -6841,6 +6792,12 @@ function _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal = false) {
6841
6792
  content = content.replace(/~\/\.claude\//g, pathPrefix);
6842
6793
  content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
6843
6794
  content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
6795
+ // Bare forms (no trailing slash) — use (?![\w-]) instead of \b so that
6796
+ // .claude-plugin / .claudeignore are NOT corrupted (the \b word-boundary
6797
+ // fires between 'e' and '-', which rewrites .claude-plugin → .cursor-plugin).
6798
+ content = content.replace(/~\/\.claude(?![\w-])/g, normalizedPathPrefix);
6799
+ content = content.replace(/\$HOME\/\.claude(?![\w-])/g, normalizedPathPrefix);
6800
+ content = content.replace(/\.\/\.claude(?![\w-])/g, `./${dirName}`);
6844
6801
  content = content.replace(/~\/\.cursor\//g, pathPrefix);
6845
6802
  content = processAttribution(content, getCommitAttribution(runtime));
6846
6803
  break;
@@ -9835,14 +9792,14 @@ function install(isGlobal, runtime = 'claude', options = {}) {
9835
9792
  const scope = isGlobal ? 'global' : 'local';
9836
9793
  installRuntimeArtifacts(runtime, targetDir, scope, _resolvedProfile);
9837
9794
 
9838
- // #774 — Codex only: write agents/openai.yaml TUI chip metadata alongside each
9839
- // installed skill so the /skills popup shows name + description for each gsd-* skill.
9840
- // The SkillMetadataFile is loaded by codex-rs/core-skills/src/loader.rs from
9841
- // <skill-dir>/agents/openai.yaml; absence is silently tolerated (fails open).
9842
- // We parse the SKILL.md frontmatter to extract short-description already emitted
9843
- // by convertClaudeCommandToCodexSkill and use it as the TUI chip description.
9795
+ // #1326 — Codex only: remove stale agents/openai.yaml sidecars from managed
9796
+ // gsd-* skill dirs. Prior installs wrote these files so Codex would show a
9797
+ // display name and description in the /skills TUI popup. Recent Codex builds
9798
+ // index BOTH SKILL.md and the sidecar, causing each GSD skill to appear twice
9799
+ // in autocomplete. Cleaning them up fixes the duplication; SKILL.md alone is
9800
+ // sufficient for Codex discovery. User-owned dirs are never touched.
9844
9801
  if (isCodex) {
9845
- writeCodexSkillMetadataFiles(path.join(targetDir, 'skills'));
9802
+ cleanupCodexSkillMetadataSidecars(path.join(targetDir, 'skills'));
9846
9803
  }
9847
9804
 
9848
9805
  // Hermes only: write DESCRIPTION.md for the gsd/ category after layout install
@@ -12124,8 +12081,7 @@ module.exports = {
12124
12081
  convertClaudeToGeminiAgent,
12125
12082
  convertClaudeAgentToCodexAgent,
12126
12083
  generateCodexAgentToml,
12127
- generateCodexSkillMetadataYaml,
12128
- writeCodexSkillMetadataFiles,
12084
+ cleanupCodexSkillMetadataSidecars,
12129
12085
  generateCodexConfigBlock,
12130
12086
  stripGsdFromCodexConfig,
12131
12087
  migrateCodexHooksMapFormat,
@@ -2,7 +2,7 @@
2
2
  name: gsd:autonomous
3
3
  description: Run all remaining phases autonomously — discuss→plan→execute per phase
4
4
  argument-hint: "[--from N] [--to N] [--only N] [--interactive] [--converge]"
5
- effort: xhigh
5
+ effort: max
6
6
  allowed-tools:
7
7
  - Read
8
8
  - Write
@@ -2,7 +2,7 @@
2
2
  name: gsd:execute-phase
3
3
  description: Execute all plans in a phase with wave-based parallelization
4
4
  argument-hint: "<phase-number> [--wave N] [--gaps-only] [--interactive] [--tdd]"
5
- effort: xhigh
5
+ effort: max
6
6
  allowed-tools:
7
7
  - Read
8
8
  - Write
@@ -2,7 +2,7 @@
2
2
  name: gsd:plan-phase
3
3
  description: Create detailed phase plan (PLAN.md) with verification loop
4
4
  argument-hint: "[phase] [--auto] [--research] [--skip-research] [--research-phase <N>] [--view] [--gaps] [--skip-verify] [--prd <file>] [--ingest <path-or-glob>] [--ingest-format <auto|nygard|madr|narrative>] [--reviews] [--text] [--tdd] [--mvp]"
5
- effort: xhigh
5
+ effort: max
6
6
  allowed-tools:
7
7
  - Read
8
8
  - Write
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gsd-core",
3
- "version": "1.5.0-rc.4",
3
+ "version": "1.5.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
  }