diff --git a/.github/workflows/free-tests.yml b/.github/workflows/free-tests.yml index 65d14b08f..948b597ee 100644 --- a/.github/workflows/free-tests.yml +++ b/.github/workflows/free-tests.yml @@ -146,7 +146,7 @@ jobs: resolved="$(realpath "$tmp")" echo "temp root: $tmp -> $resolved" test "$resolved" != "${tmp%/}" - files=(test/gstack-design-detect.test.ts browse/test/chromium-profile-isolation.test.ts test/cso-git-hardening.test.ts test/plan-count-file-permission.test.ts test/spec-quality-gate-secret-sink.test.ts test/gstack-memory-ingest.test.ts test/regression-issue3024-zsh-sourced-helpers.test.ts test/codex-probe-subcommands.test.ts) + files=(test/gstack-design-detect.test.ts browse/test/chromium-profile-isolation.test.ts test/cso-git-hardening.test.ts test/plan-count-file-permission.test.ts test/spec-quality-gate-secret-sink.test.ts test/gstack-memory-ingest.test.ts test/regression-issue3024-zsh-sourced-helpers.test.ts test/codex-probe-subcommands.test.ts browse/test/proxy-env-daemon.test.ts browse/test/proxy-direct-launch-args.test.ts) for file in "${files[@]}"; do test -f "$file"; done bun test "${files[@]}" diff --git a/.gstack/ship-policy.json b/.gstack/ship-policy.json index cd1e67330..f5b84ce2c 100644 --- a/.gstack/ship-policy.json +++ b/.gstack/ship-policy.json @@ -12,6 +12,6 @@ "stamp_sensitive_gates": ["tests"], "pr_count": { "default_pr_count": 1, "split_requires_owner": true }, "maintainers": ["garrytan"], - "train": { "gate_workflow": "evals.yml", "free_workflow": "free-tests.yml", "full_gate_check": "gstack/full-gate", "record_artifact": "gstack-train-record", "max_size": 5, "fill_hours": 3, "screen_selection": "gstack-paid", "trial_reservation_usd": 1.5 }, + "train": { "gate_workflow": "evals.yml", "free_workflow": "free-tests.yml", "full_gate_check": "gstack/full-gate", "record_artifact": "gstack-train-record", "max_size": 3, "fill_hours": 1, "screen_selection": "gstack-paid", "trial_reservation_usd": 1.5 }, "min_gstack_version": "1.91.146.0" } diff --git a/AGENTS.md b/AGENTS.md index ac84585d1..b49cc15a4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -64,6 +64,7 @@ Invoke them by name (e.g., `/office-hours`). | `/learn` | Manage what gstack learned across sessions. | | `/retro` | Weekly retro with per-person breakdowns and shipping streaks. | | `/health` | Code quality dashboard (type checker, linter, tests, dead code). | +| `/captain` | Run a crew from one controller: READY contract, merge trains, digest, approvals shapes, daily flaky pass. Also `/crew`. Templates in `captain/templates/`, team guide in `docs/captain.md`. | | `/benchmark` | Performance regression detection (page load, Core Web Vitals). | | `/benchmark-models` | Cross-model benchmark for skills (Claude, GPT, Gemini side-by-side). | | `/eval-plan` | Preregister a paid eval (bars, arms, decoys, stop rules), dry-run it for $0, pilot three priced items, then approve the spend. | diff --git a/BROWSER.md b/BROWSER.md index 20ce73954..886016ced 100644 --- a/BROWSER.md +++ b/BROWSER.md @@ -970,9 +970,47 @@ first one set wins: 3. `HTTPS_PROXY`, then `https_proxy` 4. `HTTP_PROXY`, then `http_proxy` 5. `ALL_PROXY`, then `all_proxy` -6. no proxy (a direct connection) +6. no proxy configured by `$B` (a direct connection on Linux; the system proxy + settings on macOS and Windows) + +Whatever `$B` resolves is what Chromium gets, explicitly: a proxy is passed +as `--proxy-server=`, and a direct connection as +`--proxy-server=direct://`. On Linux, Chromium launched without a proxy +switch reads `HTTPS_PROXY`, `HTTP_PROXY` and `ALL_PROXY` itself, so every +direct connection there carries the switch: with `BROWSE_PROXY_URL=direct` +(or an unparseable variable, below) a set variable is ignored, and to use +one, name it through steps 1-5. Earlier releases left Chromium to read those +variables in the direct case (#3440), so a session that reported a direct +connection could still route through a proxy. Chromium on macOS and Windows +never reads those variables; it follows the system proxy settings (PAC and +WPAD included), and step 6 leaves it to them, so GStack Browser.app behind a +corporate proxy keeps working. `BROWSE_PROXY_URL=direct` bypasses the system +settings too, on every platform; on macOS and Windows it is part of the +daemon's settings, so a running daemon started without it is not reused (for +a headless daemon the refusal below names `BROWSE_PROXY_URL`; a `connect` +daemon gets the generic flags refusal) and a crash restart keeps it. The +daemon log (`browse-daemon.log` next to the state file) records the +effective mode each time the daemon starts, as `[proxy] using from +`, `[proxy] none: direct connection (--proxy-server=direct://; +ignoring HTTPS_PROXY)` (on macOS and Windows, `ignoring the system proxy +settings`), or `[proxy] none: system proxy settings` (an authenticated +`socks5://` upstream logs its `[proxy] upstream test ok` and `[proxy] bridge +listening on 127.0.0.1:` lines instead). A managed Chromium +proxy policy (`ProxyMode`/`ProxySettings`) still overrides the switch; `$B` +cannot see it, so on a managed machine the log line describes what `$B` +asked for, not what the policy enforces. Values from the environment follow the same credential policy as `--proxy`. +A variable without a scheme (curl-style `host:port` or `user:pass@host:port`) +is read as `http://host:port` (port 80 when none is given). For `host:port` +that is what Chromium's own environment reader took it as, so a value +Chromium on Linux used to pick up by itself now reaches the same proxy +through `$B`; `user:pass@host:port` follows curl's reading and is newly +proxied, because Chromium rejects any proxy spec with a userinfo. On macOS and +Windows such a value is now a configured proxy too, where it used to be +ignored with a warning in favour of the system proxy settings. `--proxy` and +`BROWSE_PROXY_URL` still require the scheme. + `socks5h://` is treated as `socks5://`, because Chromium's SOCKS5 already resolves names through the proxy. `NO_PROXY` (or `no_proxy`) is translated to Chromium's bypass list: `example.com` and `.example.com` cover the domain and @@ -982,10 +1020,19 @@ proxy source. When a server starts, `$B` prints one line naming the proxy and where it came from, with credentials redacted. - **`warning: HTTPS_PROXY is not a proxy URL browse can use (...)`.** The - variable doesn't parse as `scheme://host:port`: a curl-style `host:port`, an - unsupported scheme, or a bad port. `$B` connects directly instead of failing + variable doesn't parse even as `http://` plus the value: an unsupported + scheme (`ftp://`), a bad port, a missing host, a path other than a bare + `/`, a query or a fragment (where a `DOMAIN\user` login or a `/` in a + password ends up after URL parsing, so such values are refused for every + source rather than printed unredacted; percent-encode `/ ? #` in + credentials for every scheme, and `\` in `http://` and `https://` + credentials), or credentials that are not valid percent-encoding. A + credential-free value with a path (`http://proxy.corp:3128/pac`) is refused + the same way, where it used to be read with the path dropped. `$B` connects directly (on Linux, + Chromium is told so and does not read the variable itself) instead of failing every command. Fix: `export HTTPS_PROXY=http://host:port` (or `socks5://`), - or set `BROWSE_PROXY_URL=direct` to silence the warning. A bad `--proxy` or + or set `BROWSE_PROXY_URL=direct` to silence the warning (on macOS and + Windows that also makes Chromium bypass the system proxy settings). A bad `--proxy` or `BROWSE_PROXY_URL` is still an error, because those are explicit browse settings and a silent direct connection could expose your real IP address. - **`the running browse daemon was started with different proxy settings ( differs), so it was not reused.`** @@ -994,7 +1041,10 @@ where it came from, with credentials redacted. silently reuse a daemon that routes differently. The refusal prints the daemon's settings and this session's. Fix: run `browse stop` to restart the daemon with this session's settings (stop works whatever the proxy settings - are), or set `BROWSE_PROXY_URL=direct` when the running daemon has no proxy. + are), or match the daemon: unset the variable that differs, or on Linux set + `BROWSE_PROXY_URL=direct` when the running daemon has no proxy (on macOS and + Windows that opt-out is itself part of the daemon settings, so it only + matches a daemon started with it). **Stealth scope (Layer C, always on).** Every context — headless `launch`, `--headed`/`--proxy`, `handoff`, and the `useragent`/`viewport --scale` @@ -1705,7 +1755,7 @@ browse/ │ ├── port-allocator.ts # Fixed 10000-49151 scan range for every long-lived listener (never port:0) │ ├── xprotect-heal.ts # macOS XProtect launch-kill classify + quarantine-clear + bounded reinstall │ ├── socks-bridge.ts # Local 127.0.0.1 SOCKS5 bridge that handles auth handshakes Chromium can't speak -│ ├── proxy-config.ts # --proxy URL parsing + cred resolution (URL vs env, fail-fast on both) +│ ├── proxy-config.ts # --proxy URL parsing + cred resolution (URL vs env, fail-fast on both); direct:// launch switch + proxy-mode log line │ ├── proxy-redact.ts # Cred-redaction helper for any proxy URL surfaced to logs/errors │ ├── xvfb.ts # Xvfb auto-spawn + orphan cleanup with PID + start-time validation │ ├── stealth.ts # Layer C: webdriver mask + window.chrome.* + Notification/Permissions + per-install hardware + toString proxy + automation-global sweep; buildGStackLaunchArgs (GSTACK_* cmdline switches); GSTACK_STEALTH=extended opt-in diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b83ba12e..bf13dd628 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,160 @@ ### Gate integrity - No threshold, rubric, trial count, case kind or judge prompt changed. A screen's verdict is read as evals.yml bound it; nothing here changes what a case must pass. +## [1.91.255.0] - 2026-10-26 + +**Merge trains fill at 3 READY PRs or one hour after the first READY, and a train verdict's cost line no longer hides the trials it could not price.** On 2026-10-26 the 5-PR / 3-hour fill left READY PRs waiting most of a day, and `train verdict` printed `cost=$24.41` for a train whose slice report said `cost $32.92 known + 54 of 136 trial(s) cost unknown`. The slice report was right: 54 E2E trials had no cost attributed because their result records are named by a display label ("/benchmark workflow") while the case is named by its id (`benchmark-workflow`), so the per-case sum the train record carries was short by $8.51. Every train cost the coordinator reported until this fix was that undercounted verdict figure (about $24 per train against about $33 real). + +### Changed +- **`.gstack/ship-policy.json`**: `train.max_size` 5 → 3 and `train.fill_hours` 3 → 1. `fillDue` already dispatches at either bound, whichever comes first; a fourth READY PR is `HELD:` for the next train. docs/ship-policy.md says so in the fill paragraph and its example block. + +### Fixed +- **`scripts/lib/paid-report.ts`**: when a shard ran exactly one case (every case shard since W5a), its result records belong to that case even when they are named by a display label, so the trial's `cost_usd` is the record's and `cost_known` is no longer false; a failed trial in such a shard also takes its `failure_class`, `exit_reason` and `error` from that record (train-gate classes a red `infra` record INFRA instead of VERDICT) where it was a bare `assertion` with the JUnit message. No case's pass or fail changes. A multi-case shard whose records carry display labels keeps those trials cost-unknown rather than guessing which case paid. The train record's per-case `cost_usd` (scripts/train-gate.ts) and the slice report's headline now agree for case shards; the headline was never wrong. +- **`lib/ship-queue-train-verdict.ts`**: the `ATTEMPT:` line prints `cost=$ known + of case(s) cost unknown` when some but not every panel is priced, the bare `cost=$` only when every panel is priced, and no cost at all when none is (unchanged). + +### Tests +- `test/gstack-ship-policy.test.ts` pins the live fill rule (3 PRs or one hour) and exercises `fillDue` with the live block at 59 and 60 minutes and at 3 PRs. +- `test/paid-report-census.test.ts`: a display-labelled record in the periodic fixture's `qa-fix-loop` case shard gives the trial `cost_usd: 1.23` with no `cost_known: false`, `verdict.costUsd` 1.23 and the headline `cost $1.23 known + 6 of 7 trial(s) cost unknown` (red before the fix: `cost_usd: 0, cost_known: false`); a failed display-labelled record gives the trial `failure_class: 'timeout'`, its exit reason, error and cost (red before the fix: `assertion` with the JUnit message); the two-case `skill-e2e-cso` shard keeps its display-labelled trial unknown while the id-named one is priced. +- `test/ship-queue-train-cost.test.ts`: a green train whose record has two unpriced panels prints `cost=$0.70 known + 2 of 3 case(s) cost unknown`; a fully priced record prints `cost=$1.50` alone (red before the fix: the bare `cost=$0.70`); every panel priced at zero prints `cost=$0.00`. +- `scripts/lib/windows-curation.ts` excludes `test/ship-queue-train-cost.test.ts` from the Windows subset like `test/ship-queue-train.test.ts`: both drive `bin/gstack-ship-queue` against the `#!/bin/sh` gh shim, which windows-latest cannot run. + +### Gate integrity +- No threshold, rubric, trial count, case kind or judge prompt changed. Cost attribution changes what a trial record reports, never whether a case passed. + +## [1.91.254.0] - 2026-10-26 + +**Pregate ergonomics for a repository that is not gstack (Lane D, GSTA-112; plan harness-codify E4–E6; Captain issues 6, 11, 12).** Three Captains lost their first `/ship` pass to `PREGATE_NO_LANE` in a repo that had no `.gstack/pregate.json` yet (FOUN #292, harness-evals #81/#82); the non-downgradable `secrets` check refused 1,800 dummy per-trial tokens, 11-digit Actions run ids read as phone numbers and a public Firebase key every bundle rebuild re-adds (harness-evals #81/#83, GSTA-102, GSTA-106); a lane whose runner could not start read as the diff's failure and was waived by test name (truther #617); and pregate's logs under `os.tmpdir()` were shared between parallel runs and wiped by a Capy wake (gbrain 16). + +### Added +- **No policy → warn plus a starter, never a hard `PREGATE_NO_LANE`** (`lib/pregate/starter.ts`, `lib/pregate/checks/lanes.ts`). When the base branch carries no `.gstack/pregate.json` and no user-owned policy exists, a touched file with no lane is a `warn` row under the new code `PREGATE_NO_POLICY`; the selected tests still run (a red lane is still `fail`, an unknown platform matrix is still a `requires-remote` obligation), and the table prints a starter policy drafted from the repository: the detected runner, the test directories, the CI lanes (naming a planner-computed matrix that needs `selection`), the JSON between `PREGATE_STARTER: begin` and `end` with a `dependencies` entry per unmapped touched file mapped to the tests nearest it or sharing its stem (never `[]`), and a `TODO` line per file nothing covers. `run` writes nothing; `gstack-pregate init` is the explicit write. Once any policy exists (base copy, user-owned file, or `--policy-from head`), an unmapped file is `PREGATE_NO_LANE` as before. +- **`allowlist` in `.gstack/pregate.json`** (`lib/pregate/allowlist.ts`, `lib/pregate/config.ts`, `lib/pregate/checks/secrets.ts`): `[{ finding, paths?, pattern?, reason }]`, where `finding` is a gstack-redact finding id or one of the categories `pii`, `internal`, `hygiene`, `legal`; `paths` are contained globs; `pattern` is a regex matched at the finding's position in the added line and ending at a token boundary; one scope is required and `reason` always is (64 entries at most). The `secrets` row says `N allowlisted ()`, lists each allowlisted finding with its entry and reason, and records the entries' digest as `inputs.allowlist`. **`env.value` (a credential-named environment variable's real value in the added lines, #3449) and the whole `secret` category can never be allowlisted:** validation refuses the entry (`PREGATE_CONFIG_INVALID` at `allowlist[i].finding`) and the matcher never consults the list for `env.value`, so a stale or hand-edited policy cannot silence it; the scanner's own `engine.*` refusals (an oversize input fails closed) are refused and kept the same way. A `pattern` entry that named a finding the scanner gave no position for is reported as not tested (the finding is kept) instead of silently inert. An unparsed scanner report keeps its exit-code verdict. +- **Environment failures reported apart from diff failures** (`lib/pregate/environment.ts`; new code `PREGATE_ENVIRONMENT` with the troubleshooting anchor `pregate-environment`). A runner or guard binary missing from PATH (exit 127, `command not found`, a spawn `ENOENT`, a tool `not on PATH`), a network error or a quota refusal before any test was counted, a full disk, an unverified runtime or gstack's own scanner not finishing is `incomplete` with `cause: "environment"` and `environment: { kind, evidence }` in `pregate.json`, an `environment: — ` table line and a fix; `PREGATE_LANE_FAILED`, `PREGATE_GUARD_FAILED` and `PREGATE_SECRETS` carry `cause: "diff"`. `environment.proof` records how the cause was established: `launch` (a spawn failure, a binary off PATH, a runtime or tool refusal; the fix ends "the diff is not at fault") or `output` (a rule matched the command's text before any test was counted, which the diff's own output could have written; the table line says `(inferred from output)` and the fix asks for confirmation on another machine or with `--remote`). The classifier is conservative: a local lane that exited non-zero is read from its output only when its runner reported no test summary and was not seen starting a test file; a counted failure is the diff's whatever its log says; exit 127 and `command not found` are `missing-binary` only when the command's own binary is off PATH, so a runner, guard or gate script the diff controls that exits 127 is the diff's; a remote gate is environment only when it printed no `PREGATE_REMOTE:` line and the cause sits in its first 40 lines, before any test output could stream; a runner killed by a signal, stalls and timeouts keep `PREGATE_INCOMPLETE` / `PREGATE_STALLED`. `gstack-pregate verify` prints `PREGATE_ENVIRONMENT: check(s) blocked by the environment (), not by the diff` when every incomplete check is environment-caused, naming any check inferred from output alone, and names the kinds beside `PREGATE_INCOMPLETE` otherwise, so a lane waives by cause and proof, never by test name. +- **One scratch directory per run under the state root** (`lib/pregate/run-scratch.ts`): `/projects//pregate/runs//` (0700; `--run ` names it, else `--<4 hex>`) holds the secrets scan's added-line copies (deleted after the scan), `lanes/lanes.log`, `guards/.log` and the remote gate's log and file list, never `os.tmpdir()`. `pregate.json` records it as `scratch`, the table prints `scratch: `, a taken `--run` name takes a numbered sibling (`-2` … `-9`) rather than sharing the directory; runs older than 7 days under the same project are pruned when a new run starts; an unwritable state root falls back to a private per-run directory under `os.tmpdir()` and says so. + +### Fixed +- **`literals` no longer floods on a common module stem or a one-digit value** (`lib/pregate/checks/literals.ts` `consumersOf`). A test file counted as a consumer of a changed constant when it merely contained the source file's stem as a bare word (`install`, `retry`), so `GSTACK_CAPY_WRAPPER=2 → 3` in `capy/skill/gstack/scripts/install.sh` flagged 575 test files (`PREGATE_LITERALS`; GSTA-114, #3475). The module now counts only as a path fragment (`/install.sh`, `/retry.js`, `/retry'`), and an old value shorter than two characters needs the constant's name in the file. The same edit now flags the 5 files that name `GSTACK_CAPY_WRAPPER`. Regression: `test/pregate-selection.test.ts` "consumersOf: a module stem is a path fragment…". + +### Changed +- `docs/pregate.md`: the `allowlist` row, the "No policy yet" paragraph, three sections (allowlist, environment failures, scratch), the synopsis (`--run `), the stage-1 `guards` and stage-2 `lanes` bullets and the Publication code list. `docs/troubleshooting.md`: `PREGATE_NO_POLICY` and `PREGATE_ENVIRONMENT` anchors; `PREGATE_NO_LANE` and `PREGATE_INCOMPLETE` point at them; `PREGATE_GUARD_FAILED` carves out a guard whose own binary is off PATH, `PREGATE_SECRETS` points at the allowlist, `PREGATE_INCOMPLETE` names a signal-killed runner. `lib/headless-artifacts.ts`: the pregate schema accepts `cause`, `environment` and `scratch` (additive). `gstack-pregate --help` documents `--run`. +- Existing pregate tests whose fixtures had no policy and asserted the hard `PREGATE_NO_LANE` (`test/pregate-selection.test.ts`, `pregate-docs-lane-review.test.ts`, `pregate-non-js.test.ts`, `pregate-rust.test.ts`) now assert the no-policy warning and that the file is still named as unmapped; the Dart missing-SDK cases in `test/pregate-dart.test.ts` assert `PREGATE_ENVIRONMENT` and the new fix text. + +### Tests +- `test/pregate-starter.test.ts`: no policy → exit 0, `warn`/`PREGATE_NO_POLICY`, the mapped test still ran, the starter validates, maps by stem, and prints the TODO, test directories and CI lanes; a planner-computed platform lane keeps `requires-remote` with the code; a failing selected test is still `PREGATE_LANE_FAILED`; a base or user-owned policy keeps `PREGATE_NO_LANE`; `run` leaves no file and `init` writes one `validate` accepts. Red before the change: exit 1 with `PREGATE_NO_LANE`. +- `test/pregate-allowlist.test.ts`: validation (id or category with a scope and a reason; `env.value`, the `secret` wildcard, an empty scope, a bad regex and an uncontained glob refused); a dummy `sk-trial-…` token allowlisted by path and pattern and named, failing outside the path or under a pattern that covers only its head; a category entry for a run id; `env.value` survives a `pattern: ".*"` entry at the matcher and the config naming it is refused before any check runs. Red before: `PREGATE_SECRETS` on the dummy token and `allowlist` an unknown key. +- `test/pregate-environment.test.ts`: the classifier's vocabulary; a missing runner binary, a network error with no summary, a quota refusal from the remote gate (obligation uncleared) and a missing guard binary are `PREGATE_ENVIRONMENT` with their kinds and `verify` says so; a counted test failure beside an `ECONNREFUSED` line and a guard that exits 2 stay `cause: "diff"`. Red before: `PREGATE_LANE_FAILED` with no cause. +- `test/pregate-scratch.test.ts`: two `--run` ids get two directories under the fixture's state root, logs and the record name them, an unnamed run is unique, the secrets copies are gone after the scan, and prune keeps fresh runs and the current one. Red before: logs under `/tmp/gstack-pregate-lanes-*` and no `scratch` field. + +## [1.91.253.0] - 2026-10-26 + +**`/captain` (also `/crew`): the crew operating model that keeps many agents merging into one base branch without turning it red, as a gstack skill with templates and a team guide.** Teams running a controller agent over worker threads had the model only in one crew's private notes. It now ships generic: one controller merges and lanes never merge; a READY block (full head SHA, base, CI counts, pregate, gate evidence, `SHIP_RECEIPT` at the pin, screen choice with justification, receipt path, logged flaky cases) is the only merge trigger, and a green PR page or a cheap test tier never is; merge trains (build, verdict, per-slot stamp on call, verify, merge with the expected head, verify again, prune), the red-train rules (in-run flaky rerun once, bisect on the red cases, exactly one unchanged rebuild, one infra re-dispatch), the tree rule, the backstop and the freeze, and the post-merge babysit; the digest shape (Needs you / New / In progress with ages); the three approval shapes (the per-thread record, the plan ask with before → after and a "do it" reply, the merge ask with captioned before/after images); and the daily flaky-fix pass (ledger format, the gate runs' flake records and the free suites' retries as sources, red-before-green fix PRs, one named infra rerun allowance, never thresholds, trials or kinds). Where crews that run this model disagree (squash or merge, lane or controller stamps, gate-ahead, the severe race, a gate run id in the READY) the skill ships both as options for the crew rules file. Every repo-specific name is a parameter (`REPO`, `BASE`, `REQUIRED_CHECKS`, `FULL_GATE_CHECK`, `MERGE_METHOD`, `STAMP_FILES`, `KNOWN_INFRA_FLAKES`). + +### Added +- **`captain/SKILL.md.tmpl`**: the skill, tier 2, routed from the root router on "captain", "crew", a merge train, a slot call, a READY block or the crew digest; its own triggers add "validate this ready" and "flaky pass". +- **`captain/templates/`**: `CREW.md` (the 23 lane rules as one-line pointers into gstack's docs and skills, plus the crew's parameters and its standing-approval table), `AGENTS.md` (the controller's standing rules with the owner's dated words), `NOTES.md` (the append-only timestamped log), `DIGEST.md`, `flaky-ledger.md`, `report.md` (Outcome, PR, Needs, Cost, Lesson, Approvals), `READY.md` (the block, the optional lines, the refusal list), `slot-call.md`, `plan-ask.md`, `merge-ask.md` and `automation-flaky-pass.md` (the schedule, what it reads, what it starts). +- **`docs/captain.md`**: how a team sets up a Captain crew on Capy with gstack: the roles, the drive layout, the crew rules file, the READY contract, the lane's /ship sequence before READY, trains with the slot-2 recipe, approvals, the digest, the flaky pass, the owner's device, the lessons loop, and the setup steps; linked from the README docs table, `docs/skills.md`, `docs/capy-install.md` and the skill. +- **`test/captain-skill.test.ts`**: pins the contract the skill teaches, the template set, the generic wording (no crew codes, names or repository literals) and the registration points. + +### Changed +- **`gstack-owner-brief render --captain`** closes "What we need from you" with the **do it** reply (every recommendation at full scope, the plan's follow-ons included, carried through with no further ask) and invites `rec` or `do it`; `capy/skill/gstack/captain.md` documents it as step 6 and points at the controller's side. +- `README.md` (power tools), `AGENTS.md`, `docs/skills.md`, `docs/PROJECT_STRUCTURE.md`, the root router and the doctor component table register the skill; `test/fixtures/context-budget.json` gains the `captain` ceiling and the router's, grown by its one new rule; `test/catalog-budget.test.ts`, `CLAUDE.md`, `CONTRIBUTING.md` and `docs/CONTRIBUTOR_REFERENCE.md` carry the catalog ceiling the new skill ratchets to; `gstack-autoplan answer` accepts `do it` as a `rec` alias. + +## [1.91.252.0] - 2026-10-26 + +**`/retro` can now say whether the factory is getting better, and a new team has one page that takes it from zero to its first merged train on gstack + Capy.** `bin/gstack-factory-scorecard` reads one window (default 7 days) from GitHub through `gh` and prints a Markdown table plus a CSV: merged PRs (crew vs outside contributors), lead time (PR created → merged, and first READY comment → merged), gate runs green / red / cancelled with their known cost, cost per merge, the red-train rate, flaky events from the same two sources the daily flaky pass reads, issues opened vs closed, and releases cut. Each line carries its counting rule and what one of the best software factories would show on it; a line whose input cannot be read says `unavailable ()`, never 0. `docs/recommended-harness.md` is the adoption guide: install, the Captain and its Drive, the first train, the daily flaky pass, the weekly release, the scorecard with gstack's own numbers against the bar, what is Capy-specific, the options where two crews differ, and the known Capy limits as of 2026-10-26. + +### Added +- **`bin/gstack-factory-scorecard`** (`lib/factory-scorecard.ts`): `--repo`, `--base`, `--since-days`, `--crew login,login` (else author association OWNER/MEMBER/COLLABORATOR), `--gate-workflow`, `--gate-event`, `--free-workflow`, `--train-prefix`, `--ready-marker`, `--pass-marker` and `--pass-cap` (review passes per PR: median, p90 and how many PRs sat at the cap, from the ship receipt's `"passes"` in the PR body or a `passes=N` / `REVIEW_PASSES: N` comment), `--max-artifact-runs`, `--out-dir`, `--csv`, `--json`, `--fixtures` (recorded gh responses, no network) and `--now`. `--repo` defaults to the origin remote's owner/name and is required without one; every run appends its rows to `trend.csv` in `--out-dir`. Gate cost comes from the run's `report-verdict-a*` artifact (`collector-outcomes.json` `.verdict.costUsd`), else the `**$N** rule/judge cost` or `cost=$N` line of the PR's verdict comment; flaky events from `flake-records-a*` (paid) and `flake-ledger-*` (free) artifacts. A READY comment posted after the merge is ignored, a PR's verdict-comment cost backs one gate run, and a run whose artifacts cannot be read is counted and named rather than scored 0. Read-only; exits 0 with the table even when an input is unavailable, 2 on bad usage. +- **`/retro` Step 7.5 (Factory scorecard, optional)**: the report format gains a **Factory scorecard** slot; the step runs the bin for the retro window when `gh` is authenticated and pastes the table, third column included, into the report. +- **`docs/recommended-harness.md`**, linked from the README docs table; `docs/standalone-binaries.md` lists the new CLI. + +### Tests +- `test/factory-scorecard.test.ts`: the recorded window (`test/fixtures/factory-scorecard/recorded.json`) yields the expected merges, lead times, gate kinds and outcomes, artifact and comment cost sources, flake counts, free retries, issues and releases; a crew login list overrides the association rule; runs older than the window, non-dispatch events and draft releases are left out; the artifact cap leaves later runs without cost or flake data; a missing input marks only its lines unavailable; the markdown and CSV carry every row; gate classification, READY-marker matching, verdict-cost and flake-record parsing and nearest-rank percentiles; `--gate-event any` keeps non-dispatch runs; an empty window prints n/a instead of NaN; a failed merged-PR read leaves the gate rows standing; the bin end to end with `--fixtures`, `--crew`, `--out-dir`, `--csv`, `--ready-marker`, bad usage (including `--now`) and `--help`; the retro template pins the fence. +- `test/host-conformance.test.ts` C3 ("--status names a symlinked router and a broken section link per host") deletes a named section (`scope.md`) instead of the first `.md` that `readdirSync` returns: two gstack-qa sections are referenced only in table form, which `setup --status` does not count as links, so on an ext4 directory order that listed one of them first the case failed deterministically on remote-gate VMs while passing on Actions (#3479). + +## [1.91.251.0] - 2026-10-26 + +**A free, deterministic design lint ships with gstack: `bin/gstack-design-lint` checks a page's HTML, CSS and components for the tells that make it read as machine-made, in under a second, and with `--render` measures what only a browser can.** Until now gstack's only mechanical design check was the opt-in impeccable engine most users never install, plus `gstack-design-craft`'s seventeen copy and ship-floor regexes; everything else in the Slop Gate was prose the model judged itself. The lint ports the roughly fifty-five machine-checkable gates of [Nutlope/hallmark](https://github.com/Nutlope/hallmark)'s `sloplint` (MIT; see NOTICE.md) into TypeScript under `lib/design-lint/`, rewritten in gstack's voice and mapped onto gstack's severity words. This release is the tool alone (Phase 1 of the hallmark absorption plan, approved by Garry on 2026-10-26); /design-html and /design-review start calling it in Phase 2, so no skill prose changes here. + +### Added +- **`bin/gstack-design-lint [--render] [--url] [--scope page|component] [--job ] [--json] [--rules]`**: reads `.html`, `.css`, `.jsx`, `.tsx`, `.vue`, `.svelte` and `.astro` (Tailwind class strings included, `cn()`/`clsx()`/`cva()` calls too), links each page to the stylesheets it reaches through `` and `@import`, resolves CSS variables, converts colours to OKLCH and computes contrast, and prints one line per finding: `LINT: : | fix: `, then `LINT_SUMMARY: blocks=N ask=N waived=N polish=N render=`. Exit 2 on blocks, 1 on an unanswered ask, 0 otherwise. `docs/design-lint.md` is the contract and lists every rule. +- **Three tiers in gstack's words, no fourth scale.** `blocks` (hallmark's Floor: contrast, focus rings, reduced motion, a card in a card, a purple-blue gradient, content hidden until JavaScript runs; never waivable), `ask` (hallmark's Reflex: three equal cards, Inter as the display, the centred-stack hero; waivable on the record) and `polish` (hallmark's Finish: straight quotes, a lonely last grid item; never blocks). Blocking and waived counts are never summed, so `blocks=0` is always a true sentence about the Floor. Three house rules hallmark holds at Floor stay at ask here: the eyebrow above a heading, the 80-word screen and the dash. +- **Waivers on the record.** `` (or the `/* */` and `//` comment forms; `impeccable-disable : ` is an alias) answers an ask or polish finding when the reason is about this page: 24 characters or more, not boilerplate, not reused on another waiver, sharing a word with the page's own text. A waiver of a blocking rule is refused and the rule still fires; more than three waivers on one page is reported as a house style that belongs in DESIGN.md. +- **The render tier** (`--render`, or `--url` for a dev server) runs on the same engine `gstack-render` picks, Aside first and gstack's own headless browser otherwise, with nothing to install: the hero fits at 1280x800 (the headline's bottom and the first action; the lede and the visual are not measured), about 80 words per 900px window, contrast against the painted pixels, focus rings found by focusing every control, headings crowded against the section above, text painted under another element, horizontal scroll between 320 and 1920, wrapping button labels, a hero offering more than two actions, section gaps under 120px or all alike and more than nine sections, thin sections, dead bands, drawn illustrations, empty picture slots, counting marks and photographs covering more than a third of the page. Without a browser the summary says `render=skipped: ` and the static tier still runs. +- `lib/aside-render.ts`: an eval step takes an optional viewport (`width`, `height`, `deviceScaleFactor`, `mobile`), set before the eval and cleared after it the way the screenshot step already does, and a spec's `file` may be an http(s) URL, opened as it is with no loopback server. Both engines take the size; `deviceScaleFactor` and `mobile` on an eval step reach Aside only (the browse daemon's `viewport` takes a size alone). Nothing else in the file changes. +- **The copy tier**: `gstack-design-craft`'s content-realism and ship-floor tells (placeholder names, lorem, buzzwords, round numbers, `[Needs proof]`, dead links, images without alt, the missing title, description, skip link and `aria-current`) report under the same contract and ids, so one command prints one list. `bin/gstack-design-craft` itself is unchanged until Phase 2 swaps the call. +- Fixtures under `test/fixtures/design-lint/` (hallmark's `slopped.html`, eyebrow and Tailwind pass/fail pairs and the waiver page, MIT-attributed, plus a clean page every rule family must leave alone); `licenses/MIT-hallmark.txt`; a NOTICE.md entry. + +### Tests +- `test/design-lint.test.ts`: the parsers (nested CSS, tokens, var() fallbacks, colour spaces, JSX pseudo-HTML), the rule table against `docs/design-lint.md`, the output lines and exit codes, waiver acceptance and every refusal reason, the waiver fixture. One test per rule family (`test/design-lint-.test.ts`) holds a negative control and a planted positive for every rule id. `test/design-lint-render.test.ts` drives the browse daemon when it is built and skips with a named reason otherwise. `test/design-lint-recorded.test.ts` lints the repository's recorded design slop page (the review eval's ground truth) and pins which rule answers each annotated issue, the same gate set hallmark's sloplint reports on the same files. + +### Gate integrity +- No threshold, budget, timeout, trial count or assertion was weakened. No template, catalog or eval changed. Gate edits in the diff: `test/fixtures/module-size-ratchet.json` registers the 27 new `lib/design-lint` modules under the existing 800-line cap, and `test/helpers/free-fixtures-data.ts` maps `test/fixtures/design-lint/**` to the design-lint test files; `lib/aside-render.ts` gains the viewport on eval steps and http(s) `file`, described above. + +## [1.91.250.0] - 2026-10-26 + +**Three free-tier fixtures stop racing themselves (GSTA-107 daily flaky pass).** Two tests polled for a result file's existence while its writer created it with a plain `writeFileSync`, so a poll between create and write read an empty file; a third copied a shared template repository with `fs.cpSync` and threw once inside the directory walk. + +### Fixed +- **`browse/test/dotenv-isolation.test.ts`** (Free Tests 39902683183 and 40322888041, shard 2, "JSON Parse error: Unexpected EOF" at the `agent-env.json` read): the probe writes its result to a temp file and renames it into place, and `waitForFile` waits for content, not only for the file. A new test feeds the old race (an empty file filled 150 ms later) and runs the probe source itself. +- **`test/claude-code-runner.test.ts`** (Free Tests 39694715116, shard 13, `running(pid)` false at the timeout case): the descendant publishes `descendant.pid` by rename, and the test polls `publishedPid()`, a positive integer, instead of the file's existence, so an empty read can no longer become pid 0. +- **`test/cso-git-hardening.test.ts`** (Free Tests 40288142343, shard 17, `fs.cpSync` threw in `fixture()` with an empty message and passed on the serial retry): every fixture builds its own repository with Git instead of copying a shared template, with auto-maintenance off (`maintenance.auto=false`, `gc.auto=0`) so the detached `git maintenance run` a commit starts no longer touches `.git` while a test walks or removes it; the `beforeAll` still pays the cold macOS Git spawns once, on a throwaway repository. No timeout changed. + +## [1.91.249.0] - 2026-10-26 + +**Five paid-case flakes from the 2026-10-26 train gates, repaired at their causes (GSTA-107 daily flaky pass).** The "ship-docsync family" was three distinct slips, not one shared cause: a mistyped `--help` path the detector refused although the usage had already been read, an undeclared refusal of gstack's own state directory as an artifact location, and a fixture that did not install the shared preamble a generated skill requires. tpa-present's consent question paraphrased "defer" as "wait for now", and qa-functional-webhook-fix ran the probe's `--help` bare before its first capture. + +### Fixed +- **ship-docsync-store (gate 40323989742 slice 3):** a candidate `--help` typed at a wrong, non-existent path (exit 127) is ignored when the declared binary's `--help` ran anywhere in the transcript, before or after, since a help probe reads nothing the agent still has to recover (`test/helpers/docsync-observer.ts` `candidateNotFound`); snapshot and compare still need the declared retry after the slip, and a wrong path that exists, a failed declared run, or no declared run at all still fail. +- **ship-docsync-completion (gate 40290033102 slice 2):** the parent put its snapshot `--out`, saved section and report insert under `/state`, gstack's own state directory (GSTACK_HOME), which the authority refuses but the fixture interface never named. The interface now says a private artifact lives directly under the fixture home or in a new directory there, and names every refused root: the repository, the installed skills, the Claude config dir, `/state` and `/remote.git` (`test/helpers/docsync-interface.ts`). The authority is unchanged. +- **docsync-spawned (PR #3434 screen 40269992077 slice 13):** `fixtureDocs` installs `/sections/preamble.md` beside the required sections when the generated skill has one, so a spawned child whose SKILL.md names the shared preamble as a required asset no longer blocks on its absence (`test/helpers/docsync-fixture.ts`). A generated skill without one installs exactly the files it did before. +- **tpa-present (gate 40290033102 slice 14):** the defer-option detector accepts "wait for now" and "hold off (on this) for now" as the option's own opening or sentence (`test/helpers/third-party-actions.ts` `offersDefer`); a drive option that waits while the user signs in, a "do not wait for now", or the phrase outside a lettered option still does not count. +- **qa-functional-webhook-fix (gate 40290033102 slice 9):** the fixture interface states that the evidence helper's `--help` is the only one it grants and that a bare `bun run probe` with `--help` or any argument outside a capture is a bypass (`test/helpers/qa-functional-eval.ts`); the grammar and the bypass detector are unchanged. + +### Tests +- `test/docsync-lifecycle-interface.test.ts` replays the captured store run (`test/fixtures/docsync-help-not-found-public.json`) as a pass, with controls for no declared `--help`, a failed one, and a wrong-path compare that only an earlier declared run precedes. +- `test/docsync-authority.test.ts` pins the interface sentence that names each refused root and replays the captured state-dir calls (`test/fixtures/docsync-state-dir-artifacts-public.json`): four refused commands and one refused write, all accepted once relocated to a new directory under the home. +- `test/ship-document-release-dispatch.test.ts` builds a generated root with and without a document-release preamble and checks the fixture installs it in the first case only. +- `test/third-party-actions.test.ts` adds the 40290033102 tpa-present render to the Opus consent captures (29 consent questions, 5 paraphrased defers) with new positive and negative wordings. +- `test/qa-functional-prompt.test.ts` pins the helper-only `--help` sentence before the observation forms and keeps `bun run probe -- --help` outside the grammar. + +## [1.91.248.0] - 2026-10-26 + +**`$B` in direct mode no longer lets Chromium pick a proxy from the environment (#3440 item 8).** `BROWSE_PROXY_URL=direct` (and an unparseable `HTTPS_PROXY`) only stopped gstack from configuring a proxy. The daemon inherits the whole environment and the launch passed no proxy switch, so Chromium read `HTTPS_PROXY`, `HTTP_PROXY` or `ALL_PROXY` itself and every `goto` still went through that proxy while `$B` reported a direct connection. That made the BROWSER.md precedence list false and any certificate check on a proxied machine unreliable. Reproduced on Linux with Playwright 1.62.1: a logging CONNECT proxy in `HTTPS_PROXY` received `CONNECT :443` with `BROWSE_PROXY_URL=direct` set. + +### Fixed +- **`browse/src/browser-manager.ts`**: each of `BrowserManager`'s four Chromium launch sites (headless `launch`, the `BROWSE_EXTENSIONS_DIR` persistent context, `launchHeaded`, `handoff`) adds `--proxy-server=direct://` when no proxy is configured and `forcesDirect()` says Chromium would otherwise pick one browse did not resolve, from one `proxyLaunchArgs()`. A configured proxy was already explicit (Playwright passes it as `--proxy-server`), so the environment can no longer win silently in either mode. `--no-proxy-server` was measured and rejected: chrome-headless-shell, the default headless build, ignores it and still took `HTTPS_PROXY`; the full Chromium honors both switches, and `direct://` works on both. The cookie-import worker (`cookie-import-native-worker.ts`), which launches the user's own browser only to read its cookie store and never navigates, is unchanged. +- **`browse/src/proxy-config.ts`**: `forcesDirect(env, platform)` is true on Linux always, where the leak lives, and on macOS and Windows only with `BROWSE_PROXY_URL=direct`. Chromium there never read the proxy variables and follows the system proxy settings (PAC and WPAD included), so a user of GStack Browser.app behind a corporate proxy keeps that connectivity, and a PAC cannot be named through `BROWSE_PROXY_URL`. An explicit `direct` bypasses the system settings on every platform. A standard proxy variable without a scheme (curl-style `host:port` or `user:pass@host:port`) is now read as an `http://` proxy (port 80 when none is given). For `host:port` that is what Chromium's own environment reader took it as: before this change browse rejected it with a warning and Chromium on Linux read the variable anyway, so that session was proxied; with the launch now explicit, reading it the same way keeps those sessions on their proxy instead of sending them direct. `user:pass@host:port` follows curl's reading and is newly proxied: Chromium rejects any proxy spec that carries a userinfo (`net/base/proxy_string_util.cc`), so that session was direct before and now sends its credentials to the named proxy. On macOS and Windows the same value is now a configured proxy too, where it used to be ignored in favour of the system proxy settings, so a daemon started under the old warning meets the mismatch refusal once. Only the plain `[user:pass@]host[:port]` shape is read that way; a value with a scheme (including the slash-less `https:user:pass@host:port`, which the URL parser reads as `https://`), a `/`, `\`, `?`, `#`, `@` or whitespace anywhere else in the value, or a bare number goes to the URL parser as written, so a bare number such as `1234` keeps its diagnostic and a one-slash spelling such as `http:/host:3128` is read as the URL parser always read it (`http://host:3128/`). A proxy URL with a path other than a bare `/`, a query or a fragment (a literal `?` or `#` anywhere) is now refused for every source (`--proxy` included), because that is where a `DOMAIN\user` login or a `/` in a password lands after URL parsing and only the userinfo is redacted; percent-encode `/`, `?` and `#` in credentials for every scheme and `\` in `http://`/`https://` credentials (a `socks5://` userinfo keeps a backslash as written). This narrows the environment path: a credential-free value with a path, such as `http://proxy.corp:3128/pac`, used to be read with the path dropped and the session proxied; it now warns and goes direct like any other unusable standard-variable value, and `--proxy`/`BROWSE_PROXY_URL` refuse it. Credentials that are not valid percent-encoding (a literal `%`) are a proxy configuration error instead of a crash, for every source. For a standard variable each of these parse problems is a warning and a direct connection, like any other unusable value; a scheme-less value with credentials beside `BROWSE_PROXY_USER`/`BROWSE_PROXY_PASS` is now refused like its `http://` spelling, which has always been a hard error (that value used to warn and go direct because it did not parse). A value that does not parse even as `http://` (an unsupported scheme, a bad port, a missing host) still warns and connects directly; `--proxy` and `BROWSE_PROXY_URL` still require the scheme. +- **`browse/src/cli.ts`, `browse/src/server.ts`**: on macOS and Windows the explicit `direct` is part of the daemon config hash (`explicitDirect()`), is recorded in the daemon state as `proxyDirect` and is re-emitted on a crash auto-restart from the session's own flags, so a running daemon started on the system proxy is refused rather than reused by a session that opted out (the refusal names `BROWSE_PROXY_URL` and offers `browse stop` or unsetting it; a daemon started before this release meets that refusal once, and a daemon with no recorded config hash is refused like one started without `--proxy`; the mismatch refusal offers `BROWSE_PROXY_URL=direct` as a remedy only where it would match the running daemon, on Linux or against a daemon started with it, and otherwise says to unset the variable that differs). The unparseable-variable warning's hint now says that `BROWSE_PROXY_URL=direct` also bypasses the system proxy settings on macOS and Windows. On Linux the opt-out hashes like no proxy, as before, because every direct connection there already carries the switch. +- **`browse/src/server.ts`** logs the effective proxy mode each time the daemon starts, beside the existing `[proxy] using from ` line: `[browse] [proxy] none: direct connection (--proxy-server=direct://; ignoring HTTPS_PROXY)` on Linux, naming each standard variable that was set and ignored; `(...; ignoring the system proxy settings)` for an explicit direct on macOS and Windows; and `[browse] [proxy] none: system proxy settings (Chromium's default on ; BROWSE_PROXY_URL=direct connects directly)` when those platforms are left to the system. +- **BROWSER.md, "Proxy from environment"**: says that whatever `$B` resolves is passed to Chromium explicitly, that on Linux a set proxy variable is ignored in direct mode, that macOS and Windows follow the system proxy settings unless `BROWSE_PROXY_URL=direct` is set (and that the opt-out is part of the daemon's settings there), where the daemon log records the effective mode, and that a managed Chromium proxy policy still overrides the switch. The eMudhra/TLS trust section is unchanged. + +### Tests +- `browse/test/proxy-direct-launch.test.ts` (Linux): two logging CONNECT proxies on loopback (`browse/test/helpers/connect-proxy.ts`) and a target name that does not resolve. With `HTTPS_PROXY` set and `BROWSE_PROXY_URL=direct`, the goto fails at DNS, no CONNECT reaches the proxy, the daemon log names the ignored variable and the state file has no proxy; this case fails on the unfixed tree with `ERR_TUNNEL_CONNECTION_FAILED` and a logged CONNECT. A scheme-less `HTTPS_PROXY` (`host:port`) is the configured proxy and the CONNECT reaches it (this case fails on the tree before the lenient parse); an `HTTPS_PROXY` that does not parse even as `http://` (`ftp://`) warns and still forces direct, so no CONNECT reaches the proxy it named. Two controls: `HTTPS_PROXY` alone is the configured proxy and receives the CONNECT; `--proxy` beside `HTTPS_PROXY` sends the CONNECT only to the configured proxy. Each case asserts on the daemon-log slice its own invocation wrote. +- `browse/test/proxy-direct-launch-paths.test.ts` (Linux, real Chromium under Xvfb): the `BROWSE_EXTENSIONS_DIR` persistent context, `launchHeaded()` and the `handoff()` relaunch each ignore an inherited `HTTPS_PROXY` (no CONNECT, `ERR_NAME_NOT_RESOLVED`), and a headed control with a configured proxy still sends the CONNECT to it. +- `browse/test/proxy-config.test.ts`: `forcesDirect` per platform, and `directProxyLogLine` for the Linux variable list, the explicit-direct line on macOS/Windows, and the system-proxy line. +- `browse/test/proxy-env.test.ts`, `browse/test/restart-env.test.ts`: the config hash separates an explicit direct from no proxy on macOS and Windows and not on Linux (`extractGlobalFlags` takes the platform); the mismatch refusal for both directions (explicit session against a system-proxy daemon, default session against an explicit-direct daemon) and the remedy an env-proxied session gets against a system-proxy daemon (unset the variable, not `BROWSE_PROXY_URL=direct`, which that daemon would refuse); `buildRestartEnv` re-emits `BROWSE_PROXY_URL=direct` from the invocation's flags only, never from the persisted state, and a proxied invocation wins over it; `resolveProxy` reads scheme-less standard variables as `http://` (with and without `user:pass@`, IPv6, no port, surrounding whitespace), leaves a one-slash scheme and a bare number to the URL parser, gives `h:3128` and `http://h:3128` the same config hash, warns for an unsupported scheme, a bad port, a missing host, a credential that would spill past the userinfo (`/`, `\`, `?`, `#`, including a `DOMAIN\user` login) or one that is not percent-encoded, without echoing credentials, refuses a proxy URL with a path other than a bare `/`, a query or a fragment (a bare trailing `?` or `#` included) for every source, keeps the protocol of a slash-less `https:user:pass@host:port` spelling, refuses mixed credential sources for the scheme-less spelling as for the `http://` one, and still fails closed for `--proxy` and `BROWSE_PROXY_URL`. +- `browse/test/proxy-env-daemon.test.ts`: through the real CLI and daemon, a scheme-less `HTTPS_PROXY` with credentials starts the daemon on that `http://` proxy, redacted in the start line and the state, and an `ftp://` value warns and falls back to direct; `BROWSE_PROXY_URL=direct` is written to the daemon state as `proxyDirect` where it bypasses the system proxy and a default session is then refused with the `BROWSE_PROXY_URL differs` lines; on Linux the daemon is reused. This file and `proxy-direct-launch-args.test.ts` join the macOS named-regressions CI job, so the refusal arm and the "no switch without an opt-out" arm run on macOS. `buildHeadedServerEnv` carries the opt-out to the headed server, and BROWSER.md's section is checked against the real switch and log literals. +- `browse/test/proxy-direct-launch-args.test.ts`: reads the real command line of the headless Chromium `BrowserManager` launched (from the pid it records) and asserts the switch is present on Linux and absent elsewhere without an opt-out, and present on every platform with `BROWSE_PROXY_URL=direct`; the per-platform expectations are written out, not read back from `forcesDirect`. Skipped on Windows, where no command line reader exists. + +## [1.91.247.0] - 2026-10-26 + +**/plan-eng-review no longer points its reader at three things its own text never defines.** The plan-eng-review sections judge marked down clarity and actionability for these references. All three live in shared resolvers, so the fixes reach every skill that renders them. The Claude Code hook that backs up AskUserQuestion carried the same stale pointer, so it's fixed too: + +### Fixed +- **Claims Need Evidence kernel** (`scripts/resolvers/preamble/generate-evidence-directive.ts`): the evidence-kind bullet ended with "Reuse rules: Step 16." That step exists only in /ship, so in every other skill the pointer led nowhere. It's gone. /ship's Step 16 still states its own reuse rule ("Reuse a check when its inputs match"), and every tier's DONE line still asks you to name reuse. +- **AskUserQuestion Tool resolution** (`generate-ask-user-format.ts`): "per its Unattended session block" read like a section of the skill, but the block is runtime output. The change removes the dangling pointer from item 1 and from the failure fallback's `unattended` branch. Both keep the destructive/irreversible carve-out the pointer used to supply: item 1 now names destructive or irreversible options among the pending gate items itself, and the fallback branch says a destructive or irreversible option stays pending, as the printed `unattended-session` block does. +- **AskUserQuestion fallback hook** (`hosts/claude/hooks/auq-error-fallback-hook.ts`): its unattended directive ended with "(Unattended session block; docs/unattended.md names each skill's record)", the same section-style pointer to runtime output. It now ends with "(docs/unattended.md names each skill's record)". The rest of the directive, including which skills print a GSTACK_RESULT line, is unchanged. +- **Reality rows, eng/ceo branch** (`scripts/resolvers/plan-reality.ts`): the Completion Summary row and plan-eng-review's checklist ask for the `PLAN_REALITY` line, but the operative block never said where that line comes from. It now says `gstack-plan-reality check` prints it. + +### Tests +- `test/honesty-kernel.test.ts` no longer requires the Step 16 pointer. It now rejects any `Step ` cross-reference in the kernel, which every skill renders. +- `test/resolver-ask-user-format.test.ts` checks that item 1 states the unattended pending set with no section or block pointer, in any spelling of the unattended-session block or "per its printed". It also checks that the failure fallback's `unattended` branch keeps destructive or irreversible options pending, and that the hook's unattended directive says the same. `test/gstack-skill-start.test.ts` checks that the block gstack-skill-start actually prints keeps destructive or irreversible options pending for every skill it samples. `test/auq-error-fallback-hook.test.ts` asserts the hook's unattended directive no longer says "Unattended session block". +- `test/plan-reality.test.ts` checks that the reality-rows block for the eng and ceo phases says its `gstack-plan-reality check` line prints `PLAN_REALITY`. +- 89 pins in `test/fixtures/skill-md-size-allowlist.json` were moved to their new exact sizes, and every pin went down. 78 went down by 32 characters. The 11 non-Claude plan-eng-review pins went down by 8, because they also inline the 24-character "prints `PLAN_REALITY`" pointer from `review-sections.md`. Both figures already include the destructive/irreversible carve-out that item 1 and the fallback branch keep. The /ship goldens were regenerated. + +### Gate integrity +- No threshold, rubric, trial count, case kind or judge prompt changed. Every size pin went down (78 by 32, 11 by 8); none was raised. + ## [1.91.246.0] - 2026-10-26 **The README is a first-ten-minutes page again: what gstack is, how to install it, the first three skills to try, the skills tables, platform notes, and links to docs/ for everything else.** It was 862 lines and 90 KB, with CSO producer sandboxing, Codex model-selection order and the uninstall script in the path of someone deciding whether to try it. It is now about 300 lines and 33 KB. Nothing was deleted: every removed section moved to a docs/ page, verbatim except the "Proactive skill suggestions" paragraph in docs/why-gstack.md, which now describes the core-set default and `gstack-config set proactive core|all|off` landing in #3432, and every anchor other files link to (`#install--about-30-seconds`, `#gbrain--persistent-knowledge-for-your-coding-agent`) still resolves. diff --git a/CLAUDE.md b/CLAUDE.md index 095a8d7b9..3b874a2ab 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -146,7 +146,7 @@ a skill's own bash. **Size budgets:** generated SKILL.md files warn above 160KB (~40K tokens); `test/catalog-budget.test.ts` caps the always-loaded skill catalog at -`CATALOG_BUDGET_TOKEN_EQUIVALENTS` (1,197 today; each new skill ratchets it); and +`CATALOG_BUDGET_TOKEN_EQUIVALENTS` (1,251 today; each new skill ratchets it); and `test/context-budget-ratchet.test.ts` pins per-skill token ceilings against `test/fixtures/context-budget.json` (for legitimate growth or a landed reduction, re-run `bun test/helpers/capture-context-budget.ts` and commit the diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e3a9220eb..44aa2d71c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -427,7 +427,7 @@ eval files, and misses the strict classifier. No API keys needed. - **Generator tests** (`test/gen-skill-docs.test.ts`) — Tests the template system: verifies placeholders resolve correctly, output includes value hints for flags (e.g. `-d ` not just `-d`), enriched descriptions for key commands (e.g. `is` lists valid states, `press` lists key examples). - **Design detector, catalog, and DESIGN.md** (`test/gstack-design-detect.test.ts`, `test/design-detect-contract.test.ts`, `test/design-catalog.test.ts`, `test/design-checklist-sync.test.ts`, `test/design-md.test.ts`, `test/frontend-scope.test.ts`, `test/impeccable-fixtures.test.ts`) — Drive `bin/gstack-design-detect.ts` through the fake engine in `test/fixtures/fake-impeccable.ts` (probe order, the never-execute-a-repository-file rule, the `--changed` target allow-list, `design_detector: off`, analytics lines, output sanitizing), pin the catalog invariants and the generated `review/design-checklist.md`, round-trip the open DESIGN.md reader/writer, and check the real engine captures (`test/fixtures/impeccable-*.json`, engine 0.1.3) against the contract. `test/dom-dump-hygiene.test.ts` runs `lib/dom-dump.js` in a real Chromium page through the built browse binary; it self-skips without the binary and is opt-in outside CI (`GSTACK_DOM_DUMP_HYGIENE=1`). - **Tier-alignment invariant** (`test/e2e-tier-alignment.test.ts`) — For every self-gated `test/skill-e2e-*.test.ts` named in a touchfiles dep list, the file's `EVALS_TIER` self-gate must match its declared tier in `E2E_TIERS`. Kills the "inert demotion" class where a test is re-tiered in `touchfiles.ts` but the file still gates on the old tier and keeps running in the wrong lane. Unmapped or mixed-tier files are reported, never silently skipped. -- **Catalog budget** (`test/catalog-budget.test.ts`) — Caps the aggregate discovery surface: the sum of every skill's frontmatter `name` + `description` (what every host loads at discovery, every session) must stay under 1,171 token-equivalents, with a 260-byte per-skill cap. Counting goes through the shared census in `test/helpers/skill-census.ts` (physical files vs authored skills vs registry entries — three deliberately different counts). Adding a skill? The failure message carries the re-measure + ratchet protocol. +- **Catalog budget** (`test/catalog-budget.test.ts`) — Caps the aggregate discovery surface: the sum of every skill's frontmatter `name` + `description` (what every host loads at discovery, every session) must stay under 1,251 token-equivalents, with a 260-byte per-skill cap. Counting goes through the shared census in `test/helpers/skill-census.ts` (physical files vs authored skills vs registry entries — three deliberately different counts). Adding a skill? The failure message carries the re-measure + ratchet protocol. - **Context-budget ratchet** (`test/context-budget-ratchet.test.ts`) — CI ceilings on the two token ledgers the catalog budget doesn't cover: the always-on full-frontmatter aggregate and each skill's per-invocation eager tokens (SKILL.md + forced-read references), graded against `test/fixtures/context-budget.json` via `lib/context-bill.ts`. New skills fail until they have a ceiling; ceilings for removed skills must be pruned. Legitimate growth or a landed reduction: re-run `bun test/helpers/capture-context-budget.ts` and commit the refreshed fixture in the same commit, so the change is a visible decision in the diff. - **Dependency security regressions** (`test/dependency-security.test.ts`) — Run `bun test test/dependency-security.test.ts` to check the resolved `sharp` and `adm-zip` version floors, load Sharp, verify ordinary ZIP extraction, and reject extraction through destination-file and destination-directory symlinks. The symlink cases skip Windows. These checks complement the OSV scan; they do not change its existing exceptions. diff --git a/NOTICE.md b/NOTICE.md index 831e0162a..2c9acedc8 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -50,6 +50,24 @@ signature components are not carried over): ship-floor tells, turned into a free check. - `design-shotgun/SKILL.md.tmpl`: page archetypes as a variant axis. +## hallmark — Copyright (c) 2026 Hallmark contributors — MIT License + +https://github.com/Nutlope/hallmark (`skills/hallmark/scripts/sloplint.mjs`, +`sloplint-render.mjs`, `references/gates.md`, `scripts/fixtures/`, at 7ec8682). +License text: `licenses/MIT-hallmark.txt`. + +Derived, modified (every gate rewritten in TypeScript and in gstack's voice, +mapped onto gstack's severity words; hallmark's theme stamp, genres, posture +guards, photo index and judged gates are not carried over): + +- `lib/design-lint/` and `bin/gstack-design-lint`: the static tier's parsers + (CSS rules and tokens, HTML and component pseudo-HTML, Tailwind class + strings), the colour maths, the waiver record, and the rule families under + `lib/design-lint/rules/`; the render tier in `lib/design-lint/render*.ts`, + driving gstack's own browser instead of puppeteer. +- `test/fixtures/design-lint/{slopped.html,eyebrow-pass,eyebrow-fail,tailwind-pass,tailwind-fail,waiver}`: + the fixtures, with their waiver comments rewritten to the lint's form. + ## DESIGN.md specification — Copyright Google LLC — Apache License 2.0 https://github.com/google-labs-code/design.md diff --git a/README.md b/README.md index eea86d521..75f85a9d0 100644 --- a/README.md +++ b/README.md @@ -196,6 +196,7 @@ The same `/qa` and `/qa-only` commands test CLIs, APIs, jobs, workers and webhoo | `/setup-gbrain` | **GBrain Onboarding** — from zero to running gbrain in under 5 minutes. PGLite local, Supabase existing URL, or auto-provision a new Supabase project via Management API. MCP registration for Claude Code + per-repo trust triad (read-write/read-only/deny). [Full guide](USING_GBRAIN_WITH_GSTACK.md). | | `/sync-gbrain` | **Keep Brain Current** — re-index this repo's code into gbrain via `gbrain sources add` + `gbrain sync --strategy code`, refresh the `## GBrain Search Guidance` block in CLAUDE.md, and auto-remove guidance when the capability check fails. `--incremental` (default), `--full`, `--dry-run`. Idempotent; safe to re-run. | | `/gstack-upgrade` | **Self-Updater** — upgrade gstack to latest. Detects global vs vendored install, syncs both, shows what changed. | +| `/captain` | **Crew Controller** — run many coding agents from one controller: lanes send a READY block and never merge, the controller merges through trains (stamp on call, verify, merge with the expected head), keeps the digest, relays approvals in a fixed shape and runs a daily flaky-fix pass. Also answers to `/crew`. [Team guide](docs/captain.md). | | `/ios-qa` | **iOS Live-Device QA (v1.43.0.0+)** — drive a real iPhone or iPad over USB CoreDevice via an embedded `StateServer` in the app. Read Swift source, codegen typed `@Observable` accessors, run the agent loop. Optional `--tailnet` flag exposes the device to OpenClaw or any HTTP-capable agent on your Tailscale tailnet so remote agents can run iOS QA without ever touching the hardware. Capability-tier allowlist (observe/interact/mutate/restore), per-device session lock, audit log. | | `/ios-fix`, `/ios-design-review`, `/ios-clean`, `/ios-sync` | iOS bug-fix loop, designer's-eye HIG audit, debug-bridge cleanup, and accessor resync. See `docs/skills.md`. End-to-end walkthrough: [docs/howto-ios-testing-with-gstack.md](docs/howto-ios-testing-with-gstack.md). | @@ -234,6 +235,7 @@ On Windows without Developer Mode (MSYS2 / Git Bash), `setup` falls back to file | Doc | What it covers | |-----|---------------| | [Skill Deep Dives](docs/skills.md) | Philosophy, examples, and workflow for every skill (includes Greptile integration) | +| [The recommended harness](docs/recommended-harness.md) | gstack + Capy from zero to the first merged train: install, the Captain and its Drive, trains, the daily flaky pass, the weekly release, the factory scorecard, what is Capy-specific, known Capy limits | | [Why gstack](docs/why-gstack.md) | The story, the productivity numbers and their caveats, Karpathy's failure modes, parallel sprints | | [Install details](docs/install.md) | Setup timing, requirements in detail, team mode, the hooks setup registers | | [Other agents](docs/hosts.md) | OpenClaw and ClawHub, per-host notes, the Codex profile and model tiers, outside-review providers, the design binary's models | @@ -252,6 +254,7 @@ On Windows without Developer Mode (MSYS2 / Git Bash), `setup` falls back to file | [Contributing](CONTRIBUTING.md) | Dev setup, testing, contributor mode, and dev mode | | [Memorable recall bridge](docs/memorable-workflow-memory.md) | Opt-in third-party workflow memory through gstack: two consents, what gstack hands over and can attest, removal, troubleshooting | | [Troubleshooting](docs/troubleshooting.md) | Every `not run` / `unavailable` message, what it means, and the fix | +| [Running a Captain crew](docs/captain.md) | One controller, many lanes on Capy: the drive layout, CREW.md, the READY contract, merge trains, approvals, the digest and the daily flaky pass, with templates | | [Changelog](CHANGELOG.md) | What's new in every version | ## Privacy & Telemetry diff --git a/SKILL.md b/SKILL.md index 1e879554f..a03f92690 100644 --- a/SKILL.md +++ b/SKILL.md @@ -214,6 +214,7 @@ quality gates that produce better results than answering inline. - User asks to update docs after shipping → invoke `/document-release` - User asks to write docs from scratch, generate documentation, "document this feature/module" → invoke `/document-generate` - User asks for a weekly retro, what did we ship, "how'd we do" → invoke `/retro` +- User asks to run a crew of agents, coordinate lanes, build a merge train, call a slot, validate a READY block, write the crew digest, "set up a captain", "run the crew" → invoke `/captain` Generic “second opinion”, “outside review”, or “cross-model review” requests use `/codex` (namespaced: `/gstack-codex`). This selection follows the **claude harness**, independently of model configuration. Explicit provider requests take precedence: Codex means `/codex`; Claude Code means `/claude-code`. Never silently substitute another provider. If that provider is the current harness, report that no outside invocation ran and suggest the other wrapper only as a separate user choice. Wrapper availability: Claude Code installs only /codex; Codex installs only /claude-code; other harnesses install both. Repair stale installations with `setup --host claude`. There is no /claude compatibility alias. - User asks for safety mode, careful mode → invoke `/careful` or `/guard` - User asks to restrict edits to a directory → invoke `/freeze` or `/unfreeze` diff --git a/SKILL.md.tmpl b/SKILL.md.tmpl index f2885faf5..0c7baa07f 100644 --- a/SKILL.md.tmpl +++ b/SKILL.md.tmpl @@ -90,6 +90,7 @@ quality gates that produce better results than answering inline. - User asks to update docs after shipping → invoke `/document-release` - User asks to write docs from scratch, generate documentation, "document this feature/module" → invoke `/document-generate` - User asks for a weekly retro, what did we ship, "how'd we do" → invoke `/retro` +- User asks to run a crew of agents, coordinate lanes, build a merge train, call a slot, validate a READY block, write the crew digest, "set up a captain", "run the crew" → invoke `/captain` {{OUTSIDE_VOICE_ROUTING}} - User asks for safety mode, careful mode → invoke `/careful` or `/guard` - User asks to restrict edits to a directory → invoke `/freeze` or `/unfreeze` diff --git a/VERSION b/VERSION index ef4799669..952c5c0e8 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.91.246.0 +1.91.255.0 diff --git a/agents-digest/gstack-AGENTS.md b/agents-digest/gstack-AGENTS.md index 82640804c..c4fbcd7dc 100644 --- a/agents-digest/gstack-AGENTS.md +++ b/agents-digest/gstack-AGENTS.md @@ -1,4 +1,4 @@ -# gstack digest v1.91.246.0 — regenerate/re-copy after upgrading gstack +# gstack digest v1.91.255.0 — regenerate/re-copy after upgrading gstack Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed for agent hosts without a full skill install. The full skills add workflows, diff --git a/autoplan/DRIVER.md b/autoplan/DRIVER.md index aaaca6b2c..ad88c3d4e 100644 --- a/autoplan/DRIVER.md +++ b/autoplan/DRIVER.md @@ -109,7 +109,7 @@ RESULT FORMAT (required by gstack-autoplan; the parent binds your file by this c A reply binds to the `gate_rev` you read (`--gate-rev`). Bare `yes`/`no` is unparsed, never guessed. -- `rec`: every item takes its recommendation, plan changes included (each prints APPLY:); `all` is an alias. +- `rec`: every item takes its recommendation, plan changes included (each prints APPLY:); `all` and `do it` are aliases. - `as-is`: keep every direction; no plan change. - `d3b uc1a`: one