From 49cc4ff9c99e9b24f39aa7dcbfc456e840be29a8 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Sun, 10 May 2026 06:57:24 -0700 Subject: [PATCH] v1.31.1.0 fix wave: 3 community PRs (careful BSD sed, codex Step 0 rename, make-pdf setup ordering) (#1413) * fix(careful): BSD sed compatibility for safe exception detection on macOS The sed regex in check-careful.sh uses \s+, which is a GNU sed extension not supported by BSD sed (macOS default). On macOS, this causes the RM_ARGS strip to fail silently, making rm -rf of safe exceptions (node_modules, .next, dist, etc.) trigger the destructive warning instead of being permitted as designed. Fix: replace \s+ with POSIX [[:space:]]+, which works on both GNU sed (Linux) and BSD sed (macOS). The existing test/hook-scripts.test.ts already documented this limitation via a detectSafeRmWorks() helper and a platform-conditional assertion ("if GNU sed: expect undefined, else: expect ask"). Now that the regex works on both platforms, this dead path is removed and the safe-exception tests assert the same expectation on every OS. Note: the grep regex in the same file also uses \s+, but BSD grep -E on macOS does support \s (verified via bash -x trace), so only the sed expression needs the fix. Discovered while translating the careful skill for a Japanese derivative project (uzustack). Reference: https://github.com/uzumaki-inc/uzustack/commit/bc67c8d * docs(codex): rename Step 0 to avoid collision with platform-detect prelude The codex skill template had its own '## Step 0: Check codex binary' heading (line 42), which after gen-skill-docs collided with the platform-detection prelude '## Step 0: Detect platform and base branch' (injected by scripts/resolvers/utility.ts). The generated codex/SKILL.md ended up with two H2 headings labeled Step 0, which is ambiguous to an agent reading the skill in order. Renamed the local heading to Step 0.4, slotting it between the prelude (Step 0) and the existing Step 0.5 / Step 0.6 sections. No renumbering of downstream steps needed. Closes #1388 Co-Authored-By: Claude Opus 4.7 (1M context) * docs(codex): regenerate SKILL.md after Step 0 rename Co-Authored-By: Claude Opus 4.7 (1M context) * fix(make-pdf): move setup before preamble footer * chore: bump version and changelog (v1.31.1.0) Co-Authored-By: Claude Opus 4.7 (1M context) --------- Co-authored-by: ToraDady Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) Co-authored-by: Jayesh Betala --- CHANGELOG.md | 15 ++++++++ VERSION | 2 +- careful/bin/check-careful.sh | 2 +- codex/SKILL.md | 2 +- codex/SKILL.md.tmpl | 2 +- make-pdf/SKILL.md | 72 +++++++++++++++++------------------ make-pdf/SKILL.md.tmpl | 2 - package.json | 2 +- scripts/resolvers/preamble.ts | 4 +- test/gen-skill-docs.test.ts | 20 ++++++++++ test/hook-scripts.test.ts | 22 +---------- 11 files changed, 81 insertions(+), 64 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 674d8b2a..bca6f8fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## [1.31.1.0] - 2026-05-10 + +## **Three small community fixes land cleanly.** +## **`/careful` works on macOS again, Codex Step 0 stops colliding, `/make-pdf` setup runs in the right place.** + +A short patch wave from three contributors. macOS users who ran `/careful` with `rm -rf node_modules` were silently hitting the warning gate instead of the safe exception path because BSD sed doesn't understand `\s`. The Codex skill's `## Step 0: Check codex binary` header was colliding with the platform-detect prelude that also runs first. `/make-pdf`'s SETUP block was rendered after the Telemetry footer instead of immediately after the Preamble Bash, so `$P` could be referenced before it was set. Each fix is tightly scoped and ships with a regression test (or template ordering invariant) that catches the original failure shape. + +This release came out of a contributor-wave triage pass that closed ~75 stale PRs, dropped 11 candidates that needed focused review with specific feedback to each contributor, and lined the survivors through `/plan-eng-review` + Codex outside-voice review before merge. One additional security PR (token-registry timing-safe comparison) was rejected at the codex-review gate after Codex caught a subtle multi-byte UTF-8 buffer-mismatch bug that would have thrown on the auth path instead of returning false; that finding now lives as feedback on the original PR. + +### Fixed + +- **#1242** `careful/bin/check-careful.sh` uses `[[:space:]]` instead of `\s` in the safe-rm exception regex. macOS sed -E does not support `\s`, which silently broke the exception detection — `rm -rf node_modules` now correctly skips the warning gate on macOS, matching Linux behavior. Removes the `detectSafeRmWorks()` platform-conditional from `test/hook-scripts.test.ts` so both platforms are tested at the same bar. Contributed by @ToraDady. +- **#1394** Codex skill `## Step 0: Check codex binary` renamed to `## Step 0.4: Check codex binary` so the header no longer collides with the new platform-detect prelude (also numbered Step 0). Affects both `codex/SKILL.md.tmpl` and the regenerated `codex/SKILL.md`. Contributed by @mvanhorn. +- **#1393** `/make-pdf` MAKE-PDF SETUP block moves from after the Telemetry footer to right after the Preamble Bash, so `$P` is set before any subsequent step references it. The implementation switches from the `{{MAKE_PDF_SETUP}}` placeholder pattern to programmatic insertion via `generateMakePdfSetup` in `scripts/resolvers/preamble.ts`, gated on `ctx.skillName === 'make-pdf'`. New `make-pdf setup ordering` test in `test/gen-skill-docs.test.ts` asserts the SETUP block sits after the Preamble heading and before Plan Mode / Telemetry / workflow headings. Contributed by @jbetala7. + ## [1.31.0.0] - 2026-05-09 ## **AskUserQuestion stops getting silently buried in plan files.** diff --git a/VERSION b/VERSION index 52c3b4a5..7a251efb 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.31.0.0 +1.31.1.0 diff --git a/careful/bin/check-careful.sh b/careful/bin/check-careful.sh index c8bc2c7a..d9c39e48 100755 --- a/careful/bin/check-careful.sh +++ b/careful/bin/check-careful.sh @@ -28,7 +28,7 @@ CMD_LOWER=$(printf '%s' "$CMD" | tr '[:upper:]' '[:lower:]') # --- Check for safe exceptions (rm -rf of build artifacts) --- if printf '%s' "$CMD" | grep -qE 'rm\s+(-[a-zA-Z]*r[a-zA-Z]*\s+|--recursive\s+)' 2>/dev/null; then SAFE_ONLY=true - RM_ARGS=$(printf '%s' "$CMD" | sed -E 's/.*rm\s+(-[a-zA-Z]+\s+)*//;s/--recursive\s*//') + RM_ARGS=$(printf '%s' "$CMD" | sed -E 's/.*rm[[:space:]]+(-[a-zA-Z]+[[:space:]]+)*//;s/--recursive[[:space:]]*//') for target in $RM_ARGS; do case "$target" in */node_modules|node_modules|*/\.next|\.next|*/dist|dist|*/__pycache__|__pycache__|*/\.cache|\.cache|*/build|build|*/\.turbo|\.turbo|*/coverage|coverage) diff --git a/codex/SKILL.md b/codex/SKILL.md index 6be6ccdf..464401fd 100644 --- a/codex/SKILL.md +++ b/codex/SKILL.md @@ -792,7 +792,7 @@ assumptions, catches things you might miss. Present its output faithfully, not s --- -## Step 0: Check codex binary +## Step 0.4: Check codex binary ```bash CODEX_BIN=$(which codex 2>/dev/null || echo "") diff --git a/codex/SKILL.md.tmpl b/codex/SKILL.md.tmpl index 90dd1119..ed118a11 100644 --- a/codex/SKILL.md.tmpl +++ b/codex/SKILL.md.tmpl @@ -39,7 +39,7 @@ assumptions, catches things you might miss. Present its output faithfully, not s --- -## Step 0: Check codex binary +## Step 0.4: Check codex binary ```bash CODEX_BIN=$(which codex 2>/dev/null || echo "") diff --git a/make-pdf/SKILL.md b/make-pdf/SKILL.md index f116687d..927b637d 100644 --- a/make-pdf/SKILL.md +++ b/make-pdf/SKILL.md @@ -102,6 +102,42 @@ echo "CHECKPOINT_PUSH: $_CHECKPOINT_PUSH" [ -n "$OPENCLAW_SESSION" ] && echo "SPAWNED_SESSION: true" || true ``` +## MAKE-PDF SETUP (run this check BEFORE any make-pdf command) + +```bash +_ROOT=$(git rev-parse --show-toplevel 2>/dev/null) +P="" +[ -n "$MAKE_PDF_BIN" ] && [ -x "$MAKE_PDF_BIN" ] && P="$MAKE_PDF_BIN" +[ -z "$P" ] && [ -n "$_ROOT" ] && [ -x "$_ROOT/.claude/skills/gstack/make-pdf/dist/pdf" ] && P="$_ROOT/.claude/skills/gstack/make-pdf/dist/pdf" +[ -z "$P" ] && P="$HOME/.claude/skills/gstack/make-pdf/dist/pdf" +if [ -x "$P" ]; then + echo "MAKE_PDF_READY: $P" + alias _p_="$P" # shellcheck alias helper (not exported) + export P # available as $P in subsequent blocks within the same skill invocation +else + echo "MAKE_PDF_NOT_AVAILABLE (run './setup' in the gstack repo to build it)" +fi +``` + +If `MAKE_PDF_NOT_AVAILABLE` is printed: tell the user the binary is not +built. Have them run `./setup` from the gstack repo, then retry. + +If `MAKE_PDF_READY` is printed: `$P` is the binary path for the rest of +the skill. Use `$P` (not an explicit path) so the skill body stays portable. + +Core commands: +- `$P generate [output.pdf]` — render markdown to PDF (80% use case) +- `$P generate --cover --toc essay.md out.pdf` — full publication layout +- `$P generate --watermark DRAFT memo.md draft.pdf` — diagonal DRAFT watermark +- `$P preview ` — render HTML and open in browser (fast iteration) +- `$P setup` — verify browse + Chromium + pdftotext and run a smoke test +- `$P --help` — full flag reference + +Output contract: +- `stdout`: ONLY the output path on success. One line. +- `stderr`: progress (`Rendering HTML... Generating PDF...`) unless `--quiet`. +- Exit 0 success / 1 bad args / 2 render error / 3 Paged.js timeout / 4 browse unavailable. + ## Plan Mode Safe Operations In plan mode, allowed because they inform the plan: `$B`, `$D`, `codex exec`/`codex review`, writes to `~/.gstack/`, writes to the plan file, and `open` for generated artifacts. @@ -489,42 +525,6 @@ On Linux, install `fonts-liberation` for correct rendering — Helvetica and Ari aren't present by default, and Liberation Sans is the standard metric-compatible fallback. CI and Docker builds install it automatically via Dockerfile.ci. -## MAKE-PDF SETUP (run this check BEFORE any make-pdf command) - -```bash -_ROOT=$(git rev-parse --show-toplevel 2>/dev/null) -P="" -[ -n "$MAKE_PDF_BIN" ] && [ -x "$MAKE_PDF_BIN" ] && P="$MAKE_PDF_BIN" -[ -z "$P" ] && [ -n "$_ROOT" ] && [ -x "$_ROOT/.claude/skills/gstack/make-pdf/dist/pdf" ] && P="$_ROOT/.claude/skills/gstack/make-pdf/dist/pdf" -[ -z "$P" ] && P="$HOME/.claude/skills/gstack/make-pdf/dist/pdf" -if [ -x "$P" ]; then - echo "MAKE_PDF_READY: $P" - alias _p_="$P" # shellcheck alias helper (not exported) - export P # available as $P in subsequent blocks within the same skill invocation -else - echo "MAKE_PDF_NOT_AVAILABLE (run './setup' in the gstack repo to build it)" -fi -``` - -If `MAKE_PDF_NOT_AVAILABLE` is printed: tell the user the binary is not -built. Have them run `./setup` from the gstack repo, then retry. - -If `MAKE_PDF_READY` is printed: `$P` is the binary path for the rest of -the skill. Use `$P` (not an explicit path) so the skill body stays portable. - -Core commands: -- `$P generate [output.pdf]` — render markdown to PDF (80% use case) -- `$P generate --cover --toc essay.md out.pdf` — full publication layout -- `$P generate --watermark DRAFT memo.md draft.pdf` — diagonal DRAFT watermark -- `$P preview ` — render HTML and open in browser (fast iteration) -- `$P setup` — verify browse + Chromium + pdftotext and run a smoke test -- `$P --help` — full flag reference - -Output contract: -- `stdout`: ONLY the output path on success. One line. -- `stderr`: progress (`Rendering HTML... Generating PDF...`) unless `--quiet`. -- Exit 0 success / 1 bad args / 2 render error / 3 Paged.js timeout / 4 browse unavailable. - ## Core patterns ### 80% case — memo/letter diff --git a/make-pdf/SKILL.md.tmpl b/make-pdf/SKILL.md.tmpl index 0827492a..d134ee62 100644 --- a/make-pdf/SKILL.md.tmpl +++ b/make-pdf/SKILL.md.tmpl @@ -41,8 +41,6 @@ On Linux, install `fonts-liberation` for correct rendering — Helvetica and Ari aren't present by default, and Liberation Sans is the standard metric-compatible fallback. CI and Docker builds install it automatically via Dockerfile.ci. -{{MAKE_PDF_SETUP}} - ## Core patterns ### 80% case — memo/letter diff --git a/package.json b/package.json index 679bb503..ec6ad159 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gstack", - "version": "1.31.0.0", + "version": "1.31.1.0", "description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.", "license": "MIT", "type": "module", diff --git a/scripts/resolvers/preamble.ts b/scripts/resolvers/preamble.ts index b866e90b..97698bfc 100644 --- a/scripts/resolvers/preamble.ts +++ b/scripts/resolvers/preamble.ts @@ -58,6 +58,7 @@ import { generateContextHealth } from './preamble/generate-context-health'; // Tier 3+ repo mode + search import { generateRepoModeSection } from './preamble/generate-repo-mode-section'; import { generateSearchBeforeBuildingSection } from './preamble/generate-search-before-building'; +import { generateMakePdfSetup } from './make-pdf'; // Standalone export used directly by the resolver registry export { generateTestFailureTriage } from './preamble/generate-test-failure-triage'; @@ -81,7 +82,8 @@ export function generatePreamble(ctx: TemplateContext): string { } const sections = [ generatePreambleBash(ctx), - // Plan-mode-skill semantics at position 1: after bash (so _SESSION_ID / + ...(ctx.skillName === 'make-pdf' ? [generateMakePdfSetup(ctx)] : []), + // Plan-mode-skill semantics stays near the top: after bash (so _SESSION_ID / // _BRANCH / _TEL env vars are live) and before all onboarding gates so // models read the authoritative "AskUserQuestion satisfies plan mode's // end-of-turn" rule before any other instruction. Renders for all skills diff --git a/test/gen-skill-docs.test.ts b/test/gen-skill-docs.test.ts index 86cdac95..23a4965e 100644 --- a/test/gen-skill-docs.test.ts +++ b/test/gen-skill-docs.test.ts @@ -1098,6 +1098,26 @@ describe('Plan status footer in preamble', () => { }); }); +// --- make-pdf setup ordering --- + +describe('make-pdf setup ordering', () => { + test('MAKE-PDF SETUP appears before generic preamble footer sections', () => { + const content = fs.readFileSync(path.join(ROOT, 'make-pdf', 'SKILL.md'), 'utf-8'); + const preambleIdx = content.indexOf('## Preamble (run first)'); + const setupIdx = content.indexOf('## MAKE-PDF SETUP'); + const planModeIdx = content.indexOf('## Plan Mode Safe Operations'); + const telemetryIdx = content.indexOf('## Telemetry (run last)'); + const workflowIdx = content.indexOf('# make-pdf: publication-quality PDFs from markdown'); + + expect(preambleIdx).toBeGreaterThanOrEqual(0); + expect(setupIdx).toBeGreaterThan(preambleIdx); + expect(setupIdx).toBeLessThan(planModeIdx); + expect(setupIdx).toBeLessThan(telemetryIdx); + expect(setupIdx).toBeLessThan(workflowIdx); + expect(content.match(/^## MAKE-PDF SETUP/gm)?.length ?? 0).toBe(1); + }); +}); + // --- Skill invocation during plan mode in preamble --- describe('Skill invocation during plan mode in preamble', () => { diff --git a/test/hook-scripts.test.ts b/test/hook-scripts.test.ts index 850b5b98..f1ffe123 100644 --- a/test/hook-scripts.test.ts +++ b/test/hook-scripts.test.ts @@ -56,13 +56,6 @@ function withFreezeDir(freezePath: string, fn: (stateDir: string) => void) { } } -// Detect whether the safe-rm-targets regex works on this platform. -// macOS sed -E does not support \s, so the safe exception check fails there. -function detectSafeRmWorks(): boolean { - const { output } = runHook(CAREFUL_SCRIPT, carefulInput('rm -rf node_modules')); - return output.permissionDecision === undefined; -} - // ============================================================ // check-careful.sh tests // ============================================================ @@ -88,24 +81,13 @@ describe('check-careful.sh', () => { test('rm -rf node_modules allows (safe exception)', () => { const { exitCode, output } = runHook(CAREFUL_SCRIPT, carefulInput('rm -rf node_modules')); expect(exitCode).toBe(0); - if (detectSafeRmWorks()) { - // GNU sed: safe exception triggers, allows through - expect(output.permissionDecision).toBeUndefined(); - } else { - // macOS sed: safe exception regex uses \\s which is unsupported, - // so the safe-targets check fails and the command warns - expect(output.permissionDecision).toBe('ask'); - } + expect(output.permissionDecision).toBeUndefined(); }); test('rm -rf .next dist allows (multiple safe targets)', () => { const { exitCode, output } = runHook(CAREFUL_SCRIPT, carefulInput('rm -rf .next dist')); expect(exitCode).toBe(0); - if (detectSafeRmWorks()) { - expect(output.permissionDecision).toBeUndefined(); - } else { - expect(output.permissionDecision).toBe('ask'); - } + expect(output.permissionDecision).toBeUndefined(); }); test('rm -rf node_modules /var/data warns (mixed safe+unsafe)', () => {