mirror of
https://github.com/garrytan/gstack.git
synced 2026-08-19 12:37:18 +02:00
94993f74012782fd94416dd44b8314f6363a13a4
3
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
94993f7401 |
v1.61.0.0 fix wave: guards failing open / silent failures (9 fixes, 4 community PRs absorbed) (#2472)
* fix(careful): warn on chained rm even when the last target is safe The safe-exception block whitelisted rm -rf of build artifacts by extracting targets with a single greedy match (.*rm ...), which only ever inspects the LAST rm in the command. A chain like 'rm -rf /; rm -rf node_modules' was therefore judged solely by its trailing safe target and allowed without warning, waving through the destructive 'rm -rf /'. Gate the shortcut to single rm invocations: when any shell separator (; | & newline, incl. JSON-escaped \n/\r from the grep extraction path) is present, fall through to the destructive-pattern check, which warns on any recursive rm. Single-command artifact cleanups still allow. Adds 3 regression tests covering semicolon and && chains in both orders. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * harden(careful): substitution separators + capital -R recursive flag (#2039) Two residual fail-opens in the same guard PR #2040 hardened, both verified by executing the script pre-fix: - rm -rf $(./wipe-all)/node_modules silently allowed: the substitution token ends in a whitelisted suffix and the safe-exception early exit skipped ALL downstream checks. $( and backtick now count as chain separators; plain $VAR expansion stays allowed. - rm -R / silently allowed: both greps required a lowercase r in the flag cluster; capital -R is the documented BSD/macOS recursive flag. Both greps now match -[a-zA-Z]*[rR]. Six new tests: substitution x2 -> ask, capital-R x2 -> ask, rm -Rf node_modules single-command -> still allowed, escaped-newline branch (existing code, previously untested), and a pinned deliberate FP (cd app && rm -rf node_modules -> ask) documenting the fail-closed direction on chains. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(context-restore): prefer the current branch's own checkpoint (#2052) All worktrees of a repo share one origin-derived slug, so they share one `~/.gstack/projects/<slug>/checkpoints/` dir. `/context-restore` loaded the newest checkpoint across the whole dir, so in one worktree it could silently restore a *sibling worktree's* newer checkpoint. Step 1 now orders candidates current-branch-first (read from each file's `branch:` frontmatter), keeping other branches as a fallback. A branch is checked out in at most one worktree, so this stops cross-worktree contamination while preserving Conductor cross-branch handoff: when the current branch has no checkpoint of its own, the full newest-first set is still used. - scan the 200 newest before partitioning so a current-branch checkpoint sitting below a burst of sibling saves is still found; output still capped at 20 - non-git / detached HEAD / branchless legacy saves fall back to the old newest-first behavior (back-compat) - +5 regression tests in context-save-hardening.test.ts (the #2052 bug case fails on the old pipeline); regenerated SKILL.md + proactive-suggestions.json Fixes #2052 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(gbrain): pass --confirm-destructive on drift re-register (#1985) ensureSourceRegistered() handles match-but-different-path by removing the old source then re-adding it at the new path. The remove was issued as `gbrain sources remove <id> --yes`, but gbrain >= 0.42 gates `sources remove` behind `--confirm-destructive` (`--yes` alone no longer suppresses the data-loss prompt). The remove therefore fails with "To proceed, pass --confirm-destructive", which ensureSourceRegistered surfaces as "source registration failed" — aborting the entire /sync-gbrain code stage for any already-registered source whose path has drifted. The memory and brain-sync stages still pass, so the code index silently stops refreshing. The orchestrator's own safeSourcesRemove() already passes --confirm-destructive; this brings the lib helper in line with that convention. Keeps --yes for older gbrain. Tests: extend the fake gbrain shim in gbrain-sources.test.ts to simulate the gbrain >= 0.42 guard (remove without --confirm-destructive exits 1), update the drift re-register assertion, and add a regression test that proves the drift path no longer throws. Both fail on main with the exact "To proceed, pass --confirm-destructive" error and pass with the fix. Fixes #1985 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * harden(gbrain-sources): route drift remove through #1734 guards + realpath drift check Absorbing #2031 un-blocked a destructive remove that bypassed the #1734 data-loss guards: ensureSourceRegistered's drift path issued `gbrain sources remove` directly, without the detectAutopilot + decideSourceRemove checks every other remove routes through via safeSourcesRemove. gbrain >= 0.42's own prompt was accidentally blocking that path; with --confirm-destructive passed it is live again. - Drift remove now refuses LOUDLY (throws, actionable message) while an autopilot is active or when decideSourceRemove disallows; a silent changed=false would hide the drifted registration. - decideSourceRemove's extraArgs (--keep-storage when supported) propagate to the remove call, matching safeSourcesRemove. - Drift is realpath-normalized before being declared: a symlink alias of the same directory (macOS /tmp -> /private/tmp) is a match, not drift — the probable cause of #1985's reporter hitting the remove on an unmoved repo. - Drift fires a loud stderr line (old -> new path); perpetual drift in logs is the trigger for promoting #1985's reindex-in-place design. Tests: autopilot-active refusal (no remove in call log), fail-closed refusal on unreadable sources list, --keep-storage propagation, symlink-alias no-drift; existing drift tests pin the guard probes so a live autopilot on the dev machine can't flip them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(developer-profile): exclude mode:resources rows from SESSION_COUNT, TIER, NUDGE_ELIGIBLE (#2067) Every /office-hours run appends a mode:"resources" bookkeeping row alongside the real session row, so --read double-counted sessions (~2x): tiers promoted early and the builder-to-founder nudge armed prematurely. The file already filtered resources rows for LAST_*/CROSS_PROJECT; the same realSessions filter now feeds SESSION_COUNT/TIER, and the nudge predicate is the faithful allowlist (mode === 'builder') so a future mode #4 fails closed instead of re-opening this bug. 8 regression tests: count vs resources noise, tier boundaries both sides, nudge false-with-noise / true-at-3-builders, cross-project trailing row. Absorbed from PR #1991 by @mvann (fix + tests commits; the PR's version-bump commit is superseded by this wave's consolidated release commit). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(hooks): passThrough() two-branch contract — never emit permissionDecision:'defer' (#2035, #2006) Every AskUserQuestion died with "Tool result missing due to internal error" on current Claude Code builds (Desktop 1.14271.0, CC 2.1.177). Root cause: the question-preference-hook emitted permissionDecision:'defer' on every pass-through path. 'defer' is a real PreToolUse value, but since CC v2.1.89 its semantics are "pause this tool call for external resumption" (headless resume) — never "abstain". Interactive sessions have nothing to resume the paused call, so the tool orphaned. Pre-2.1.89 builds ignored the unknown value, which is why the hook worked when it shipped and broke later. The fix is the two-branch pass-through contract: - no context -> exit 0 with EXACTLY empty stdout - memory nuggets present -> hookSpecificOutput with hookEventName + additionalContext ONLY (the documented shape; plan-tune Layer 8 memory injection ships through this branch and keeps working) defer() is renamed passThrough() so the function says what it does, and docs/spikes/claude-code-hook-mutation.md's protocol contract (cited by the hook header) is corrected in the same commit — it taught '"defer" — let permission flow continue' and was the reintroduction vector. Test contract rewritten in the same commit (13 assertions across 3 files, verified fail-first against the unfixed hook): pass-through paths assert exact-empty stdout (a garbage/partial write cannot slip past an optional-chained parse), the nugget path asserts permissionDecision is ABSENT while additionalContext survives, and a new tripwire asserts no non-deny path ever puts the string "permissionDecision" on stdout. The deny (auto-decide) and Conductor prose-redirect paths are unchanged. Deployment: no migration needed — settings.json points at the absolute bash shim which execs the .ts live; /gstack-upgrade delivers the fix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(one-way-doors): unify credential noun net + wire it into the runtime (#2024) Library fix: revoke/reset/rotate now share ONE noun alternation (api key, token, secret, credential, access key, password) with optional plural s?. Pre-fix leaks: "reset my secret", "reset my access key", "revoke my secret" (mismatched per-verb lists) and every plural form ("rotate the credentials", "revoke all tokens" — \b(...)\b cannot match a trailing s). Runtime wiring — the regexes could never fire in production before: - gstack-question-preference --check gains --summary-stdin: the question text pipes via stdin (never argv — summaries carry quotes/newlines/shell metacharacters) and feeds isOneWayDoor alongside the id, so an ad-hoc destructive question with a stored never-ask preference now forces ASK_NORMALLY. Empty/absent stdin keeps exact id-only semantics. - question-preference-hook falls back to classifyQuestion(question text) when the registry lookup misses, so unregistered destructive questions pass through to a human instead of auto-deciding. - question-tuning resolver prose shows the piped form (SKILL.md regen lands in the wave's release commit). Tripwires (verified fail-first): full verbs x nouns x singular/plural matrix with the #2024 repro rows, benign-summary no-over-match rows, stdin transport survival (quotes/newlines), empty-stdin fail-safe, and hook fallback both directions (destructive -> pass-through, benign -> deny). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(design): loud integer-flag contract for --count/--retry/--timeout (#2032) design variants --count abc silently generated ZERO variants and exited 0: parseInt(NaN) flowed through Math.min into the generation loop bound. The same NaN class was live on the two sibling flags in the same file: --retry abc made generate() a silent no-op (attempt <= NaN never true, null output, exit 0) and --timeout abc killed the serve board ~immediately (setTimeout(NaN)). New design/src/flag-utils.ts: parseIntFlag (pure, unit-testable) + normalizeIntFlag (CLI wrapper). Contract matches the --viewports precedent (error loudly on nonsense — these commands spend real image-API money, a silent fixup hides typos from calling agents): undefined -> default; bare flag/empty/non-integer ("3.7" rejected, not truncated)/below-min -> exit 1 with usage hint; above-max -> clamp with stderr warning. --count normalizes at the variants() consumption site so programmatic callers are covered, with the ceiling derived from STYLE_VARIATIONS.length instead of a magic 7; the CLI passes the raw flag through (a pre-parseInt would truncate "3.7"). Tripwires live in test/design-flag-utils.test.ts — deliberately under test/, not design/test/, which is invisible to the bun test glob, TEST_ROOTS, and every workflow (wiring design/test/ into CI is a captured TODO). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(gbrain): thin-client state — remote-MCP brains no longer classify as broken-config (#2051) A thin client (remote-HTTP MCP brain, no local engine by design) probed `gbrain sources list`, which gbrain's dispatch guard REFUSES on thin clients (exit 1, no recognized error string), so the classifier fell to its defensive broken-config default and every suppression gate silently hid brain-aware blocks from exactly the users on a shared team brain. New 'thin-client' state, detected PRE-probe from gbrain's own remote_mcp config marker via the existing gbrainConfigPath() helper (mirrors gbrain's isThinClient(); honors GBRAIN_HOME; zero network, immune to error-string drift), with a /thin[- ]client/ stderr backstop in the probe catch. Remote reachability is deliberately NOT probed by the classifier — that is the #1964 pathology; gbrain calls degrade gracefully at use time, and the detect JSON says so honestly (gbrain_thin_client: {probed: false}). The state is admitted at every suppression gate — gstack-gbrain-detect --is-ok (drives setup + gbrain-refresh), gen-skill-docs' detection override, gstack-config gbrain-refresh — while the sync stages (code/memory/dream) SKIP with an accurate reason: code indexing runs on the brain server, memory syncs via the remote brain's artifacts pull. The two consumer classes need opposite answers, which is why this is a distinct state and not a skip-the-probe special case. sync-gbrain Step 1.5 and setup-gbrain prose route thin-client to proceed, never into broken-config remediation. detectMcpMode secondary generalization: url-match against the config's remote_mcp.mcp_url (deterministic — gbrain mounts at the generic /mcp path) -> name pattern gbrain[-_]* -> stdio command token; gbrain_mcp_mode stays a 3-value enum. Tripwires: end-to-end --is-ok exits 0 on a thin-client fixture AND still exits 1 on broken-config (the gate didn't widen); pre-probe + stderr-fallback classifier paths; 4 detectMcpMode identification cases incl. a non-matching url that must NOT false-positive. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * release: v1.60.0.0 — regen SKILL.md, VERSION, CHANGELOG, TODOS follow-ups - Regenerate all SKILL.md from templates (question-tuning --summary-stdin prose from #2024, context-restore branch preference from PR #2054, sync-gbrain/setup-gbrain thin-client prose from #2051) + llms.txt. - VERSION + package.json -> 1.60.0.0 (bin/gstack-next-version, queue-aware: #1815 claims 1.59.0.0, #2213 claims 1.59.1.0). - CHANGELOG release summary + itemized entry crediting @jbetala7 (x3) and @mvann. - TODOS.md: three eng-review follow-ups (design/test CI wiring + documented pre-existing retry-after flake, /context-save worktree identity, gbrain reindex-in-place conditional on the new drift log). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(resolvers): compress --summary-stdin preamble prose to fit parity budget; re-bless ship goldens The v1.57.7.0 parity suite caps investigate's generated size at 1.09x baseline; the #2024 question-tuning prose (duplicated into every tier->=2 skill) tipped it to 1.092. Compressed to a single inline command + short pointer (the full rationale lives in bin/gstack-question-preference's header and the one-way-doors module docs). Ship goldens re-blessed against the final resolver text (conscious template-change acknowledgment, per the golden-file regression contract). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(e2e): office-hours-spec-review turn budget fits the carved skill layout (#2473) The test failed deterministically with error_max_turns at 9 turns on main and this branch alike (CI attempt logs + local main repro). Root cause from the failing transcript: the Spec Review Loop content is carved out of office-hours/SKILL.md into office-hours/sections/, so the agent needs discovery hops (grep SKILL.md -> ls sections/ -> read the section) before it can write — 8 tool turns + the closing text turn = 9 > the 8-turn budget, which predates the carve. Observed failures wrote a CORRECT summary on tool turn 8 and died on the closing turn. maxTurns 8 -> 12. Verified: PASS locally post-fix (7 turns this run — the extra headroom absorbs discovery-path nondeterminism). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(e2e): review-dashboard-via session budget survives runner contention (#2473) The test failed on CI (and its baseline run) with the timeout signature: 0 turns, $0.00, exactly 183s, 3/3 attempts — the spawned claude -p session never emitted a single stream event before the 180s inner timeout. The file's tests run concurrently on one runner; session startup queues behind sibling sessions, and this test had the tightest budget in the file (the 240s-budget tests in the same job passed). A clean local run takes 270s wall for 4 turns, confirming 180s was too tight even without contention. Inner timeout 180s -> 300s; outer bun timeout 240s -> 360s to keep headroom over the inner budget. Verified: PASS locally post-fix (4 turns, 270s). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(e2e): retro-base-branch session budget survives runner contention (#2473) Same class as review-dashboard-via, one test over in the same file: /retro is a long multi-step flow whose clean pass measures 225-239s — a coin flip against the 240s inner budget. First CI run passed at 225s; the rerun timed out at the 240s line on all 3 attempts (exitReason "timeout"); the local verification run passed at 239s, ONE second under the old cap. Inner timeout 240s -> 360s; outer bun timeout 300s -> 480s for headroom. Verified: PASS locally post-fix (17 turns, 239s). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Jayesh Betala <jayesh.betala7@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Michael Vann <9221873+mvann@users.noreply.github.com> |
||
|
|
0570ef93a5 |
v1.24.0.0 feat: cross-platform hardening — curated Windows lane + Bun.which resolver + path-portability helper (#1252)
* feat(paths): bin/gstack-paths helper + migrate 8 skills off inline state-root chains
New bin/gstack-paths emits GSTACK_STATE_ROOT, PLAN_ROOT, TMP_ROOT exports for
skill bash blocks to source via eval. Honors GSTACK_HOME → CLAUDE_PLUGIN_DATA →
$HOME/.gstack → .gstack (and parallel chains for plan/tmp roots) so skills work
the same in plugin installs, global installs, and CI containers without HOME.
Eight skills migrate off inline ${CLAUDE_PLUGIN_DATA:-...} or ${GSTACK_HOME:-...}
chains: careful, freeze, guard, unfreeze, investigate, context-save,
context-restore, learn, office-hours, plan-tune, codex. Resolved values are
identical, so existing tests cover correctness; the win is consolidating 11
copy-pasted fallback chains behind one helper.
codex/SKILL.md.tmpl gets a new Step 0.6 Resolve portable roots that sources
gstack-paths once, then replaces hardcoded ~/.claude/plans/*.md and
/tmp/codex-*-XXXXXX.txt with "$PLAN_ROOT"/*.md and "$TMP_ROOT/codex-*-XXXXXX.txt".
Hardening direction credited to the McGluut/gstack fork; this is upstream's
factoring of the per-skill chain the fork inlined.
Tests: test/gstack-paths.test.ts covers all three fallback chains with 8 unit
tests (HOME unset, CLAUDE_PLUGIN_DATA set, GSTACK_HOME wins, etc).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(claude-bin): Bun.which wrapper for cross-platform claude resolution
Replaces 75 LOC of fork-side reimplementation (PATH parsing, Windows PATHEXT,
case-insensitive Path/PATH, X_OK) with a thin wrapper around Bun.which() — the
runtime built-in that already does all of it. New file is ~70 LOC including
the override + arg-prefix logic the runtime doesn't cover.
Override branch fixed: GSTACK_CLAUDE_BIN=wsl now resolves through Bun.which()
just like a bare claude lookup would. The McGluut fork's claude-bin.ts only
handled absolute-path overrides; bare commands silently returned null. Passing
the override value through Bun.which fixes the documented use case for free.
Five hardcoded claude spawn sites rewired through resolveClaudeCommand:
- browse/src/security-classifier.ts:396 — version probe
- browse/src/security-classifier.ts:496 — Haiku transcript classifier
- scripts/preflight-agent-sdk.ts — preflight binary pinning
- test/helpers/providers/claude.ts — LLM judge availability + run
- test/helpers/agent-sdk-runner.ts — SDK harness binary resolver
All retain their existing degrade-on-missing semantics.
Tests: browse/test/claude-bin.test.ts has 9 unit tests including the
override-PATH-resolution case the fork's version got wrong.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs+test: AGENTS.md/docs/skills.md inventory sync + private-path leak detector
Inventory sync (codex-flagged drift):
- /debug → /investigate (skill renamed in v1.0.1.0)
- AGENTS.md grows from 21 to 40+ skills, organized by category (plan reviews,
implementation, release, operational, browser, safety)
- docs/skills.md gains 11 missing entries: /plan-devex-review, /devex-review,
/plan-tune, /context-save, /context-restore, /health, /landing-report,
/benchmark-models, /pair-agent, /setup-gbrain, /make-pdf
- Stale "<5s bun test" claim dropped — slim-preamble harness + new tests means
no realistic universal claim to make
- Adds explicit "Mac + Linux full, curated Windows lane" platform statement +
"Git Bash / MSYS today, native PowerShell future" install note
New invariants in test/skill-validation.test.ts (~80 LOC):
- Private-path leak detector scans every SKILL.md / SKILL.md.tmpl for known
maintainer-only filenames (coordination-board.md, SEEKING_LOG.md,
RATIONAL_SUBJECT.md, VALUE_SIGNAL_LOOP.md, C:\LLM Playground\go).
Adapted from the McGluut fork's skill-contract-audit.ts; we don't take
the script wholesale because most of its checks are already covered by
test/gen-skill-docs.test.ts:1668-2074 and test/skill-validation.test.ts:1419
— only the private-path scan and doc-inventory cross-check are new.
- Doc-inventory cross-check: every skill directory with a SKILL.md.tmpl must
appear in both AGENTS.md and docs/skills.md. Catches the inventory drift
this commit is fixing — without this test it would just drift again.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(windows): curated windows-free-tests CI job + test-free-shards curation
Codex's v1.18.0.0 review flagged that a windows-latest matrix entry on the
existing Linux-container evals.yml workflow can't work as a drop-in, and that
the free test suite has POSIX-bound dependencies a sharded runner doesn't fix
on its own. This commit takes McGluut's test-free-shards.ts (190 LOC), adds a
Windows-fragility scan, and runs the curated subset on a separate non-container
windows-latest job.
scripts/test-free-shards.ts:
- Enumeration + paid-eval filtering + stable-hash sharding (FNV-1a). Adapted
from McGluut/gstack fork.
- Upstream-original: --windows-only filter scans each test's content for
POSIX-bound patterns: hardcoded /bin/sh, spawn('sh', ...), bash -c, raw
/tmp/, chmod, xargs, which claude. Files matching are excluded with the
reason logged. Currently filters 25 of 128 free tests; remaining 103 run
on windows-latest.
.github/workflows/windows-free-tests.yml:
- Separate non-container job (NOT a matrix entry on evals.yml). Runs:
bun run test:windows # curated subset
bun test browse/test/claude-bin.test.ts # PATHEXT+overrides on Windows
bun test test/gstack-paths.test.ts # state-root resolution
package.json: new test:free + test:windows scripts.
Honest about scope (codex-flagged): this does NOT make the full free suite
Windows-safe. The 25 excluded tests need POSIX-only surfaces ported off shell
primitives (test/ship-version-sync.test.ts:72 hardcodes /bin/bash, etc).
Tracked as a P4 follow-up TODO. Full Windows parity is the next wave; this
release ships the curated lane.
Tests: test/test-free-shards.test.ts has 14 unit tests covering enumeration,
paid-eval filtering, Windows-fragility detection (POSIX patterns + safe code),
and stable sharding determinism.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(release): v1.20.0.0 — cross-platform hardening, curated Windows lane
Cross-platform hardening. Mac + Linux full, curated Windows lane added.
Workspace-aware queue at ship time:
- v1.17.0.0 claimed by garrytan/setup-gbrain-run (PR #1234)
- v1.19.0.0 claimed by garrytan/browserharness (PR #1233)
- This branch claims v1.20.0.0 (next available slot)
(Initially bumped to v1.18.0.0 during plan-mode implementation; rebumped to
v1.20.0.0 at /ship time when gstack-next-version detected the queue had moved.)
Headline numbers (full release-note in CHANGELOG.md):
- 2 new shared resolvers: bin/gstack-paths (61 LOC), browse/src/claude-bin.ts (73 LOC)
- 8 skills migrated off inline state-root chains
- 5 hardcoded claude spawn sites rewired through the shared resolver
- 75 LOC of fork-side reimplementation replaced by Bun.which()
- 103 of 128 free tests run on windows-latest (curated, ~80%)
- +31 new unit tests + 3 new invariants
- AGENTS.md inventory grows from 21 to 40+ skills
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(windows-ci): configure git identity + extend Windows-fragility curation
First windows-free-tests CI run surfaced 34 failures across two patterns:
1. Tests that init a temp git repo via execSync('git commit ...') — Windows
runner has no default git user.email/user.name, so the commit fails.
Fix: add a "Configure git identity" step to .github/workflows/windows-free-tests.yml
that sets a CI-only identity globally.
2. Tests that use POSIX-only APIs unconditionally:
- file-mode bitmask checks (`stat.mode & 0o600`, `mode & 0o111`) — Windows
fakes mode bits and these assertions don't compose
- hardcoded forward-slash path assertions (`file.endsWith('/tab-42.json')`)
— Windows path separators are '\\'
Fix: extend WINDOWS_FRAGILE_PATTERNS in scripts/test-free-shards.ts to
detect both. 8 additional tests now excluded from the curated Windows
subset with logged reasons:
- browse/test/security-review-flow.test.ts (file mode)
- browse/test/security-sidepanel-dom.test.ts (forward-slash path)
- browse/test/url-validation.test.ts (forward-slash path)
- test/gbrain-repo-policy.test.ts (file mode)
- test/relink.test.ts (file mode)
- test/skill-validation.test.ts (file mode — single assertion at :934)
- test/team-mode.test.ts (file mode — also kills its 30 git-init beforeEach failures)
- test/upgrade-migration-v1.test.ts (file mode)
Curated Windows subset: 103 → 95 tests (still ~74% of free suite). All
14 test-free-shards unit tests still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(windows-ci): enforce LF + build server-node.mjs in CI
Second round of windows-free-tests fixes after the first push. Curated subset
went from 386/34 to 58/4 fails. Remaining 4 fails + 1 error trace to two root
causes:
1. Line-ending sensitivity. Windows checkout with core.autocrlf=true converts
.md/.tmpl files to CRLF. Tests that parse YAML frontmatter with
`/^---\n([\\s\\S]+?)\n---/` then return zero matches — skill-collision-
sentinel.test.ts:120 enumerated 0 skills on Windows, cascading into 3
downstream test failures (sanity, KNOWN_COLLISIONS, /checkpoint resolved).
Fix: add .gitattributes that pins LF for .md/.tmpl/.yml/.json/.toml/.sh/
.ts/.tsx/.js/.mjs/.cjs/.bash. Root-cause fix; prevents future similar
tests from hitting the same trap. Also keeps bash scripts LF on Linux
runners (CRLF in shebangs produces "bad interpreter" errors).
2. Module-level Windows assertion in browse/src/cli.ts:82 throws if
browse/dist/server-node.mjs is missing. Any test that transitively loads
cli.ts (e.g., browse/test/tab-isolation.test.ts via shard mate imports)
then fails to even start. server-node.mjs is generated by bash
browse/scripts/build-node-server.sh, which `bun run build` calls but
`bun install` does not.
Fix: add a "Build server-node.mjs" step to .github/workflows/
windows-free-tests.yml. Calls only the node-server build script, not
full `bun run build` — we don't need the compiled binaries for tests
and the full build is slow.
Expected: skill-collision-sentinel goes 0→3 pass (sanity, KNOWN_COLLISIONS,
/checkpoint resolved). tab-isolation's "unhandled error between tests"
disappears. Remaining tests should be green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(windows-ci): platform-aware claude-bin test + curate bin/ shebang spawns
Round 3 of windows-free-tests fixes. Round 2 (LF gitattributes + server-node.mjs
build) cleared shard 1 entirely (skill-collision-sentinel and tab-isolation
green). Shard 2 surfaced two more issues:
1. browse/test/claude-bin.test.ts:50 — the "PATH-resolvable override" test
creates a fake binary 'fake-claude-cli' (no extension) and expects
Bun.which to find it. On Windows, Bun.which probes PATHEXT extensions
(.cmd, .exe, .bat) — a bare-name file is not discoverable. Production
behavior is correct; the test was Mac/Linux-shaped.
Fix: branch on process.platform. On Windows, write 'fake-claude-cli.cmd'
with a Windows batch payload instead of a POSIX shebang script.
2. test/gstack-question-log.test.ts (and 18 sibling tests) — spawn a bash
shebang script via spawnSync(BIN, args). Git Bash on Windows can run
`bash /path/to/script` but spawnSync invokes CreateProcess directly,
which doesn't parse #!/usr/bin/env bash. All these tests are
Windows-fragile and can't run as-is.
Fix: extend WINDOWS_FRAGILE_PATTERNS with `path.join(.., 'bin', ..)`
detector. Curates 19 additional tests (benchmark-cli, brain-sync,
builder-profile, explain-level-config, gbrain-*, gstack-question-*,
hook-scripts, learnings, plan-tune, review-log, secret-sink-harness,
taste-engine, telemetry, timeline, uninstall).
Curated Windows subset: 95 → 76 tests (~59% of free suite). Still
meaningful Windows coverage. The 52 excluded tests are tracked as a
follow-up TODO for full Windows parity (shebang-bin spawns + POSIX file
modes + raw /tmp/ etc).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(windows-ci): curate Playwright-launching tests
Round 4 of windows-free-tests fixes. Round 3 cleared shard 2 except for
browse/test/batch.test.ts:35 which calls `await bm.launch()` and triggers
Playwright Chromium launch. The windows-latest runner doesn't have
Chromium installed (browser bring-up is a separate concern, tracked by
PR #1238 windows-pty-bun-pty-fix).
Fix: extend WINDOWS_FRAGILE_PATTERNS with `await \\w+\\.launch\\(` matcher.
Catches batch.test.ts plus 7 sibling tests (commands, compare-board,
content-security, handoff, security-live-playwright, security-sidepanel-dom,
snapshot — most already excluded by other patterns).
Curated Windows subset: 76 → 72 tests (~56% of free suite). Net curation
across all 4 rounds: 56 of 128 free tests excluded, each with a logged
reason. The 56 excluded fall into 6 buckets — POSIX shells, raw /tmp/,
chmod/xargs, file mode bitmasks, forward-slash path assertions, bin/
shebang spawns, and Playwright launches — all tracked as a P4 follow-up
TODO for full Windows parity.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(windows-ci): catch destructured join() bin-spawns + browse server tests
Round 5 of windows-free-tests fixes. Round 4 caught Playwright launchers
but two more failure shapes appeared in shard 5:
1. test/diff-scope.test.ts uses `import { join }` (destructured) and
`join(import.meta.dir, '..', 'bin', 'gstack-diff-scope')`. My round-3
pattern only matched `path.join(...)` — the destructured form slipped
through. Tightened the pattern to match the literal `, 'bin', '<name>'`
path-segment shape regardless of whether it's `path.join` or `join`
directly.
2. browse/test/sidebar-integration.test.ts spawns the browse server via
`spawn(['bun', 'run', server.ts])` with BROWSE_HEADLESS_SKIP=1. The
Bun-run-server.ts path is the same Playwright-on-Windows broken path
that the windows-free-tests job intentionally avoids — the server-node.mjs
route only kicks in for the compiled binary, not direct Bun runs of the
TypeScript source. Added a BROWSE_HEADLESS_SKIP / spawn-bun-run pattern.
Curated Windows subset: 72 → 73 tests (~57% of free suite). Net up by 1
because the tightened bin pattern released one test that was a false
positive in the loose `path\\.join` form.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(windows-ci): broaden bin/ pattern to match path.join(ROOT, 'bin')
Round 6. Round 5 tightened the bin/ pattern to require a script-name segment
after 'bin', which inadvertently released test/brain-sync.test.ts that uses:
const BIN = path.join(ROOT, 'bin');
const full = bin.startsWith('/') ? bin : path.join(BIN, bin);
The 'bin' segment is the LAST argument to path.join — there's no literal
script name to match. The earlier looser pattern caught this; round 5
broke that.
Fix: revert to `,\\s*['"]bin['"]\\s*[,)]` which matches both forms:
- `, 'bin', 'script-name')` (path.join with name) — typical
- `, 'bin')` (path.join ending at bin) — brain-sync style
Curated subset: 73 → 66 tests (~52% of free suite). The 7 additional
exclusions are all bin-script tests that were misclassified by the round-5
tightening.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(find-browse): guard main() with import.meta.main
Round 7 of windows-free-tests fixes (and a genuine bug fix beyond Windows).
browse/src/find-browse.ts called main() unconditionally at module load.
main() calls process.exit(1) when no compiled `browse` binary exists at the
known install paths. Any test that imports `locateBinary` from this module
then exits the entire test process before any tests run.
This affected the windows-free-tests CI lane because the runner intentionally
doesn't compile the browse binary (only server-node.mjs is built — full
binary compilation is slow and not needed for the curated subset). It would
also affect any Mac/Linux contributor who runs tests in a fresh checkout
before running ./setup, though the symptom is rarer there.
Fix: wrap `main()` in `if (import.meta.main) { main() }`. The CLI invocation
(via the find-browse binary or `bun run browse/src/find-browse.ts`) still
runs main() and emits the path. Imports get only the named exports.
Verified locally:
- `bun run browse/src/find-browse.ts` still prints the binary path.
- `import { locateBinary } from '...'` no longer exits the process.
- `bun test browse/test/find-browse.test.ts` passes 4/4 (was crashing
at module load).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(windows-ci): pin LF on extensionless executables (setup, bin/*, scripts/*)
Round 8 of windows-free-tests fixes. Round 7 cleared find-browse + most
shards; one fail left in shard 7:
test/setup-codesign.test.ts > codesign shell snippet is syntactically valid
expect(received).toBeTruthy() — match was null
The test extracts a bash codesign block from the `setup` file via a
\\n-anchored regex, then syntax-checks it with `bash -n`. On Windows the
regex returned null because the `setup` file was checked out with CRLF
endings — my round-2 .gitattributes only covered files matched by extension
patterns (*.md, *.sh, *.ts) and `setup` is extensionless.
Fix: extend .gitattributes with explicit rules for extensionless executables:
setup text eol=lf
bin/* text eol=lf
**/scripts/* text eol=lf
This also LF-pins all the bash bin/ scripts (gstack-paths, gstack-slug,
gstack-codex-probe, ...) which would otherwise break with "bad interpreter"
errors on Linux if a Windows contributor accidentally committed CRLF
versions. Defense in depth.
Verified locally: `git check-attr eol setup bin/gstack-paths` reports
`eol: lf` for both. Renormalized via `git add --renormalize` so any
already-LF files in the repo stay LF after the .gitattributes change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(windows-ci): gen:skill-docs in workflow + known-bad list for env-specific tests
Round 9 of windows-free-tests fixes. Round 8 cleared shard 7; shard 8
surfaced 4 fails:
1+2. test/gen-skill-docs.test.ts golden-file regression for Codex + Factory
ship skills failed with ENOENT on `.agents/skills/gstack-ship/SKILL.md`
and `.factory/skills/gstack-ship/SKILL.md`. These are gitignored
gen-skill-docs outputs that the Mac/Linux CI workflows already
regenerate elsewhere — the windows-free-tests lane never did.
Fix: add `bun run gen:skill-docs --host all` step to
windows-free-tests.yml after `bun install`.
3. test/host-config.test.ts:377 "detect finds claude" asserts the `claude`
binary is on PATH. True when running inside Claude Code; false on a
bare CI runner.
4. browse/test/findport.test.ts:117 asserts Bun.serve.stop() is
fire-and-forget (returns undefined). Bun's Windows behavior for this
polyfill differs; the assertion is Bun-on-non-Windows-specific.
Both 3 and 4 are environment/runtime-specific failures that don't fit a
regex pattern. Added a KNOWN_WINDOWS_INCOMPATIBLE explicit list to
scripts/test-free-shards.ts so they're curated by exact path, with a
reason string. The list is for cases where pattern matching can't infer
the failure shape from the source file alone.
Curated subset: 66 → 64 tests (~50% of free suite). 14 unit tests in
test/test-free-shards.test.ts still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(windows-ci): curate pre-existing breakage from v1.14.0.0 sidebar refactor
Round 10 of windows-free-tests fixes. Round 9 cleared shards 7+8; shard 9
surfaced ENOENT for browse/src/sidebar-agent.ts. That file was DELETED in
v1.14.0.0 (sidebar REPL refactor — sidebar-agent.ts and the chat queue
path were ripped in favor of the interactive xterm.js PTY). 10 security
tests still reference it via top-level fs.readFileSync and fail on import.
Verified locally: `bun test browse/test/security-source-contracts.test.ts`
on this branch reports 0 pass, 1 fail, 1 error. Mac/Linux CI exits 0
because Bun reports module-load failures as "error" not "fail" and the
exit code is 0; Windows CI exits 1 (stricter). Same pre-existing
breakage on every platform — just only visible in shard 9 of the
Windows lane.
Fix: add WINDOWS_FRAGILE_PATTERNS entry matching `sidebar-agent.ts` /
`src/sidebar-agent` references. Curates browse/test/sidebar-ux.test.ts
(other 9 likely caught by paid-eval filter or earlier patterns).
Tracked as a follow-up TODO: update or delete the 10 security tests that
reference deleted source. Out of scope for v1.20.0.0 portability wave.
Curated subset: 64 → 63 tests (~49% of free suite).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(windows-ci): broaden sidebar-agent.ts pattern to catch all references
* fix(windows-ci): catch ./bin/<name> direct path spawns
* fix(windows-ci): scope Windows job to v1.20.0.0 new portability work
12 rounds of curation revealed that gstack has a long tail of tests with
environment-specific assumptions (POSIX paths, /tmp, mode bits, bash
spawns, deleted v1.14 sidebar refs, HOME=unset guards, Bun polyfill
specifics). Each round of pattern-matching curation caught 1-2 new
buckets but kept surfacing more.
Honest scope for v1.20.0.0: this PR delivers two new portability
primitives (bin/gstack-paths + browse/src/claude-bin.ts). The Windows
CI job should verify those primitives work on Windows. Full-suite
Windows parity is a P4 follow-up that requires touching many tests
that aren't part of this PR's scope.
Change: windows-free-tests.yml now runs:
bun test test/gstack-paths.test.ts \\
browse/test/claude-bin.test.ts \\
test/test-free-shards.test.ts
That's 31 tests targeting exactly the new code paths shipped here.
The release-note headline ("curated Windows lane added") becomes
truthful when this passes — we have a real Windows CI gate on the
new portability work, not a rebadged failure-tolerant attempt at the
full suite.
Retained: scripts/test-free-shards.ts curation logic (informational
output via `--list`, useful for future expansion of the Windows lane
when contributors port specific tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(test): invoke bin/gstack-paths via bash (Windows shebang fix)
Round 13 of windows-free-tests fixes. Round 12 (scope pivot) revealed all
8 gstack-paths tests fail on Windows because the test invokes the bash
shebang script directly:
spawnSync(BIN, []) # BIN = path.join(ROOT, 'bin', 'gstack-paths')
Windows CreateProcess can't parse `#!/usr/bin/env bash` from the file.
The script never runs on Windows via this invocation path.
Fix: change to `spawnSync('bash', [BIN], ...)`. This matches production
usage — the script is sourced from inside skill bash blocks via
`eval "$(~/.claude/skills/gstack/bin/gstack-paths)"`, where bash is
always the executor. Mac/Linux behavior is identical (bash invocation
of a bash script).
Verified locally: 8/8 tests still pass on macOS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(release): rebump v1.20.0.0 → v1.22.0.0 (queue drift)
Version-gate workflow rejected v1.20.0.0 because the queue moved during
the windows-free-tests fix loop:
v1.16.0.0 → garrytan/gbrowser-unleashed (PR #1253) [new since last bump]
v1.17.0.0 → garrytan/setup-gbrain-run (PR #1234)
v1.19.0.0 → garrytan/browserharness (PR #1233)
v1.21.1.0 → garrytan/pty-plan-mode-e2e (PR #1255) [new since last bump]
Two new sibling PRs landed slot claims while we iterated on Windows.
Next free MINOR slot is v1.22.0.0.
Updated VERSION, package.json, CHANGELOG header + body. Also pushing the
round-13 windows-fix in parallel (test invokes bin/gstack-paths via bash
to handle Windows shebang).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(test): clear USERPROFILE alongside HOME (Git Bash auto-populates HOME)
Final Windows fix. 29/31 pass; 2 fail in gstack-paths HOME-unset tests:
(fail) CWD fallback when HOME also unset (container env)
(fail) PLAN_ROOT chain: GSTACK_PLAN_DIR > CLAUDE_PLANS_DIR > HOME > CWD
Root cause: Git Bash on Windows auto-populates `HOME` from `USERPROFILE`
at shell startup if HOME is empty/unset. Passing `HOME: ''` to spawnSync
does set HOME='' for the child, but Git Bash overwrites it from
USERPROFILE during init, so the script sees `${HOME:-}` as non-empty
(C:\\Users\\runneradmin) and never reaches the CWD-fallback branch.
Fix: clear USERPROFILE='' too. On Linux/Mac it's a no-op (env var doesn't
exist in normal env); on Windows Git Bash it stops the HOME auto-populate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(test): skip HOME-unset assertions on Windows (Git Bash auto-populates)
29/31 → 31/31 expected on Windows. Final fix:
The 2 still-failing gstack-paths tests assert CWD-fallback behavior when
HOME is genuinely unset (Linux container scenario). On Windows Git Bash,
HOME gets auto-derived from USERPROFILE → HOMEDRIVE+HOMEPATH → /c/Users/<user>
during shell startup. Clearing all three of those env vars in the spawn
still results in HOME being non-empty by the time the script runs.
The bash script's CWD-fallback logic IS correct — it just isn't exercisable
through the Git Bash test surface. Skip those specific assertions on
Windows; they continue to verify on Linux/Mac.
This is the only platform-specific test guard introduced; it's narrowly
scoped to the unreachable code path, not a bypass of the real check.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
12260262ea |
fix(checkpoint): rename /checkpoint → /context-save + /context-restore (v1.0.1.0) (#1064)
* rename /checkpoint → /context-save + /context-restore (split) Claude Code ships /checkpoint as a native alias for /rewind (Esc+Esc), which was shadowing the gstack skill. Training-data bleed meant agents saw /checkpoint and sometimes described it as a built-in instead of invoking the Skill tool, so nothing got saved. Fix: rename the skill and split save from restore so each skill has one job. Restore now loads the most recent saved context across ALL branches by default (the previous flow was ambiguous between mode="restore" and mode="list" and agents applied list-flow filtering to restore). New commands: - /context-save → save current state - /context-save list → list saved contexts (current branch default) - /context-restore → load newest saved context across all branches - /context-restore X → load specific saved context by title fragment Storage directory unchanged at ~/.gstack/projects/$SLUG/checkpoints/ so existing saved files remain loadable. Canonical ordering is now the filename YYYYMMDD-HHMMSS prefix, not filesystem mtime — filenames are stable across copies/rsync, mtime is not. Empty-set handling in both restore and list flows uses find+sort instead of ls -1t, which on macOS falls back to listing cwd when the input is empty. Sources for the collision: - https://code.claude.com/docs/en/checkpointing - https://claudelog.com/mechanics/rewind/ * preamble: split 'checkpoint' routing rule into context-save + context-restore scripts/resolvers/preamble.ts:238 is the source of truth for the routing rules that gstack writes into users' CLAUDE.md on first skill run, AND gets baked into every generated SKILL.md. A single 'invoke checkpoint' line points at a skill that no longer exists. Replace with two lines: - Save progress, save state, save my work → invoke context-save - Resume, where was I, pick up where I left off → invoke context-restore Tier comment at :750 also updated. All SKILL.md files regenerated via bun run gen:skill-docs. * tests: split checkpoint-save-resume into context-save + context-restore E2Es Renames the combined E2E test to match the new skill split: - checkpoint-save-resume → context-save-writes-file Extracts the Save flow from context-save/SKILL.md, asserts a file gets written with valid YAML frontmatter. - New: context-restore-loads-latest Seeds two saved-context files with different YYYYMMDD-HHMMSS prefixes AND scrambled filesystem mtimes (so mtime DISAGREES with filename order). Hand-feeds the restore flow and asserts the newer- by-filename file is loaded. Locks in the "newest by filename prefix, not mtime" guarantee. touchfiles.ts: old 'checkpoint-save-resume' key removed from both E2E_TOUCHFILES and E2E_TIERS maps; new keys added to both. Leaving a key in one map but not the other silently breaks test selection. Golden baselines (claude/codex/factory ship skill) regenerated to match the new preamble routing rules from the previous commit. * migration: v0.18.5.0 removes stale /checkpoint install with ownership guard gstack-upgrade/migrations/v0.18.5.0.sh removes the stale on-disk /checkpoint install so Claude Code's native /rewind alias is no longer shadowed. Ownership guard inspects the directory itself (not just SKILL.md) and handles 3 install shapes: 1. ~/.claude/skills/checkpoint is a directory symlink whose canonical path resolves inside ~/.claude/skills/gstack/ → remove. 2. ~/.claude/skills/checkpoint is a directory containing exactly one file SKILL.md that's a symlink into gstack → remove (gstack's prefix-install shape). 3. Anything else (user's own regular file/dir, or a symlink pointing elsewhere) → leave alone, print a one-line notice. Also removes ~/.claude/skills/gstack/checkpoint/ unconditionally (gstack owns that dir). Portable realpath: `realpath` with python3 fallback for macOS BSD which lacks readlink -f. Idempotent: missing paths are no-ops. test/migration-checkpoint-ownership.test.ts ships 7 scenarios covering all 3 install shapes + idempotency + no-op-when-gstack-not-installed + SKILL.md-symlink-outside-gstack. Critical safety net for a migration that mutates user state. Free tier, ~85ms. * docs: bump VERSION to 0.18.5.0, CHANGELOG + TODOS entry User-facing changelog leads with the problem: /checkpoint silently stopped saving because Claude Code shipped a native /checkpoint alias for /rewind. The fix is a clean rename to /context-save + /context-restore, with the second bug (restore was filtering by current branch and hiding most recent saves) called out separately under Fixed. TODOS entry for the deferred lane feature points at the existing lane data model in plan-eng-review/SKILL.md.tmpl:240-249 so a future session can pick it up without re-discovering the source. * chore: bump package.json to 0.18.5.0 (match VERSION) * fix(test): skill-e2e-autoplan-dual-voice was shipped broken The test shipped on main in v0.18.4.0 used wrong option names and wrong result fields throughout. It could not have passed in any environment: Broken API calls: - `workdir` → should be `workingDirectory` The fixture setup (git init, copy autoplan + plan-*-review dirs, write TEST_PLAN.md) was completely ignored. claude -p spawned with undefined cwd instead of the tmp workdir. - `timeoutMs: 300_000` → should be `timeout: 300_000` Fell back to default 120s. Explains the observed ~170s failure (test harness overhead + retry startup). - `name: 'autoplan-dual-voice'` → should be `testName: 'autoplan-dual-voice'` No per-test run directory was created. - `evalCollector` → not a recognized `runSkillTest` option at all. Broken result access: - `result.stdout + result.stderr` → SkillTestResult has neither field. `out` was literally "undefinedundefined" every time. - Every regex match fired false. All 3 assertions (claudeVoiceFired, codex-or-unavailable, reachedPhase1) failed on every attempt. - `logCost(result)` → signature is `logCost(label, result)`. - `recordE2E('autoplan-dual-voice', result)` → signature is `recordE2E(evalCollector, name, suite, result, extra)`. Fixes: - Renamed all 4 broken options in the runSkillTest call. - Changed assertion source to `result.output` plus JSON-serialized `result.transcript` (broader net for voice fingerprints in tool inputs/outputs). - Widened regex alternatives: codex voice now matches "CODEX SAYS" and "codex-plan-review"; Claude voice now matches subagent_type; unavailable matches CODEX_NOT_AVAILABLE. - Added Agent + Skill + Edit + Grep + Glob to allowedTools. Without Agent, /autoplan can't spawn subagents and never reaches Phase 1. - Raised maxTurns 15 → 30 (autoplan is a long multi-phase skill). - Fixed logCost + recordE2E signatures, passing `passed:` flag into recordE2E per the neighboring context-save pattern. * security: harden migration + context-save after adversarial review Adversarial review (Claude + Codex, both high confidence) identified 6 critical production-harm findings in the /ship pre-landing pass. All folded in. Migration v1.0.1.0.sh hardening: - Add explicit `[ -z "${HOME:-}" ]` guard. HOME="" survives set -u and expands paths to /.claude/skills/... which could hit absolute paths under root/containers/sudo-without-H. - Add python3 fallback inside resolve_real() (was missing; broken symlinks silently defeated ownership check). - Ownership-guard Shape 2 (~/.claude/skills/gstack/checkpoint/). Was unconditional rm -rf. Now: if symlink, check target resolves inside gstack; if regular dir, check realpath resolves inside gstack. A user's hand-edited customization or a symlink pointing outside gstack is preserved with a notice. - Use `rm --` and `rm -r --` consistently to resist hostile basenames. - Use `find -type f -not -name .DS_Store -not -name ._*` instead of `ls -A | grep`. macOS sidecars no longer mask a legit prefix-mode install. Strip sidecars explicitly before removing the dir. context-save/SKILL.md.tmpl: - Sanitize title in bash, not LLM prose. Allowlist [a-z0-9.-], cap 60 chars, default to "untitled". Closes a prompt-injection surface where `/context-save $(rm -rf ~)` could propagate into subsequent commands. - Collision-safe filename. If ${TIMESTAMP}-${SLUG}.md already exists (same-second double-save with same title), append a 4-char random suffix. The skill contract says "saved files are append-only" — this enforces it. Silent overwrite was a data-loss bug. context-restore/SKILL.md.tmpl: - Cap `find ... | sort -r` at 20 entries via `| head -20`. A user with 10k+ saved files no longer blows the context window just to pick one. /context-save list still handles the full-history listing path. test/skill-e2e-autoplan-dual-voice.test.ts: - Filter transcript to tool_use / tool_result / assistant entries before matching, so prompt-text mentions of "plan-ceo-review" don't force the reachedPhase1 assertion to pass. Phase-1 assertion now requires completion markers ("Phase 1 complete", "Phase 2 started"), not mere name occurrence. - claudeVoiceFired now requires JSON evidence of an Agent tool_use (name:"Agent" or subagent_type field), not the literal string "Agent(" which could appear anywhere. - codexVoiceFired now requires a Bash tool_use with a `codex exec/review` command string, not prompt-text mentions. All SKILL.md files regenerated. Golden fixtures updated. bun test: 0 failures across 80+ targeted tests and the full suite. Review source: /ship Step 11 adversarial pass (claude subagent + codex exec). Same findings independently surfaced by both reviewers — this is cross-model high confidence. * test: tier-2 hardening tests for context-save + context-restore 21 unit-level tests covering the security + correctness hardening that landed in commit |