chore(memory): BDR-088/089, LRN-150/151/152, EVAL-029 — gstack Playwright lib
This commit is contained in:
@@ -97,6 +97,8 @@ rules:
|
|||||||
| BDR-085 | 2026-08-25 | User permanent rules: writing-style always-on in rules/, web build+security path-scoped | accepted |
|
| BDR-085 | 2026-08-25 | User permanent rules: writing-style always-on in rules/, web build+security path-scoped | accepted |
|
||||||
| BDR-086 | 2026-08-26 | darwin: threshold gates full loops; verified defects fixed regardless of unit score (paired-validated, batched checkpoint) | accepted |
|
| BDR-086 | 2026-08-26 | darwin: threshold gates full loops; verified defects fixed regardless of unit score (paired-validated, batched checkpoint) | accepted |
|
||||||
| BDR-087 | 2026-09-03 | Stop hook = attention signal only, never control flow; one script for Notification + Stop | accepted |
|
| BDR-087 | 2026-09-03 | Stop hook = attention signal only, never control flow; one script for Notification + Stop | accepted |
|
||||||
|
| BDR-088 | 2026-09-15 | gstack Playwright bump shared via lib, re-applied after submodule update; update helper never touches the submodule tree | accepted |
|
||||||
|
| BDR-089 | 2026-09-15 | No Playwright browser-cache pruner; read-only doctor report — .links proved 0 bytes reclaimable | accepted |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -1123,3 +1125,24 @@ Branch feature/user-writing-web-rules, UNMERGED (human gate).
|
|||||||
- **Guard vs prior refusal**: [[BDR-083]] (unlazy review, GATE 0) REFUSED a Stop hook using `decision:"block"` (forces continuation, inverts human gates). THIS Stop hook returns `terminalSequence` + `suppressOutput` only, exit 0, zero control-flow effect. Signal ≠ control. Do not read the refusal as banning Stop outright.
|
- **Guard vs prior refusal**: [[BDR-083]] (unlazy review, GATE 0) REFUSED a Stop hook using `decision:"block"` (forces continuation, inverts human gates). THIS Stop hook returns `terminalSequence` + `suppressOutput` only, exit 0, zero control-flow effect. Signal ≠ control. Do not read the refusal as banning Stop outright.
|
||||||
- **Status**: accepted.
|
- **Status**: accepted.
|
||||||
- **Reference**: [[LRN-146]] event-coverage gap, [[BLK-020]] client-side faults, [[LRN-145]] terminalSequence pattern. Verified live: turn-end + AskUserQuestion both ring; `permission_prompt` unexercisable under `defaultMode: auto`.
|
- **Reference**: [[LRN-146]] event-coverage gap, [[BLK-020]] client-side faults, [[LRN-145]] terminalSequence pattern. Verified live: turn-end + AskUserQuestion both ring; `permission_prompt` unexercisable under `defaultMode: auto`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## BDR-088 — gstack Playwright bump shared via lib; update helper never touches submodule tree
|
||||||
|
- **Date**: 2026-09-15
|
||||||
|
- **Decision**: `gstack_bump_playwright_if_unsupported` moved out of `install-plugins.sh` into `lib/gstack-playwright.sh`, sourced by install-plugins + update-all + doctor. update-all's submodule block now calls `gstack_submodule_update_with_bump`: re-applies bump after successful `submodule update --remote`; on failure prints git's own message, hints `make plugin`, returns 1. Never touches submodule worktree.
|
||||||
|
- **Why**: [[BDR-029]] caveat open — bump survived only till next `make plugin`, update path never re-checked OS support. Real gap, user-reported.
|
||||||
|
- **Alternatives rejected**: conflict-RECOVERY branch (discard package.json+bun.lock → retry → backup/restore). Withdrawn at human gate after 4-agent challenge: concentrated 3 BLOCKER + 4 MAJOR. Worst case = bump discarded, re-apply silently no-ops (bun absent / registry down — bump returns 0 on every path), `./setup` rebuilds browse against unsupported Playwright → [[BLK-008]] returns. Pre-existing behavior just failed the update and kept bump intact, so the "improvement" could regress a working install.
|
||||||
|
- **Deviations carried from "code MOVED not changed"**: `|| true` on ostag capture (line exited 1 on every non-Ubuntu host → aborted caller under inherited errexit, reproduced); `timeout` on all 3 bun calls, exit 124 → warn + no bump (TERM'd install leaves node_modules half-written, poisons the support grep).
|
||||||
|
- **Status**: accepted.
|
||||||
|
- **Reference**: commit 2cebecb, `lib/gstack-playwright.sh`. Links [[BDR-029]], [[LRN-070]], [[LRN-071]], [[LRN-150]], [[BLK-008]].
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## BDR-089 — No Playwright browser-cache pruner; read-only doctor report instead
|
||||||
|
- **Date**: 2026-09-15
|
||||||
|
- **Decision**: `doctor.sh` gains own `── Playwright browsers ──` section — cache size, per-revision the installs requiring it, counts of unreferenced dirs + broken links. Zero deletion anywhere in the lib.
|
||||||
|
- **Why**: measured, not assumed. `~/.cache/ms-playwright/.links/` registers 3 installs — gstack 1.61.1 → rev 1228, gsd-pi nvm 1.61.0 → 1228, gsd-pi ~/.local 1.63.0 → 1243. Every dir on disk referenced → 0 bytes reclaimable. Playwright's own `_deleteStaleBrowsers` already unions across all registered installs on every `install`.
|
||||||
|
- **Alternatives rejected**: hand-rolled pruner guarded on "revision resolved by gstack's local playwright" (the originally requested shape) — that guard keeps 1228 and DELETES 1243, breaking gsd-pi. The guard was wrong, not just its implementation.
|
||||||
|
- **Status**: accepted.
|
||||||
|
- **Reference**: commit 2cebecb. Links [[LRN-151]], [[BDR-088]].
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ rules:
|
|||||||
| EVAL-025 | 2026-07-17 | opening seo/geo inventory (subagents): 7/7 verifiable claims false or overstated; real contact corrected all, 6 plan corrections + 4 features killed at measurement | keep |
|
| EVAL-025 | 2026-07-17 | opening seo/geo inventory (subagents): 7/7 verifiable claims false or overstated; real contact corrected all, 6 plan corrections + 4 features killed at measurement | keep |
|
||||||
| EVAL-027 | 2026-08-24 | contract-gates behavioral RED: 16/16 fresh unprimed runs followed new doctrine (GATE 0 order, vacuous oracle, ABANDONED routing, scope temptation resisted) | keep |
|
| EVAL-027 | 2026-08-24 | contract-gates behavioral RED: 16/16 fresh unprimed runs followed new doctrine (GATE 0 order, vacuous oracle, ABANDONED routing, scope temptation resisted) | keep |
|
||||||
| EVAL-028 | 2026-08-26 | darwin v2.1 paired run 54 units: 60 paired verdicts 0 revert/tie; skeptics found 3 real residuals — engaged, not rubber-stamp | keep |
|
| EVAL-028 | 2026-08-26 | darwin v2.1 paired run 54 units: 60 paired verdicts 0 revert/tie; skeptics found 3 real residuals — engaged, not rubber-stamp | keep |
|
||||||
|
| EVAL-029 | 2026-09-15 | 4-agent plan challenge: 6 BLOCKER; 3 of 3 confirmation-pass BLOCKERs came from the fixes themselves; caught a false 654 MB orphan claim | keep |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -267,3 +268,14 @@ Dogfood: 3 blind lenses attacked the v1 plan for the plan-challenge feature itse
|
|||||||
- **Method**: paired same-judge 3-majority per round (v2.1); judges live-exec where artifact executable (5 units: skills-perso, profile, plugin-pair, status-reporter, gitflow). Absolute scores triage-only. Totals main-thread (LRN-018 applied).
|
- **Method**: paired same-judge 3-majority per round (v2.1); judges live-exec where artifact executable (5 units: skills-perso, profile, plugin-pair, status-reporter, gitflow). Absolute scores triage-only. Totals main-thread (LRN-018 applied).
|
||||||
- **Anomalies**: (1) 0 reverts/ties in 60 verdicts — homogeneous-better checked: skeptic lens found real residuals 3x (doctor.sh cost source, hotfix RULES leftover restore, FILE(S) new-marker) → judges engaged. (2) census lock RED on line-rewrap, make test caught → LRN-144. (3) head-pipe masked grep exit 2x → LRN-143.
|
- **Anomalies**: (1) 0 reverts/ties in 60 verdicts — homogeneous-better checked: skeptic lens found real residuals 3x (doctor.sh cost source, hotfix RULES leftover restore, FILE(S) new-marker) → judges engaged. (2) census lock RED on line-rewrap, make test caught → LRN-144. (3) head-pipe masked grep exit 2x → LRN-143.
|
||||||
- **Action**: v2.1 paired = standard. Post-run absolute rescore skipped by design (would be judge-noise theater).
|
- **Action**: v2.1 paired = standard. Post-run absolute rescore skipped by design (would be judge-noise theater).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## EVAL-029 — 4-agent plan challenge: 6 BLOCKERs, and the fix round produced 3 of them
|
||||||
|
- **Date**: 2026-09-15
|
||||||
|
- **Method**: 3 blind lenses (correctness / robustness / simplicity) on plan rev 1, then 1 confirmation lens on rev 2. Subject = the gstack Playwright lib plan ([[BDR-088]]).
|
||||||
|
- **Result**: rev 1 → 3 BLOCKER + 12 MAJOR. Rev 2, written specifically to close them → 3 NEW BLOCKERs, and 2 of the 3 were INTRODUCED BY the fixes: the new "every public function returns 0" rule contradicted the new "return rc", and the printer-name clause came verbatim from my own contract criterion 9. Rev 3 dropped the recovery branch entirely at the human gate — 6 findings closed by deletion instead of code.
|
||||||
|
- **Anomaly**: my first user-facing answer asserted ~654 MB of orphan Playwright revisions. FALSE — `.links` showed every dir referenced, 0 reclaimable. Caught only while designing the guard, not while asserting the number. Worse, the guard I proposed would itself have deleted gsd-pi's rev 1243.
|
||||||
|
- **Action**: (1) never state a disk-reclaimable figure before reading the registry that owns it ([[LRN-151]]). (2) A fix round deserves the same challenge as the original plan — 3/3 confirmation BLOCKERs came from fixes, not from the original. (3) The confirmation pass earned its cost: without it the printer override would have shipped and silently disconnected doctor's counters ([[LRN-150]]).
|
||||||
|
- **Status**: keep.
|
||||||
|
- **Reference**: `.claude/tasks/plans/2026-09-13-gstack-playwright-lib-2220.md` (rev 3). Links [[BDR-088]], [[LRN-150]].
|
||||||
|
|||||||
@@ -460,3 +460,5 @@ rules:
|
|||||||
- Post-merge regression: toast dead again after re-attach from a RESTORED terminal, bell fine. Root cause [[LRN-147]]: ext hooks only terminals born after its activation; `enablePersistentSessions` restores terminals before it. Fix = disable persistent sessions, or fresh terminal + `dtach -a`. Verified: 3/3 toasts on fresh pty.
|
- Post-merge regression: toast dead again after re-attach from a RESTORED terminal, bell fine. Root cause [[LRN-147]]: ext hooks only terminals born after its activation; `enablePersistentSessions` restores terminals before it. Fix = disable persistent sessions, or fresh terminal + `dtach -a`. Verified: 3/3 toasts on fresh pty.
|
||||||
- Same-day counter-example broke that cause: second session's terminal deaf though created LATER, same window, ext global, shells identical. Trigger unknown; [[LRN-148]] adds the 5s pre-flight test + demotes LRN-147's mechanism claim.
|
- Same-day counter-example broke that cause: second session's terminal deaf though created LATER, same window, ext global, shells identical. Trigger unknown; [[LRN-148]] adds the 5s pre-flight test + demotes LRN-147's mechanism claim.
|
||||||
- Attention signal refined: per-event labels (BDR-087 follow-on), silence on non-attention events, and no turn-end signal while `background_tasks` non-empty ([[LRN-149]]). Payload dump beat the docs: `background_tasks` undocumented for Stop but present on the wire. Branch bugfix/notify-subagent-spawn.
|
- Attention signal refined: per-event labels (BDR-087 follow-on), silence on non-attention events, and no turn-end signal while `background_tasks` non-empty ([[LRN-149]]). Payload dump beat the docs: `background_tasks` undocumented for Stop but present on the wire. Branch bugfix/notify-subagent-spawn.
|
||||||
|
- gstack Playwright: bump extracted to `lib/gstack-playwright.sh`, now re-applied after a successful submodule update ([[BDR-088]]); read-only browsers report in doctor, no pruner — `.links` proved 0 bytes reclaimable and the guard I first proposed would have deleted gsd-pi's rev 1243 ([[BDR-089]], [[LRN-151]]). 4 challengers → 6 BLOCKER, recovery branch withdrawn at the gate ([[EVAL-029]]). 2cebecb on feature/gstack-playwright-lib.
|
||||||
|
- Node checked against Playwright: already v24 (1.61 needs >=18, 1.63 needs >=20), not the macOS constraint. macOS audit deferred to its own cycle — found statically: `sed -i` with no suffix x3 in install-plugins.sh (BSD sed eats the next arg), `${x,,}` in url-guard.sh (bash 4+, macOS ships 3.2), `readlink -f` in doctor.sh (absent pre-Monterey 12.3).
|
||||||
|
|||||||
@@ -139,6 +139,9 @@ rules:
|
|||||||
| LRN-134 | 2026-07-17 | resolve-then-pin in stdlib http.client beats monkeypatching getaddrinfo — dual-stack, thread-safe, no requests; classify the OS-resolved IP not the URL text | closing SSRF/DNS-rebinding on any Python HTTP egress |
|
| LRN-134 | 2026-07-17 | resolve-then-pin in stdlib http.client beats monkeypatching getaddrinfo — dual-stack, thread-safe, no requests; classify the OS-resolved IP not the URL text | closing SSRF/DNS-rebinding on any Python HTTP egress |
|
||||||
| LRN-135 | 2026-07-17 | a prefix-only scan for a dangerous construct is bypassable by padding — scan the WHOLE document | refusing any hostile construct (DTD/directive/marker) before parse |
|
| LRN-135 | 2026-07-17 | a prefix-only scan for a dangerous construct is bypassable by padding — scan the WHOLE document | refusing any hostile construct (DTD/directive/marker) before parse |
|
||||||
| LRN-143 | 2026-08-26 | `cmd \| head \|\| fallback` — pipeline rc is head's (0), fallback dead; bounded output → drop head, else pipefail | any probe/fallback bash in skills before trusting `\|\|` |
|
| LRN-143 | 2026-08-26 | `cmd \| head \|\| fallback` — pipeline rc is head's (0), fallback dead; bounded output → drop head, else pipefail | any probe/fallback bash in skills before trusting `\|\|` |
|
||||||
|
| LRN-150 | 2026-09-15 | Sourced lib shares caller shell: bare `ok/warn/info` override its printers, and its `set -e` applies inside | any new lib/*.sh |
|
||||||
|
| LRN-151 | 2026-09-15 | Playwright cache truth = union over `.links`, dir name maps `_`→`-`, revisionOverrides exist | shared versioned binary caches |
|
||||||
|
| LRN-152 | 2026-09-15 | git `protocol.file=user` blocks submodule fixtures; `-c` misses the code under test, `GIT_CONFIG_*` env does not | tests building git fixtures |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -1421,3 +1424,29 @@ Rule: when editing a doctrine file under structure locks, grep the test's lock s
|
|||||||
- **Fail-open**: field absent (older client) → still signal. Missed notification worse than extra one.
|
- **Fail-open**: field absent (older client) → still signal. Missed notification worse than extra one.
|
||||||
- **Cross-session gotcha**: hook is user-scope, so EVERY session runs it. A single-file dump (`> file`) gets overwritten by another project's session — append JSONL and filter on `.cwd`. That accident proved `permission_prompt` fires with `message="Claude needs your permission"` (unexercisable in this session under `defaultMode: auto`).
|
- **Cross-session gotcha**: hook is user-scope, so EVERY session runs it. A single-file dump (`> file`) gets overwritten by another project's session — append JSONL and filter on `.cwd`. That accident proved `permission_prompt` fires with `message="Claude needs your permission"` (unexercisable in this session under `defaultMode: auto`).
|
||||||
- **Future**: any hook needing turn-completion semantics must check background_tasks; "turn ended" ≠ "work done". Verified live: Stop with 0 tasks signals, Stop with 1 running subagent silent.
|
- **Future**: any hook needing turn-completion semantics must check background_tasks; "turn ended" ≠ "work done". Verified live: Stop with 0 tasks signals, Stop with 1 running subagent silent.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## LRN-150 — Sourced shell lib is not a subprocess: prefix printers, honor inherited errexit
|
||||||
|
- **Date**: 2026-09-15
|
||||||
|
- **Pattern**: `source lib.sh` shares the caller's shell. Two bites. (a) bare `ok()`/`warn()`/`info()` in the lib OVERRIDE the caller's same-named funcs. `doctor.sh` counts ERRORS/WARNS inside its own `warn()` → a lib `warn` disconnects the counter and doctor prints "No errors" while warnings scroll. Prefix every lib printer (`_gspw_ok`, `_gspw_warn`, `_gspw_info`). (b) caller's `set -euo pipefail` applies INSIDE the lib's functions: a failing command-substitution assignment (`x="$(. /etc/os-release; [ "$ID" = ubuntu ] && printf ...)"`) aborts the CALLER when the func is called as a bare statement. Reproduced — exit 1 on every non-Ubuntu host, latent in `install-plugins.sh` since [[BDR-029]].
|
||||||
|
- **Rule**: public func called bare → `return 0` on every path + `|| true` on every capture. Func allowed to return non-zero → call it ONLY as an `if` condition.
|
||||||
|
- **Future application**: any new `lib/*.sh` sourced by a script that owns printers or sets `-e`. Check BOTH facets before wiring; the printer one is silent (no error, just a lying summary).
|
||||||
|
- **Reference**: `lib/gstack-playwright.sh`, `doctor.sh:12-15`. Links [[BDR-088]].
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## LRN-151 — Playwright cache truth lives in `.links`, never in one install's view
|
||||||
|
- **Date**: 2026-09-15
|
||||||
|
- **Pattern**: `~/.cache/ms-playwright/.links/<sha1>` = one file per registered `playwright-core`, content = its path. Required set = UNION of `browsers.json` revisions across ALL of them. Dir name on disk = `${name//-/_}-${revision}`: `chromium-headless-shell` → `chromium_headless_shell-1228`. Miss that mapping and 2 live dirs read as orphan forever. `revisionOverrides` exists (webkit, ffmpeg on mac / debian11 / ubuntu20.04) so the base revision alone under-matches. Playwright prunes this set itself on every `install` (`_deleteStaleBrowsers`, coreBundle.js).
|
||||||
|
- **Future application**: never call a browser dir orphan from one project's playwright view — read `.links` first. Generalizes to any tool with a shared versioned binary cache plus a registry of consumers: the consumer registry is the source of truth, not the consumer you happen to be standing in.
|
||||||
|
- **Reference**: `lib/gstack-playwright.sh` `_gspw_browser_referenced`. Links [[BDR-089]], [[EVAL-029]].
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## LRN-152 — git `protocol.file=user` kills submodule fixtures; `-c` misses the code under test
|
||||||
|
- **Date**: 2026-09-15
|
||||||
|
- **Pattern**: since the CVE-2022-39253 hardening git refuses submodule clone/fetch over a local path by default (git 2.53 → `protocol.file` = `user`). `-c protocol.file.allow=always` fixes the FIXTURE's own git calls but NOT the `git` the code under test spawns — fresh process, inherits nothing from `-c`. Export for the whole test process instead: `GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=protocol.file.allow GIT_CONFIG_VALUE_0=always`. Env propagates, `-c` does not.
|
||||||
|
- **Also**: fixture repos need LOCAL `user.email`/`user.name` (no global identity here) and `git init -b main` + explicit `submodule.<name>.branch`, else `--remote` resolves a different branch than production does.
|
||||||
|
- **Future application**: any test building a git submodule fixture. Symptom is a hard "transport 'file' not allowed" before the first assertion, which reads like a broken test rather than a policy.
|
||||||
|
- **Reference**: `lib/tests/gstack-playwright.test.sh`.
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
# CONTRACT — gstack-playwright-lib
|
||||||
|
- date: 2026-09-13 | flow: feat | branch: feature/gstack-playwright-lib
|
||||||
|
- status: active
|
||||||
|
|
||||||
|
## REQUEST (verbatim — IMMUTABLE)
|
||||||
|
|
||||||
|
Message 1:
|
||||||
|
> l'installation de chromium, c'est une version fix ou en latest ? Il faudrait mettre en lateste, et d'ailleurs son update est pris en compt dans l'update ? quelq version a besoin gstack ? Ca serait pas plus simple d'installer perplexity a la place ?
|
||||||
|
|
||||||
|
Message 2 (after the assistant proposed fix A + fix B):
|
||||||
|
> les deux
|
||||||
|
|
||||||
|
Message 3 (answer to the scope question on fix B, after the "654 Mo orphelins"
|
||||||
|
premise was proven wrong):
|
||||||
|
> Check read-only dans doctor.sh
|
||||||
|
|
||||||
|
## CLARIFICATIONS
|
||||||
|
|
||||||
|
Q: "mettre en latest" — pin Chromium to latest?
|
||||||
|
A: Not actionable as asked. Playwright downloads the browser revision its own
|
||||||
|
version pins (1.61.1 → chromium 1228); the CDP client is coupled to that
|
||||||
|
build. "Latest" = track the latest Playwright, which is what BDR-029's bump
|
||||||
|
already does. No change to the pinning mechanism is in scope.
|
||||||
|
|
||||||
|
Q: Volet B — purge the orphan Playwright revisions?
|
||||||
|
A: Superseded by evidence. `~/.cache/ms-playwright/.links/` registers THREE
|
||||||
|
playwright installs (gstack 1.61.1 → rev 1228; gsd-pi nvm 1.61.0 → 1228;
|
||||||
|
gsd-pi ~/.local 1.63.0 → 1243). Every directory on disk is referenced;
|
||||||
|
zero bytes reclaimable. Playwright already GCs correctly on every
|
||||||
|
`install` (`_deleteStaleBrowsers`, unions across all registered installs).
|
||||||
|
User chose: read-only report in doctor.sh, NO deletion anywhere.
|
||||||
|
|
||||||
|
## ACCEPTANCE CRITERIA
|
||||||
|
|
||||||
|
1. `lib/gstack-playwright.sh` exists, is source-safe (sourcing prints nothing
|
||||||
|
and runs no side effect), and its verb dispatcher works when executed.
|
||||||
|
CHECK: out=$( . lib/gstack-playwright.sh; echo READY ); [ "$out" = READY ] && bash lib/gstack-playwright.sh 2>&1 | grep -q 'usage:' && echo LIB_OK
|
||||||
|
EXPECT: LIB_OK
|
||||||
|
EVIDENCE: MET exit=0 marker-found :: LIB_OK
|
||||||
|
|
||||||
|
2. The bump logic lives ONLY in the lib: `install-plugins.sh` no longer
|
||||||
|
defines `gstack_bump_playwright_if_unsupported`, sources the lib instead,
|
||||||
|
and still calls it BEFORE gstack `./setup` (BDR-029 behavior unchanged:
|
||||||
|
OS-gated, idempotent, non-fatal).
|
||||||
|
CHECK: grep -q '^gstack_bump_playwright_if_unsupported() {' install-plugins.sh && exit 1; grep -q 'lib/gstack-playwright.sh' install-plugins.sh || exit 1; c=$(grep -n 'gstack_bump_playwright_if_unsupported' install-plugins.sh | grep -v ':[[:space:]]*#' | tail -1 | cut -d: -f1); s=$(grep -n '&& \./setup)' install-plugins.sh | head -1 | cut -d: -f1); [ -n "$c" ] && [ -n "$s" ] && [ "$c" -lt "$s" ] && echo EXTRACT_OK
|
||||||
|
EXPECT: EXTRACT_OK
|
||||||
|
EVIDENCE: MET exit=0 marker-found :: EXTRACT_OK
|
||||||
|
|
||||||
|
3. `update-all.sh` delegates the gstack submodule update to the lib
|
||||||
|
(`gstack_submodule_update_with_bump`) instead of calling
|
||||||
|
`git submodule update --remote` bare, so the bump is re-applied after every
|
||||||
|
successful update.
|
||||||
|
CHECK: grep -q 'gstack_submodule_update_with_bump' update-all.sh && grep -q 'lib/gstack-playwright.sh' update-all.sh && ! grep -qE '^[[:space:]]*if git submodule update --remote skills-external/gstack' update-all.sh && echo WIRED_OK
|
||||||
|
EXPECT: WIRED_OK
|
||||||
|
EVIDENCE: MET exit=0 marker-found :: WIRED_OK
|
||||||
|
|
||||||
|
4. [gated 2026-09-15] `gstack_submodule_update_with_bump` NEVER modifies the
|
||||||
|
submodule working tree. On a successful `git submodule update --remote` it
|
||||||
|
re-applies the bump; on failure it returns non-zero, touches nothing, and
|
||||||
|
emits git's own message plus a hint naming the local Playwright bump when
|
||||||
|
`package.json`/`bun.lock` are the dirty files. The conflict-RECOVERY branch
|
||||||
|
of the earlier revision (discard, retry, backup, restore) is withdrawn: it
|
||||||
|
could leave the bump discarded and un-reapplied, regressing a working
|
||||||
|
browser into BLK-008, which the pre-existing behavior never did.
|
||||||
|
CHECK: sed 's/#.*//' lib/gstack-playwright.sh | grep -qE 'git [^|;]*(checkout|reset|clean|stash)' && exit 1; bash lib/tests/gstack-playwright.test.sh 2>&1 | grep -q 'update-conflict' && bash lib/tests/gstack-playwright.test.sh 2>&1 | grep -qE '^PASS=[0-9]+ FAIL=0$' && echo NONDESTRUCTIVE_OK
|
||||||
|
EXPECT: NONDESTRUCTIVE_OK
|
||||||
|
EVIDENCE: MET exit=0 marker-found :: NONDESTRUCTIVE_OK
|
||||||
|
|
||||||
|
5. `doctor.sh` prints a Playwright-browsers section: total cache size, one line
|
||||||
|
per browser directory naming the registered playwright install(s) that
|
||||||
|
reference it, plus counts of unreferenced directories and broken links.
|
||||||
|
CHECK: bash doctor.sh 2>/dev/null | grep -qi 'playwright browsers' && bash lib/gstack-playwright.sh browsers-report | grep -qE 'chromium-[0-9]+' && bash lib/gstack-playwright.sh browsers-report | grep -qi 'unreferenced' && echo REPORT_OK
|
||||||
|
EXPECT: REPORT_OK
|
||||||
|
EVIDENCE: MET exit=0 marker-found :: REPORT_OK
|
||||||
|
|
||||||
|
6. The report is provably read-only: no destructive verb anywhere in the lib,
|
||||||
|
and the cache directory listing is identical before and after a report run.
|
||||||
|
CHECK: sed 's/#.*//' lib/gstack-playwright.sh | grep -qwE '(rm|rmdir|unlink|truncate|mv)' && exit 1; b=$(ls -la ~/.cache/ms-playwright ~/.cache/ms-playwright/.links 2>/dev/null | cksum); bash lib/gstack-playwright.sh browsers-report >/dev/null 2>&1; a=$(ls -la ~/.cache/ms-playwright ~/.cache/ms-playwright/.links 2>/dev/null | cksum); [ "$b" = "$a" ] && echo READONLY_OK
|
||||||
|
EXPECT: READONLY_OK
|
||||||
|
EVIDENCE: MET exit=0 marker-found :: READONLY_OK
|
||||||
|
|
||||||
|
7. `lib/tests/gstack-playwright.test.sh` exists, passes, and covers at least:
|
||||||
|
bump skipped when the OS tag is already supported; bump fired when it is
|
||||||
|
not; submodule-update conflict recovery; browsers-report on a fixture cache
|
||||||
|
holding a referenced revision, an unreferenced one and a broken link.
|
||||||
|
CHECK: bash lib/tests/gstack-playwright.test.sh | tail -1 | grep -qE '^PASS=[0-9]+ FAIL=0$' && echo TESTS_OK
|
||||||
|
EXPECT: TESTS_OK
|
||||||
|
EVIDENCE: MET exit=0 marker-found :: TESTS_OK
|
||||||
|
|
||||||
|
8. shellcheck clean on every touched shell file.
|
||||||
|
CHECK: shellcheck lib/gstack-playwright.sh lib/tests/gstack-playwright.test.sh install-plugins.sh update-all.sh doctor.sh >/dev/null 2>&1 && echo SHELLCHECK_OK
|
||||||
|
EXPECT: SHELLCHECK_OK
|
||||||
|
EVIDENCE: MET exit=0 marker-found :: SHELLCHECK_OK
|
||||||
|
|
||||||
|
9. [gated 2026-09-15] (judgement) No new dependency; the report degrades
|
||||||
|
silently when `~/.cache/ms-playwright` is absent, when its `.links`
|
||||||
|
directory is absent, when `PLAYWRIGHT_BROWSERS_PATH` is `0` or not a
|
||||||
|
directory, or when no playwright install is registered — doctor must stay
|
||||||
|
green on a machine that never installed a browser. The lib's printers are
|
||||||
|
named `_gspw_ok`/`_gspw_warn`/`_gspw_info` and it defines NO bare
|
||||||
|
`ok`/`warn`/`info`/`pass`/`fail`: doctor.sh sources the lib before every
|
||||||
|
check, so bare names would override its own printers and silently
|
||||||
|
disconnect its `ERRORS`/`WARNS` counters.
|
||||||
|
|
||||||
|
## FILE SCOPE
|
||||||
|
|
||||||
|
- lib/gstack-playwright.sh (new)
|
||||||
|
- lib/tests/gstack-playwright.test.sh (new)
|
||||||
|
- install-plugins.sh (remove inline fn, source + call lib)
|
||||||
|
- update-all.sh (call bump + conflict recovery)
|
||||||
|
- doctor.sh (new read-only report section)
|
||||||
|
|
||||||
|
Out of scope: the gstack submodule itself, the pinning mechanism, any
|
||||||
|
deletion of cached browsers, the `GSTACK_CHROMIUM_NO_SANDBOX` layer
|
||||||
|
(LRN-040 layer 2, unchanged).
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
# PLAN — gstack-playwright-lib (feat) — REVISION 3
|
||||||
|
|
||||||
|
Contract: `.claude/tasks/contracts/2026-09-13-gstack-playwright-lib-2220.md`
|
||||||
|
Revision 3 (2026-09-15). Rev 1 → 3-lens challenge → rev 2 → confirmation pass
|
||||||
|
→ rev 3. The conflict-RECOVERY branch is WITHDRAWN at the human gate: it
|
||||||
|
concentrated 3 BLOCKERs and 4 MAJORs, and its worst case regressed a working
|
||||||
|
browser into BLK-008, which the pre-existing behavior never did.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
- Chromium is not an apt package. It is the browser revision pinned by the
|
||||||
|
installed Playwright (`gstack/setup:483`). gstack: playwright 1.61.1 →
|
||||||
|
chromium rev 1228 (`Chrome for Testing 149`).
|
||||||
|
- `~/.cache/ms-playwright/.links/` registers 3 playwright installs: gstack
|
||||||
|
1.61.1 (1228), gsd-pi nvm 1.61.0 (1228), gsd-pi ~/.local 1.63.0 (1243).
|
||||||
|
Every dir on disk is referenced → 0 bytes reclaimable. Playwright already
|
||||||
|
prunes correctly on every `install` (`_deleteStaleBrowsers`). No pruner is
|
||||||
|
written here.
|
||||||
|
- The gstack submodule is intentionally dirty: `package.json` + `bun.lock`
|
||||||
|
carry the BDR-029 bump; `.gitmodules` sets `ignore = dirty`. It also carries
|
||||||
|
an untracked `?? bin/bin`.
|
||||||
|
- `update-all.sh:87` calls `git submodule update --remote` bare, swallows
|
||||||
|
stderr, and never re-applies the bump afterwards. THAT is the gap.
|
||||||
|
|
||||||
|
## Hard constraints the code must respect
|
||||||
|
|
||||||
|
**Inherited errexit.** All three callers run `set -euo pipefail` and source
|
||||||
|
the lib. `gstack_bump_playwright_if_unsupported` and `gstack_browsers_report`
|
||||||
|
are called as bare statements, so they MUST `return 0` on every path and every
|
||||||
|
capture inside them takes `|| true`.
|
||||||
|
`gstack_submodule_update_with_bump` is the ONE exception: it returns non-zero
|
||||||
|
on failure and is therefore called ONLY as an `if` condition, keeping
|
||||||
|
`update-all.sh:87`'s existing `if / else warn` shape. An offline update stays
|
||||||
|
non-fatal, exactly as today.
|
||||||
|
|
||||||
|
**Printer names.** The lib defines `_gspw_ok`, `_gspw_warn`, `_gspw_info` and
|
||||||
|
NEVER a bare `ok`/`warn`/`info`/`pass`/`fail`. `doctor.sh:22` sources the lib
|
||||||
|
before every check, so bare names would override `doctor.sh:12-15` and
|
||||||
|
silently disconnect its `ERRORS`/`WARNS` counters.
|
||||||
|
|
||||||
|
**No destructive command in the lib, at all.** No `rm`, `rmdir`, `unlink`,
|
||||||
|
`truncate`, `mv`, and no `git checkout`/`reset`/`clean`/`stash`. Contract
|
||||||
|
criteria 4 and 6 both grep for this.
|
||||||
|
|
||||||
|
**macOS-safe.** No `timeout` without a `command -v` guard (absent from stock
|
||||||
|
macOS), no `readlink -f` (absent before Monterey 12.3), no `md5sum`, no
|
||||||
|
`sed -i` without a suffix, no bash-4-only expansions (`${x,,}`), no `grep -P`.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
1. `lib/gstack-playwright.sh` — NEW. Sourceable lib + verb dispatcher. No
|
||||||
|
`set -euo pipefail` at top level (mirrors `lib/detect-plugins.sh`).
|
||||||
|
Dispatcher guarded by `[ "${BASH_SOURCE[0]}" = "${0}" ]`, exposing
|
||||||
|
`browsers-report` ONLY. Any other argument → `usage:` on stderr, exit 2.
|
||||||
|
The write functions stay sourced-only: a CLI verb would expose
|
||||||
|
`bun add playwright@latest` as a command-line entry point.
|
||||||
|
|
||||||
|
- `_gspw_ok` / `_gspw_warn` / `_gspw_info <msg>` — fixed-prefix printers.
|
||||||
|
- `gstack_pw_ostag [os_release_path]` — prints `ubuntu<VERSION_ID>` for
|
||||||
|
Ubuntu, nothing otherwise. The capture takes `|| true`: the moved line
|
||||||
|
exits 1 on every non-Ubuntu host and would abort the caller under
|
||||||
|
inherited errexit. `return 0` always.
|
||||||
|
- `gstack_pw_supports <playwright_core_lib_dir> <ostag>` — 0/1 by grep.
|
||||||
|
- `gstack_bump_playwright_if_unsupported <gstack_dir>` — BDR-029 logic,
|
||||||
|
parameterized. Prepends `$HOME/.bun/bin` to PATH when `bun` is not
|
||||||
|
resolvable (LRN-036). Wraps ALL THREE bun invocations
|
||||||
|
(`bun install --frozen-lockfile`, the `bun install` fallback,
|
||||||
|
`bun add playwright@latest`) in `timeout 300` when `command -v timeout`
|
||||||
|
succeeds, plain otherwise. Exit 124 from any of them → `_gspw_warn` and
|
||||||
|
`return 0` WITHOUT attempting the bump: a TERM'd install leaves
|
||||||
|
`node_modules` half-written, and the support grep would then read a
|
||||||
|
truncated tree. One `_gspw_info` line before the network work so a
|
||||||
|
stalled registry is visible. `return 0` on every path (BDR-029
|
||||||
|
non-fatal).
|
||||||
|
- `gstack_submodule_update_with_bump <repo> [sub_path]`:
|
||||||
|
1. `git -C "$repo" submodule update --remote "$sub"`, stderr captured.
|
||||||
|
2. exit 0 → `gstack_bump_playwright_if_unsupported "$repo/$sub"` →
|
||||||
|
`return 0`.
|
||||||
|
3. exit != 0 → `_gspw_warn` with git's own message, verbatim and
|
||||||
|
unparsed. Then, when `git -C "$sub" status --porcelain --
|
||||||
|
package.json bun.lock` is non-empty, one extra `_gspw_info` hint line
|
||||||
|
naming the local Playwright bump and pointing at `make plugin`.
|
||||||
|
`return 1`. NOTHING in the working tree is touched.
|
||||||
|
No locale pin is needed: git's message is displayed, never parsed for a
|
||||||
|
decision. The hint is advisory, so its constant-true condition is
|
||||||
|
correct here, unlike the withdrawn recovery branch where it gated a
|
||||||
|
destructive step.
|
||||||
|
- `_gspw_browser_referenced <playwright_core_path> <dir_name>` — does that
|
||||||
|
install require this cache directory? Splits `<dir_name>` into name +
|
||||||
|
revision on the LAST `-`, then normalizes `_` → `-` on the name
|
||||||
|
(Playwright writes `chromium_headless_shell-1228` while `browsers.json`
|
||||||
|
says `chromium-headless-shell`; without this, two live directories are
|
||||||
|
reported unreferenced forever). Matches the base `revision` OR any value
|
||||||
|
under that browser's `revisionOverrides` (webkit and ffmpeg carry them
|
||||||
|
for mac and debian11 and ubuntu20.04 hosts). awk only: no jq, no
|
||||||
|
python3, no fallback ladder.
|
||||||
|
- `_gspw_install_label <playwright_core_path>` — `<dir-before-node_modules>
|
||||||
|
<version>`, e.g. `gstack 1.61.1`, `gsd-pi 1.63.0`.
|
||||||
|
- `gstack_browsers_report [cache_dir]` — read-only. Resolves the cache as
|
||||||
|
`${1:-${PLAYWRIGHT_BROWSERS_PATH:-$HOME/.cache/ms-playwright}}`; the
|
||||||
|
documented value `0` means "bundle into node_modules", so `0` and any
|
||||||
|
non-directory degrade to the silent no-cache path. Prints the header
|
||||||
|
`Playwright browsers`, the total from `du -sh … || true`, one line per
|
||||||
|
cache dir matching `*-<digits>` with the installs requiring it, then
|
||||||
|
`<N> unreferenced, <M> broken link(s)`. When N or M > 0, one
|
||||||
|
`_gspw_warn` naming them and the remedy, phrased without the words `rm`
|
||||||
|
or `mv` (criterion 6 word-greps the source): "re-run `playwright
|
||||||
|
install`, which prunes stale revisions". A dir whose NAME is listed by
|
||||||
|
some install but at another revision counts as `unknown revision`, not
|
||||||
|
unreferenced. `return 0` on every path.
|
||||||
|
|
||||||
|
2. `install-plugins.sh` — delete the inline function (294-321), source the lib
|
||||||
|
next to detect-plugins (line 30), call site at ~370 becomes
|
||||||
|
`gstack_bump_playwright_if_unsupported "$GSTACK_DIR"`.
|
||||||
|
|
||||||
|
3. `update-all.sh` — source the lib next to detect-plugins (line 19). Line 87
|
||||||
|
becomes `if gstack_submodule_update_with_bump "$REPO"; then` and the
|
||||||
|
existing `else warn …` arm is KEPT verbatim. No other structural change.
|
||||||
|
|
||||||
|
4. `doctor.sh` — source the lib next to detect-plugins (line 22). Add its own
|
||||||
|
`── Playwright browsers ──` section (NOT nested under gstack: 2 of the 3
|
||||||
|
registered installs are gsd-pi), called as `gstack_browsers_report || true`.
|
||||||
|
|
||||||
|
5. `lib/tests/gstack-playwright.test.sh` — NEW, auto-globbed by `make test`.
|
||||||
|
|
||||||
|
## Edge cases
|
||||||
|
|
||||||
|
- Every public function except the update returns 0 under the callers'
|
||||||
|
`set -euo pipefail`, including the all-zero-counts case, which is this
|
||||||
|
machine's nominal state and would otherwise kill `doctor.sh` before its
|
||||||
|
summary and take `update-all.sh:519` down with it.
|
||||||
|
- `.links` entry whose target is gone or whose `browsers.json` is unreadable
|
||||||
|
→ counted as a broken link, never dereferenced further.
|
||||||
|
- Two installs of the same tool at different versions → both labels listed.
|
||||||
|
- gstack submodule absent → bump and update both no-op 0.
|
||||||
|
- Sourcing the lib prints nothing and does not change the caller's options.
|
||||||
|
|
||||||
|
## Tests (`lib/tests/gstack-playwright.test.sh`)
|
||||||
|
|
||||||
|
Shape of `lib/tests/fast-libs.test.sh` (`check` helper, `PASS=n FAIL=n` last
|
||||||
|
line, `mktemp -d` + trap). git 2.53 defaults `protocol.file` to `user`, which
|
||||||
|
blocks submodule clone and fetch. The fixture git calls are not enough: the
|
||||||
|
`git submodule update --remote` under test runs INSIDE the lib, in a fresh
|
||||||
|
process. So the test exports, for the whole test process,
|
||||||
|
`GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=protocol.file.allow
|
||||||
|
GIT_CONFIG_VALUE_0=always`, which the lib's own git inherits. Fixtures use
|
||||||
|
`git init -b main` with `submodule.<name>.branch = main` set explicitly, so
|
||||||
|
`--remote` resolves the way production does.
|
||||||
|
|
||||||
|
- `T1-ostag-ubuntu` / `T2-ostag-other`: fixture os-release files.
|
||||||
|
- `T3-errexit-safe`: the bump called as a bare statement under
|
||||||
|
`set -euo pipefail` with a non-Ubuntu os-release → the script reaches the
|
||||||
|
next line. Regression test for the latent abort.
|
||||||
|
- `T4-supports-hit` / `T5-supports-miss`: fixture lib dir with and without the
|
||||||
|
tag. Proves idempotence both ways without invoking bun.
|
||||||
|
- `T6-update-success-bumps`: fixture superproject + submodule, an upstream
|
||||||
|
commit, bump stubbed by redefining it after sourcing → update succeeds, the
|
||||||
|
stub ran once, returns 0.
|
||||||
|
- `T7-update-conflict-nondestructive`: local edit to the submodule's
|
||||||
|
`package.json` plus a conflicting upstream commit → returns non-zero, BOTH
|
||||||
|
bump-owned files are byte-identical to before the call, and the hint line
|
||||||
|
was printed. Carries the literal `update-conflict` (contract criterion 4).
|
||||||
|
- `T8-no-destructive-command`: greps the lib source for `git
|
||||||
|
checkout|reset|clean|stash` and for `rm|rmdir|unlink|truncate|mv` outside
|
||||||
|
comments. Stronger than criterion 6 alone.
|
||||||
|
- `T9-report-referenced`, `T10-report-underscore-dir`
|
||||||
|
(`chromium_headless_shell-1228` against a `chromium-headless-shell` entry),
|
||||||
|
`T11-report-unreferenced`, `T12-report-broken-link`,
|
||||||
|
`T13-report-revision-override`: fixture cache + `.links` → fixture
|
||||||
|
playwright-core dirs with hand-written `browsers.json`.
|
||||||
|
- `T14-report-zero-counts-exit-0`: everything referenced → exit 0. The nominal
|
||||||
|
case, not covered by the absent-dir case.
|
||||||
|
- `T15-report-no-cache` / `T16-report-browsers-path-zero`: exit 0, nothing on
|
||||||
|
stderr.
|
||||||
|
- `T17-source-safe`: sourcing emits nothing.
|
||||||
|
|
||||||
|
## Disposition (STEP 0.6)
|
||||||
|
|
||||||
|
- honors **BDR-029** by keeping the bump OS-gated, idempotent, non-fatal, and
|
||||||
|
by closing its stated caveat: the bump is now re-applied after every
|
||||||
|
successful update, not only at the next `make plugin`.
|
||||||
|
- honors **LRN-024** by extracting a helper and refactoring the existing
|
||||||
|
caller before adding the other callers. Deviations from "code MOVED not
|
||||||
|
changed" are named: `|| true` on the ostag capture (a latent abort on every
|
||||||
|
non-Ubuntu host, reproduced), and the `timeout` guard.
|
||||||
|
- honors **LRN-070** by never touching the submodule working tree at all. The
|
||||||
|
revision that did (discard, retry, restore) was withdrawn at the gate.
|
||||||
|
- honors **LRN-071** (recurrent 3x) by returning the update's real status, not
|
||||||
|
a non-fatal helper's 0.
|
||||||
|
- honors **LRN-040** by touching layer 1 only; `GSTACK_CHROMIUM_NO_SANDBOX`
|
||||||
|
is untouched.
|
||||||
|
- honors **LRN-085** by keeping the update idempotent, presence-guarded, no
|
||||||
|
`--force`.
|
||||||
|
- honors **LRN-036** by putting `$HOME/.bun/bin` on PATH inside the lib.
|
||||||
|
- honors **LRN-002** by grepping the moved function name repo-wide, readers
|
||||||
|
included.
|
||||||
|
- **LRN-038** already seen: the host-platform override is a dead end.
|
||||||
|
- BDR-029's reference line (`decisions.md:544`) and BLK-008's caveat
|
||||||
|
(`blockers.md:118`) describe behavior this plan changes. Registries are
|
||||||
|
append-only, so the plan does NOT edit them: the /feat CAPITALIZE step
|
||||||
|
owns the superseding entry.
|
||||||
Reference in New Issue
Block a user