mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-31 14:06:50 +01:00
docs: approved severe fix wave plan (Oct 2026)
This commit is contained in:
1 parent
ac20ef1e13
commit
ff5fbd09ee
1 file changed
+295
@@ -0,0 +1,295 @@
|
||||
<!-- /autoplan restore point: "/home/user/.gstack/projects/garrytan-gstack/garrytan-severe-fix-wave-oct3-autoplan-restore-20261003-192325.md" -->
|
||||
# gstack severe fix wave (October 3, 2026): no silent failures, nothing lost, every host works
|
||||
|
||||
## Implementation plan
|
||||
|
||||
### Context
|
||||
|
||||
Base: origin/main `ac20ef1e1` (v1.91.16.0). Main CI is green. The weekly periodic eval lane and gate census have been red for five straight weeks (#2755, #2756).
|
||||
|
||||
Triage on 2026-10-03 covered all 299 open issues and 493 open PRs against current main. Each S1/S2 claim was checked with a code read or a local probe, and the evidence is recorded in `~/.capy/work/wave/triage-*.md`. 48 issues were already fixed on main and have been closed with evidence. About 190 open PRs are superseded or junk.
|
||||
|
||||
The previous two waves (#3013, #3014) fixed install/upgrade safety, worktree skill start, Codex model precedence and review gates. This wave targets what is left. The remaining severe bugs fall into four patterns:
|
||||
|
||||
1. **Silent loss.** gstack drops user data and reports success: transcripts never ingested, paid design variants overwritten, project state split across worktrees.
|
||||
2. **False "all clear".** A check that did not run reports ready or pass: Codex reviews whose sandbox could not start, a retired Codex model, a design binary that dies on launch, search scripts that exit 0 when bun is missing.
|
||||
3. **Non-Claude hosts half-work.** Codex and the other env-var hosts lose every `$GSTACK_*` path between shell calls, so skill-end, browse, design and state-root all break after the first block.
|
||||
4. **Push blockers and platform gaps.** Redaction false positives block legitimate pushes, Windows /freeze denies every edit, and /ship stops in repos without a VERSION file.
|
||||
|
||||
### Release promises (the finish line)
|
||||
|
||||
The wave is done when each promise holds and is backed by a test that fails on main today:
|
||||
|
||||
- **P1. Nothing is silently lost.** A transcript, design variant or learning that gstack could not store is reported and retried, never marked done.
|
||||
- **P2. No false all-clear.** Every gate that could not run says "not run" or "unavailable" with the reason, never "ready", "pass" or "no results".
|
||||
- **P3. Every generated skill resolves its paths on every env-var host.** Every fenced bash block of every generated skill passes a fresh-shell conformance test on all 10 env-var hosts, and Codex (the one host we certify live) runs a multi-block skill end to end. Host tiers stay as declared in `scripts/host-config.ts` (Claude full, Codex experimental, others best-effort); this wave does not promise feature parity beyond paths, sections and discovery.
|
||||
- **P4. Legitimate work is not blocked, and real secrets still are.** Known redaction false positives, Windows drive-letter paths and versionless repos stop blocking /ship, /freeze and pushes. Every redaction relaxation ships with true-positive controls that must still flag.
|
||||
- **P5. Local control surfaces accept only gstack's own clients.** The browse terminal-agent socket rejects foreign extensions, a project's `.env` cannot loosen security settings, and navigation (including redirects) rejects link-local and metadata addresses (D0, D1, D2). Connection-time DNS rebinding is not covered by this release.
|
||||
|
||||
### Three invariants first (they turn the promises into enforced properties)
|
||||
|
||||
Most bugs in this plan are instances of three missing invariants. Build these first; the instance fixes then land against them.
|
||||
|
||||
- **INV-1. Gate outcome contract.** One typed outcome for every gate and outside review: `ran(findings) | not_run(reason) | unavailable(reason)`. "pass" requires positive evidence the check executed. All Codex invocation sites route through `lib/outside-review-result.ts`, which takes text, stderr and exit status (B1). A free lint fails on `2>/dev/null || exit 0` in `gstack-learnings-search` and `gstack-timeline-read`, and on calls to an explicit list of gate binaries wrapped in `|| true`; best-effort tails elsewhere carry a same-line reason.
|
||||
- **States.** `not_run` means gstack chose not to run the check (no plan bound, feature disabled, prerequisite missing on purpose). `unavailable` means it tried and could not (sandbox, retired model, binary died, bun missing, network). Every outcome this wave adds or changes maps to one state, a reason code and a fix action in a table in `docs/troubleshooting.md`.
|
||||
- **What users see.** Every outcome that is not `ran` prints one line: `<gate>: not run (<reason>). Fix: <command>` or `<gate>: unavailable (<reason>). Fix: <command>`, with the real values (exit code, path, host). Existing machine markers (`OUTSIDE_STATUS`, `CODEX_MODE`, `DESIGN_READY`) keep their names.
|
||||
- **What callers do.** A new `classifyOutsideReview({text, stderr, exit})` returns three separate fields: execution (`ran | unavailable(reason)`), findings (highest severity, or none), and verdict (`clean | findings | unverified | unavailable`); `validateOutsideReview` stays as a wrapper so the three direct importers in `claude-code/SKILL.md.tmpl:130,198,268` keep working. The two-argument CLI form keeps today's exit codes exactly (0 completed, 1 unavailable, 2 usage), because `lib/` runs live from the checkout and older rendered skills call it with `|| exit 1`. With `--verdict` (implied by `--stderr` or `--exit`) it prints `VERDICT: ...` and exits 0 / 3 / 4 / 1; every generated caller moves to that form in the same commit and branches on it explicitly. `unverified` covers a completed structured review with neither severity tags nor an explicit no-findings line (/codex review's existing UNVERIFIED state). /codex consult and challenge use a new `execution` gate that checks only execution evidence, not review markers. Reason codes live in `lib/gate-outcomes.ts`, imported by the validator and by the troubleshooting anchor test. A test runs an old rendered caller against the new lib. /ship and /review treat `unavailable` as missing coverage: they continue, show it in the readiness dashboard and the PR body, and never count it as a pass. A `findings` verdict with P0/P1 blocks exactly as a native P0/P1 does. Tested through the real CLI and the generated caller together.
|
||||
- **INV-2. Write, verify, then stamp.** Storage marks work done only after confirming it landed: memory ingest (A1), design variant rounds (F4), learnings/question logs (G3). Staged, published and indexed are separate states.
|
||||
- **INV-3. Host conformance test (free suite).** For every host with `usesEnvVars: true`, render all generated SKILL.md and section files, run `bash -n` on every fenced bash block, and execute each block's root-resolution lines in a fresh `env -i`-style shell against a temp install root with stub binaries. Fail on any empty expansion or a path starting with `/bin/`, `/browse`, `/design` or `/gstack-`; fail if any Codex SKILL.md is a symlink or any host SKILL.md exceeds 160,000 bytes. A generated-bash lint in the same suite fails on a `~/` path inside double quotes and on `mktemp` without a `${TMPDIR:-/tmp}` template. Both fail on origin/main today. Details: install-level checks extend the existing `test/host-conformance.test.ts` (codex, kiro, factory, opencode, cursor, copilot: no installed SKILL.md is a symlink, design/make-pdf dist present, every section pointer resolves to a readable file); render-level checks cover all 10 env-var hosts in one new test that renders once per host. Each fence that uses `GSTACK_*`, `$B` or `$D` must carry the shared prelude exactly once, and the prelude is executed under `env -i` and `set -u` with stub binaries, including the B/D consumers. Angle-bracket placeholders are normalized before `bash -n` (231 of 10,136 fences fail today only because of them). The lint tokenizes quotes and fails on: a `~/` word inside double quotes; `mktemp` with no template or a literal `/tmp/` template; `cd`, `rm -r*`, `mv` or `git -C` on a variable not assigned in the same fence unless written `cd -- "${VAR:?...}" || exit 1`. The lint and the size gate run on every host's render, Claude included. Sizes are UTF-8 bytes.
|
||||
|
||||
### Delivery tiers (one PR; the tiers set order and what drops if it slips)
|
||||
|
||||
- **Tier 1, must ship:** C9 (first: it can damage a user's repo today), INV-1/2/3, I1 baseline, A1 (with reconcile), A3, A4, A9, B1 (with the P0 gate bug), B11, C1-C4, D1, D2, E1, F3, F4, G1, G3, H, plus the dotenv hardening below. Each release promise lists the Tier 1 items and tests that prove it (a promise-to-test matrix in the PR body).
|
||||
- **Tier 2, ship if green:** the rest of A-H.
|
||||
- **Tier 3, moved to TODOS.md if it slips:** E3 (Windows Node CLI lane), C8's routing-wording change (the disabled-skill half is Tier 2), G6 riders. Nothing in Tier 3 blocks the PR.
|
||||
- Cuts happen by dependency-complete group (for example "all of A1+A4+reconcile" or none), never half a group; cutting a group rewrites its release promise in the CHANGELOG.
|
||||
- Land C1 before C4, and C2/C3/F5 before the live Codex check. Rebase on main daily.
|
||||
|
||||
### Workstream A: data loss and privacy in memory ingest and gbrain (P1)
|
||||
|
||||
- **A1. #2778 (S1, data loss):** `bin/gstack-memory-ingest.ts:1127-1140` only reads `~/.gbrain/sync-failures.jsonl`, which gbrain writes only for git import dirs. A page gbrain skipped from the staging dir is stamped ingested and never retried (field reports: 37 and 69 lost entries). Main's count guard (`memory-ingest.ts:2507-2535`) counts gbrain's exit-0 "skipped" as landed, and gbrain stops printing per-file warnings after 5 per class, so stderr parsing alone is not enough. Fix: stamp a page only when (a) no per-file failure names it (gbrain `--json` failures when available, else stderr `Skipped <rel>:` lines) and (b) a landing check confirms the page exists with the staged content hash (batched lookup if the installed gbrain supports one, else a bounded number per run with the rest carried). An unnamed failure refuses the whole batch. Remote-HTTP mode records "staged" rather than "ingested" (`memory-ingest.ts:2302`). A `--reconcile` pass (run once automatically after upgrade, then on demand) re-checks stamped entries and un-stamps pages absent from the brain so already-lost transcripts re-ingest. Refused and re-queued pages print even under `--quiet`. If the installed gbrain has no page lookup, pages that passed (a) are stamped `unverified`, not `ingested`, and are re-checked when a lookup becomes available; check gbrain's capability before implementation and record the minimum version in the plan. `--reconcile` is a fourth mode beside `--probe|--incremental|--bulk`, listed in `--help`, supports `--dry-run` and `--limit N`, is bounded, resumable and idempotent, backs up the state file first, and prints `reconcile: checked N, present M, re-queued K, not yet checked L (run again to continue)`. The upgrade migration only records that a reconcile is pending; the next ingest run does one bounded pass and prints the summary even under `--quiet`. Setup never calls gbrain for reconcile. Engineering contract: (1) a versioned state schema with an explicit transition table (`staged → imported_unverified → ingested`, plus `refused`, `quarantined`, `unrecoverable`), migrated from today's file; (2) the source fingerprint is captured from the exact bytes parsed, so a transcript that grows during import is not stamped as its newer self; (3) each entry records its gbrain `source_id` (`default` for entries stamped before this release), and the landing check and `--reconcile` query only the recorded source, so pre-wave pages are never re-ingested into new sources; (4) a lookup error or `pglite_busy` means "not yet checked", never absent; reconcile re-queues a page only when it is missing from its recorded source and the transcript file still exists, otherwise it counts it `unrecoverable`; (5) presence is checked with one `gbrain list` per source per run, content with `gbrain get <slug> --json` for at most 50 pages per run against a `gstack_content_sha256` frontmatter field gstack writes (gbrain stores its own hash), and pages over the cap are `imported_unverified` and never re-imported; (6) after 3 refused batches the batch is split in halves to find the bad page, which is quarantined with a reason printed even under `--quiet`; (7) one lock protects every writer (ingest, reconcile, migration flags) and a state-save failure fails the run; (8) this logic lives in a new `lib/memory-ingest-landing.ts` (ratchet new module, ≤800 lines). Capability facts: gbrain installs at latest HEAD (floor 0.20.0); `import --json` reports `failures` from 0.48.5.0 and `source_id` from 0.50.1.0; managed-import `Pending:` receipts count as unchanged, so the landing check is needed even with `failures`; capability is detected by behavior. Fix the stale comments at `memory-ingest.ts:912-919,1127-1135`. Fold in PR #2778 with co-author credit.
|
||||
- **A2. #2884:** a slug starting with a dot (`.claude`) is pruned by gbrain's walker, so the ingest refuses forever (`memory-ingest.ts:1055`). Normalize staged filenames, and map state recorded under the old filename so no page is duplicated or re-ingested. Also `lib/gbrain-local-status.ts:484` treats DNS failure (`ENOTFOUND`, offline sandbox) as `broken-db` and routes users to "move your config aside". Classify network errors as a new `db-unreachable` status with the reason `database host unreachable (<code> <host>); your gbrain config is unchanged. Fix: check network or VPN, then re-run /sync-gbrain.` For skill rendering it counts as configured-but-transient like `timeout`, so running setup offline does not strip brain-aware blocks. Fold in PR #2884.
|
||||
- **A3. #1917:** with a PGLite brain, `lib/gbrain-exec.ts:111-125` returns the caller's env unchanged, so a project's `.env` `DATABASE_URL` (Bun autoloads it) reaches gbrain: false `broken-db` at best, gbrain connecting to the app's own Postgres at worst. Strip inherited `DATABASE_URL`/`GBRAIN_DATABASE_URL` when the config is PGLite, and run the gbrain child from a working directory that is not the user's project so gbrain cannot reload the project's `.env` / `.env.local`; keep honoring the explicit `GSTACK_RESPECT_ENV_DATABASE_URL` override. Tested with a real child launched from a project containing conflicting dotenv files. PR #1931; the same fix exists in Garry's stale PR #2971.
|
||||
- **A4. #2140:** `gbrain import` at `memory-ingest.ts:1967-1973` passes no `--source-id`, so transcripts can land in the shared/federated default source. The ingest walks transcripts across all projects (`memory-ingest.ts:439`), so a single destination id would misattribute other projects' transcripts. Partition the batch by each transcript's originating repository and import each partition into that repository's own transcript source, registered before import as machine-local and non-federated (id via the existing `constrainSourceId`) unless the user gave publication consent; if the installed gbrain cannot register sources, the partition stays local and the run says why; transcripts with no attributable repository stay local and are reported, never sent to the default or federated source. The existing `--include-unattributed` flag (which today sends them to the default source) instead imports them into a machine-local source `gstack-transcripts-unattributed` that is never federated, refuses when the brain is remote (Postgres or HTTP), and keeps those pages out of the publishable `transcripts/**` staging that brain-sync pushes; `--help` says so. Take the source-id half of PR #2232.
|
||||
- **A5. #2670:** brain-sync reports "curated artifacts pushed" on git success with zero indexed pages (`bin/gstack-gbrain-sync.ts:1648`). Fail loud when the source has zero pages. PR #2670.
|
||||
- **A6. #2688:** a deleted Conductor worktree leaves a `gstack-code-<hash>` gbrain source, so autopilot fails on it forever. Source ids deliberately distinguish machines sharing a federated brain (`gstack-gbrain-sync.ts:540`), so local absence is not proof a source is dead. First, gstack's sync stage skips sources whose path is missing locally and reports "path unavailable" once instead of failing forever (gstack only detects gbrain's external autopilot and cannot reschedule it, so the plan relies on the skip in gstack's own stage). Indexed pages of a deleted worktree may be the only copy of work that was never committed, so this wave never deletes them automatically. A new explicit `gstack-gbrain-sync --prune-gone-worktrees` (with `--dry-run`, which is also what it prints first) removes a source only when recomputing `deriveCodeSourceId()` for its recorded path on this host yields its id (so it was created on this machine), the path has been absent on two consecutive syncs, the repository's git common dir still exists and no longer lists the worktree, and the parent directory is readable. It re-checks existence immediately before `safeSourcesRemove` and logs each removal. The sync output names the command when unavailable sources exist.
|
||||
- **A7. #2783 (cost):** `/sync-gbrain` forces a full `dream` cycle (~35 min, LLM phases) when it only needs `resolve_symbol_edges`. Scope it, only when the installed gbrain supports the phase-scoped dream (detected, not assumed); otherwise skip the forced dream and say the full cycle costs about 35 minutes and how to run it. PR #2783.
|
||||
- **A8. #1437:** the v1.27.0.0 migration passes a bare repo name to `gh repo rename`, which always fails, then points the artifacts remote at the new name anyway. Rewrite the remote only after the rename succeeded; on upgrade, restore the old URL when only the old repo exists, and say so. Adopt #1437 with credit.
|
||||
- **A9. New (found in DX review): `memory-ingest --no-write` destroys future ingestion.** Its help calls it a dry run, but it stamps every prepared page `ingested_at` and saves state (`bin/gstack-memory-ingest.ts:2054`), so a later real run skips those pages forever. `--no-write` leaves state untouched; help and docs say what it does. Test: a `--no-write` run followed by a real run imports every eligible page. About a dozen existing tests use `GSTACK_MEMORY_INGEST_NO_WRITE=1` to seed state; they move to an explicit test seam (`--stamp-only` or a fake gbrain) in the same commit, because the contract changes, and the CHANGELOG lists it under behavior changes.
|
||||
|
||||
### Workstream B: no false all-clear (P2)
|
||||
|
||||
- **B1. #1892 (S1):** when Codex's bwrap sandbox cannot start (containers, devcontainers), every Codex command fails, but `lib/outside-review-result.ts` accepts "could not run commands... no issues found" as `completed:true, gate:pass` (probe). Under INV-1, `lib/outside-review-result.ts` takes text, stderr and exit status and returns `unavailable(sandbox)` when stderr (or a non-zero exit) shows sandbox setup failure (bwrap, user namespaces, landlock, seccomp), or the review text contains an exact execution-failure phrase (commands could not run, the diff could not be read). Sandbox keywords in the review text alone never trigger it, so a real review of sandbox-related code still passes (fixture required). Positive evidence: Codex runs with `--json`, and a review counts as executed only when at least one command or diff read succeeded in its event stream; phrase matching is the last fallback. `codex sandbox` exists in codex-cli 0.160.0; captured fixtures (Docker default seccomp: exit 1 `bwrap: No permissions to create new namespace`; bubblewrap missing: exit 101) map to `sandbox_unavailable`, so the free preflight ships. It runs before the paid model probe, and an inconclusive model probe (timeout, network) reports `CODEX_MODE: unverified` instead of `ready`. A 404 message names both causes: a retired model or a wrong `base_url` on a custom provider. The message reads `Codex outside review unavailable: Codex's sandbox could not start here (<first stderr line>). No review ran; this is missing coverage, not a pass. Fix: enable unprivileged user namespaces for this container, or set GSTACK_CODEX_NO_SANDBOX=1.` Events `codex_sandbox_unavailable` and `codex_model_retired` are logged. Every Codex site routes through it: `scripts/resolvers/outside-voice.ts`, `codex/sections/review-mode` (which today has its own prose gate at `review-mode.md.tmpl:124-139`), `challenge-mode`, `consult-mode` and the /review adversarial pass; a static test fails if any template invokes `codex exec`/`codex review` without it. `GSTACK_CODEX_NO_SANDBOX=1` (exactly `1`, like `GSTACK_CHROMIUM_NO_SANDBOX`, env var only) is honored only when the user sets it; Codex has no read-only mode without a sandbox, so it switches every site to `sandbox_mode="danger-full-access"` and prints on every use `WARNING: GSTACK_CODEX_NO_SANDBOX=1: Codex runs this review without a sandbox and can read and write anything your user can. Use it only inside a container you trust.` It is recorded in the review log; the hint names it only after a sandbox failure. On Linux, a free `codex sandbox ... true` preflight (10-second bound) runs before the first paid Codex call; a matching failure sets `CODEX_MODE: sandbox_unavailable` and skips the paid call; any other result defers to the post-run classification. Fixtures are checked in under `test/fixtures/codex-sandbox/` with their capture command. A clean "I did not find any issues" with empty stderr still passes.
|
||||
- **B1b. New (found in CEO review):** the `structured` gate fails only on `[P1]` findings (`lib/outside-review-result.ts:20`), so a review reporting `[P0]` findings passes. Fail on P0 and P1.
|
||||
- **B2. #2843:** a retired-model 404 ("does not exist") falls through the probe classifier to fail-open `ready` (`bin/gstack-codex-probe:165`). Classify 404 model errors as `model_unusable`. PR #2843.
|
||||
- **B3. #2192:** the Codex auth probe ignores custom OpenAI-compatible providers (`config.toml` `env_key`), so /codex reports AUTH_FAILED for working setups. PR #2192.
|
||||
- **B4. #1076 + #1254:** DESIGN SETUP declares `DESIGN_READY` when the binary is merely executable (`scripts/resolvers/design.ts:1124-1127`); a binary that SIGKILLs on launch still passes. Print DESIGN_READY only when `"$D" --version` exits 0 within 10 seconds (using the existing portable timeout wrapper, since macOS has no `timeout`; a success is cached by the binary's inode and mtime so Gatekeeper's first-launch scan is not re-paid), else `DESIGN_NOT_AVAILABLE: <path> --version exited <code>` (or `timed out after 10s`), adding on exit 137 `(killed at launch; on macOS usually an invalid code signature). Fix: cd <checkout> && ./setup`. Setup's arm64 re-sign keeps a backup, restores it when signing fails, and reports design unavailable when the post-sign exec probe returns 137 (today it exits 0).
|
||||
- **B5. #2790:** `gstack-learnings-search` and `gstack-timeline-read` end with `2>/dev/null || exit 0`, so a missing bun looks like "nothing recorded". Exit non-zero with `bun not found` when bun is missing and keep stderr with a non-zero exit when the embedded script fails; only a successful run with no matches exits 0 with empty output. The message reads `<script>: bun not found on PATH, so learnings could not be read. Fix: install Bun (https://bun.sh), then re-run ./setup.` (exit 127). Generated callers (`scripts/resolvers/learnings.ts:61,76,78` and the timeline caller) stop discarding stderr and status and print `LEARNINGS: unavailable (<first stderr line>)` on failure. PR #2794.
|
||||
- **B6. #2762:** a more specific learnings query drops the exact match (token-OR filter, confidence-only sort, silent truncation, `bin/gstack-learnings-search:124-137`). Rank by matched-token count and disclose truncation. Take PR #2796's code (33 lines) and #2799's regression vectors with credit; #2799's 177-line stemmer is not needed.
|
||||
- **B7. #2940:** the /cso scanner catalog ships empty, and the message blames the host architecture. Say plainly that no scanner image is qualified yet; keep the honest `not_assessed` status.
|
||||
- **B8. #1675:** `bin/gstack-gbrain-install` needs `jq` but never checks for it, so a successful install reports failure. Read package.json with `bun -e` instead; same fix for `gstack-gbrain-source-wireup` (#2369).
|
||||
- **B9. #2918 residual:** `/sync-gbrain` reads CYCLE from `gbrain doctor --fast`, which omits cycle freshness, so it always reports "unknown". Use the field that carries it; when the installed gbrain does not expose it, say `unknown: installed gbrain does not expose cycle_freshness`.
|
||||
- **B11. New (found in DX review): the auto-updater announces success after setup failed.** `bin/gstack-session-update:181-200` logs `SETUP_FAILED` (or `SETUP_SKIPPED bun_missing`), then still writes the "just upgraded" marker; setup only reruns when HEAD moves, so a failed install is never retried. Track pull, setup and migrations as separate stages, announce the upgrade only when all succeed, persist pending stages and retry them with backoff (at most once per update-check interval, then 6h, then 24h) even with HEAD unchanged, and show one failure line with the repair command at the next session start. Because new `bin/` and `lib/` can run under old skill text after a failed setup, every changed CLI keeps its old call form working (see INV-1).
|
||||
- **B10. plan-tune (#2623, honest-status half only):** never report CALIBRATED when zero events contributed; say `not calibrated: no recorded signals`.
|
||||
|
||||
### Workstream C: every host runs every skill (P3)
|
||||
|
||||
- **C1. #1159 (S1, non-Claude hosts):** Codex and every `usesEnvVars` host run each shell block in a fresh shell (probe on codex-cli 0.160: `call2 [][]`). Generated skills set `GSTACK_ROOT/BIN/BROWSE/DESIGN` only in the preamble block, but 49 of 56 Codex skills use them later, so later blocks resolve `/gstack-skill-end`, `/bin/gstack-slug`, `D=/design`, `B=/browse`. One function in `scripts/resolvers/` produces both the preamble's root resolution and a per-fence prelude for env-var hosts (`scripts/resolvers/outside-voice.ts:16` already does this for one block; the preamble today ignores `CODEX_HOME`). The prelude honors `CODEX_HOME`, prefers the install-root literal when setup's per-install render knows it, then the repo-local root, then the global root; re-derives `B`/`D` where a fence uses them; respects a `GSTACK_ROOT` the user already exported; when no root exists, prints `gstack: no install found (tried <paths>). Fix: ./setup --host <host> from your gstack checkout; ./setup --status shows it.` and exits non-zero; is emitted only in fences that reference these variables; and adds at most 400 bytes per fence (a prelude meeting every requirement measures 448 bytes inline, so under a known install root it uses the literal root and no `git` call). It is inserted by one post-render pass in `scripts/gen-skill-docs.ts` for env-var hosts, over SKILL.md and section output, before `rewriteInstallRoot`, into each fence that references `GSTACK_*`, `$B` or `$D` without assigning them. `D` and `B` come from the same path helper DESIGN SETUP and BROWSE SETUP use; the prelude never probes binaries. An exported `GSTACK_ROOT` wins only if it contains `bin/` and `lib/`. `outsideVoiceRuntime()` is replaced by the shared function so there is one owner. Claude-host output bytes must not change (keeps the paid eval selection narrow). INV-3 is the test.
|
||||
- **C2. #2891:** link `design/dist` and `make-pdf/dist` into the Codex runtime root and the agents sidecar (`setup` ~2371-2425). Fold in PR #2891.
|
||||
- **C3. #333:** Codex skips a symlinked `SKILL.md` file (probe with a real-file control); setup links the root router that way (`setup:2387`), so the gstack router is invisible on Codex. Copy router and nested SKILL.md files instead of linking; setup rewrites the copies on every run and prints what it refreshed or removed; setup stores the hash of each copy it writes; when a copy differs from that hash, setup saves it to a uniquely named backup under the state root before activating the new tree (so the backup survives `_activate_runtime_root` replacing the old tree) and says where. The auto-update setup path refreshes the copies too. `./setup --status` shows, per host, whether the router is a real file and whether section links resolve. The live Codex check confirms `codex debug prompt-input` lists the `gstack` router.
|
||||
- **C4. #2777:** the generated ship skill is 202-241 KB on all 10 non-Claude hosts (Codex 227 KB, about 57K tokens) against the 160 KB ceiling, because section carving runs only for Claude. Extend `usesLazySections()` to every skill whose external-host render exceeds 160,000 bytes after C1's prelude: Tier 1 carves ship and plan-ceo-review (160,039 bytes on Factory and 159,331 on Codex today; 77,986 carved). On non-Claude hosts a carved skill points at sections relative to the installed skill directory, using QA's existing form, never `$GSTACK_ROOT/<skill>/sections/...` (that path does not exist in the Codex runtime root); Codex, Factory and OpenCode already link whole skill directories, so setup needs no new section linking. The hard 160,000-byte gate lives in the free test (every env-var host, every model overlay setup can select). The generator fails only in default repo mode; under `--install-root` (setup on user machines) it warns and exits 0, so an oversized render never leaves a user with no skills. Both print printing `<host>/<skill>/SKILL.md is <bytes> bytes, over the 160,000-byte limit. Fix: carve sections with usesLazySections() for this skill.`
|
||||
- **C5. #2626:** OpenCode skills still carry Claude config paths. PR #2759. Also #2651: install a namespaced `/gstack-review` OpenCode command so the builtin `/review` does not shadow it.
|
||||
- **C6. #2263:** `bin/gstack-relink` skips `browse`, so `/browse` disappears after toggling `skill_prefix`. Discover skills structurally. PR #2263.
|
||||
- **C7. #2229:** team mode's required-skill hook matches `Skill` but Copilot CLI sends `skill`, so Copilot runs protected skills. PR #2229.
|
||||
- **C9. New (found in DX review), data-loss risk on fresh-shell hosts:** `gstack-upgrade`'s fences run `cd "$INSTALL_DIR"` and then `git checkout -- '*/SKILL.md'`, `git stash` and `git reset --hard origin/main`. In a fresh shell `INSTALL_DIR` is empty, `cd ""` succeeds and stays put, so those commands would run in the user's own project (9 unguarded sites). This hits Claude Code too: it keeps the working directory between calls but not shell variables, and Step 2 sets relative paths. Fix for every host's render: Step 2 prints an absolute path (`pwd -P`); every destructive fence starts with `cd -- "${INSTALL_DIR:?INSTALL_DIR is not set: re-run Step 2 and substitute the printed path}" || exit 1` and then checks it is really gstack's checkout (`VERSION` and `setup` present and `git rev-parse --show-toplevel` equal to it) before any git command. The INV-3 lint finds 19 such sites (17 in gstack-upgrade, 2 in spec). Tests: unset, empty, nonexistent, unreadable and wrong-repository values each exit before any git command and leave a scratch repo unchanged. Also (Tier 2): Step 2 falls back to the install registry's source checkout for the current host, so Codex users with the README's `~/gstack` clone stop getting `ERROR: gstack not found`.
|
||||
- **C8. #3018:** the root router names every skill and ends "When in doubt, invoke the skill", so it routes to skills the user disabled (Claude Code `skillOverrides`, gstack `disabled_skills`). Tier 2: omit disabled skills from the rendered router (a plain bug fix); `gstack-config set disabled_skills ...` re-renders the Claude router in the same command. Tier 3: change the "when in doubt" wording only if a before/after routing panel (positive and negative prompts) shows missed routes do not rise.
|
||||
|
||||
### Workstream D: security hardening
|
||||
|
||||
- **D1. #1324 residual:** the terminal-agent `/ws` accepts any `chrome-extension://` origin because `BROWSE_EXTENSION_ID` is never set (`browse/src/terminal-agent.ts:46,638-644`). Move `GSTACK_EXTENSION_ID` into a small shared module imported by both `server.ts` and `terminal-agent.ts`; `/ws` rejects every other origin with 403. Forks and self-built extensions override the ID with `gstack-config set browse_extension_id <id>`, honored by both `/extension-token` and `/ws`; the `BROWSE_EXTENSION_ID` env var is no longer read (a project `.env` must never be able to change it).
|
||||
- **D0. Dotenv hardening (found in Eng review).** Bun autoloads `.env` from the working directory, and the terminal agent is spawned with `bun run` in the user's project (`browse/src/terminal-agent-control.ts:200-203`); the compiled binaries are built without `--no-compile-autoload-dotenv` (`scripts/build.sh:35-39`). So an untrusted repo's `.env` can set security-relevant variables, including the existing `GSTACK_CHROMIUM_NO_SANDBOX`. Build compiled binaries with dotenv autoload off, spawn Bun children with autoload off (verified on the CI-pinned Bun 1.4.0), and read security-loosening switches (`GSTACK_CODEX_NO_SANDBOX`, `GSTACK_CHROMIUM_NO_SANDBOX`) only from the shell environment in bash or from `gstack-config`. Test: a project `.env` setting these variables changes nothing. Called out in the CHANGELOG security section.
|
||||
- **D2. #2811:** browse URL validation blocks only `169.254.169.254`, not the 169.254.0.0/16 link-local range, and it checks literals, `isMetadataIp` and DNS answers in three separate places (`url-validation.ts:11-110`). Centralize address classification for literals and resolved A/AAAA answers (IPv4-mapped IPv6, decimal/octal/hex forms, `100.100.100.200`), share it with `session-persist.ts:73`, and apply it to redirects and page-driven navigations, not only `page.goto`. Connection-time DNS rebinding stays out of scope and P5 says so. PR #2811.
|
||||
- **D3. #2818:** plan reviews follow instructions embedded in design docs and handoff notes written by other agents. Treat them, and any repo design doc the plan-completion audit newly reads, as data, not instructions. PR #2818.
|
||||
- **D4. #1078:** `/setup-deploy` runs `echo $RENDER_API_KEY | head -c 4`, printing key bytes into transcripts. Report presence only. PR #1096.
|
||||
- **D5. #2492:** the headed Chromium profile is machine-wide, so starting headed browse in one project SIGKILLs another project's live headed Chromium (`browse/src/cli.ts:283,614`). Per-project profiles live under the project's state directory. `killOrphanChromium` kills a PID from SingletonLock only when the lock's hostname matches this host and the process is a Chromium using that profile. A profile owned by a live process is never killed; browse reports which process holds it instead. On first headed use in a project, seed the new profile from the old machine-wide one only when that profile is not in use (no live SingletonLock), copy it atomically into a 0700 directory without singleton files, and say so once; otherwise start fresh and print how to import later. The CHANGELOG notes it and the disk use; `browse profiles` lists and prunes them.
|
||||
|
||||
### Workstream E: Windows and macOS platform gaps (P4)
|
||||
|
||||
- **E1. #2876 (S1 on Windows):** `/freeze`, `/guard` and `/investigate` deny every edit with a drive-letter path (`freeze/bin/check-freeze.sh:127-131`; probe reproduces). Normalize both the saved boundary and the incoming path through one platform-aware routine (`C:\...`, `C:/...`, lowercase drive letters, case differences, UNC paths without collapsing their leading slashes) before comparison, and exercise the actual registered Windows hook, including cmd.exe quoting, sibling-prefix paths, junction escapes and nonexistent targets. PR #2876. Also #2354's residual: hook commands use `bash $HOME/...`, which cmd.exe does not expand.
|
||||
- **E2. #3015:** the Windows /cso build fails on `/W4 /WX` without `/wd5105`, and every failure is reported as a missing toolchain. PR #3015.
|
||||
- **E3. #2595 / #2124:** Smart App Control blocks the unsigned `browse.exe`, and there is no Node CLI lane, so every browser skill is dead on SAC machines. Add the Node CLI fallback, which prints a clear error naming both launchers when node is also absent. PR #2596 (smaller; #2265 adds a broader launcher rewrite this wave does not need). Tier 3.
|
||||
- **E4. #2260, #2439, #2329:** Windows node-server bundle externals (socks, sharp), eager server-script resolution in `browse/src/cli.ts:86`, and Bun's spurious EEXIST in the generators (use the existing `lib/fs-utils.ts` helper).
|
||||
- **E5. #1674 / #1686:** every Codex invocation passes the prompt as argv, which breaks over 32 KB on Windows and mangles quotes. Feed prompts on stdin (`codex exec -`) in `outside-voice.ts` and `codex/sections/*.tmpl`.
|
||||
- **E6. #1967:** `gstack-config gbrain-refresh` passes an MSYS path into native Python, so Windows always reports `unknown`. Read via stdin. PR #1981.
|
||||
- **E7. #2902:** the Aside probe only checks `command -v aside`, but the installer puts the CLI in `~/.local/bin`, which is not on the default macOS PATH, so users get a wrong "download Aside" message. PR #2965.
|
||||
- **E8. #1968 / #2771:** headless browse ignores `GSTACK_CHROMIUM_PATH` (`browser-manager.ts:568-571`), so NixOS and macOS 13 users cannot run headless browse or make-pdf. When the path is set, skip the XProtect self-heal (it would reinstall the Playwright cache) and name the custom path in launch failures. PR #2771. Also #2281: load extensions via `--headless=new` instead of an off-screen headed window (same launch function).
|
||||
|
||||
### Workstream F: the right plan and design files
|
||||
|
||||
- **F1. #2768 / #2800:** /ship and /review's Plan Completion Audit never searches `docs/designs/` and falls back to matching branch names, which plan-mode files never contain, so it audits PRs against an unrelated plan. Prefer an explicit binding: the plan or design path recorded when work began (plan-mode file, `/autoplan` ACTIVE_PLAN, or a `Plan:` line in the PR body). Without a binding, search repo-committed `docs/designs/` and offer candidates; when nothing matches, print `Plan completion audit: not run (no plan is bound to this branch and no docs/designs/ file matches). Fix: add "Plan: <path>" to the PR body, or run /autoplan.` instead of falling back to the newest unrelated file. PR #2797.
|
||||
- **F2. #2839:** design-doc discovery prefers a root `DESIGN.md` (a design system) over `docs/designs/` feature docs. PR #2840.
|
||||
- **F3. New (found in triage):** `/design-consultation` and `/design-shotgun` never load the taste profile, because `scripts/resolvers/design.ts:1390,1392` double-quotes a `~/` path (probe always prints `NO_TASTE_PROFILE`). Use `quoteSafePath()`. The block also hides stderr, so a broken slug lookup still prints `NO_TASTE_PROFILE`; print `TASTE_PROFILE_UNAVAILABLE: could not resolve the project slug (gstack-slug failed). Fix: run ./setup.` in that case.
|
||||
- **F4. #1529 (paid artifacts):** each regenerate round overwrites the previous round's `variant-*.png` (`design/src/variants.ts:189`). Write each round to a collision-safe directory with a manifest of what succeeded; separate the remote-generation retry from local-write retries so a disk error never re-buys an image that was already paid for; give responsive variants unique names. List and update every reader (comparison board, approved-variant references, design-html handoff) and test choosing a round-1 variant after round 2. Previously overwritten variants cannot be recovered, and the fix does not claim otherwise. PR #1737.
|
||||
- **F5. #2706:** setup links installed `sections/` from the checkout instead of the render dir, so gbrain-aware section blocks never reach the agent. Probe on a gbrain install first, then PR #2707.
|
||||
|
||||
### Workstream G: /ship and daily-driver papercuts outside gstack's own repo (P4)
|
||||
|
||||
- **G1. #2334 / #2343:** /ship stops (`bin/gstack-version-bump:294` DRIFT_UNEXPECTED) or invents `0.0.0.0` in repos with no VERSION file, which is most repos. Resolve the version file through `resolveVersionRel` (`bin/gstack-version-bump:131`: `--version-path`, then the `.gstack/version-path` pin, then `VERSION`); `lib/version-source.ts` handles JSON and 3-digit shapes. When the project configures one, use it. When it is unspecified or ambiguous (release automation, monorepos, placeholder versions), ship without a version change and say so, instead of inventing one: no bump, no CHANGELOG version header, no version-prefixed PR title. The notice reads `Shipped without a version change: no version source is configured (no VERSION file, no .gstack/version-path). To version releases, create VERSION or write the version file's path (for example package.json) to .gstack/version-path.` docs/skills.md documents `.gstack/version-path`. Resolution has four outcomes: absent (ship without a version), valid, ambiguous (ship without a version and say why) and broken (a pinned file that is missing, empty, unreadable or malformed stops with an error; never a silent `0.0.0.0`). One "no version" signal is read by the bump, the CHANGELOG header, the PR title, `gstack-next-version`, `/document-release` and tagging. Validate through the generated /ship flow against a versionless app, a monorepo, a release-please repo and each broken-pin case. PRs #2334 and #2343.
|
||||
- **G2. #2767:** a linked worktree gets a different project slug from the main checkout (cache keyed on pwd), so learnings and checkpoints split. Key the sticky cache on the git common dir in both implementations that share it (`bin/gstack-slug` and `lib/bin-context.ts`), with a deterministic rule for which existing identity wins when main and worktree caches disagree. Today `gstack-slug --adopt-legacy` only lists a legacy bucket and copies files that are missing at the destination, so it cannot merge two learnings logs. Add `--adopt-legacy --from <slug>`: append-only JSONL logs merge with de-duplication, missing files copy, conflicting checkpoints are listed and never overwritten, originals are kept, and it prints migrated/pending/conflicting counts. Skill-start names a non-empty former worktree bucket with that command until it is adopted or dismissed. Tested with overlapping files and reruns. PR #2792.
|
||||
- **G3. #2952:** bare `mktemp` ignores `$TMPDIR`, so learnings and question logs are silently dropped under the Claude Code macOS sandbox. PR #2952.
|
||||
- **G4. #494:** with `HTTP_PROXY` set, the browse CLI's loopback fetches go through the proxy (probe), so the daemon never looks healthy. Append `127.0.0.1`, `localhost` and `::1` to the user's existing `NO_PROXY` (never replace it) for CLI-to-daemon calls, after confirming Bun 1.4.0's `fetch` honors it (if not, bypass the proxy explicitly on loopback calls). Tested with a proxy that fails every request.
|
||||
- **G5. #2936:** `browse type` with a selector argument types the selector text into whatever is focused and reports success. Add `browse type --selector <sel> <text>` (matching browse's other flags). The bare form keeps typing literal text exactly as today, including text that looks like a selector or starts with `--` after a `--` separator; when the text has CSS selector syntax (`#`, `.`, `[`, `>`), it prints a hint to use `--selector`, but never refuses based on page contents. Both help surfaces updated. PR #2936.
|
||||
- **G6. #2334-adjacent riders, all XS with tests in open PRs:** #1728 analytics duration always 0s, #1777 rejected taste preferences stuck at confidence 0, #1419 `prettyscreenshot --hide` eats the path, #1932 `gstack-ios-qa-daemon --help` starts the daemon, #2704 `context-save` duration empty on Linux, #2812/#2809/#2037 retro test census misses pytest/XCTest and counts generated output.
|
||||
|
||||
### Workstream H: redaction false positives that block pushes (P4)
|
||||
|
||||
Each is a confirmed probe on main and an XS change in `lib/redact-patterns.ts` with a regression vector; adopt the PR with credit. Each relaxation also ships true-positive controls that must still flag at the same tier: a real E.164 number in prose, a non-default `postgres://user:secret@prod-host`, a real public IPv4 in config, a real internal hostname, and a credential-named variable assigned a literal secret. Exemptions need context, not just a value shape: each pairs the same value in a benign and a sensitive context (a four-part version after `v`/`version` vs the same digits as a config host). `postgres:postgres` is exempt only with `localhost` or `127.0.0.1`, never a bare single-label host. `.env.local` suppression already exists (`redact-patterns.ts:316`), so #2740 closes as superseded:
|
||||
- #2885 decimal coordinates flagged as phone numbers; #2827 SVG path floats (about 200 MEDIUM findings per vector file).
|
||||
- #2899 `session = _FlakySession(...)` flagged as `env.kv`.
|
||||
- #2962 `CLAUDE.local.md` / `settings.local.json` flagged as internal hostnames; #2740 `.env.local` likewise.
|
||||
- #2784 four-part version strings flagged as public IPv4.
|
||||
- #2913 `postgres:postgres@<host>` default credential.
|
||||
- #2731: `install-prepush-hook` never refreshes a stale managed hook. The DX review found `bin/gstack-redact:83` already compares and refreshes the wrapper; verify the remaining upgrade path before adopting, and close #2731 as superseded if it holds.
|
||||
|
||||
### Workstream I: eval reliability (the weekly red lanes)
|
||||
|
||||
- **I1 (runs first, before any generator change).** Dispatch the periodic lane and gate census on main (post #3014/#3017) for a current baseline. Classify every red across the last five weekly runs plus the baseline by cause: product defect, unclear instructions, leaky fixture, timing, over-strict checker, infra. Fix causes; product defects get fixed in the product. Thresholds, kinds, trial counts and retries do not change.
|
||||
- **I2.** Check the skillify family on current main first; TODOS.md:1235 says #2721 fixed it on 08-29. If green, close the TODOS.md:1102 entry with the run link.
|
||||
- **I3.** Judges score actionability at exactly the 4.0 gate. For the cases that fail most (qa-only, qa, plan-ceo modes, plan-eng sections, review), read the low-scoring judge rationales first to find the cause, then clarify that skill text.
|
||||
- **I4. Coverage accounting.** The 10-01 census skipped 112 of 148 rule cases and left 71 unattributed, which is P2's own false all-clear inside the eval system. Every skipped case names its reason and the census reports zero unattributed cases.
|
||||
- **I5.** No case in the PR lane exceeds 600 seconds, by splitting or shortening without weakening assertions (the ~10-minute lane target).
|
||||
- **Done when:** every required PR check is green with no retries; the periodic lane and gate census have run on the wave branch with every failed, timed-out or skipped case classified with evidence and every deterministic failure fixed; run links and `eval:pass-rates` output are in the PR.
|
||||
|
||||
### Workstream J: tracker hygiene
|
||||
|
||||
- Close the ~190 PRs triage marked superseded or junk with a one-line comment naming the release that contains the fix, and thank contributors. Spot-check 10% against main first, and correct the two known triage errors (close #2761 as superseded by `health/SKILL.md.tmpl:207-210`; B6 credits #2799). This does not depend on the code; approved to run now.
|
||||
- Close implemented feature requests (#292, #368, #839), the answered question #28, and the duplicates #589/#682.
|
||||
- On merge, close every issue this wave fixes with a comment naming the release, the upgrade command for the reporter's host (Claude: `/gstack-upgrade`; Codex and other env-var hosts: `cd <source from ./setup --status> && git pull && ./setup --host <host>`), and asking them to reopen with `./setup --status` output if it still reproduces.
|
||||
- Every adopted PR's author gets a `Co-authored-by` trailer and a CHANGELOG credit; their PRs are closed at merge (not at approval) with a link to the wave PR. Bulk closes are throttled under GitHub's rate limits, and the 10% spot-check is spread across triage categories.
|
||||
|
||||
### Build order and lanes (from the Eng review)
|
||||
|
||||
Start together: I1 paid baseline (no code), S1 INV-1 contract (`lib/outside-review-result.ts`, `lib/gate-outcomes.ts`, troubleshooting skeleton), S2 runtime root and carve core (`scripts/resolvers/`, `scripts/gen-skill-docs.ts`, INV-3 render test), memory/gbrain lane (A), browse and platform lane (D, E), redaction lane (H). C9 goes first inside the generator lane. When S1 lands: Codex gates lane (B1-B3, E5) and ship/review/plan/design lane (F, G, B5, B6, B10, D3, D4). When S2 lands: generator consumers lane (B4, F3, C8, C9, E7) and setup lane (C2, C3, C5-C7, B11, Q3). Each lane runs on its own machine and branch, commits sources only, and is merged into the wave branch in order; one integrator regenerates, writes docs and release metadata, runs the free suite once on Ubicloud, does the live Codex check, and drives paid lanes.
|
||||
|
||||
### Product decisions (approved by Garry at the gate, 2026-10-03)
|
||||
|
||||
- **Q1. Garry's stale PR #2971** (v1.90.1.0, 100 files, green on 09-25, now conflicting) holds privacy and consent fixes. Main already has transcript ingestion consent and publication gating (`memory-ingest.ts:230`, `gstack-brain-sync:396`), so first reconcile each #2971 group against main. **Approved:** salvage only the groups verified still missing (browser output-destination validation, bounded iOS proxy responses, separate publication consent if absent, and pieces that overlap A3, D4 and G6) as credited commits, then close #2971.
|
||||
- **Q2. #965:** Codex outside-voice reviews are on by default and use whatever Codex account is logged in. Voices split: the Claude reviewers suggested a one-time notice or a once-per-machine question; Codex suggested permission per repo and provider. **Approved:** the first outside review on a machine names the provider and account it will use and how to turn it off (`gstack-config set codex_reviews disabled`), with no blocking question; reviews stay on by default.
|
||||
- **Q3. #2207 / #2719 (Conductor):** **Approved (all reviewers agreed):** stop auto-installing the question-preference hook in Conductor, remove it where setup installed it, and adopt PR #2729's open-question prose form.
|
||||
|
||||
### Delivery
|
||||
|
||||
One PR, as usual. Commits are per fix, ordered by workstream, so any single fix can be reverted. The PR body and the squash message carry every adopted author's `Co-authored-by` trailer. The release version comes from the workspace-aware queue at /ship (expected v1.91.17.0, patch). Release notes lead with P1 and P2 (gates that tell the truth, memory that does not lose data). The CHANGELOG tells users how to upgrade per host (Claude: `/gstack-upgrade`; Codex and other env-var hosts: terminal `git pull && ./setup --host <host>`, then a new session) and gains "Behavior changes you may notice", each with what to do: Codex reviews in containers say unavailable instead of pass; the structured gate fails on P0; versionless /ship ships without a version; `browse type --selector`; the plan audit says `not run` instead of auditing an unrelated plan; learnings and timeline report a missing bun; design says DESIGN_NOT_AVAILABLE when the binary cannot start; `/sync-gbrain` no longer forces the 35-minute dream; a one-time transcript catch-up after reconcile; disabled skills leave the router; per-project headed browser profiles; `--no-write` no longer stamps state. A new `docs/troubleshooting.md`, linked from the README and organized by the exact message users see (meaning, what data is kept, next command, expected result), holds every new message with a stable anchor; a free test checks each anchor exists. New gbrain messages also go into `docs/gbrain-sync-errors.md`. The wave's migration script is named for the version /ship assigns (renamed if the queue moves it), and a free test asserts every migration has a matching CHANGELOG header and is not newer than VERSION.
|
||||
|
||||
**File ownership during implementation (one owner per shared file):** `bin/gstack-memory-ingest.ts` (A1, A2, A4), `bin/gstack-gbrain-sync.ts` (A5, A6, A7), Codex invocation files (B1, B2, B3, E5), `browse/src/browser-manager.ts` (E8, #2281, D5), `setup` (C2, C3, F5, B4's re-sign, H #2731, Q3), `scripts/resolvers/` generator core (C1, C4, C8, F3). Implementation lanes commit templates and sources only; one integrator regenerates and commits every generated SKILL.md and golden, and owns CHANGELOG.md, TODOS.md, README.md, AGENTS.md, `docs/troubleshooting.md`, `test/helpers/touchfiles-data.ts`, VERSION and the migration rename. More owners: ship and review templates (one owner), `bin/gstack-config` (setup owner), `scripts/resolvers/design.ts` (generator owner). `scripts/test-free-shards.ts` and `browse/src/server.ts` are at their module-size cap, so edits there are net zero lines. AGENTS.md key conventions gain two lines: every env-var-host fence starts with the shared prelude; every Codex run's verdict goes through `lib/outside-review-result.ts`.
|
||||
|
||||
### Validation
|
||||
|
||||
- Each fix lands with a regression test that fails on main, labeled by evidence type in the PR body: unit test, captured-output fixture (gbrain skips, Codex sandbox stderr, Conductor), or platform job (Windows, macOS). Fixes that cannot be reproduced on Linux CI say so.
|
||||
- Every E-workstream test appears in `bun run scripts/test-free-shards.ts --windows-only --list`; that output goes in the PR.
|
||||
- Free suite: `bun run test` once at the end on frozen code (Ubicloud), plus `windows-free-tests` and the macOS job for E-workstream fixes.
|
||||
- Paid evals: the PR lane will run the full gate census (103 gate cases, 24 judges), because `setup`, `bin/gstack-memory-ingest.ts`, `bin/gstack-gbrain-sync.ts`, `bin/gstack-session-update`, `bin/gstack-version-bump`, `bin/gstack-relink` and `design/src/variants.ts` have no touchfile entry. I1's baseline finishes first so known flakes are classified. Before merge, the periodic cases this wave changes run on the wave branch: codex-review, codex-review-findings, codex-discover-skill, outside-voice-codex-to-claude-code, outside-voice-claude-code-to-codex, shared-libs-codex-read-only, outside-plan-disabled-no-fallback, autoplan-dual-voice, carve-section-loading (codex), ship-section-loading, plan-ceo-section-loading, setup-gbrain-path4-local-pglite, setup-gbrain-bad-token, sync-gbrain-read-ready, sync-gbrain-read-unknown, design-consultation-core, design-review-fix, llm-judge-recommendation. Kinds, trials and thresholds are unchanged. Claude-host SKILL.md bytes stay unchanged by C1/C4 (tested).
|
||||
- "Fails on main" is shown mechanically: the new tests run against origin/main's code and the PR body lists the expected failures.
|
||||
- Platform tests are registered in the jobs that actually execute them (the macOS job uses an explicit file list); a listed-but-skipped test is not coverage.
|
||||
- Workstream I classification is time-boxed to two days; anything still nondeterministic after that goes to TODOS.md with its pass-rate data, but no required PR check may be red.
|
||||
- C1 needs a live Codex check: from a fresh `CODEX_HOME` and a fresh clone, run setup and one multi-block skill (`/review`) under codex-cli 0.160, confirm later blocks resolve paths and `codex debug prompt-input` lists the router, and time it from `git clone` to the skill finishing (target 2-5 minutes, prerequisites reported separately). Commands and wall-clock time go in the PR body.
|
||||
|
||||
### Not in scope
|
||||
|
||||
- Redaction repo allowlist (#2598, new config surface), global-discover Codex session schema (#2750), ios-qa boot-token fixes (#1837, #1975; need a device), absolute bun path for compiled browse spawns (#931; macOS repro unclear). All four go to TODOS.md with reasons.
|
||||
- New hosts (Gemini, Antigravity #2893/#2609, Kimi #2674, Pi, Hermes full port #2711), new skills (/grok #2947, /pm, /plan-status), Agent Skills distribution (#113), `CLAUDE_CONFIG_DIR` support across every skill (#349, L), Windows sidebar terminal via ConPTY (#1221), qualifying real /cso scanner images (#2940 beyond the message), plan-tune psychographic redesign (#2623).
|
||||
|
||||
## Review record
|
||||
|
||||
Autoplan run started 2026-10-03 (GSTA-21). Triage evidence: ~/.capy/work/wave/triage-*.md.
|
||||
|
||||
### CEO phase (Phase 1), 2026-10-03
|
||||
|
||||
Voices: Claude primary review (full plan-ceo-review methodology, SELECTIVE EXPANSION, `~/.capy/work/wave/reviews/ceo-primary.md`), Claude native blind voice (`reviews/ceo-native.md`, INPUT hash c32ad956 matched), Codex outside voice (gpt-6-astra via API key, `voices/ceo-codex.text`, OUTSIDE_STATUS completed). Process note: the three voices ran in parallel on the same snapshot rather than Step 0 first, then voices; Step 0 amendments were reconciled afterward.
|
||||
|
||||
```
|
||||
CEO DUAL VOICES — CONSENSUS TABLE:
|
||||
Dimension Claude Codex Consensus
|
||||
1. Premises valid? partly partly CONFIRMED: A1 premise wrong (exit-0 skips, no recovery); P3 unproven
|
||||
2. Right problem to solve? yes yes CONFIRMED (trust: gates and memory)
|
||||
3. Scope calibration correct? no no CONFIRMED: too many instances; add invariants and a cut line
|
||||
4. Alternatives sufficiently explored? no no CONFIRMED: invariants over instances; host tiers
|
||||
5. Competitive/market risks covered? no no CONFIRMED: lead with truthful gates and safe memory
|
||||
6. 6-month trajectory sound? risky risky CONFIRMED: omnibus landing risk (#2971 precedent)
|
||||
```
|
||||
|
||||
Disagreements (taste, surfaced at the gate): Codex recommended staged releases (several PRs); both Claude voices recommended one PR with tiers. Not a user challenge (only one model). Kept one PR with a written cut line. Q2 consent shape split three ways (see Q2).
|
||||
|
||||
Single-voice criticals accepted: A4 cross-project misattribution (Codex, verified `memory-ingest.ts:439`), A6 deleting other machines' sources (Codex, verified `gstack-gbrain-sync.ts:540`), structured gate passes P0 findings (Codex, verified `outside-review-result.ts:20`), D1 under-weighted (native).
|
||||
|
||||
Decision Audit Trail (CEO): 23 primary amendments (CEO-1..23) accepted, 10 taste decisions (T1 per-fence prelude; T2 reconcile conditional on gbrain lookup; T3 #2796 code + #2799 vectors; T4 merge on required checks with weekly lanes classified; T5 include #2281; T6 defer #2750; T7 salvage #2971; T8 Q2 disclosure; T9 Q3; T10 defer ios-qa), plus native/outside additions: INV-1..3, delivery tiers, P3 narrowed to tested scope, P5 security promise, I1 first, C8 split, A4 partition, A6 positive-evidence removal, B1b, P4 true-positive controls, F1 explicit binding, G1 version ownership, tracker hygiene now. Full rows: `reviews/ceo-primary.md` "Decision Audit Trail".
|
||||
|
||||
<!-- autoplan-accepted:ceo -->
|
||||
- INV-1 gate outcome contract with lint; INV-2 write-verify-stamp; INV-3 host conformance test and generated-bash lint, each failing on origin/main and passing on the branch.
|
||||
- Delivery tiers as written; C1 before C4; Claude-host SKILL.md bytes unchanged by C1/C4.
|
||||
- A1 landing check plus `--reconcile` (conditional on gbrain lookup, else TODOS with reason); remote-HTTP records staged, not ingested.
|
||||
- A4 partitions by originating repo; unattributable transcripts stay local. A6 never deletes without positive ownership evidence.
|
||||
- B1 routes every Codex site through one classifier (static test); B1b fails structured gate on P0.
|
||||
- P4 relaxations ship with true-positive controls. P5 security promise with D1/D2 tests.
|
||||
- F1 reports `not run: no matching plan` instead of guessing. G1 never invents a version.
|
||||
- Workstream I runs first and finishes on the written done-condition; no threshold, trial or retry changes.
|
||||
- One PR, per-fix commits, every co-author trailer in the squash message; tracker hygiene runs right after approval with a 10% spot-check.
|
||||
<!-- /autoplan-accepted:ceo -->
|
||||
|
||||
Close note (CEO): `prepare-close` refused because amendments were applied as direct rewrites rather than recorded baseline edits; the packet export was skipped and the full current Implementation plan was re-read end to end (lines 1-166) and checked against every accepted item above instead. A duplicated-section edit error found during that read was repaired.
|
||||
|
||||
### DX phase (Phase 2.5), 2026-10-03
|
||||
|
||||
Voices: Claude primary review (full plan-devex-review methodology, DX POLISH, `reviews/dx-primary.md`, 23 amendments), Claude native blind voice (`reviews/dx-native.md`, INPUT hash 39329df9 matched, 25 findings: 0 critical, 8 high), Codex outside voice (gpt-6-astra, `voices/dx-codex.text`, completed, 9 findings). Same parallel-voices process note as CEO.
|
||||
|
||||
DX overall: 5.5/10 before amendments, 7.9/10 after (Getting started 5→7, CLI 6→8, Errors 5→8, Docs 4→8, Upgrade 4→8, Dev env 7→8, Community 8→9, Measurement 5→7). TTHW on Codex: today a multi-block skill never finishes; target 2-5 minutes, measured in the live check.
|
||||
|
||||
```
|
||||
DX DUAL VOICES — CONSENSUS TABLE:
|
||||
Dimension Claude Codex Consensus
|
||||
1. Getting started < 5 min? no no CONFIRMED: not demonstrated; add timed clean Codex journey
|
||||
2. API/CLI naming guessable? partly partly CONFIRMED: G5 page-dependent refusal is wrong; --reconcile shape
|
||||
3. Error messages actionable? no no CONFIRMED: INV-1 needs states, exit codes, fix lines
|
||||
4. Docs findable & complete? no no CONFIRMED: message-keyed troubleshooting page
|
||||
5. Upgrade path safe? no no CONFIRMED: auto-updater lies on setup failure; Codex upgrade path; migrations
|
||||
6. Dev environment friction-free? partly partly CONFIRMED: B1 keyword false positive; G4 NO_PROXY append
|
||||
```
|
||||
|
||||
Single-voice findings verified and accepted: `--no-write` stamps state (Codex, verified `memory-ingest.ts:2054`, now A9); auto-updater writes "just upgraded" after SETUP_FAILED and never retries (Codex, verified `gstack-session-update:181-200`, now B11); validator CLI exits 0 on `[P1]` (Codex, verified by probe); unguarded `cd "$INSTALL_DIR"` before destructive git in gstack-upgrade (Claude primary, verified `gstack-upgrade/SKILL.md.tmpl:134,176`, now C9); tier gap C4/G1 (Codex). Disagreement resolved as taste: D1 keeps `BROWSE_EXTENSION_ID` as a fork override (native) instead of removing it (primary). Taste: T-DX1 env-var-only NO_SANDBOX; T-DX2 registry fallback in gstack-upgrade (Tier 2); T-DX3 `db-unreachable` renders like timeout; T-DX4 sandbox preflight conditional on a real fixture; T-DX5 G5 hint-not-refuse. Deferred: a separate `gstack-host-check` command (folded into `./setup --status` instead).
|
||||
|
||||
<!-- autoplan-accepted:dx -->
|
||||
- INV-1 states, one-line `not run/unavailable (...). Fix:` format, validator CLI verdict and exit codes, consumer behavior in /ship and /review, tested through CLI plus generated caller.
|
||||
- Tier 1 adds A9, B11, C4, C9, G1.
|
||||
- A1 `unverified` state without lookup; `--reconcile` mode with `--dry-run`, `--limit`, bounded, resumable, state backup, summary line; migration marks pending only.
|
||||
- A2 `db-unreachable`; A4 `--include-unattributed` to a non-federated local source; A6 `--dry-run` and opt-out.
|
||||
- B1 stderr-only keywords plus exact text phrases with a clean-mention fixture; NO_SANDBOX semantics and warning; conditional preflight.
|
||||
- C1 respects user `GSTACK_ROOT` and prints the setup fix line; C3 preserves hand edits; C9 `${VAR:?}` guards plus lint; G2 `--adopt-legacy --from`; G5 `--selector` without page-dependent refusal; G4 appends NO_PROXY; D5 seeds per-project profile.
|
||||
- `docs/troubleshooting.md` with anchor test; CHANGELOG behavior-changes list and per-host upgrade commands; migration naming test; timed fresh Codex journey in the PR body.
|
||||
<!-- /autoplan-accepted:dx -->
|
||||
|
||||
### Eng phase (Phase 3, last), 2026-10-03
|
||||
|
||||
Voices: Claude primary review (full plan-eng-review methodology, `reviews/eng-primary.md`, 18 amendments, test plan at `~/.gstack/projects/garrytan-gstack/garrytan-severe-fix-wave-oct3-test-plan-20261003.md`, 113 planned paths, 101 gaps closed by the plan's tests), Claude native blind voice (`reviews/eng-native.md`, INPUT hash 0ff816c3 matched, 3 critical, 7 high), Codex outside voice (gpt-6-astra, `voices/eng-codex.text`, completed, 20 findings). Same parallel-voices process note.
|
||||
|
||||
```
|
||||
ENG DUAL VOICES — CONSENSUS TABLE:
|
||||
Dimension Claude Codex Consensus
|
||||
1. Architecture sound? partly partly CONFIRMED: validator compat, A1 state identity, C4 pointers need fixes
|
||||
2. Test coverage sufficient? no no CONFIRMED: INV-3 must check installed files and consumers; fail-on-main proof
|
||||
3. Performance risks addressed? partly partly CONFIRMED: bounded landing checks; full gate census expected
|
||||
4. Security threats covered? no no CONFIRMED: dotenv override path (Claude), D2 three paths and redirects (both)
|
||||
5. Error paths handled? no no CONFIRMED: C9 failed cd, poison page, lookup errors, broken version pins
|
||||
6. Deployment risk manageable? partly partly CONFIRMED: old skills vs new lib after failed setup; dependency-complete cuts
|
||||
```
|
||||
|
||||
Critical findings accepted: two-argument validator exit codes kept (all three voices); dotenv can set security switches (native, verified `terminal-agent-control.ts:200-203`, `scripts/build.sh:35-39`), now D0; C9 covers every host and failed `cd`, not just empty variables (native and Codex). Taste: T-ENG1 400-byte inline prelude with literal root under install roots (native preferred a sourced helper file); T-ENG2 `unverified` verdict and `execution` gate; T-ENG3 A6 never deletes automatically, explicit `--prune-gone-worktrees` instead (Codex: indexed pages may be the only copy; CEO had accepted guarded automatic removal); T-ENG4 browser extension ID override moves from env var to `gstack-config` (DX native wanted the env var kept). No user challenges: no change to Garry's direction was recommended by both models.
|
||||
|
||||
<!-- autoplan-accepted:eng -->
|
||||
- Validator: `classifyOutsideReview` with separate execution/findings/verdict; two-arg CLI exit codes unchanged; `--verdict` form 0/3/4/1; old-caller compatibility test; `unverified` verdict; `execution` gate; reason codes in `lib/gate-outcomes.ts`.
|
||||
- INV-3 extends host-conformance with installed-file checks, runs prelude plus consumers under `env -i`/`set -u`, normalizes placeholders, quote-aware lint on every host incl. Claude, UTF-8 byte gate in the free test only.
|
||||
- C1 prelude ≤400 bytes via one post-render pass, literal root under install roots, validated exported root; C4 relative section pointers, ship and plan-ceo-review carved in Tier 1.
|
||||
- A1 versioned state with transition table, parsed-bytes fingerprint, recorded source_id, lookup errors not absent, bounded list/get checks, poison-page quarantine, single lock, `lib/memory-ingest-landing.ts`. A3 child cwd isolation. A4 sources registered non-federated before import; unattributed pages never in publishable staging. A6 explicit prune only.
|
||||
- D0 dotenv hardening; D1 config-based override for both routes; D2 central classifier for literals, DNS and redirects; D5 never kills a live profile owner, seeds only from quiescent profile.
|
||||
- B1 `--json` execution evidence, preflight before model probe with checked-in fixtures, inconclusive probe is `unverified`; B11 backoff; B4 portable timeout and cache.
|
||||
- C9 `cd -- "${VAR:?}" || exit 1` plus checkout identity check, 19 sites, all hosts.
|
||||
- G1 four resolution outcomes; G2 both slug implementations; E1 one normalization for boundary and target; F4 separate paid-generation retry from local writes; H context-paired controls.
|
||||
- Full gate census expected plus the listed periodic cases on the branch; tests shown failing on main; platform tests registered in executing jobs; I time-boxed to two days; lanes commit sources, one integrator.
|
||||
<!-- /autoplan-accepted:eng -->
|
||||
|
||||
### TODOS.md additions (all phases)
|
||||
|
||||
Redaction repo allowlist (#2598); global-discover Codex session schema (#2750); ios-qa boot-token fixes (#1837, #1975); absolute bun path for compiled browse (#931); GitLab CI `curl | bash` (#1713); `engines.bun` floor vs CI pin (#2815); marketplace distribution channel; gate-outcome telemetry; /gstack-upgrade redesign around the install registry; container user-namespace docs; Tier 3 items if they slip (E3, C8 wording, G6 riders); upstream gbrain batch lookup and `pending` list; live Codex multi-block check as a periodic paid case; touchfile entries for setup and memory/gbrain bins; placeholder convention for generated bash; close the skillify TODO if I2 is green.
|
||||
|
||||
### Phase coverage
|
||||
|
||||
| Phase | Host | Outside voice | Native voice | Primary review | Findings |
|
||||
|---|---|---|---|---|---|
|
||||
| CEO | Capy (Claude) | Codex gpt-6-astra: completed | completed (hash matched) | completed | 23 + 10 native + 10 outside |
|
||||
| Design | — | skipped (no UI scope) | skipped | skipped | — |
|
||||
| DX | Capy (Claude) | Codex gpt-6-astra: completed | completed (hash matched) | completed, 5.5→7.9 | 23 + 25 native + 9 outside |
|
||||
| Eng | Capy (Claude) | Codex gpt-6-astra: completed | completed (hash matched) | completed | 18 + 3 crit/7 high native + 20 outside |
|
||||
|
||||
### Final approval gate, 2026-10-03
|
||||
|
||||
Garry approved the plan as written (A: approve as-is), one PR. Q1 salvage still-missing #2971 groups then close it; Q2 one-time non-blocking Codex account notice, reviews stay on; Q3 Conductor hook change (bundled in approval); bulk-close ~190 superseded/junk PRs now after a 10% spot-check. Status: APPROVED.
|
||||
|
||||
Reference in new issue
Block a user