Introduce `runtime-slash.cjs` as the single source of truth for emitting
GSD slash-command references in user-facing runtime output and persisted
artifacts. `formatGsdSlash(commandName, runtime)` produces `/gsd-<cmd>`
for skills-based runtimes (Claude/Cursor/OpenCode/Kilo/etc.) and
`$gsd-<cmd>` for Codex. The deprecated `/gsd:<cmd>` colon form is never
emitted — pasting a recommended-action command into Claude Code now
routes correctly instead of failing with `Unknown command`.
Wired into the high-impact emitters identified in #3584:
- `init.cjs` `cmdInitManager` recommended_actions[].command (the
original failure path in the bug report) plus the no-ROADMAP /
no-STATE error hints.
- `phase.cjs` `cmdPhaseAdd`, `cmdPhaseAddBatch`, `cmdPhaseInsert` —
ROADMAP.md `Plans:` references now persist the routable form
instead of the legacy colon form.
- `verify.cjs` `cmdValidateHealth` — every fix-hint addIssue() call
(E001/E002/E003/E004/E005, W002/W003/W008/W009/W011/W016/W018) and
the persisted STATE.md regenerate / MILESTONES.md backfill notes.
- `milestone.cjs` `cmdMilestoneComplete` — Operator Next Steps tail
rewrite.
- `validate-command-router.cjs` — `validate context` recommendation
strings for WARNING/CRITICAL utilization bands.
- `workstream.cjs` — missing .planning hint.
- `profile-output.cjs` — `generate-claude-md` workflow enforcement
block, project/skills fallbacks, profile placeholder, and the
dev-preferences refresh hints.
- `drift.cjs`, `gsd2-import.cjs`, `commands.cjs scaffold context` —
remaining one-off persisted references.
Runtime detection: `resolveRuntime(projectDir)` reads
`process.env.GSD_RUNTIME` first, then a side-effect-free direct read of
`.planning/config.json` (NOT `loadConfig`, which would normalize legacy
keys and re-write the file just to read the runtime name).
Tests:
- `tests/bug-3584-runtime-slash-formatter.test.cjs` — 22 unit tests
covering the pure formatter and resolver (hyphen vs codex, prefix
normalization, defensive returns, env/config/default chain).
- `tests/bug-3584-runtime-slash-emitters.test.cjs` — 6 integration
tests exercising `init manager`, `phase add` (via the structured
`roadmap get-phase` payload to avoid raw-text matching on the
on-disk artifact), `validate health`, `validate context`, and the
codex variant.
- Existing tests updated to assert the new contract: validate-context
recommendations, claude-md workflow block, milestone complete
Operator Next Steps. Copilot-install engine-conversion test now
asserts against a synthetic input since bin/lib/*.cjs no longer
contains literal `/gsd:` references for the install-time converter
to rewrite.
INVENTORY.md and INVENTORY-MANIFEST.json updated for the new module
(64 CLI modules shipped, +1).
Fixes#3584
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(#2982): extend no-source-grep lint to catch var-binding readFileSync.includes()
The base lint (scripts/lint-no-source-grep.cjs) only catches
readFileSync(...).<text-method>() chained directly. The much more
common var-binding form escapes it:
const src = fs.readFileSync(p, 'utf8');
// 50 lines later
if (src.includes('foo')) {} // ← still grep, lint missed it
Scan of the test suite found ~141 files using this pattern.
Implementation built TDD per #2982 with structured-IR assertions:
scripts/lint-no-source-grep-extras.cjs
- detectVarBindingViolations(src) — pure detector, two passes:
pass 1 collects vars bound from readFileSync, pass 2 finds any
<var>.<includes|startsWith|endsWith|match|search>( on those vars.
- detectWrappedAssertOkMatch(src) — flags
assert.ok(<expr>.match(...)) which escapes the assert.match rule.
- VIOLATION enum exposes stable codes for tests to assert on.
scripts/lint-no-source-grep.cjs
- Wires the new detectors into the existing per-file check; one
additional violation row per file with the first 3 sample tokens.
tests/bug-2982-lint-var-binding.test.cjs
- 13 tests, all assertions on typed VIOLATION enum / structured
records. Covers all 5 text-match methods, multi-var, no-bind,
string literal (must NOT trigger), wrapped assert.ok(.match),
and assert.match (must NOT double-flag).
Migration backlog (#2974 expanded scope):
- 42 files annotated `// allow-test-rule: source-text-is-the-product`
(legitimate — they read .md/.json/.yml files whose deployed text
IS the product)
- 3 files annotated `// allow-test-rule: pending-migration-to-typed-ir [#2974]`
(read .cjs/.js source — clear migration debt)
- 95 files annotated `pending-migration-to-typed-ir [#2974]` with
`Per-file review may reclassify as source-text-is-the-product
during migration` (mixed — manual review under #2974)
After this lands the lint reports 0 violations on main; new
violations in PRs surface immediately.
Closes#2982
Refs #2974
* test(#2982): fix truncated test name per CR
The label ended with a bare '(' from a copy-paste mishap. Now reads
'does NOT flag .matchAll(...) — matchAll is not match, so
assert.ok(.matchAll(...)) is not flagged'.
* chore(#2982): add changeset fragment for PR #2985
* chore(#2982): add changeset fragment for PR #2985
* refactor(tests): standardize to node:assert/strict and t.after() per CONTRIBUTING.md
- Replace require('node:assert') with require('node:assert/strict') across
all 73 test files to enforce strict equality (no type coercion)
- Replace try/finally cleanup blocks with t.after() hooks in core.test.cjs
and hooks-opt-in.test.cjs per the test lifecycle standards
- Utility functions in codex-config and security-scan retain try/finally
as that is appropriate for per-function resource guards, not lifecycle hooks
Closes#1674
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* perf(tests): add --test-concurrency=4 to test runner for parallel file execution
Node.js --test-concurrency controls how many test files run as parallel child
processes. Set to 4 by default, configurable via TEST_CONCURRENCY env var.
Fixes tests at a known level rather than inheriting os.availableParallelism()
which varies across CI environments.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(security): allowlist verify.test.cjs in prompt-injection scanner
tests/verify.test.cjs uses <human>...</human> as GSD phase task-type
XML (meaning "a human should verify this step"), which matches the
scanner's fake-message-boundary pattern for LLM APIs. This is a
false positive — add it to the allowlist alongside the other test files
that legitimately contain injection-adjacent patterns.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
After gathering milestone goals and determining version, the workflow
now presents a summary and asks for confirmation before writing any
files. Users can adjust until satisfied, preventing the previous
behavior where GSD would immediately write PROJECT.md without verifying
its understanding of the milestone scope.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Refactoring:
- Extract stateReplaceFieldWithFallback() to state.cjs as single source of truth
for the try-primary-then-fallback pattern that was duplicated inline across
phase.cjs, milestone.cjs, and state.cjs
- Replace all inline bold-only regex patterns in cmdPhaseComplete with shared
stateReplaceField/stateExtractField helpers — now supports both **Bold:**
and plain Field: STATE.md formats (fixes the same bug as #924)
- Replace inline bold-only regex patterns in cmdMilestoneComplete with shared helpers
- Replace inline bold-only regex for Completed Phases/Total Phases/Progress
counters in cmdPhaseComplete with stateExtractField/stateReplaceField
- Replace inline bold-only Total Phases regex in cmdPhaseRemove with shared helpers
Security:
- Fix command injection surface in isGitIgnored (core.cjs): replace execSync with
string concatenation with execFileSync using array arguments — prevents shell
interpretation of special characters in file paths
Tests (7 new):
- 5 tests for stateReplaceFieldWithFallback: primary field, fallback, neither,
preference, and plain format
- 1 regression test: phase complete with plain-format STATE.md fields
- 1 regression test: milestone complete with plain-format STATE.md fields
854 tests pass (was 847). No behavioral regressions.
The summary template puts the one-liner as a `**bold**` line after the
`# Phase N` heading, but `cmdSummaryExtract` and `cmdMilestoneComplete`
only checked frontmatter `one-liner` field — which is often empty.
Adds `extractOneLinerFromBody()` to core.cjs as a fallback that parses
the first `**...**` line after the heading.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The summary template writes task count as `**Tasks:** N` in the
Performance section, but `cmdMilestoneComplete` only counted
`## Task N` markdown headers — which don't exist in that format.
Now checks three patterns in order: `**Tasks:** N` field (primary),
`<task` XML tags, then `## Task N` headers (legacy fallback).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Previously, calling `mark-complete` on already-completed requirements
reported them as `not_found`, since the regex only matched unchecked
`[ ]` checkboxes and `Pending` table cells.
Now detects `[x]` checkboxes and `Complete` table cells and returns
them in a new `already_complete` array instead of `not_found`.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
cmdMilestoneComplete previously appended new milestone entries at the
end of MILESTONES.md. Over successive milestones this produced
chronological order (oldest first), which is counterintuitive — users
expect the most recent milestone at the top.
This fix detects the file header (h1-h3) and inserts the new entry
immediately after it, pushing older entries down. Files without a
recognizable header get the entry prepended. New files still get a
default '# Milestones' header.
Adds 2 tests: single insertion ordering assertion and three-sequential-
completions verification (v1.2 < v1.1 < v1.0 in file order).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
cmdMilestoneComplete previously iterated ALL phase directories in
.planning/phases/, including phases from prior milestones that remain on
disk. This produced inflated stats, stale accomplishments from old
summaries, and --archive-phases moving directories that belong to
earlier milestones.
This fix parses ROADMAP.md to extract phase numbers for the current
milestone, builds a normalized Set for O(1) lookup, and filters all
phase directory operations through an isDirInMilestone() helper.
The helper handles edge cases: leading zeros (01 -> 1), letter suffixes
(3A), decimal phases (3.1), large numbers (456), and excludes
non-numeric directories (notes/, misc/).
Adds 5 tests covering scoped stats, scoped archive, prefix collision
guard, non-numeric directory exclusion, and large phase numbers.
Complements upstream PRs #756 and #783 which fix getMilestoneInfo()
milestone detection — this fix addresses milestone completion scoping.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- --archive-phases flag moves phase dirs to milestones/vX.Y-phases/
- archived REQUIREMENTS.md contains header with version, SHIPPED, date
- STATE.md status/activity/description updated during milestone complete
- missing ROADMAP.md handled gracefully without crash
- empty phases directory handled with zero counts
Move 81 tests (18 describe blocks) from single monolithic test file
into 7 domain-specific test files under tests/ with shared helpers.
Test parity verified: 81/81 pass before and after split.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>