GSD 1.42.2 reported a successful Codex install but printed
`Skipped Codex skill-copy generation (Codex discovers official skills
directly)` and left users with no routable `$gsd-*` entrypoints in
Codex CLI 0.130.0+. Confirmed reproducible on Windows 11 and macOS by
three independent reporters.
Root cause: Codex CLI 0.130.0 does NOT auto-discover commands from
`~/.codex/get-shit-done/workflows/*.md` or `agents/*.md`. It only
registers slash commands derived from `~/.codex/skills/<name>/SKILL.md`.
The "Codex discovers official skills directly" assumption was wrong.
The fix already shipped in #3562 — `bin/install.js` now calls
`copyCommandsAsCodexSkills()` for the Codex install path, materializing
one `SKILL.md` per `commands/gsd/*.md` with Codex-flavored frontmatter
and the `<codex_skill_adapter>` body. Verified locally: a Codex global
install into a temp `CODEX_HOME` produces 67 `gsd-*/SKILL.md` files
including `gsd-map-codebase` (the literal command from the bug report).
This change adds the regression test the triage diagnose pass called
for. It pins five facets of the contract so the 1.42.2 failure mode
cannot silently come back:
1. `<CODEX_HOME>/skills/gsd-*/SKILL.md` exists for every shipped
command (count matches `commands/gsd/**/*.md` exactly).
2. Each generated SKILL.md has YAML frontmatter with `name:` matching
the directory and a non-empty `description:`.
3. Every SKILL.md body contains the `<codex_skill_adapter>` block —
without it Codex can't route `$gsd-<cmd>` invocations.
4. The five representative commands named in the report and triage
(`gsd-map-codebase`, `gsd-execute-phase`, `gsd-plan-phase`,
`gsd-new-project`, `gsd-health`) are all present.
5. Installer output never prints "Skipped Codex skill-copy generation"
or "Codex discovers official skills directly", and DOES print
"Installed N skills to skills/" — locking the success-while-skipping
anti-pattern out.
Validation:
- New test: 5/5 pass on Mac and Linux Docker.
- Full suite (`gsd-test-both` per CLAUDE.md): 10,682/10,682 on Mac,
10,682/10,682 on Linux Docker, zero cross-platform diff.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two follow-up adjustments after running the full suite:
1. Reverted the rewriteTomlKeyLines() change. The original
`match.keyRaw || key` fallback respects user ownership of pre-existing
legacy lines (#2760 defensive principle). My fix now applies the
canonical-vs-legacy split at the INSERTION points only: fresh installs
write `hooks = true`, but a pre-existing user-authored
`codex_hooks = true` is preserved verbatim. Codex's own runtime
legacy_key alias handles backward-compat at the Codex layer.
2. Updated 12 test cases in tests/codex-config.test.cjs that pinned the
old fresh-write key. These assertions now expect canonical `hooks` for
fresh-write scenarios; tests covering legacy-line preservation already
pass against the narrowed fix without further edits.
Also updated bug-3566 regression-test cases for the legacy-preservation
path — they now verify that user-owned `codex_hooks` survives an install.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes#3566
Codex itself marks codex_hooks as a legacy_key in
codex-rs/features/src/legacy.rs. The canonical current Codex feature flag
under [features] is hooks. The GSD installer was still writing codex_hooks
on every fresh install / reinstall, leaving deprecated config behind on
Codex CLI >= 0.130.0.
Introduces a canonical/legacy split in bin/install.js:
CODEX_HOOKS_FEATURE_KEY = 'hooks'
CODEX_HOOKS_FEATURE_LEGACY_KEYS = ['codex_hooks']
isCodexHooksFeatureKey(key) // recognizes canonical OR any legacy alias
Threaded through:
- ensureCodexHooksFeature(): emits canonical hooks; recognizes legacy
codex_hooks; migrates legacy -> canonical in section, root-dotted, and
block-fallback insertion paths.
- hasEnabledCodexHooksFeature(): accepts either canonical or legacy.
- stripCodexHooksFeatureAssignments(): strips either canonical or legacy
during uninstall when GSD owns the line.
- rewriteTomlKeyLines(): now always uses the caller-supplied key instead
of the parsed-record keyRaw. The old `match.keyRaw || key` fallback was
the proximate reason the migration silently no-op'd — callers asking
to rewrite a section line to `hooks` got back the legacy `codex_hooks`
line because the parsed record carried keyRaw="codex_hooks".
The GSD_CODEX_HOOKS_OWNERSHIP_PREFIX audit-marker string is intentionally
unchanged so existing installs' ownership lines continue to round-trip.
Tests:
- New tests/bug-3566-codex-hooks-feature-canonical-key.test.cjs (6 cases):
fresh install writes hooks; section-form legacy migrated; root-dotted
legacy migrated; user-owned hooks preserved; uninstall removes
GSD-owned canonical; uninstall preserves user-owned hooks.
- Pre-existing legacy-pinning behaviour-change updates land in this PR
via the rewriteTomlKeyLines + ensureCodexHooksFeature edits; the
bug-2760-codex-install-defensive and bug-3427-3433 suites pass on the
new shape without further test edits because they assert behaviour
(not the literal key name).
Docs:
- docs/ARCHITECTURE.md row for Codex notes [features].hooks (canonical,
legacy codex_hooks recognized and migrated forward).
- docs/installer-migrations.md row updated to reflect canonical key
and the new Codex 0.130.0 features.hooks compatibility sentinel.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes#3562
Codex CLI 0.130.0 only registers commands from skills/<name>/SKILL.md; it
does NOT auto-discover from get-shit-done/workflows/*.md or agents/*.md.
Prior installer logic (#3427/#3433) removed the gsd-* skill copies under the
assumption that Codex would discover the official skills directly. That
assumption does not hold — users ended up with workflows on disk and zero
$gsd-* entrypoints after restart.
Fix: re-wire copyCommandsAsCodexSkills() (line 5519, already present) into
the Codex install dispatch path (line 8090). Generates one
~/.codex/skills/gsd-<name>/SKILL.md per commands/gsd/*.md — same shape the
Copilot/Antigravity/Cursor/Windsurf/Augment/Trae installs use.
Behaviour change: the pre-existing test in bug-3427-3433-codex-install-shape
asserted "does not regenerate gsd-* skill copies". Updated it to assert
the new behaviour (regenerate gsd-* with refreshed body, preserve non-gsd
user skills).
Tests:
- New tests/bug-3562-codex-install-skill-surface.test.cjs (4 cases):
- skills/gsd-help/SKILL.md exists
- SKILL.md has YAML frontmatter with name: gsd-help
- >= 10 gsd-* skill directories produced (lower-bound, currently 67)
- Pre-existing custom-user-skill directory preserved
- tests/bug-3427-3433-codex-install-shape.test.cjs: updated to assert
regeneration + body refresh + unrelated-skill preservation.
Verified by re-running the issue's repro: `node bin/install.js --codex
--global --config-dir <tmp>` now produces 67 gsd-* skills and the target
~/.codex/skills/gsd-help/SKILL.md exists with valid frontmatter.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PR #3540 was originally labeled `no-changelog` on the premise that it
was a pure internal-module refactor. On second look, two surfaces are
actually user-facing and warrant a changelog entry:
1. New `gsd-tools migrate-config` CLI subcommand — explicit, opt-in
on-disk migration of legacy-key shapes. Idempotent.
2. `mergeDefaults` semantic change — recursive deep-merge instead of
spread-per-section. Preserves sibling keys under partial overlays.
Strict improvement, but a behavior change.
Adds `.changeset/patient-lemurs-sing.md` (`type: Changed`). The
`no-changelog` label should be removed in tandem with this commit.
* fix(3537): route every phase-number ROADMAP regex through phaseMarkdownRegexSource
v1.42.1 added the padding-tolerant `phaseMarkdownRegexSource()` helper but
wired it into only 1 of 8 call sites that build phase-number regexes against
ROADMAP/STATE prose. The other 7 used raw `escapeRegex(phaseNum)` or partial
`0*${escapeRegex(...)}` (tolerated extra padding, not missing), so when
skills passed the resolved padded form (`02.7`) against un-padded ROADMAP
prose (`### Phase 2.7:`, `- [ ] **Phase 2.7:**`), the verbs silently no-op'd
while reporting success.
This consolidates every phase-number ROADMAP/STATE regex through the
canonical helper:
- Promote `phaseMarkdownRegexSource` from `roadmap.cjs` to `core.cjs` so
`phase.cjs` and `core.cjs` itself can consume it (no circular dep —
both already import `core.cjs`).
- Wire the helper into the 7 remaining sites:
- `core.cjs:getRoadmapPhaseInternal` (replaces hand-rolled `isNumeric`
branch that only padded integers, not decimals).
- `roadmap.cjs:cmdRoadmapGetPhase` (searchPhaseInContent escapedPhase).
- `roadmap.cjs:cmdRoadmapAnalyze` checkbox lookup.
- `roadmap.cjs:cmdRoadmapAnnotateDependencies` phase header lookup.
- `phase.cjs:cmdPhaseNextDecimal` ROADMAP prose scan.
- `phase.cjs:cmdPhaseInsert` target anchor + decimal scan + header.
- `phase.cjs:cmdPhaseComplete` (3 regexes: checkbox, plan-count,
REQUIREMENTS extraction).
Adds `tests/bug-3537-padded-id-against-unpadded-roadmap.test.cjs` — a
parity-style regression matching CONTEXT.md DEFECT.GENERATIVE-FIX: for
each user-facing verb, asserts that the padded form (`02.7`) and the
un-padded form (`2.7`) produce identical ROADMAP.md against an identical
fixture. Includes one control case (`update-plan-progress`, already wired
in 1.42.1) to prove the parity assertion is non-vacuous.
Closes#3537
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(3537): add changeset fragment (pr: placeholder, amended post-create)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(3537): pin changeset pr: field to #3538
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes#3522. When --respect-staged is passed the git add loop is skipped
entirely so per-hunk staging from git add -p is preserved. Default behavior
(full re-stage) is unchanged. The #3061 pathspec invariant holds under both
modes. Nothing-staged within scope returns { committed: false, reason:
'nothing staged' } without error.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fixes two root causes behind bug #3517:
1. Idempotency: completed_phases was blindly incremented (parseInt + 1),
causing phase.complete N run twice to double-count (4 → 5 → 6).
Now derives from ROADMAP progress table Complete-row count, making
the operation idempotent.
2. Field coverage: eight STATE.md fields were left stale after phase
completion. Now updates in the same atomic lock section:
- frontmatter: stopped_at, last_updated, total_plans, completed_plans
- body: Current focus, Status line, By Phase table row
completed_plans = count of *-SUMMARY.md files across all phase dirs
total_plans = sum of M/N plan counts from ROADMAP progress table
percent = recomputed from fresh derived counts
Closes#3517
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extract composeStatusline() helper from duplicated inline template logic in
runStatusline() and renderStatusline(). Both call sites now route through the
helper, which accepts a position param ('end' | 'front', default 'end').
- 'end' (default) preserves byte-identical output to v1.38.x and earlier
- 'front' renders ctx immediately after model name, before the first │
- Invalid values silently coerce to 'end' at runtime (belt-and-suspenders;
config-set rejects invalid values upfront via enum validator)
Adds statusline.context_position to VALID_CONFIG_KEYS in both CJS and TS
schemas, enum validator in config.cjs, docs row in CONFIGURATION.md,
and a changeset. Closes#2937.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>