mirror of
https://github.com/garrytan/gstack.git
synced 2026-08-24 06:52:31 +02:00
9da6692930db209e8632e8da3d5e5b5995d8af9c
2
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9da6692930 |
v1.68.1.0 fix: phantom AskUserQuestion hooks — canonical-only registration + self-healing settings.json (#2631)
* fix(settings-hook): KNOWN_HOOKS identity healer — per-item ownership, mutation lock, fail-closed parse
Claude Code strips the unknown _gstack_source key when it rewrites
settings.json, so tag-based dedupe degraded to exact-command equality and
every Conductor worktree's setup appended a fresh hook entry; deleted
worktrees left dead hooks erroring on every AskUserQuestion fire.
- KNOWN_HOOKS identity table (shared JS prelude, single source of truth):
ownership is intrinsic and PER HOOK ITEM — basename + relpath suffix +
event (+ matcher where defined). Tags never claim foreign items.
- New `prune-stale [--repoint <root>] [--all]`: prune dead gstack items,
re-point survivors at the stable install (tag restore from the table),
exact-duplicate collapse, uninstall/no-team identity sweep. Explicit
plan_tune_hooks:no is honored (dead pruned, live never re-pointed).
- add-event / remove-source become item-aware: replace/remove only the owned
item; a user's co-located hook in the same entry is never collateral.
- Mutation safety: mkdir lock with owner token, ownership-checked release,
atomic stale takeover; per-process-unique tmp + backup names;
backup-on-change everywhere; fail-closed on parse failure (a corrupt
settings.json is never overwritten — previously catch{} clobbered it);
locked atomic rollback.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(gstack-config): `has <key>` — key-presence provenance through STATE_DIR resolution
`get` returns the DEFAULTS value for absent keys, so callers that need to
know whether the USER decided something (vs inherited a default) had no
correct primitive — setup's consent logic was about to grep a hardcoded
~/.gstack/config.yaml, which misclassifies under GSTACK_STATE_ROOT /
GSTACK_HOME / GSTACK_STATE_DIR overrides. `has` exits 0 iff the key is
literally present in the resolved config file, with the same C-locale key
validation as get/set.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(setup): canonical-only hook registration, heal-first, PT_EXPLICIT consent provenance
Three root causes of the phantom-AskUserQuestion-hooks class, all in the
registration path:
- Bug A: the Conductor auto-opt-in upgraded PT_DECISION "prompt" -> "yes"
even when "prompt" was dev-setup's EXPLICIT --plan-tune-hooks=prompt pin,
so every new Conductor workspace installed hooks. PT_EXPLICIT (flag/env/
config-key-presence via `gstack-config has`) now gates the auto-opt-in to
the true silent fall-through.
- Bug B: hook commands were baked from $SOURCE_GSTACK_DIR (`pwd -P` of the
running tree — ephemeral for worktrees). Registration is now CANONICAL-ONLY
via _hook_command_path (${CLAUDE_CONFIG_DIR:-$HOME/.claude}/skills/gstack);
missing canonical hook = skip + log, never a baked tree path. SessionStart
moves to schema-aware add-event under its identity source; whitespace paths
are quoted.
- Bug C: nothing ever pruned, and dead tagged entries blocked the
"already installed" guards forever. Setup now heals FIRST on every run
(prune-stale --repoint at the stable install), surfaces a one-line summary
only when something changed, surfaces the plan_tune_hooks:no-vs-live-hooks
contradiction, and --no-team tears down all three sources plus an identity
sweep for untagged strays.
dev-setup's no-mutation guarantee gains its stated repair exception (prune
dead / re-point existing, never ADD).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(uninstall): run hook cleanup BEFORE install-root deletion + full identity sweep
SETTINGS_HOOK resolves via $(dirname "$0") INSIDE the install root, but the
cleanup ran after `rm -rf ~/.claude/skills/gstack` — a real global uninstall
(running the installed copy) silently no-op'd and orphaned every hook entry.
Tests masked it by running the uninstaller from the repo checkout.
The relocated block also removes the auq-error-fallback source (registered by
setup, previously never torn down) and finishes with a prune-stale --all
identity sweep so untagged strays (Claude Code strips _gstack_source) go too.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test: phantom-hooks heal coverage — incident facsimile, per-item safety, lock, canonical tripwires
- gstack-settings-hook-schema-aware: 16 new cases — identity re-point (tag
restore), foreign-basename rejection, mixed-entry per-item safety for
add-event/remove-source/--all, prune-stale modes incl. bash-prefix +
Windows-backslash + spaced-path idempotence, duplicate collapse preferring
the tagged twin, plan_tune_hooks:no split, backup-on-change no-churn,
fail-closed corrupt-JSON for every mutator, stale-lock takeover,
fresh-foreign-lock skip, two-writer concurrency smoke, and an INCIDENT
FACSIMILE replaying the exact 2026-08-17 production damage (6/3/2 entries,
mixed tags, live-ephemeral Stop) healing to 2/1/1 canonical.
- NEW setup-hook-canonical-paths: static tripwires — canonical-only resolver
(no $SOURCE_GSTACK_DIR anywhere in it), heal-before-guards ordering,
unsuppressed heal output, ${VAR:-0} counter idiom, shared-prelude
concatenation at every bun call site, KNOWN_HOOKS completeness vs setup's
registrations, uninstall cleanup-before-deletion ordering, defect-class
warning present.
- setup-plan-tune-hooks-noninteractive: PT_EXPLICIT pins + `gstack-config
has` provenance + has-subcommand behavior (env-resolution, malformed keys).
- auq-error-fallback-hook: registration + both-teardown wiring (previously
untested).
- uninstall: behavioral ordering test running the INSTALLED copy from inside
the root it deletes.
- setup-windows-fallback / gstack-config-key-locale: pins updated for the new
HOOK_CMD shape and the third C-locale validator.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(test): banner-tripwire exec used JSON.stringify as shell quoting — vacuous pass + stray artifact
JSON escaping is not shell escaping. Interpolating JSON.stringify(script)
into `bash -c ${...}` left every JSON "\n" as a literal backslash-n inside
shell double quotes, collapsing the extracted release-body tripwire block
onto one line: `then\n` parsed as the command word `thenn`, and
`>&2\nelse\n` parsed as the redirect `>&2nelsen` — so every full-suite run
littered a `2nelsen` file (containing "bash: thenn: command not found") in
the repo root, and the test's single not-contains assertion passed
VACUOUSLY because all output had been redirected into that file. The
"and it actually fires" functional check never verified anything.
Fix: pass the script as an argv element (spawnSync array form) and assert
both branches for real — ABORT case must print the leak message to stderr,
clean case must print "banner tripwire clean" to stdout.
Verified: `bun test test/binding-template-drift.test.ts` previously created
the artifact deterministically; the full free suite now runs artifact-free.
The other shell-interpolation sites (evidence, schema-aware concurrency,
empty-find-fallthrough, branch-slug-hygiene) already use correct quoting.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test: regression pin for legacy remove mixed-entry filtering + ownership negatives
Coverage-audit iron rule: the rewritten legacy `remove` action filters
per-item (pre-v1.67.2 it dropped the whole entry, destroying a user's
co-located SessionStart hook) — modified existing behavior, previously
untested. Also pins two ownership negatives: an owned basename+relpath under
the WRONG matcher stays foreign, and prune-stale on an absent settings file
exits 0 with removed 0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: pre-landing review fixes — review-army findings hardened
Specialist review (testing, maintainability, security, performance,
data-migration) findings, each verified against code before fixing:
- legacy remove: preserve malformed/foreign entries (hooks absent, non-array,
or pre-existing empty) — only entries THIS pass emptied are dropped
- add-event: never tag a mixed entry (old gstack versions in sibling
worktrees treat tags as entry-level ownership and would destroy the user's
co-located items); tag only single-item entries; prune-stale drops tags
from mixed entries for the same reason
- prune-stale: within-entry twin collapse (two dead copies of one hook
re-pointed to the same canonical command no longer double-fire); command
quoting hardened via gsQuoteCmd (escapes \\ " $ backtick; gsStripWrap
unescapes so identity round-trips); NUL bytes in the dedupe key replaced
with a JSON.stringify key (bash silently dropped the NULs, degrading the
separator; the file also read as binary to tooling)
- gsIsAlive: only provable absence (ENOENT/ENOTDIR) counts as dead —
EACCES/EIO/unmounted volumes no longer prune (one-way-ratchet guard)
- gsWriteIfChanged: preserves the live settings.json mode across rewrites
(a user-tightened 0600 carrying API keys was silently broadened to 0644);
fresh files start 0600; backups rotate (keep 10)
- remove-source: command-less items default to foreign (gstack only writes
type:command items); single-item stray claim requires a command
- rollback: pointer target must be a sibling settings.json.bak.* file
- uninstall + setup --no-team + SessionStart registration: stderr stays
attached — a lock give-up or fail-closed parse during TEARDOWN must be
visible ("the next setup retries" does not apply after uninstall)
- setup: team-mode banner no longer claims an auto-update hook when
registration was skipped; heal log documents the rollback-pointer caveat;
SESSION_UPDATE_CMD quoting mirrors gsQuoteCmd; lock constants named
- list-sources: corrupt settings.json reports to stderr instead of silently
printing nothing (setup guards must not misread corrupt as no-hooks)
- tests: 10 new pins (malformed-entry preservation, mixed no-tag, twin
collapse, 0600 mode, metachar escaping round-trip, backup rotation,
rollback pointer refusal, held-lock uninstall warning, matcher-drift
tripwire, ownership negatives)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: red-team findings — verify-gate identity, single quoting authority, Windows paths
Red-team pass over the hardened diff (several findings empirically verified
by the reviewer before reporting):
- KNOWN_HOOKS gains the sixth identity: gstack-verify-gate (README-documented
opt-in Stop hook). A tag-stripped verify-gate entry previously survived
prune-stale --all and errored at the end of EVERY turn after uninstall
deleted the install root — the exact phantom-hook class this branch fixes.
Uninstall also sweeps its tagged form.
- add-event is now the single quoting authority: every registered command is
normalized through the same gsQuoteCmd/gsStripWrap round-trip the healer
uses. Pre-fix, only SessionStart got caller-side quoting — a spaced/metachar
canonical root registered broken plan-tune/AUQ/timeline hooks that the very
next heal rewrote (the codebase disagreed with its own registrations).
- Windows: MSYS-form paths (/c/Users/...) are drive-translated for fs checks
only (gsWinPath) — native bun resolved them drive-relative, so the heal
judged every LIVE Windows hook dead and pruned it. The three AskUserQuestion
hooks and the Stop hook now also get the mandatory 'bash ' prefix on
Windows (previously only SessionStart did; extensionless bash shims
otherwise hit the file-association dialog).
- CANONICAL_GSTACK_ROOT falls back to $HOME/.claude/skills/gstack when a
CLAUDE_CONFIG_DIR-derived root was never installed (the installer hardcodes
the home path — split-brain left such users permanently hookless).
- prune-stale preserves foreign entries that STARTED empty (they were
silently deleted, uncounted, on every heal).
- The timeline Stop registration and its list-sources guard join the
zero-silent-mutations contract (stderr attached).
Tests: verify-gate tag-stripped heal+sweep, started-empty preservation,
add-event quoting-authority round-trip.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore: bump version and changelog (v1.68.1.0)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: update project documentation for v1.68.1.0
README: document canonical-only hook registration + the prune-stale
self-heal in the setup hooks section; expand the manual-uninstall note
to cover every gstack hook identity, not just timeline-stop-hook.
CONTRIBUTING: record PT_EXPLICIT provenance (Conductor auto-opt-in
fires only on the true silent fall-through) and the heal-first repair
exception in the dev-setup paragraph.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(settings-hook): fail-loud hardening — gsMain umbrella, lock exit 5, prototype-safe ownership
bun in -e mode swallows uncaught exceptions thrown after a require() and
exits 0 (verified on 1.3.13; uncaughtException handlers never fire either),
so any runtime throw in a mutator was a SILENT SUCCESS. Every script body
now runs inside a gsMain try/catch that prints "internal error ... refusing
to mutate" and exits 4.
Also: lock give-up now exits 5 instead of 0 (callers must not report a
skipped mutation as registered); basename lookup uses hasOwnProperty so a
foreign hook named "toString"/"constructor" can't resolve to an inherited
Object.prototype member and abort the sweep; ownership-checked release also
clears an empty/missing owner file; backup rotation sorts by mtime, not
name; Windows-only backslash normalization (a legal Unix path containing a
backslash is no longer rewritten); GSTACK_SWEEP_EXCLUDE_SOURCES lets a
sweep spare named sources; lock tradeoffs documented at the lock helper.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(setup): honest hook-registration reporting + verify-gate sweep exclusion
_install_plan_tune_hooks now propagates per-add-event failures (lock
contention exits 5, fail-closed settings errors exit 3) and both caller
sites branch on it: success logs the installed message, failure logs a
visible "NOT registered — re-run ./setup" warning instead of claiming
success for a mutation that never happened.
--no-team's identity sweep runs with GSTACK_SWEEP_EXCLUDE_SOURCES=
verify-gate: turning team mode off must not delete the user-registered
verify-gate opt-in whose binary still exists (uninstall still sweeps it,
correctly, because there the binary itself is being removed).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test: adversarial regression pins — wrong-shape fail-loud, prototype basename, sweep exclusion, lock exit 5
New pins for the fail-loud hardening: a wrong-shape hooks value (object
where an array belongs) exits 4 with "refusing to mutate" and leaves the
file byte-identical (pre-gsMain this was a silent exit-0 no-op); a foreign
hook whose basename collides with Object.prototype ("toString") survives
an --all sweep that still removes gstack rows; GSTACK_SWEEP_EXCLUDE_SOURCES
preserves the verify-gate row during --all; the fresh-foreign-lock test now
asserts the loud exit 5 instead of a quiet skip.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(verify-gate): allow the --no-team sweep exclusion, keep registration banned
setup now legitimately mentions verify-gate once: the --no-team identity
sweep excludes it via GSTACK_SWEEP_EXCLUDE_SOURCES so team-mode teardown
can't delete a user-registered gate. The opt-in pin tightens from a blanket
not-contains to: every mention must be a comment or that exclusion, and no
mention may sit on an add-event line.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(settings-hook): GNU-first stat in the lock stale check — Linux abort on held locks
On Linux, BSD-style `stat -f %m` prints a multi-line FILESYSTEM block to
stdout before exiting 1, so the BSD-first || chain captured that garbage
concatenated with the real `stat -c %Y` epoch. The non-numeric mtime made
`$(( now - mtime ))` a syntax error and set -e killed the binary with
exit 1 whenever a lock dir already existed — every contention path (stale
takeover, give-up, concurrent writers) broke on CI while staying green on
macOS, where BSD stat -f succeeds cleanly.
GNU `stat -c %Y` now goes first (BSD stat rejects -c with no stdout, so
macOS falls through cleanly), and a numeric guard blanks any residual
garbage so a future platform quirk degrades to the normal give-up path
instead of an arithmetic abort. Same defect class as gstack-repo-mode's
GNU-first ordering (#2195). Verified in an oven/bun Linux container:
the four CI-failing lock tests now pass (62/62 across both files).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(uninstall): 30s budgets for the two subprocess-heavy behavioral tests
Both tests spawn the copied uninstaller, which itself runs several
settings-hook bun -e children (the lock-contention one also waits out a
300ms give-up per call). On a loaded box those cold starts blow bun's
default 5s per-test timeout, and a timeout kill reports as a bare fail
with no assertion diff — observed at 5.6-8.5s under load avg 25+.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
1cab5e1108 |
v1.66.1.0 feat: content binding — evidence ledger, wtree staleness, tracker trust envelope, fail-closed hooks (#2603)
* fix(hooks): fail-closed freeze + shared extractor + careful HIGH tier Freeze boundary hook had four verified bugs: the grep-first JSON extractor truncated at escaped quotes and failed OPEN on unparseable payloads; the deny JSON was printf-interpolated so a quote- or newline-bearing path silently no-oped the block; the freeze path read stripped INTERNAL spaces (a boundary like ~/My Project could never match); and the path resolver skipped the final component, letting an in-boundary symlink write through to an out-of-boundary target. Fixes, structurally: one shared sourced helper (careful/bin/hook-extract.sh) now owns JSON extraction and JSON-encoded decision envelopes for BOTH hooks -- the two-copy drift is how freeze kept a broken extractor after careful's was fixed. Freeze is now deny-tier fail-closed (unparseable payload denies, parsed-but-no-file_path still allows), trims only leading/trailing whitespace, and resolves symlinks through the final path component. Careful gains a HIGH tier (hard deny, simple commands only): recursive delete of /, ~, or $HOME, and force-push to the repo's default branch. Compound commands always fall through to the MEDIUM ask; --force-with-lease is never HIGH. Documented as a best-effort advisory hard-stop, not a policy boundary. Plus additive-only project patterns (~/.gstack/careful-patterns.txt + per-project file): config can only ADD warn rules, never suppress a baseline family. test/hook-scripts.test.ts: 89 tests incl. malformed-payload deny, parseable deny JSON for hostile paths, space-bearing boundaries, symlink escape, HIGH tier splits, additive invariant, invalid-regex resilience. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(review): content-addressed staleness via working-tree fingerprint Review records now bind to the content they were made on. bin/gstack-review-log stamps every appended record with commit_full, tree, dirty (informational) and wtree — a working-tree fingerprint from the new bin/gstack-wtree (temp index seeded from HEAD + git add -A + write-tree). The binding fields are computed authoritatively; caller-supplied values for those keys are ignored, so a stale rendered template or a forged field can't bind a record to content it wasn't made on. Why a working-tree fingerprint instead of HEAD^{tree}: committing identical content doesn't change it (a record made on a dirty tree stays valid after the same content is committed), untracked new source files DO change it (new code can't hide from freshness), and gitignored scratch stays out. Rebase, amend and squash with identical content grade CURRENT instead of stale. Grading: the dashboard (scripts/resolvers/review.ts) and /land-and-deploy Step 3.5a apply a content-first rule to diff-scoped review rows — wtree match with both sides clean is CURRENT, full stop. Plan-tier reviews grade a plan file, not the repo tree, so they keep the 7-day logic (optional plan_sha256 caller field noted). The rev-list fallback no longer errors when the stored commit was rebased away: it grades UNKNOWN and treats it as stale. bin/gstack-review-read emits ---WTREE---/---TREE---/---DIRTY--- so graders consume one tool output. Old records without wtree fall back to the existing heuristics; no migration. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(evidence): verification-evidence ledger mechanizes /ship's IRON LAW New bin/gstack-evidence: a transparent wrapper that records every verification run as {ts, label, command, cmd_sha256, exit, duration_s, commit, tree, dirty, wtree, log_path} in ~/.gstack/projects/<slug>/<branch>-evidence.jsonl, plus a read-only `check` that grades FRESH/STALE/MISSING per label. "Tests passed" now binds to the exact working-tree content it ran on (bin/gstack-wtree fingerprint), so evidence recorded on uncommitted code stays FRESH after the exact tested content is committed — the /ship Step 5 -> Step 16 case — while an untracked new source file or any content change invalidates it. Check semantics: every named label's latest record must be green, within --max-age, matching --expect-cmd's hash when given, and fingerprint-identical (or diff confined to --allow-paths — mechanizing Step 16's existing "CHANGELOG edits don't count" carve-out). No --any mode: a green lane can never mask a red sibling. Any git failure inside check (gc'd tree object, not a repo) degrades to STALE/MISSING, never an error into the calling skill flow. Transparency invariant (load-bearing, test-pinned): the child's exit code is ALWAYS the wrapper's exit code; ledger/log/redact failures are stderr warnings. Logs are per-run (0600, exclusive-open, 2MB truncation marker, 30-day opportunistic prune) — no more shared /tmp collisions between concurrent ships. Command strings are redact-scanned before recording (HIGH credential -> stored redacted). Machine-local by design: neither ledger nor logs brain-sync. Wired: ship Step 5 lanes run wrapped (per-lane labels), ship Step 16 and land-and-deploy 3.5b check the ledger first and cite FRESH evidence instead of re-running; a failed CHECK never blocks (run live), a failed RUN does. test/evidence.test.ts: 21 tests incl. the keystone dirty-record -> commit -> FRESH case. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(security): trust envelope for tracker text at every model-context ingress Web page content has had a trust envelope since v1.38; tracker text did not — PR bodies, PR/issue comment bodies, and model-judged issue titles entered agent context raw. Anyone who can comment on a PR could put instructions in front of the agent. New lib/tracker-guard.ts + bin/gstack-issue-guard: every tracker-text read now emits inside a "BEGIN UNTRUSTED TRACKER CONTENT" envelope. Content is enveloped even when clean (a pattern scan is not proof of safety); injection-shaped lines get a visible [INJECTION-PATTERN] label; NFKC + zero-width normalization runs for DETECTION only (fullwidth/invisible evasion caught, content bytes never rewritten); forged END banners are zero-width-spliced so they can't close the envelope early. Fetch failure exits non-zero with NO envelope — never a fake-trusted empty one. Issue numbers are validated and gh is spawned via argv arrays. Patterns reuse lib/jsonl-store's INJECTION_PATTERNS single copy plus a separate TRACKER_EXTRA list (kept separate so decision/learning store write-rejection semantics don't change). 8 sites wired: greptile findings + replies fetches (metadata/body split — ids and paths stay machine-raw for reply POSTs), review.ts PR-body reads x2, land-and-deploy 3.5c, document-release PR/MR body (two-artifact flow: the enveloped rendering is what the agent READS, the raw tempfile is what the pipeline mutates, and a write-side banner tripwire aborts any edit that leaked envelope markup), and spec's issue-title dedupe (titles are model-judged for similarity, so they're ingress). Title-prefix rewrites and state-routing fetches are mechanical, not ingress — deliberately not enveloped. test/tracker-guard-wiring.test.ts is the CI tripwire: raw tracker-text reads outside the guard fail the suite unless carried by a reasoned SCANNER_EXEMPT entry; exemptions are liveness-checked so a moved site forces a re-audit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(binding-wave): drift tripwire, golden fixtures, TODOS follow-ups test/binding-template-drift.test.ts pins the load-bearing prose rules in the GENERATED templates (ship Step 16 evidence check, per-lane wrapped test lanes, land-and-deploy wtree-first grading + UNKNOWN fallback, dashboard content-first rule, release-body banner tripwire, greptile guard pipes) so a template refactor can't silently drop a rule while the bins keep passing their unit tests. Golden ship fixtures re-pinned to the new intentional output (claude/codex/ factory variants). TODOS.md gains the five deferred follow-ups from the review wave: eval-run evidence records, spec-spawn outcome ledger, merge-SHA custody, default-if-silent escalations, and the paid eval case proving agents apply the staleness grading rule. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(careful): trim HIGH-tier + project-pattern docs under the size budget The new sections pushed careful/SKILL.md to 2551 -> 3879 bytes (x1.52, gate caps growth at x1.5 of the v1.47 baseline). Same content, tighter prose: 3516 bytes (x1.38). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(tests): scratch-repo fixtures never invoke the operator's gpg The evidence/review-log/hook fixtures inherited global commit.gpgsign, so fixture commits called the operator's gpg-agent — which fails with "Cannot allocate memory" under parallel shard load, breaking test SETUP (not the code under test). All fixture git invocations now pass -c commit.gpgsign=false -c tag.gpgsign=false. Hermetic repos, no pinentry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: pre-landing review fixes (27 specialist findings, 3 critical) Specialist army findings, all quote-verified before fixing: Security: careful force-push guard now catches git's plus-refspec force syntax (git push origin +main carried force with no flag — silently allowed before) and refspec-form targets (HEAD:main); default-branch matching is tokenized FIXED-STRING comparison on the full branch path (slashed defaults like release/2.0 work; no ERE interpolation), glob-safe via noglob. HIGH rm tier is tokenized too: trailing long options (--no-preserve-root) and /* are root-class. Stored evidence fingerprints are 40-hex re-validated before reaching git argv. normalizeForDetection sweeps ALL Unicode format chars (\p{Cf}: soft hyphens, bidi marks, tag chars) instead of five enumerated zero-widths. The wiring scanner gains flagless gh pr/issue view patterns. The release-body banner tripwire diffs against the fetched original so a hostile pre-existing banner string can't permanently DoS doc updates. Ship/land evidence checks now pass --expect-cmd (a green `echo ok` recorded under the label can never mint FRESH); package.json stays allow-listed with the residual documented. Performance: gstack-wtree seeds its temp index by COPYING the real index (stat cache preserved — measured 40x faster than read-tree seeding, identical hash) with read-tree fallback; evidence uses findLast and one gstack-slug spawn; the stream pump honors backpressure via drain; careful's pattern block short-circuits before slug resolution when no pattern file exists. Testing: the gh-failure envelope test was VACUOUS (killing PATH killed the bun shebang before the code under test ran) — replaced with a PATH gh shim that exercises the real branch, plus shimmed happy paths (issue/pr-body/ unparseable JSON); evidence check --all + empty ledger + non-numeric --max-age (now a usage error, was silent fail-open) covered; HIGH-tier variants pinned; hook analytics respect GSTACK_HOME so tests stop writing the operator's real skill-usage.jsonl. Maintainability: dead exit ternary removed; flagValue deduped into bin-context; sentinel defusal derived from the banner constants (no invisible literals — \u escapes only); scratch-repo git fixture extracted to test/helpers/scratch-repo.ts (one hermetic incantation, three consumers); shared gstack_hook_log_fire in hook-extract.sh; the dashboard/land diff-scoped row lists are aligned (codex-review) and drift-pinned. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: red-team review fixes (9 findings, 2 critical) Red team reviewed what four specialists missed — cross-cutting and self-contradiction class: CRITICAL: the release-body banner tripwire failed OPEN on the exact leak it guards (grep -c prints 0 AND exits 1 on no-match, so a fallback echo double-emitted "0" twice and the -gt comparison fell into the clean branch) — counts now default via parameter expansion, and a functional drift test executes the rendered tripwire block against a 0->1 banner delta to prove the ABORT branch fires. CRITICAL: evidence fingerprints were captured AFTER the child exited, so a working-tree edit made DURING a long suite was certified as tested content — wtree is now captured before spawn and re-checked after; mid-run drift omits the fingerprint (grades STALE) with a warning. Also: the review-grading rule dropped its dirty-gates (they nullified the keystone dirty-record->commit->CURRENT property that evidence checks already honor — wtree equality alone proves identical content); careful's HIGH force-push tier falls back to probing origin/main|master when the origin/HEAD symbolic ref is absent (Conductor worktrees — the tier was silently inert in the primary deploy environment); quoted tokens (rm -rf "/", push "main") no longer dodge the deny; freeze fails CLOSED when its own helper file is missing (bash makes a missing source target fatal non-interactively, so an existence pre-check guards it); spec dedupe distinguishes pipeline failure from zero matches instead of silently skipping dedupe on gh/jq breakage; land 3.5b sets the cross-session --expect-cmd mismatch expectation; hook analytics JSON fields are encoder-built per this wave's own rule. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: re-pin codex/factory golden fixtures post-regeneration The suite regenerates .agents/.factory in place mid-run; the prior pin snapshotted them before the dashboard-rule regen landed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: bump version and changelog (v1.66.1.0) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: adversarial review fixes (Claude pass, 14 findings, 1 verified-live critical) The fresh-context adversarial pass caught a live bug in this branch's own performance fix: gstack-wtree exported GIT_INDEX_FILE BEFORE resolving the real index path, so `git rev-parse --git-path index` returned the temp index itself, the stat-cache copy self-copied and failed, and every invocation fell back to the full re-hash — the fast path was dead code (verified with bash -x). Resolution now happens before the export; measured 0.08s per call on this repo. Also fixed: careful fails to an ASK (not silence) when its own helper file is missing (same partial-install state freeze already defends against); the --source label is sanitized inside the envelope lib (newline-stripped, sentinel-defused, length-capped — it sits in trusted framing); the HIGH rm tokenizer skips redirections/backgrounding/`--` (rm -rf / 2>/dev/null now denies) and knows ${HOME}; user pattern lines starting with a dash work (grep --); greptile bodies carry per-comment id headers inside the envelope so multi-comment PRs stay attributable (ids verified against raw metadata, never trusted in-body); the release-body tripwire fails CLOSED when its input files are missing (separate-shell $$ reality); land 3.5b gets the same allow-paths as ship; the "either side dirty" fallback leftover is gone from both grading surfaces; the evidence pump races drain against error (EPIPE consumers can't hang the wrapper); an unset HOME skips bookkeeping instead of creating a literal ~ dir inside the repo; a write-failure log ends with a visible marker; freeze expands a literal leading ~ in the boundary; review-log documents its log-time binding window. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: pin golden fixtures from --host all generation `bun run gen:skill-docs` generates the claude host only; .agents/.factory regenerate when the suite's --host codex/factory tests run in place. Fixture pins must come from `gen-skill-docs --host all` output or they lag one resolver edit behind and fail the next full-suite run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: assemble the fixture PAT by concatenation (no live-format literal) The repo's own pre-push credential guard (correctly) blocked the push: the redaction test's fabricated GitHub PAT was a live-format literal in the diff. The token is now concatenated at runtime — the source carries nothing the scanner can match, the engine still receives a live-format value. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: update project documentation for v1.66.1.0 CLAUDE.md: add gstack-wtree/gstack-evidence/gstack-issue-guard to the bin/ structure line and tracker-guard.ts to the lib/ line. README.md + docs/skills.md: /careful descriptions no longer claim every warning is overridable — the HIGH tier hard-denies root/home recursive deletes and default-branch force-pushes; skills.md also documents the additive-only careful-patterns.txt warn rules. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: doc-review fixes — new bins in README table, careful claims precise README.md: add gstack-wtree, gstack-evidence, and gstack-issue-guard to the Standalone binaries table (they shipped in v1.66.1.0 with no user-facing reference outside CHANGELOG). docs/skills.md: the safety-skills intro said "no configuration files" which the optional careful-patterns.txt now contradicts, and the hard-deny description undersold the deny set (the hook also denies /*, ~/, and $HOME/ forms, not just bare / and ~). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: guard reflects the hard-deny tier; changelog stats current guard/SKILL.md claimed every destructive warning was overridable — the shared careful hook now hard-denies the catastrophic shapes. CHANGELOG numbers updated to the final measured state (0.09s fingerprint, 50 findings/6 critical across all review passes). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |