Tom Boucher ca8d9d4459 fix(#4429): stop a large commit_types config blocking or bypassing the gate (#4723)
* test(#4429): regression coverage for three defects in the commit hook

Failing-first coverage. Every conforming-subject row is red against the
unfixed hook, and each defect gets an explicit CONTROL row that reconstructs
the pre-fix form and asserts the defect reproduces -- without those, the
passing rows would pass with or without the fix.

1. SIGPIPE (the reported defect). The pre-fix first-line extraction used a
   `head -1` pipeline; once CONFIG_OUT exceeds the 64 KiB pipe buffer printf
   is killed and `set -euo pipefail` aborts the hook. That fix is already on
   next -- it landed incidentally in #4537, whose message never mentions
   #4429 -- and nothing in the tree would notice its removal.

2. regcomp. The commit-type alternation grew with the CONFIGURED list and
   exceeded bash's 64 KiB compiled-pattern cap. Boundary rows pin the cliff
   at 6051/6052, with controls on BOTH sides so limit-1 is not vacuous.

3. Ambient subprocess statuses (found by this change's security review).

Defects 1 and 2 cannot be separated: each configured type adds len+1 bytes to
CONFIG_OUT and len+1 to the alternation, so the smallest payload that
overflows the pipe (N=6059) already puts the alternation past the ceiling.

The SIGPIPE control accepts either SIGPIPE (141, Linux) or a reported write
error (macOS bash 3.2's builtin printf, exit 1). Asserting only the message
would go red on every CI lane, since the remote matrix is Linux-only.

Named to bucket with gsd-validate-commit-crash-policy.test.cjs, which covers
this same hook: lint-test-file-count derives a test's owning module from its
filename prefix, and `validate-commit-*` collided with the `validate` module,
already at its 2-file cap.

Harness note, learned from three vacuous control runs: hooks/lib/git-cmd.js
requires ../gsd-core/bin/lib/token-scanner.cjs relative to the hooks dir's
parent, so a copy in a bare tmpdir fails open and returns 0 for any input.
The layout symlinks gsd-core beside the copy, and every row that can prove it
asserts the run was substantive.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(#4429): bound the commit-type regex and isolate subprocess statuses

Two fixes in the same file, both of the same shape: a value computed for one
purpose was being read as authority about something else.

1. The commit-type alternation could not be compiled.

COMMIT_TYPE_ALT joined every CONFIGURED type into one regex, so the pattern
grew without bound. bash caps a compiled pattern at 64 KiB. Bisected on bash
3.2.57 (this repo's macOS target): a 65504-byte alternation compiles, 65515
fails. `[[ =~ ]]` returns 2 on a compile failure, and `if !` cannot tell that
from "the subject does not conform" -- so the hook blocked a valid
`feat(auth): ...` with CONVENTIONAL_COMMITS_VIOLATION while printing `feat`
in its own valid_types.

Match the shape with a fixed-size pattern, capture the type, then test
membership against the COMMIT_TYPES array. The character class is exactly the
`^[a-z][a-z0-9-]*$` safe-token filter the config loader already applies, so it
captures every type that can legally reach COMMIT_TYPES and no token that
cannot. Review verified equivalence over 46 handcrafted plus 6000 randomized
adversarial subjects against a type list containing prefix-overlapping,
digit-bearing and trailing-hyphen types: zero divergences. The loop adds no
subprocess and no pipe, which is the hazard class #4429 is about.
COMMIT_TYPE_ALT is now unused and removed.

  types   pre-fix `feat(auth): ...`   fixed
  10      accept                      accept
  6051    accept                      accept
  6052    BLOCK                       accept
  20000   BLOCK                       accept

2. Subprocess statuses were inherited from the environment.

Each status is captured as `... || VAR=$?`, which assigns ONLY on the failure
branch; on success the variable kept whatever it already held, and
`${VAR:-0}` defaults only when unset or empty. So an EXPORTED CONFIG_STATUS,
CMD_STATUS or CLASSIFY_STATUS -- from a CI wrapper, a .envrc, or another hook
-- survived into the success path and was read as "the subprocess failed".
Since the hook fails OPEN on a genuine subprocess failure by design (#3838),
the result was a silent bypass. Measured: `CLASSIFY_STATUS=3 git commit -m
"nope: bad"` printed "validator disabled for this call" and exited 0.

The three are now initialised before use. The fail-open path is unchanged and
verified byte-identical to origin/next with a failing node.

hooks/dist/ is gitignored and rebuilt from hooks/ by scripts/build-hooks.js,
so there is no second copy to sync.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore(#4429): register the new suite with the conformance manifests

Both conformance-tier manifests embed the test-file list, so adding a test
file makes them stale. Regenerated with their own generators:

  node scripts/gen-platform-conformance-tier.cjs --write
  node scripts/gen-platform-conformance-tier.cjs --target macos --write

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(#4429): pin both fail-open-prone controls to their named cause

Two rows in the ambient-status block asserted `status === 0`, which the hook
also returns when the harness layout is broken -- so either row could have
passed for entirely the wrong reason. This is the same vacuity trap the rest
of the suite already guards, applied inconsistently to the rows added last.

Measured, rather than reasoned about:

  genuine ambient bypass (pre-fix hook, CLASSIFY_STATUS=3)  rc=0, no CLASSIFIER_THREW
  orphaned layout (no gsd-core symlink)                     rc=0, CLASSIFIER_THREW
  genuine fail-open (node shim exits 3)                     rc=0, no CLASSIFIER_THREW

So assertSubstantive separates the intended cause from the harness failure in
both rows, and each now pins its pass to the cause it names.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(#4429): stop asserting a macOS-only regex cap on every platform

First verification run was RED: 45636/45638 passed, both failures in this new
suite on linux-node24. Cause is mine -- I measured the compiled-pattern ceiling
on macOS and encoded it as a cross-platform expectation.

Measured in the tester image itself:

  engine                            6051      6052      20000
  bash 3.2.57 / BSD libc (macOS)    compiles  rc 2      rc 2
  bash 5.2.15 / glibc  (Linux)      compiles  compiles  compiles (228943 B)

glibc has no reachable cap, so the regcomp defect cannot occur there and the
control asserting a block at 6052 was red for a behaviour the platform cannot
produce.

The control now calibrates at runtime: it runs the pre-fix form and, when this
engine compiled the alternation, it SKIPS with a message naming the reason
rather than asserting. Skipped out loud, never silently passed -- a green row
there would read as "the defect is covered" on a platform where it cannot
occur. Both branches verified: the capped branch asserts (macOS 17/17, zero
skipped), and the uncapped branch was exercised by forcing the payload to a
size that always compiles, producing a skip and not a failure.

Consequence stated rather than hidden: the remote matrix is Linux-only, so this
one control is skipped in CI and really runs only on a macOS workstation. The
rows that run everywhere are the ones carrying the regression weight -- the
shipped hook accepting a conforming commit at every payload size, the gate
still blocking unknown types, the SIGPIPE control, and all seven ambient-status
rows.

Note this also narrows the coupling claim: SIGPIPE and regcomp are coupled only
on a capped engine. On glibc the SIGPIPE defect is directly testable without
the regcomp fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(#4429): scope the regex-cap claim to the platform it applies to

The changeset told users the validator "built a regular expression bigger than
bash can compile" past ~6,000 configured types. That is false on Linux: glibc
compiled a 228,943-byte alternation without complaint, so a Linux reader would
have been misled about their own exposure. These are user-facing release notes,
so the claim is now scoped to macOS (bash 3.2 / BSD libc) and says explicitly
that glibc was never affected by this half.

The hook's own comment led with the same overstatement -- "bash caps a compiled
pattern at 64 KiB" -- before qualifying it. Reworded so the first clause states
what is actually true: the limit is a property of the platform's regex engine.

Text only; no behaviour change. Suite 17/17, eslint and lint:ci clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore(#4429): backfill changeset PR number (#4723)

* chore(#4429): backfill changeset PR number (#4723)

---------

Co-authored-by: sim <sim@local>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 05:43:59 -04:00

GSD Core

Git. Ship. Done.

English · Português · 简体中文 · 日本語 · 한국어

A light-weight meta-prompting, context engineering, and spec-driven development system for Claude Code, OpenCode, Antigravity CLI, Kimi CLI, Kilo, Codex, Copilot, Cursor, Windsurf, and more.

npm version npm downloads Tests Discord GitHub stars License


What is GSD Core

GSD Core is a context-engineering and spec-driven development framework that drives AI coding agents (Claude Code, Codex, Antigravity CLI, Kimi CLI, Copilot, Cursor, and more) through a disciplined phase loop. It solves context rot — the quality degradation that accumulates as an AI fills its context window — by running all heavy research, planning, and execution work in fresh-context subagents while keeping your main session lean.


How it works

Each milestone repeats the same five-step loop, one phase at a time:

  1. Discuss — capture implementation decisions before anything is planned
  2. Plan — research, decompose, and verify the plan fits a fresh context window
  3. Execute — run plans in parallel waves; each executor starts with a clean 200k-token context
  4. Verify — walk through what was built; diagnose and fix before declaring done
  5. Ship — create the PR, archive the phase, repeat for the next one

Quickstart

npx @opengsd/gsd-core@latest

The installer prompts for your runtime (Claude Code, OpenCode, Antigravity CLI, Kimi CLI, Kilo, Codex, Copilot, Cursor, Windsurf, and more) and whether to install globally or locally. The installer is required for cross-runtime compatibility — do not copy files from agents/ or commands/ directly.

On another runtime or without Node.js? See Install on your runtime.

Once installed, start a new project or onboard an existing repo:

/gsd-new-project   # greenfield project
/gsd-onboard       # existing codebase

New here? Follow Your first project for a guided walkthrough from install to first shipped phase, or Onboarding an existing codebase for brownfield setup.


Documentation

What's new in 1.7.0 → docs/whats-new-1.7.0.md

Tutorials — learning by doing:

How-to guides — task-focused recipes:

Reference — authoritative facts:

Explanation — concepts and design decisions:

Full index: docs/README.md. Other languages: 日本語 · 한국어 · Português · 简体中文.


Why it works

Most AI-coding setups fail at scale because context bloat silently degrades output quality, there is no shared memory between sessions, and nothing verifies that code actually works. GSD Core solves all three: heavy work runs in fresh subagents, structured artifacts like STATE.md and CONTEXT.md survive session boundaries, and the verify step walks through what was built and generates fix plans before a phase is declared done. See docs/explanation/context-engineering.md for the full reasoning.

Troubleshooting? See docs/how-to/recover-and-troubleshoot.md.


Community

Project Platform
gsd-opencode Original OpenCode port
Discord Community support

Star History

Star History Chart

License

MIT License. See LICENSE for details.


Claude Code is powerful. GSD Core makes it reliable.

Description
No description provided
Readme MIT 77 MiB
Languages
JavaScript 82.3%
TypeScript 17.4%
Shell 0.3%