close-draft-prs.yml (on pull_request_target after #760) cannot close fork draft
PRs whose head branch name looks like a Git SHA — GitHub never dispatches
pull_request_target for such branches, and a pull_request run from a fork gets a
read-only token. So a draft PR on a SHA-named fork branch evades the auto-close.
Add close-draft-prs-sweep.yml: a schedule (every 6h) + workflow_dispatch sweep
running in base-repo context with pull-requests: write that paginates open PRs,
filters to non-OWNER/MEMBER/COLLABORATOR drafts, and closes + comments them with
the identical policy/message as the event-driven workflow. Re-fetches each
candidate before mutating (TOCTOU guard), closes before commenting so
enforcement is never gated on the explanatory comment, and core.setFailed on
partial failures. The per-PR workflow remains the fast path; this is the
safety net for the documented residual bypass.
Extends tests/workflow-maintainer-skip.test.cjs with structural guards locking
the triggers, write permission, maintainer carve-out, pagination, and message.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
The rc action publishes a release candidate to @next for testing but
never surfaces the curated CHANGELOG section for the version under test —
render only runs destructively at finalize (#715), so there was no safe
way to preview the upcoming notes during the RC window.
Add a --preview mode to scripts/changeset/cli.cjs cmdRender: it renders
the dated release section to stdout via the existing renderChangelog/
serializeChangelog path (with priorChangelog: null, so only the new
section is emitted), reuses the shared injectEmptyPlaceholder helper for
zero-fragment releases, and returns WITHOUT writing CHANGELOG.md or
deleting any .changeset fragment. Wire a "Preview CHANGELOG" step into
the rc job that renders to a file (standalone command, so a malformed
fragment fails the step) and cats it to the job summary and log.
Closes#759
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Record the Runtime Install Policy Module decision and ownership
boundary: install policy projects a pure, typed install plan by
composing artifact placements (ADR-3660) and command text (ADR-0009)
plus per-runtime config intentions, with no filesystem IO; runtime
adapters consume the plan and execute concrete file mutations and
format-specific config rendering. Explicitly records what stays
outside the policy module (TOML/JSON/Markdown serialization, merge
semantics, filesystem effects).
Adds the ADR index row in docs/adr/README.md and a glossary entry in
CONTEXT.md. Leads the installer-refactor chain (#58 -> #60 -> #56).
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Bare `pull_request` hands fork PRs a read-only GITHUB_TOKEN, so the
close/comment API calls 403 and a first-time/external contributor's draft
PR survives — bypassing the auto-close for exactly the population the job
targets. Switch to `pull_request_target`, which runs in the base-repo
context with a write-capable token even for fork PRs. Safe because the job
never checks out or executes PR-supplied code; it only reads event metadata
and calls the GitHub API. The minimal `permissions: pull-requests: write`
block still constrains the token.
Add a regression guard in tests/workflow-maintainer-skip.test.cjs asserting
the workflow triggers on pull_request_target and not bare pull_request.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* refactor(#651): consolidate verification-status routing into one queryable seam
The passed/gaps_found/human_needed verification status was re-encoded as
bare strings across three prose surfaces (gsd-verifier emits, execute-phase
routes, ship gates), each independently deciding the per-status next action
with no parity coupling — the DEFECT.GENERATIVE-FIX class.
Give the enum one home: src/verification.cts (-> bin/lib/verification.cjs)
exposing `gsd_run query verification.status <phaseDir>` returning a typed
{status, next_action, next_command}. ship.md and execute-phase.md now consume
the query instead of re-deriving the routing in prose; gsd-verifier.md points
at the shared vocabulary as the single emitter (values unchanged).
Also fixes the latent broad-grep status misread (DEFECT.FRONTMATTER-SCALAR-
BROAD-GREP): execute-phase.md read `grep "^status:"` over the whole report, so
a body `status:` line could misroute a valid phase. Extraction is now
frontmatter-scoped in one place. A parity test fails if a verifier status
gains no route. Lands the two CONTEXT.md DEFECT entries captured on the issue.
Closes#651
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(#651): set changeset pr to 755
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(#52): add agent_skills_security.trusted_global_roots allowlist
Opt-in allowlist so a global: agent skill whose SKILL.md realpath resolves
outside the default global skills base (e.g. ~/.claude/skills) is accepted
when its real target lies under a user-declared trusted root. Default [] is
byte-identical to prior behavior; the symlink-escape guard is preserved and
simply re-applied against each declared root.
- src/security.cts: loadTrustedGlobalRoots — tilde-expand (~ and ~/), reject
project-relative and dangerously broad roots (filesystem/UNC root, homedir),
realpath-canonicalize each root every run and drop non-existent ones.
- src/init.cts: on base-check failure the guard consults the trusted roots
(hoisted out of the loop); emits a stderr NOTE when a skill is accepted via
a trusted root so the widened boundary is visible.
- src/core.cts: thread agent_skills_security through loadConfig.
- config-schema.manifest.json: allow the new key path.
- docs/CONFIGURATION.md: document the option and its security model.
- tests/agent-skills.test.cjs: unit + end-to-end CLI coverage (regression,
feature, negative, broad-root hardening, stderr NOTE).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(#52): add changeset fragment for trusted_global_roots (#754)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(#25): scope gsd-verifier Step 7b to enumerate-or-single-test; forbid full-suite re-runs
Step 7b's lone test example (`npm test -- --grep "$PHASE_TEST_PATTERN"`) is
mocha/vitest/jest-specific, where `--grep` filters which tests *execute*. Models
generalized it to `cargo test --workspace 2>&1 | grep X` (runs the whole suite,
filters only *output*) and repeated it once per must-have, adding minutes per
verification with no new evidence after the first run.
Replace the example with language-agnostic guidance: prove a test EXISTS via
enumeration (`cargo test -- --list` / `pytest --collect-only` / `npx vitest
list` / `go test -list`), and prove it PASSES via a single named test
(`cargo test <name> -- --exact` / `pytest -k` / `npx vitest run -t`). Add a
Spot-check constraint forbidding more than one full-suite run per verification
or piping a full run through grep per must-have, while still permitting one
saved run + grep when a full run is genuinely required. docs/AGENTS.md gains a
one-line Key-behaviors note, and a new test asserts the Step 7b content.
Scoped per the maintainer decision on the issue: folded into Step 7b (no new
top-level Step 7a) with no VERIFICATION.md label changes.
Closes#25
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(#25): add Changed changeset fragment for PR #753
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
CodeQL alert #26 (js/prototype-pollution-utility) kept firing on setConfigValue
because its dataflow does not trace the #663 Set-based, pre-loop keys.some(...)
forbidden-key check as a sanitising barrier on the write site.
- src/config.cts: replace the Set + pre-loop check with inline literal
comparisons (key === '__proto__' || 'prototype' || 'constructor') on the exact
key used to index `current`, immediately before each write (intermediate keys
in the descent loop, plus the final key). Same forbidden set, same error
message and ERROR_REASON.CONFIG_PARSE_FAILED — behaviour unchanged from #663,
but the barrier is now CodeQL-recognised.
- tests/config.test.cjs: add regression tests for schema-valid dynamic-prefix
keys (agent_skills.__proto__, agent_skills.constructor, agent_skills.prototype,
features.__proto__, review.models.constructor) that pass the isValidConfigKey
schema gate and reach the guard. Each asserts the guard's own message fires
(not the schema gate's "Unknown config key") and Object.prototype is not
polluted. The prior #663 tests never reached the guard — their keys are
rejected by the schema gate first — so the guard's real attack surface was
untested.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(#703): add --granularity override flag to /gsd:plan-phase
Add a `--granularity <coarse|standard|fine>` flag to /gsd:plan-phase that
overrides the configured planning granularity for a single invocation.
The override is a new highest-priority tier above the existing precedence
chain (granularities[phaseType] -> granularity -> planning.granularity ->
'standard') in resolveGranularityInternal; when the flag is absent, resolution
is byte-for-byte unchanged. cmdInitPlanPhase now resolves with phaseType
'planning' so granularities.planning participates, and emits the resolved
value in the init JSON, which the plan-phase workflow forwards to the planner
prompt. Invalid values are rejected at the CLI boundary via a shared
assertValidGranularityOverride helper.
Closes#703
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(#703): set changeset pr to 750
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#683): auto-degrade phase execution to sequential on worktree base mismatch
Claude Code forks worktree-isolated executors off the repository default
branch (origin/HEAD), not the orchestrator's HEAD. Running /gsd-execute-phase
on a branch diverged from the default (unmerged milestone/feature branch) left
every executor without the phase's plan files and tripped the
worktree-branch-check guard with `exit 42` — 100% reproducible, all OSes.
- New module src/worktree-base-ref.cts: HEAD-vs-fork-base drift detection
(origin/HEAD with symbolic-ref fallback) and no-clobber worktree.baseRef
management, exposed as `worktree base-check` / `worktree set-baseref`.
- execute-phase.md: pre-dispatch, for Claude Code with worktrees enabled,
auto-degrades the run to sequential on the main tree when a base mismatch
is detected, recommending worktree.baseRef:"head". The exit-42 guard stays
as a backstop.
- Installer: fresh local Claude installs set worktree.baseRef:"head" in
.claude/settings.local.json (no-clobber, respecting an explicit shared
settings.json value); upgrades print an opt-in notice pointing at
`gsd-tools worktree set-baseref`.
- Docs: how-to guide, CLI/config reference, planning-config cross-ref.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#683): auto-apply worktree.baseRef on upgrade; gate fresh+upgrade on use_worktrees
Per maintainer direction: on a local Claude Code UPGRADE, set
worktree.baseRef:"head" automatically (no opt-in notice) when the project's
workflow.use_worktrees is enabled, instead of merely printing a remediation
notice. For consistency the FRESH path is now gated the same way: both paths
compute worktrees-enabled once (bounded walk-up read of .planning/config.json,
default enabled unless workflow.use_worktrees === false) and apply the
no-clobber baseRef only when enabled — never overwriting an explicit value in
settings.local.json or a shared settings.json. gsd-tools worktree set-baseref
remains for manual use. Docs + changeset updated; tests hardened (file-exists
assertions, fresh+disabled case, upgrade idempotency).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#683): measure workflow byte-budget on LF, fixing Windows-only CI failure
The workflow-size-budget test failed only on Windows: git checks out the .md
files as CRLF (no eol=lf in .gitattributes) and byteCount used
fs.statSync().size (raw on-disk bytes), counting an extra \r per line. That
inflated execute-phase.md — the XL high-water-mark file pinned near its ceiling
by the tighten-only ratchet — from 88492 LF bytes to ~90245 on Windows, over
the 90000 XL ceiling, while passing on the LF-checkout Mac/Linux runners.
The ceilings are explicitly "calibrated against raw `wc -c`" on an LF checkout,
so the measurement should be LF-based on every platform. byteCount now reads the
file and counts Buffer.byteLength after stripping CR, making the budget
platform-independent (a no-op on LF checkouts; verified statSync === normalized
for all 88 workflow files). No ceilings changed. Added a regression test
asserting CRLF and LF content of the same file count identically.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#683): make worktree-base-ref test path mocks Windows-safe (path.join)
tests/worktree-base-ref.test.cjs keyed its injected readFile/writeFile mocks
(and a few expected `file` values) with forward-slash template literals like
`${claudeDir}/settings.local.json`. The module composes those paths with
path.join(), which emits backslashes on Windows, so the mock keys never matched
the module's lookup → readFile returned null → resolveEffectiveBaseRef /
cmdWorktreeBaseCheck / cmdWorktreeSetBaseRef (and the JSONC variants) failed on
the Windows full-test runner only (they passed on Mac/Linux, and the install
tests passed because they use the real filesystem). The module is correct;
only the test fixtures hardcoded '/'.
All mock keys and path assertions now use path.join(base, ...) mirroring the
module, so they match on every platform (no-op on POSIX). 19 path references
across 16 lines.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
`extractCurrentMilestone()` scoped the current-milestone window to its
`## Phases` checklist subsection and terminated at the milestone's own
`## Milestone … (Phase Details)` heading, so the `### Phase N:` detail
headers fell outside scope. Every parser-backed command — `init.phase-op`
(and thus `/gsd:discuss-phase`, `/gsd:plan-phase`), `state`, `roadmap list`,
and `validate health` (W006) — therefore could not resolve phases of any
milestone after the first until a `.planning/phases/` directory already
existed, blocking discuss/plan.
The parser now additionally includes the current milestone's `(Phase Details)`
section in scope, located via the already-computed version matches and anchored
(boundary-aware) to the selected milestone's version token so sibling
sub-milestones sharing a version prefix do not cross-pollinate. The existing
heading selection and primary window are unchanged.
Adds tests/bug-730-milestone-phase-details-scope.test.cjs covering the
two-milestone reproduction, first-milestone non-regression, direct
getRoadmapPhaseInternal resolution, validate-health W006 visibility, a
three-milestone roadmap, and the closed-sibling sub-milestone case.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* refactor(#712): replace Codex slash-command denylist lookbehind with positive-boundary match
The hyphen-style /gsd-<cmd> -> $gsd-<cmd> conversion in
convertSlashCommandsToCodexSkillMentions used a negative-lookbehind DENYLIST
enumerating characters that must NOT precede a real mention. #637 -> #704 showed
this is an unbounded treadmill: each new unanticipated preceding char (/, ., word
chars, then }, )) leaked the same path-corruption bug class, and a backtick-wrapped
path (`/gsd-core/workflows/update.md`) still leaked through.
Replace it with a POSITIVE two-boundary definition of a mention:
1. Left: opens at start-of-string, whitespace, or an inline-prose delimiter
(backtick/quote/paren/bracket).
2. Right: the command token is not followed by a path separator `/` (a path
continues, a command does not). The (?![a-z0-9/-]) lookahead also blocks
regex backtracking to a shorter command.
This closes the whole class by construction (no preceding-char denylist to
maintain) and fixes the backtick-wrapped-path corruption the #704 test
documented as a pre-existing gap, while preserving conversion of legitimate
backtick-wrapped mentions (e.g. CONTEXT.md's `/gsd-execute-phase` lists).
The colon-style /gsd: replace is intentionally left unguarded (it never appears
as a filesystem path segment) and is annotated as such.
Tests assert the regex directly (function now exported) across a convert/
don't-convert matrix plus one end-to-end pipeline assertion for the headline
backtick-path case.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(#712): add changeset fragment for PR #747
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* refactor(#720): lazy-load MVP-only reference bodies (eager @-import → gated Read)
Convert eager @-imports of MVP-only reference bodies into lazy "Read" instructions
gated on MVP_MODE / WALKING_SKELETON / MVP+TDD, so non-MVP planning/execution runs
no longer pull MVP guidance into context. Covers both the workflow files and the
planner/executor agent definitions (the dominant context-cost path):
- workflows/plan-phase.md: planner-mvp-mode.md + skeleton-template.md (L146/936/937/941)
- workflows/execute-phase.md: execute-mvp-tdd.md halt-report ref, now gated on gate-trip (L191)
- agents/gsd-planner.md: planner-mvp-mode.md, user-story-template.md, skeleton-template.md
- agents/gsd-executor.md: execute-mvp-tdd.md
The dedicated always-MVP mvp-phase workflow keeps its eager imports (intentional).
Behaviour is unchanged; non-MVP runs simply carry less loaded context. Adds a
regression guard mirroring the discuss-phase lazy-load test, and documents the
conformance in docs/ARCHITECTURE.md.
Refs #720
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(#720): add changeset fragment (pr #746)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Part 2 of 2 of the n/no-process-exit cleanup (completes umbrella #738; part 1
was #739/scripts). Converts the 20 flagged process.exit() calls in the three
hand-written gsd-core/bin CLI entrypoints and flips n/no-process-exit to error.
- New src/cli-exit.cts -> gsd-core/bin/lib/cli-exit.cjs (ExitError + runMain),
the gsd-core-side equivalent of scripts/lib/cli-exit.cjs; registered in
.gitignore, eslint ignores, and the inventory manifest like its siblings.
- gsd-tools.cjs: 13 apply-prompt-budget exits -> throw ExitError; main()->runMain.
- verify-reapply-patches.cjs: 6 exits -> throw ExitError / return verdict; runMain.
- check-latest-version.cjs: 1 exit -> return verdict; runMain.
- eslint.config.mjs: n/no-process-exit warn -> error.
Scope note: the gsd-core/bin/lib/*.cjs modules (core, state, profile-pipeline,
roadmap-command-router, adr-parser, ui-safety-gate) are tsc-generated and
eslint-ignored (ADR-457), so their process.exit calls were never flagged and are
intentionally left untouched. Only the linted hand-written entrypoints are in scope.
Exit codes verified unchanged for all three entrypoints.
Closes#738
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
- stage generated Kimi root and subagent YAML/prompt files under agents/
- copy and remove only GSD-owned Kimi agent files while preserving user files
- print explicit kimi --agent-file launch hint
- Wire Kimi global layout to convertClaudeCommandToKimiSkill
- Keep --kimi --local guarded as a no-op
- Add Kimi self-invocation hint without agent or tool artifacts
- Resolve Kimi global skills under the generic agents path
- Add empty Kimi layout placeholder and local install no-op guard
- Keep selection/path tests aligned without Kimi skill conversion
Part 1 of 2 of the n/no-process-exit cleanup (umbrella #738): convert every
process.exit() call in standalone scripts/** CLIs to the rule-compliant pattern.
- New shared helper scripts/lib/cli-exit.cjs: ExitError(code,message) + runMain()
which translates a thrown ExitError / returned number into process.exitCode
(never process.exit()), flushing output and still firing process.on('exit').
- main()-based entrypoints: throw new ExitError(code) for errors, return <code>
for verdicts; invoked via runMain(main). Child exit codes preserved via return.
- top-level-only scripts: imperative body extracted into main() so mid-flow
aborts (throw ExitError) actually halt; pure consts/helpers stay at module scope.
- diff-touches-shipped-paths.cjs: stdin event handling restructured to an async
read so the whole flow runs under runMain; uncaughtException/unhandledRejection
nets replaced by an in-band catch that preserves EXIT_ERROR=2.
Exit codes verified unchanged for every converted script (success/error/help and
the 0/1/2 semantic codes in diff-touches). Rule stays warn here; flipped to error
in part 2 (#738) once gsd-core/bin/** is also clean.
Refs #739
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#706): skip rescueSummaryArtifacts when SUMMARY is already committed
rescueSummaryArtifacts now probes `git cat-file -e HEAD:<path>` before
copying a SUMMARY.md into the main checkout. When the file is already
committed on the worktree branch, copying it as an untracked file causes
`git merge --no-ff` to abort with "untracked working tree files would be
overwritten by merge" — a permanent merge_failed cleanup-wave failure.
Fail-closed on timeout: if cat-file is unreliable we skip rescue (the
merge will surface the collision as it did before, which is recoverable).
Adds 4 new test cases in worktree-safety.test.cjs covering:
- committed SUMMARY skipped, merge succeeds (#706 regression case)
- committed SUMMARY skipped even when timeout (fail-closed)
- uncommitted SUMMARY still rescued (existing contract preserved)
- rescue failure on ENOSPC still propagates (unchanged)
Closes#706
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: add changeset for #706
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(#706): treat cat-file exit 128 as uncertain — skip rescue (fail-closed)
The previous guard skipped rescue only when `exitCode === 0` (committed) or
`timedOut`. Any other non-zero exit, including `128` (fatal git error: corrupt
object store, unborn HEAD, missing repo), fell through and PROCEEDED with
rescue — potentially re-creating the #706 untracked-file merge collision.
Fix: rescue ONLY when `exitCode === 1` (cat-file definitively reports the
object absent). All other outcomes — 0 (committed), 128 (fatal), null/SIGTERM
(timeout), or any other code — are treated as "uncertain → skip rescue".
Also corrects the JSDoc bullet that still referenced `git ls-files
--error-unmatch` (the old mechanism); updated to `git cat-file -e HEAD:<relPath>`.
Regression test added: asserts rescue is SKIPPED when cat-file returns exit 128,
leaving the merge to surface the issue safely rather than silently copying an
already-committed file.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: link changeset to PR #709
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(#704): exclude } and ) from Codex path-rewrite lookbehind
Shell variable expressions like \${VAR}/gsd-core/ and command-substitution
paths like \$(cmd)/gsd-local-patches were being rewritten to \$gsd-core and
\$gsd-local-patches respectively because the negative lookbehind in
convertSlashCommandsToCodexSkillMentions did not include } or ).
Add both characters to the lookbehind set:
(?<![a-zA-Z0-9./})])
Also adds regression test:
tests/bug-704-codex-launcher-path-corruption.test.cjs
Closes#704
* chore: add changeset for #704
* test: use RUNTIME_ROOT_PATH in assertion to eliminate dead-code lint warning
Replace the partial hard-coded fragment '}/gsd-core/bin/' with the
existing RUNTIME_ROOT_PATH const so the assertion both compiles clean
(no unused variable) and self-documents which canonical launcher path
must survive Codex conversion intact (#704).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: link changeset to PR #710
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace raw setTimeout/Atomics.wait synchronization sleeps in 4 test files
with a shared async delay()/waitFor() poll-for-condition helper in
tests/helpers.cjs, then flip local/no-magic-sleep-in-tests and
no-restricted-syntax from warn to error so the debt can't regrow.
- tests/helpers.cjs: add delay(ms) + waitFor(predicate, opts), exported
- bug-1974: setTimeout backoff -> await delay()
- config.test: drop Atomics.wait sleep(); async retry via await delay()
- graphify: waitForBuildStatus/cleanupHookRepo async via await delay()
- locking-bugs: 3 Atomics.wait poll loops -> await waitFor()
- eslint.config.mjs: ratchet both rules warn -> error
Refs #733
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>