From 66bf73c9b1b2625a06d00e52147c25639adfe2a4 Mon Sep 17 00:00:00 2001 From: Sean Wright Date: Sun, 9 Aug 2026 13:46:41 -0400 Subject: [PATCH 1/5] docs(m17): 17.2a Linux VM spike -- measure linux-x86_64, label the boundary Split 17.2 into 17.2a (Linux VM, done) and 17.2b (real hardware, blocked on procurement). None of the hardware D-M17-3 names exists, and closing 17.2 with a VM run would have redefined "done" by omission in the one milestone whose stated binding constraint is verification. Measured on WSL2 Ubuntu 24.04.4, tier VM-verified -- NOT hardware-verified: - capture end-to-end on POSIX (3 raw / 16 events / 1 machine in an isolated archive) - 17.1's --home confinement: 0 declared paths outside the given home, 0 naming /mnt/c - the read-side check 16.0's write-only checks missed: 1120 /proc//fd samples - tsc -b --force 9.47 s; vitest 1045 | 680 (= 1034 + 17.1's net +11 tests, and 1045 + 680 = the real repo's 1725 | 0) - quickstart breaks in the SAME two places as Windows with byte-identical errors, so INC-2026-02/03 are onboarding defects, not Windows quirks - SIGINT graceful drain 503 ms, clean exit Six incidents, INC-2026-08..13 (0 high, 5 med, 1 low): - 09: build-sea.mjs exits 0 printing PASS while emitting a working 118 MB ELF named collector-x86_64-pc-windows-msvc.exe that Tauri's externalBin cannot find - 08: build-sea.mjs --check boots serve with no --home, creating the operator's real ~/.420ai -- a double-writer hazard, and the one isolation constraint this run breached. Repaired in place under D-16.0-2 and recorded, not hidden - 10: npm run setup aims DATABASE_URL_TEST at whatever cluster is already on the host, so a second checkout's vitest run would TRUNCATE the real test DB (demonstrated, not fired) - 11: INC-2026-05's recorded root cause contradicts file-watcher.ts:136, unchanged since 2026-06-13 -- a static session file IS captured, and 16.3 was designed against the wrong cause - 12: a connector source path that does not exist leaves no trace anywhere - 13: WSL interop fabricated a plausible wrong value in the transcript 17.3 re-scored 65% -> 80%: the whole SEA recipe runs unmodified on Linux, so the risk was the mechanism and the mechanism works. 17.4/17.5/17.6 deliberately NOT re-scored, with the reason stated per row -- systemd is off in default WSL2, webkit2gtk-4.1 is missing, and installers/Gatekeeper need hardware. Reading this run as "Linux OK" would be the emulated != hardware-verified failure the slice was shaped to prevent. Introduces the verification-tier vocabulary (hardware-verified / VM-verified / CI-verified / not measured) as a required field on findings, generalizing D-M17-4. No product code changed (D-M17-5). Docs-only diff. Co-Authored-By: Claude Opus 5 (1M context) --- .../plans/m17-cross-platform-collectors.md | 86 ++- .agents/plans/m17-slice2a-linux-vm-spike.md | 630 ++++++++++++++++++ .../cleanroom-linux-x86_64-2026-08-09.md | 513 ++++++++++++++ .agents/research/incidents.md | 276 +++++++- SUMMARY.md | 48 +- 5 files changed, 1532 insertions(+), 21 deletions(-) create mode 100644 .agents/plans/m17-slice2a-linux-vm-spike.md create mode 100644 .agents/research/cleanroom-linux-x86_64-2026-08-09.md diff --git a/.agents/plans/m17-cross-platform-collectors.md b/.agents/plans/m17-cross-platform-collectors.md index 96e1534..ffed909 100644 --- a/.agents/plans/m17-cross-platform-collectors.md +++ b/.agents/plans/m17-cross-platform-collectors.md @@ -78,18 +78,32 @@ Rows 1, 2 and the second-order fingerprint effect above all shipped fixed in **1 platforms, so `--home` confines Cursor capture and the §10.4 fingerprint is stable per-machine. A registry-wide **two-home** confinement guard (`connectors/connector.test.ts`) now fails if any built-in connector's `watchGlobs` or `poll.sources` escapes the given home, or declares no capture -paths at all. **17.2 still owns measuring the darwin/linux path constants on real hardware** — they +paths at all. **17.2b still owns measuring the darwin/linux path constants on real hardware** — they are convention-derived from the VS Code user-data layout, not yet observed (D-M17-5). A mismatch is a one-line table fix in `cursor-store.ts`. +**17.2a verified the CONFINEMENT half and explicitly did NOT discharge the CONVENTION half.** On +`linux-x86_64`, `cursorStorePathFor(home,'linux')` resolves to +`/.config/Cursor/User/globalStorage/state.vscdb` — a pure function of its argument, 0 paths +escaping either of two synthetic homes. Cursor is **not installed** on that target, so whether that +path is where a real Cursor install actually keeps its store is tier **`not measured`**. The closure +of INC-2026-01 must not be read as closure of this; half-discharging it silently is the failure mode +17.2a was shaped to avoid. + **Carried INTO 17.2 as a measurement item, and to 17.4 as a fix** (found by 17.1's PR review, not fixed there because it is engine behaviour rather than connector logic): `PollOutcome.unavailable` has **zero production readers** — `pollLoop` (`capture-engine.ts:329-340`) discards it and then calls `onSuccess` → `queue.clearConnectorError()`. So a store path that is simply WRONG reports the connector healthy forever and wipes any existing fault, with no log line. That is exactly how a bad -darwin/linux constant would present in the field, so **17.2 must verify the path by observation, +darwin/linux constant would present in the field, so **17.2b must verify the path by observation, never by "the collector didn't complain"**. +**17.2a MEASURED this from the outside and it looks like nothing** (now **INC-2026-12**): after ~8 +minutes of `watch` against a Cursor store path that does not exist, the clean-room queue held +`poll_state = 0`, `connector_errors = 0`, and the watch log carried **not one line** about it. The +prediction is confirmed observationally, which is what makes the "verify by observation" instruction +binding rather than cautionary. + ### Measured by 17.0 (run 31178775260, PR #82, 2026-08-07) — measurement, not inference Five standard GitHub-hosted lanes, every push/PR to `main`. All cells below are read from the run: @@ -118,6 +132,40 @@ Two findings from the first (all-red) run, both worth more than the green table: all five lanes. Row 3 above is therefore a **runtime** concern only (the off-Windows mock backend persists nothing); 17.5 still owns it, but no compile barrier exists. +### Measured by 17.2a (2026-08-09) — tier `VM-verified`, NOT hardware-verified + +WSL2 Ubuntu 24.04.4, real kernel + real `ext4` + real POSIX `homedir()`, node v24.18.0. Full report: +[`cleanroom-linux-x86_64-2026-08-09.md`](../research/cleanroom-linux-x86_64-2026-08-09.md). + +| Claim | Result | +| ---------------------------------------------------- | --------------------------------------------------------------------------- | +| Capture works end-to-end on POSIX | ✅ 3 raw / 16 events / 1 machine / 15 heartbeats in an isolated archive | +| `--home` confines on POSIX (17.1's fix) | ✅ 0 declared paths outside the given home; 0 naming `/mnt/c` or `AppData` | +| Read-side confinement (the check 16.0 owed) | ✅ 1120 `/proc//fd` samples — only clean-room files ever open | +| `tsc -b --force` / `npx vitest run` | ✅ 9.47 s / ✅ 1045 passed \| 680 skipped (= 1034 + 17.1's net +11 tests) | +| Documented path (`quickstart.md`, 13 steps) | ⚠️ breaks in the **same two places as Windows**, byte-identical errors | +| `npm install` + blocked `esbuild`/`sharp` postinstall | ✅ exits 0; esbuild works (the plan's named residual risk resolves to no break) | +| `build-sea.mjs` on a non-Windows triple | ❌ exits 0 and prints `PASS` while emitting an unbundlable ELF — INC-2026-09 | +| SIGINT graceful drain (POSIX signal path) | ✅ 503 ms, clean exit, queue drained | + +Three findings worth more than the green cells: + +1. **Row 4 (SEA) is a re-score UP, not down.** Everything portable in the recipe works; the defect is + one hardcoded constant. See the 17.3 row. +2. **`INC-2026-05`'s recorded root cause contradicts the code** (`file-watcher.ts:136` — + `fromOffset = cursor?.byteOffset ?? 0`, unchanged since 2026-06-13), so a static pre-existing + session file **is** captured. 16.3's scorecard was designed against that cause. Raised as + **INC-2026-11**. +3. **The spike breached its own isolation, once** — `build-sea.mjs --check` boots `serve` with no + `--home` and created the operator's real `~/.420ai`. Repaired in place under the D-16.0-2 + exception and recorded as **INC-2026-08**; it is also a double-writer hazard on `queue.sqlite` + on any machine where the collector is running. + +**Row 10 of the reconnaissance table (case-sensitive FS + un-normalized dedup keys) is still +_(impact inferred)_.** 17.2a confirmed the *substrate* is case-sensitive (ext4; `/mnt/c` is not) but +did not construct a colliding-case session path, so the dedup-key question is untested. It carries to +17.2b. + --- ## Slices (dependency order) @@ -126,16 +174,25 @@ Two findings from the first (all-red) run, both worth more than the green table: | -------- | ---------------------------------------- | ---- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **17.0** | Truth: CI matrix + spike protocol | M | 90% | Add a `cross-platform.yml` matrix over `{ubuntu-latest, ubuntu-24.04-arm, macos-15-intel, macos-latest (ARM), windows-latest}`; build the Rust/Tauri crate in CI for the first time. Write 17.2's measurement protocol so it runs the day hardware lands. Output is a **table of what is green where**, replacing every inference above with a measurement. **No product code change.** See `.agents/plans/m17-slice0-cross-platform-ci-matrix.md`. | | **17.1** | Connector portability | S–M | 90% | Fix #1 (`APPDATA`) and #2 (INC-2026-01) plus the approval-fingerprint knock-on. Audit every `watchGlobs(home)`/`sources(home)` implementation against the `connector.ts` contract. Windows-verifiable today; no hardware needed. | -| **17.2** | **SPIKE** — truth on real hardware | M | _n/a_ | Clean-room runs on Intel Mac, Linux x86_64 and Linux arm64, mirroring 16.0's timed deploy. **Measures; fixes nothing** (D-M17-5). Re-scores 17.3–17.6 before any of them is planned. | -| **17.3** | Multi-target SEA build | M | 65% → | Un-pin `build-sea.mjs`'s triple. SEA copies `process.execPath`, so it is **build-on-target, not cross-compilable** — this slice depends structurally on 17.0's matrix existing. | -| **17.4** | Service install: launchd + systemd | M | 75% → | Port the WinSW definition. The non-zero-exit contract (INC-2026-07) carries over unchanged. Two real gaps, not renames: roll-by-size logging has no launchd equivalent, and launchd cannot express the 5/10/20s backoff ladder. | -| **17.5** | Desktop shell portability | M | 60% → | Per-target `keyring` feature gating; Tauri Linux `webkit2gtk` toolchain; autostart docs corrected to match the already-portable implementation. | -| **17.6** | Installers + updater feed + release lane | M–L | 50% → | Bundle targets beyond `["nsis"]`; updater feed keys beyond `windows-x86_64`; un-park the CI release workflow (`tauri-action`). Lowest confidence and most spike-dependent. | +| **17.2a** | **SPIKE** — truth on a Linux VM | M | _n/a_ | ✅ **DONE 2026-08-09.** The subset of the 17.2 protocol a WSL2 substrate can measure truthfully: `linux-x86_64`, tier **`VM-verified`**. Capture end-to-end, `--home` confinement on POSIX, filesystem semantics, the documented path timed verbatim, and `build-sea.mjs` end-to-end. Raised **INC-2026-08…13**. See [`cleanroom-linux-x86_64-2026-08-09.md`](../research/cleanroom-linux-x86_64-2026-08-09.md). | +| **17.2b** | **SPIKE** — truth on real hardware | M | _n/a_ | **BLOCKED ON PROCUREMENT** — no hardware exists (confirmed with the maintainer, 2026-08-09). Carries everything 17.2a could not see: **Intel Mac** (all of it) · **Gatekeeper / notarization** (D-M17-2 — needs a real browser download to set the quarantine xattr; `curl`/`scp` do not) · **`linux-arm64` on real silicon** (`emulated ≠ hardware-verified`) · **systemd** service semantics · **`webkit2gtk` + a real desktop session** · **installers + updater feed** · the **Cursor `darwin`/`linux` path CONVENTION** (`cursor-store.ts:11-13`, still undischarged) · a **human-run** §5.2 onboarding repeat. Re-scores **17.4–17.6**. | +| **17.3** | Multi-target SEA build | M | 65% → **80%** | Un-pin `build-sea.mjs`'s triple. SEA copies `process.execPath`, so it is **build-on-target, not cross-compilable** — this slice depends structurally on 17.0's matrix existing. **Re-scored UP by 17.2a, on measurement:** the whole recipe runs unmodified on Linux — esbuild → `--experimental-sea-config` → `copyFileSync(process.execPath)` → `postject` into an ELF, exit 0, and the produced artifact **runs**. Only the pinned constant and the `.exe` suffix are wrong (**INC-2026-09**), plus a `--home` leak in the same file (**INC-2026-08**). The risk was the mechanism; the mechanism works. | +| **17.4** | Service install: launchd + systemd | M | 75% (**not re-scored**) | Port the WinSW definition. The non-zero-exit contract (INC-2026-07) carries over unchanged. Two real gaps, not renames: roll-by-size logging has no launchd equivalent, and launchd cannot express the 5/10/20s backoff ladder. **17.2a measured nothing here** — systemd is off in default WSL2 (`ps -p 1` → `init`, `systemctl is-system-running` → `offline`) and was deliberately left off (D-M17-5). Blocked on **17.2b**. | +| **17.5** | Desktop shell portability | M | 60% (**not re-scored**) | Per-target `keyring` feature gating; Tauri Linux `webkit2gtk` toolchain; autostart docs corrected to match the already-portable implementation. **17.2a measured nothing here** — `pkg-config --exists webkit2gtk-4.1` → MISSING and there is no desktop session on this substrate. Blocked on **17.2b**. The `keyring` mock-fallback question is a *runtime* one and survives a restart-based test only, which no VM lane provides. | +| **17.6** | Installers + updater feed + release lane | M–L | 50% (**not re-scored**) | Bundle targets beyond `["nsis"]`; updater feed keys beyond `windows-x86_64`; un-park the CI release workflow (`tauri-action`). Lowest confidence and most spike-dependent. **17.2a measured nothing here** — installers are downstream of the desktop shell and of signing, and Gatekeeper needs macOS. Blocked on **17.2b**. | | **17.7** | Cross-platform UAT + sign-off | M | 85% | Extend `UAT.md` with per-platform lanes. Sign-off requires each target exercised on the hardware class it claims. | -`→` marks a confidence score the 17.2 spike is expected to move. Those four slices are -**deliberately not planned in detail yet** — planning them now would be estimating from inference, -which is exactly what 16.0's clean-room deploy proved unreliable. +`→` marks a confidence score the spike was expected to move. **Only 17.3 has actually moved.** +17.4/17.5/17.6 are explicitly **not re-scored**, and the reason is stated per row above rather than +left as an omission: 17.2a's substrate cannot observe systemd, `webkit2gtk`, a desktop session, +installers or Gatekeeper, so any number attached to them now would be the same inference the spike +exists to replace. They stay **deliberately not planned in detail** and are blocked on **17.2b**. + +**The honest consequence, stated where it cannot be missed:** the three lowest-confidence slices are +precisely the three the spike did not reach. 17.2a re-scores the collector-layer portability claim +that 17.3 rests on, and confirms the milestone's foundational assumption that the TS core is already +portable. Reading it as "Linux ✅" would be exactly the `emulated ≠ hardware-verified` failure the +slice was shaped to prevent. **Ordering rationale.** 17.0 first because it is the only slice needing no hardware that also produces real macOS and Windows signal — it converts the reconnaissance table above from inference @@ -265,5 +322,10 @@ standard too, so D-M17-4's "free ARM Mac runners" premise holds for the Intel ha documented as update-unsupported with the reason. - [ ] `darwin-aarch64` labelled "CI-verified, not hardware-verified" everywhere it is offered. - [ ] `npm run repo-health -- --require-db` passes. -- [ ] Every 17.2 finding is either fixed in a later slice or carried as an open - `.agents/research/incidents.md` entry with a stated reason. +- [ ] Every 17.2a **and** 17.2b finding is either fixed in a later slice or carried as an open + `.agents/research/incidents.md` entry with a stated reason. (17.2a raised **INC-2026-08…13**; + all six are open, five routed to 17.3/17.4 and one to a re-derivation of INC-2026-05.) +- [ ] **No claim in the release notes, docs or UAT carries a verification tier it did not earn.** + Every `linux-x86_64` claim traceable only to 17.2a is `VM-verified`, not `hardware-verified` + (`emulated ≠ hardware-verified`) — the same discipline D-M17-4 already applies to + `darwin-aarch64`. diff --git a/.agents/plans/m17-slice2a-linux-vm-spike.md b/.agents/plans/m17-slice2a-linux-vm-spike.md new file mode 100644 index 0000000..8ab7f88 --- /dev/null +++ b/.agents/plans/m17-slice2a-linux-vm-spike.md @@ -0,0 +1,630 @@ +# Feature: M17 slice 17.2a — Linux VM spike (the measurable half of 17.2) + +The following plan should be complete, but its important that you validate documentation and codebase +patterns and task sanity before you start implementing. + +Pay special attention to naming of existing utils types and models. Import from the right files etc. + +> **This is a MEASUREMENT slice. It measures; it does not fix** (D-M17-5). Every finding lands as an +> entry in [`.agents/research/incidents.md`](../research/incidents.md) and is left standing. The one +> standing exception, inherited from D-16.0-2: **a defect in the isolation the measurement itself +> depends on may be repaired in place**, and is still recorded as an incident. + +--- + +## Feature Description + +Slice **17.2** as written in [`m17-cross-platform-collectors.md`](./m17-cross-platform-collectors.md) +is a clean-room spike on three targets — **Intel Mac**, **Linux x86_64**, **Linux arm64** — all +hardware-verified (D-M17-3). **None of that hardware exists** (confirmed with the maintainer, +2026-08-09). Procurement has not happened and has no date. + +This slice therefore executes the **subset the available substrate can measure truthfully**, and is +explicit — in the plan, in every artifact, and in every incident — about the far larger subset it +cannot. It is named **17.2a** rather than 17.2 because shipping it under the original number would +quietly redefine what "17.2 is done" means, and the milestone's own framing ("**verification, not +implementation**, is the binding constraint") makes that the one substitution M17 must never make. + +The substrate is **WSL2 Ubuntu 24.04.4** on the maintainer's Windows machine: a real Linux kernel +with a real `ext4` filesystem and a POSIX `homedir()`, running Node v24.18.0. It can honestly measure +the **collector layer** on `linux-x86_64`. It cannot measure macOS, real ARM silicon, systemd, +`webkit2gtk`, installers, or Gatekeeper. + +## User Story + +As **the maintainer preparing 420AI for people running it on hardware I do not own** +I want to **measure what actually happens on a POSIX target, and to know precisely which of my +cross-platform claims are still unmeasured** +So that **slices 17.3–17.6 are planned from evidence where evidence exists, and are visibly blocked +on hardware where it does not — instead of being estimated from inference a second time.** + +## Problem Statement + +Two problems, and the second is the one that makes this slice hard to do honestly. + +1. **The collector has never been run on a POSIX machine.** 17.0's CI matrix proves the repo + typechecks, unit-tests and `cargo check`s on five lanes, but CI structurally cannot see capture, + filesystem semantics, `homedir()` shape, the documented onboarding path, or OS trust machinery. +2. **A partial spike is the ideal shape for a false green.** WSL2 is a real kernel, so most of the + protocol will genuinely pass — and a report that says "Linux ✅" without qualification would be + indistinguishable from one produced on real hardware, while having measured none of the three + things M17's lowest-confidence slices actually depend on. This repo has rediscovered that failure + four times under four names (`skipped ≠ passed`, `bypassed ≠ enforced`, `passes on fixtures ≠ +runs in production`, `derivable ≠ detected`). This slice is where the fifth would be minted: + **`emulated ≠ hardware-verified`**. + +## Solution Statement + +Run the [17.2 spike protocol](../research/m17-slice2-spike-protocol.md) against **one** target +(`linux-x86_64`, VM-verified), under an isolation setup hardened against the two breaches 16.0 +measured, and introduce a **verification-tier label** that every finding, table row and incident in +this slice must carry: + +| Tier | Meaning | Who produces it | +| --------------------- | ------------------------------------------------------------ | --------------- | +| `hardware-verified` | Ran on the metal it claims | 17.2b | +| `VM-verified` | Real kernel, virtualized; no GUI, no init system, shared host | **this slice** | +| `CI-verified` | GitHub-hosted runner, no capture, no DB | 17.0 | +| `not measured` | Explicitly absent, with the reason stated | **this slice** | + +The tier vocabulary is not new policy — it generalizes **D-M17-4**, which already ships Apple Silicon +as "CI-verified, not hardware-verified, labelled everywhere it is offered". This slice makes that +label a required field rather than a one-off courtesy. + +The deliverable is therefore **evidence plus a precisely-drawn boundary**, and a **17.2b** entry in +the milestone plan carrying everything the boundary excludes. + +## Feature Metadata + +**Feature Type**: Research spike (measurement) — no product code change +**Estimated Complexity**: Medium (low technical risk; high discipline requirement) +**Primary Systems Affected**: none modified. Exercised: `apps/collector`, `apps/ingest`, +`packages/db`, `docs/guide/quickstart.md` +**Dependencies**: WSL2 Ubuntu 24.04 (present), Docker (present), Node ≥ 24 (present, v24.18.0). +**No Rust toolchain required** — see Task 0's rationale. + +--- + +## CONTEXT REFERENCES + +### Relevant Codebase Files — IMPORTANT: YOU MUST READ THESE BEFORE IMPLEMENTING + +- [`.agents/research/m17-slice2-spike-protocol.md`](../research/m17-slice2-spike-protocol.md) (all + 153 lines) — Why: **this is the slice's specification**, written by 17.0. This plan narrows and + corrects it; where they disagree, **this plan wins** and the disagreement is called out inline. +- [`.agents/research/cleanroom-2026-08-02.md`](../research/cleanroom-2026-08-02.md) (all 193 lines) — + Why: the exact report shape to mirror — headline result, honesty caveat, isolation table with + ✅/❌ per constraint, timed step table, verification-that-isolation-held table. +- [`.agents/research/incidents.md`](../research/incidents.md) (lines 24–61 for the format; 195–285 + for INC-2026-02…05) — Why: the entry format is fixed, and **INC-2026-02/03/04/05 are still open** + and will be re-encountered on Linux. See the "Known-defect rule" below. +- `docs/guide/quickstart.md` (13 steps; headings at lines 13, 33, 43, 54, 59, 72, 81, 92, 101) — + Why: the documented path, followed **verbatim**. Deviations are findings, not improvements. +- `apps/collector/src/cli.ts` (lines 625–631 `resolveHome`; 682–692 the usage block) — Why: + `--home` is on `pair`/`watch`/`sync`/`queue`/`discover`/`git`; `resolveHome` is + `getFlag(args, "--home") ?? homedir()`. This is the isolation primitive the whole run rests on. +- `apps/collector/src/connectors/cursor-store.ts` (lines 1–35 jsdoc; **11–13** the debt; 66–77 + `cursorStorePathFor`) — Why: it carries an **explicit measurement debt addressed to this slice**: + "darwin/linux rows are convention-derived from the VS Code user-data layout; **to be + hardware-verified in 17.2 — NOT yet measured**." Read it so you know what you are (and are not) + able to discharge. +- `apps/collector/scripts/build-sea.mjs` (lines 31–39 the pinned triple; 121–124 step [3]) — Why: a + planning spike already measured its Linux behaviour and **found the protocol's prediction wrong**. + See "Corrections to the protocol" below. +- `docker-compose.yml` (line **8** `container_name: 420ai-archive`; line **16** `"5433:5432"`) — + Why: the clean room must not touch it; the fixed `container_name` also means a bare + `docker compose -p` will **collide**, which is why Task 2 uses `docker run` instead. +- `packages/db/src/provision-app-role.ts` (lines 43, 67) — Why: it issues + `CREATE ROLE` / `ALTER ROLE … LOGIN PASSWORD` — **cluster-scoped**. This is the exact mechanism of + INC-2026-06 and the reason Task 2 provisions a separate *container*, not a separate *database*. + +### New Files to Create + +- `.agents/research/cleanroom-linux-x86_64-2026-08-09.md` — the per-target clean-room report + (mirrors `cleanroom-2026-08-02.md`; note the `VM-verified` tier in its title block). +- `.agents/execution-reports/m17-slice2a-linux-vm-spike.md` — written by `/lril:execution-report`. + +### Files to Update + +- `.agents/research/incidents.md` — new entries, **starting at `INC-2026-08`** (highest existing is + `INC-2026-07`; verify before writing). +- `.agents/plans/m17-cross-platform-collectors.md` — the 17.2 row, the re-score table, and a new + **17.2b** row. +- `SUMMARY.md` — §0/§3/§6 M17 status (per CLAUDE.md, in the **same commit** as the execution report). + +### Relevant Documentation + +- [WSL2 filesystem & interop](https://learn.microsoft.com/en-us/windows/wsl/filesystems#file-storage-and-performance-across-file-systems) + - Specific section: file storage and performance across file systems + - Why: `/mnt/c` (DrvFs) is **case-insensitive and slow**; the Linux root is `ext4`. Task 1's + checkout location depends on this, and a spike run from `/mnt/c` measures Windows semantics. +- [WSL systemd support](https://learn.microsoft.com/en-us/windows/wsl/systemd) + - Specific section: enabling systemd via `/etc/wsl.conf` + - Why: background for the "not measured" claim about 17.4. **Do not enable it** — see Task 0. +- [Node.js SEA — `--experimental-sea-config`](https://nodejs.org/api/single-executable-applications.html) + - Specific section: generating the blob and injecting it + - Why: context for the `build-sea.mjs` measurement in Task 7. + +--- + +## CORRECTIONS TO THE 17.0 PROTOCOL (measured during planning) + +The protocol was written by 17.0 from inference. Three of its statements were tested during planning +and two are wrong. **Where this plan and the protocol disagree, this plan wins.** + +### C1 — `build-sea.mjs` does NOT fail on a non-Windows triple. It silently mislabels. + +Protocol line 86 says: _"if not [un-pinned by 17.3], THIS FAILING ON A NON-WINDOWS TRIPLE IS THE +EXPECTED FINDING, record and move on."_ + +**Measured on WSL2 Linux x86_64 during planning** (throwaway script, since deleted), reproducing +`build-sea.mjs:32,39,123` exactly: + +``` +platform = linux x64 +process.execPath = /usr/bin/node +RESULT: copy SUCCEEDED -> /tmp/seaspike-4cTog9/collector-x86_64-pc-windows-msvc.exe (118 MB) +artifact type: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), dynamically linked, … +VERDICT: SILENT MISLABEL — a Linux ELF named '...windows-msvc.exe' +``` + +`copyFileSync(process.execPath, outExe)` has no reason to fail on Linux — it copies a 118 MB ELF to a +path whose name asserts it is a Windows PE. The finding is **worse** than the predicted failure and +belongs to the same family as everything else in this repo's incident log: a build step that reports +success while producing a wrong artifact. + +It also has a downstream consequence worth recording: Tauri resolves `externalBin` by +`-$TARGET_TRIPLE` suffix, so on Linux the bundler would look for +`collector-x86_64-unknown-linux-gnu` and **not find this file at all** — the artifact is +simultaneously mislabelled and invisible to the thing that consumes it. + +**Scope note (be precise in the report):** the planning spike exercised **step [3] in isolation**. +Whether the full script *reaches* step [3] on Linux — steps [1] esbuild bundle and [2] +`--experimental-sea-config` run first — is what Task 7 measures. Do not report the isolated result as +if the whole script had been run. + +### C2 — The Cursor caveat is discharged, but its replacement debt is NOT dischargeable here. + +Protocol lines 30–42 treat INC-2026-01 as possibly-open. **It is resolved** (17.1, PR #83), so run +the prescribed verification (Task 5). But `cursor-store.ts:8-13` records a *new* debt aimed squarely +at this slice (`:11-13`) — the `darwin`/`linux` path rows are convention-derived and unverified. Cursor is not +installed on this target and installing it is out of scope, so this debt is **`not measured`** and +carries to 17.2b. Say so explicitly; do not let the closure of INC-2026-01 read as closure of this. + +### C3 — "A separate database" is still not isolation here, and the reason is sharper than 16.0's. + +Protocol line 64 correctly carries INC-2026-06 forward. Planning confirmed the trap is **live on this +substrate**: WSL's Docker is the *same engine* as Windows' — + +``` +$ wsl -d Ubuntu-24.04 -- docker ps --filter expose=5432 +420ai-archive 0.0.0.0:5433->5432/tcp <-- the REAL repo's archive +qms_postgres 0.0.0.0:5432->5432/tcp +kanban-postgres 0.0.0.0:5434->5432/tcp +kinkline-db 0.0.0.0:5435->5432/tcp +``` + +So a clean room that reuses `420ai-archive` and runs `db:provision-app-role` reproduces INC-2026-06 +**exactly**: a cluster-wide `ALTER ROLE 420ai_app …` (`provision-app-role.ts:67`) invalidating the +real repo's `DATABASE_URL_APP`. The durable fix is a **separate container**, because a separate +container is a separate Postgres **cluster**, and the cluster is the scope roles actually live in. +A separate database never was. + +Ports 5432–5435 are all occupied on this engine. **Use 5533.** + +--- + +## SCOPE — what this slice measures, and what it must refuse to claim + +### IN SCOPE — `linux-x86_64`, tier `VM-verified` + +| # | Question | Why WSL2 answers it truthfully | +| - | ------------------------------------------------------------------------------- | ------------------------------ | +| 1 | Does the **documented path** (`quickstart.md`, 13 steps) work on a POSIX box? | The guide is OS-agnostic prose; its breaks are code/doc facts, not kernel facts | +| 2 | Does `npm ci` + root `tsc -b` + `npx vitest run` reproduce **1034 \| 680**? | Same Node, same npm, real Linux userland | +| 3 | Does **capture** work — discover → watch → parse → sync → archive? | Real ext4, real inotify-free poll watcher, real SQLite | +| 4 | Does **`--home` confine** on POSIX (the 17.1 verification)? | `resolveHome` + `homedir()` = `/home/seanr`, genuinely POSIX | +| 5 | **Filesystem semantics** — case-sensitivity, separators, `homedir()` shape | ext4 is case-sensitive (measured); identical to real Linux | +| 6 | `build-sea.mjs` end-to-end behaviour on a non-Windows triple | Pure Node/esbuild logic, substrate-independent | + +### NOT MEASURED — record each with its reason; never substitute + +| Area | Why not, on this substrate | Carries to | +| -------------------------------------- | ------------------------------------------------------------- | ---------- | +| **macOS: everything** | No Mac exists | 17.2b | +| **Gatekeeper / notarization** (D-M17-2) | Requires macOS + a real browser download to set the quarantine xattr | 17.2b | +| **`linux-arm64` on real silicon** | Only QEMU emulation available; `emulated ≠ hardware-verified` | 17.2b | +| **systemd service semantics** | systemd is **off** in default WSL2 (`ps -p 1` → `init`, `systemctl is-system-running` → `offline`). Measured as-found — see Task 0 | 17.2b → 17.4 | +| **Tauri desktop shell / `webkit2gtk`** | `pkg-config --exists webkit2gtk-4.1` → **MISSING**; no real desktop session | 17.2b → 17.5 | +| **Installers / updater feed** | Downstream of the shell and of signing | 17.2b → 17.6 | +| **Human-scale onboarding time (§5.2)** | Dev workstation, warm caches, agent-executed — same three caveats as 16.0 | a human-run repeat | +| **Cursor `linux` path convention** | Cursor not installed on this target (C2) | 17.2b | + +> **The honest consequence, and it must appear in the report's headline:** slices **17.4, 17.5 and +> 17.6** — the three lowest-confidence slices (75%, 60%, 50%), i.e. precisely the ones 17.2 exists to +> re-score — are **not re-scored by this slice**. What 17.2a re-scores is the collector-layer +> portability claim that **17.3** rests on, plus the milestone's foundational assumption that the TS +> core is already portable. Anything else would be a claim without a measurement behind it. + +### Known-defect rule (prevents incident-log inflation) + +**INC-2026-02** (quickstart never says `npm run build`) and **INC-2026-03** (quickstart never says +`db:provision-app-role`) are **open and platform-independent**. You *will* hit both. Likewise +**INC-2026-04** (`setup-env.mjs` hardcodes `INGEST_URL=…:8420`) and **INC-2026-05** (a fresh install +captures nothing until new activity). + +- **Reproducing a known incident is a result, not a new incident.** Record it in the timed table as + `reproduced — INC-2026-0N`, and add **one line** to that entry's Evidence noting it reproduces on + `linux-x86_64`. +- **Raise a new ID only for something genuinely new**, or when a known defect behaves *differently* + on Linux (that difference is the finding — write it as a new entry that cites the old one). +- Without this rule, three targets × four known defects fabricates twelve incidents and inflates the + "incidents raised" headline that 17.3–17.6's re-score reads. + +--- + +## IMPLEMENTATION PLAN + +### Phase 1: Isolation (the part that must be right before anything downstream is trustworthy) + +Establish a clean room whose isolation is verified **before** it is relied on, hardened against both +breaches 16.0 measured (cluster-wide roles; env-derived connector paths) and against the +substrate-specific trap this plan found (DrvFs). + +### Phase 2: The measured sequence + +Follow `quickstart.md` verbatim with a stopwatch. Where it breaks, record the break and the +workaround as a deviation — **do not silently improve the path**. + +### Phase 3: Targeted measurements CI cannot make + +The `--home` confinement check, filesystem semantics, and the `build-sea.mjs` triple. + +### Phase 4: Teardown, verification, and artifacts + +Prove the clean room touched nothing real — **including on the read side**, which 16.0's write-only +checks missed — then write the report, the incidents, and the re-score. + +--- + +## STEP-BY-STEP TASKS + +Execute in order. **Capture a full terminal transcript of every step** (`script -q` or `tee`) — +summaries are not evidence (protocol §4). + +### Task 0: RECORD the substrate as-found — change nothing + +- **IMPLEMENT**: Record OS/kernel/arch, Node/npm versions, init system, toolchain presence, disk + free, and that this is a **developer workstation, not a clean machine** (protocol §0 requires + saying which). +- **GOTCHA — do NOT install or enable anything to make a measurement pass.** Specifically: do **not** + enable systemd via `/etc/wsl.conf`, and do **not** install Rust or `webkit2gtk`. Two reasons: + (a) D-M17-5 — altering the substrate to produce a green converts evidence into a guess; + (b) `cargo check` is **already answered** by 17.0 on `ubuntu-latest` (✅ 81 s), and the protocol's + §"do not re-measure" rule applies. `systemd: offline` is itself a finding for 17.4. +- **BASELINE (measured during planning — confirm, don't assume):** Ubuntu 24.04.4 LTS · Linux x86_64 · + node v24.18.0 · npm 11.16.0 · git ✅ · psql ✅ · docker ✅ · pkg-config 1.8.1 · **cargo MISSING** · + **webkit2gtk-4.1 MISSING** · `ps -p 1` → `init(Ubuntu-24.04)` · `systemctl is-system-running` → + `offline` · 833 G free on `/`. +- **VALIDATE**: `wsl -d Ubuntu-24.04 -- bash -lc 'uname -srm; node -v; npm -v; ps -p 1 -o comm=; systemctl is-system-running; df -h ~ | tail -1'` + +### Task 1: CREATE the clean-room checkout on **ext4** — never `/mnt/c` + +- **IMPLEMENT**: `git clone` the repo into `~/cleanroom/420ai` inside WSL, and create the clean-room + collector home at `~/cleanroom-home`. +- **GOTCHA — this is the substrate-specific isolation trap, and it is silent.** `/mnt/c` (DrvFs) is + **case-insensitive**; `~` (ext4) is **case-sensitive**. Measured during planning: + + ``` + /tmp (ext4): CASE-SENSITIVE <-- real Linux semantics + /mnt/c (DrvFs): CASE-INSENSITIVE <-- Windows semantics wearing a Linux path + ``` + + A clean room under `/mnt/c` would measure **Windows** filesystem semantics while every path in the + transcript looks like Linux, voiding finding #5 and quietly weakening #1 and #3. It is also + markedly slower (9p). This is the same class as 16.0's OneDrive-substring gotcha: the *name* looks + right while the *location* is wrong. +- **GOTCHA**: clone from the **GitHub remote**, not a local path, and record cold-vs-warm cache + separately — 16.0's honesty caveat #3 exists because a local-path clone (1 s) is not representative. +- **VALIDATE**: `wsl -d Ubuntu-24.04 -- bash -lc 'df -T ~/cleanroom | tail -1'` → filesystem type + must read `ext4` (not `9p`/`drvfs`), and + `cd ~/cleanroom && touch A.txt && [ -e a.txt ] && echo INSENSITIVE-ABORT || echo OK-case-sensitive` + +### Task 2: CREATE a dedicated Postgres **container** on port 5533 + +- **IMPLEMENT**: start a throwaway `postgres:17` container — **its own container, its own cluster**: + + ```sh + docker run -d --name 420ai-cleanroom-db -p 5533:5432 \ + -e POSTGRES_USER=420ai -e POSTGRES_PASSWORD=420ai -e POSTGRES_DB=420ai postgres:17 + ``` + +- **PATTERN**: mirrors the `archive` service (`docker-compose.yml:6-25`) minus the shared volume and + the init script. +- **GOTCHA — do NOT `docker compose up` and do NOT reuse `420ai-archive`.** Three distinct reasons: + (a) INC-2026-06 — `provision-app-role.ts:67` issues a **cluster-scoped** `ALTER ROLE`, so a shared + cluster leaks straight out of the clean room and broke 223 tests last time; (b) the compose file + pins `container_name: 420ai-archive`, so even `docker compose -p 420ai-cleanroom up` **collides** + on the name rather than isolating; (c) ports 5432/5433/5434/5435 are all occupied on this engine + (measured — see C3). +- **GOTCHA**: do **not** set `DATABASE_URL_TEST`. The comparison baseline is **1034 passed | 680 + skipped**, i.e. the integration layer *skipping*. Setting it changes the number you are comparing + against and invalidates Task 4. +- **VALIDATE**: `docker exec 420ai-cleanroom-db pg_isready -U 420ai` → `accepting connections`; + and `docker ps --format '{{.Names}} {{.Ports}}' | grep 5533` + +### Task 3: SNAPSHOT real state — both write-side and **read-side** + +- **IMPLEMENT**: before anything runs, capture the baseline used to prove non-contamination at + teardown: + - mtimes of the Windows-side real collector home (`/mnt/c/Users/seanr/.420ai/credentials.json`, + `queue.sqlite`) **and** of a WSL-side `~/.420ai` if one exists; + - `machines` row count in the **real** archive (`420ai-archive`, port 5433). +- **PATTERN**: `cleanroom-2026-08-02.md:92-97` (baseline block) and `:163-169` (verdict table). +- **GOTCHA — 16.0's own closing lesson**: _"They prove the clean room never **wrote** to real state. + They say nothing about **reads** — and INC-2026-01 is a read breach."_ So this run must also carry + the **read-side** check in Task 5. Record both, and state in the report which kind each check is. +- **VALIDATE**: baseline values written into the transcript before Task 4 begins. + +### Task 4: RUN the documented path (`quickstart.md`, steps 1–13) — timed, verbatim + +- **IMPLEMENT**: work through all 13 steps with a stopwatch, recording wall-clock and an + `intervention?` column per step. +- **PATTERN**: the timing table at `cleanroom-2026-08-02.md:106-128` — reproduce its columns exactly + (step · wall-clock · intervention? · notes). +- **EXPECT (do not pre-empt)**: step 3 will fail twice — `ERR_MODULE_NOT_FOUND` (**INC-2026-02**, + workaround `npm run build`) then Postgres `28P01` (**INC-2026-03**, workaround + `npm run db:provision-app-role`). Record as `reproduced — INC-2026-0N` per the known-defect rule. + Also expect **INC-2026-04** to write `INGEST_URL=…:8420` into `apps/dashboard/.env.local`; correct + it by hand to the clean-room port and record the correction as an isolation-only step. +- **GOTCHA**: use ingest **8998** and dashboard **3998** (8999/3999 were 16.0's; verify free first). +- **GOTCHA**: the UI-only steps (4, 9–11) will again be performed as API calls. That is a + **deviation**, and 16.0's honesty caveat #1 must be restated in this report — it removes exactly + the part of the budget a human spends. +- **GOTCHA (`npm ci`, genuinely unknown)**: 16.0 noted npm blocking `esbuild`/`sharp` postinstall + scripts with an `approve-scripts` warning on Windows. Those packages ship **platform-specific + binaries**, so a block that "did not bite" on Windows may bite on Linux. **If it breaks, that is a + finding (new incident), not a blocked executor** — record it and work around it. +- **VALIDATE**: `curl -fsS localhost:8998/v1/health` → 200 (note: `/v1/health`, **not** `/health` — + the guide names no path, per `cleanroom-2026-08-02.md:119`) + +### Task 5: VERIFY `--home` confinement on POSIX (the 17.1 check + the read-side check) + +- **IMPLEMENT**: run `collector discover --home ~/cleanroom-home` and assert **every printed source + path is under the clean-room home**. This is protocol lines 37–38, and it is the read-side check + Task 3 owes. +- **PATTERN**: `cursorStorePathFor(home, platform)` at `cursor-store.ts:66-77` is now a pure function + of its arguments — on this target it must resolve under + `~/cleanroom-home/.config/Cursor/User/globalStorage/state.vscdb`, and must **never** reach + `/mnt/c/Users/seanr/AppData/Roaming/Cursor/…`. +- **GOTCHA (C2)**: Cursor is not installed here, so the connector will report its source unavailable. + That confirms **confinement**; it does **not** verify the linux path **convention**. Record the + convention as `not measured` and carry it to 17.2b — `cursor-store.ts:8-13` addresses that debt to + this slice by name, and half-discharging it silently is the exact failure mode this plan exists to + avoid. +- **VALIDATE**: the `discover` output verbatim in the transcript; assert 0 paths outside the + clean-room home, and specifically 0 occurrences of `/mnt/c`. + +### Task 6: MEASURE capture end-to-end, and the test-suite baseline + +- **IMPLEMENT**: seed a real session file into the clean-room home, run `collector watch --home …`, + and confirm rows land in the clean-room archive. Separately run `npx vitest run` and the root + typecheck in the clean-room checkout. +- **BASELINE to compare against** (17.0, PR #82, run 31178775260 — `ubuntu-latest` lane): + **typecheck ✅ ~10 s** · **`npx vitest run` ✅ 1034 passed | 680 skipped**. Any deviation is a + finding **about this machine**, since CI already proves the lane green. +- **GOTCHA (INC-2026-05)**: the watcher cursors pre-existing files at **EOF** by design, so a static + seeded file produces **zero** events. Capture begins only when the file *grows*. Append to it + mid-run. If you skip this you will record a false "capture is broken on Linux". +- **VALIDATE**: `psql postgresql://420ai:420ai@localhost:5533/420ai -c 'select count(*) from raw_source_records; select count(*) from events;'` + → both **> 0**; and `npx vitest run` reports **1034 passed | 680 skipped**. + +### Task 7: MEASURE `build-sea.mjs` on a non-Windows triple + +- **IMPLEMENT**: run `node apps/collector/scripts/build-sea.mjs --check` first (cheap: bundle + run + a `status` line under node), then the full build. Record where each stops and what artifact — if + any — is produced, including `file(1)` output on it. +- **PATTERN / PRIOR RESULT**: step [3]'s mislabel mechanism is **already confirmed** (see C1). What + is unmeasured is whether steps [1] (esbuild) and [2] (`--experimental-sea-config`) succeed on + Linux, i.e. whether the full script *reaches* the mislabel. +- **GOTCHA**: report the full-script result as the finding. Cite the planning spike as prior evidence + for the mechanism, and **do not present the isolated step-[3] result as a whole-script result.** +- **GOTCHA**: this is 17.3's input. Measure it; **do not un-pin `TARGET_TRIPLE`** — that is 17.3's + work and fixing it here violates D-M17-5. +- **VALIDATE**: `file apps/desktop/src-tauri/binaries/collector-*` (if produced) — record the exact + type string. + +### Task 8: TEARDOWN and prove non-contamination + +- **IMPLEMENT**: stop and `docker rm -f 420ai-cleanroom-db`; delete `~/cleanroom` and + `~/cleanroom-home`; re-check every Task 3 baseline. +- **PATTERN**: the verdict table at `cleanroom-2026-08-02.md:163-169`. +- **VALIDATE (this is the slice's most important gate — Level 1 below)**: in the **real** repo on + Windows, `npm run repo-health` still passes. That is the direct regression guard for INC-2026-06. + +### Task 9: WRITE the artifacts + +- **IMPLEMENT**: + 1. `.agents/research/cleanroom-linux-x86_64-2026-08-09.md` — mirroring + `cleanroom-2026-08-02.md`'s structure, with the **tier stated in the title block** + (`VM-verified (WSL2) — NOT hardware-verified`) and a headline that names what was **not** + measured before it reports what was. + 2. New incidents in `incidents.md` from `INC-2026-08` up, each carrying severity + §4.4 category + + the **verification tier**. Apply the known-defect rule. + 3. Re-score in `m17-cross-platform-collectors.md`: update **17.3** from evidence; state explicitly + that **17.4/17.5/17.6 are NOT re-scored and why**; add a **17.2b** row (hardware spike: + Intel Mac + Gatekeeper + real arm64 + systemd + `webkit2gtk`, blocked on procurement). + 4. `SUMMARY.md` §0/§3/§6 — 17.2a flipped to ✅ with `DONE (PR #NN)`, M17 still IN PROGRESS, + 17.2b listed as blocked. **Same commit as the execution report** (CLAUDE.md). +- **GOTCHA**: `scripts/check-summary.mjs` derives "what shipped" from execution-report **filenames**, + so the report must be `m17-slice2a-linux-vm-spike.md` and SUMMARY must carry a ✅ next to a + matching `**17.2a**` token, or the gate fails. +- **VALIDATE**: `node scripts/check-summary.mjs` → exit 0 + +--- + +## TESTING STRATEGY + +A spike's "tests" are **evidence discipline**. There is no product code to unit-test; the failure +mode is a report that claims more than it measured. + +### Evidence requirements (protocol §4) + +- Full terminal transcripts per step — not summaries. +- `discover` output **verbatim** (the isolation evidence). +- Clean-room `queue.sqlite` row counts and archive raw/event counts before teardown. +- The Task 3 → Task 8 baseline comparison, stated explicitly in the report. + +### The one assertion that can actually fail + +**The real repo must be unharmed.** `npm run repo-health` on Windows after teardown is a genuine +pass/fail gate, and it is the regression test for INC-2026-06 — the failure that a spike designed and +reviewed as isolated produced last time. + +### Edge cases to watch for + +- **A green that came from the wrong filesystem** — clean room on `/mnt/c` (Task 1 validation). +- **A green that came from the wrong cluster** — reusing `420ai-archive` (Task 2 validation). +- **A false "capture broken"** from INC-2026-05's EOF-cursor behaviour (Task 6 gotcha). +- **A false "Linux verified"** — any table row or headline missing its tier label. +- **Incident-log inflation** — new IDs minted for INC-2026-02…05 (known-defect rule). +- **`vitest` reporting more than 680 skipped** — would mean int tests were mis-detected; re-check + that `DATABASE_URL_TEST` is unset. + +--- + +## VALIDATION COMMANDS + +### Level 1: Real-repo integrity (the gate that matters most) + +Run from the **repo root on Windows**, after Task 8: + +```sh +npm run repo-health +``` + +Expected: **exit 0**, same pass counts as before the spike. A failure here means the clean room +leaked (almost certainly the cluster-wide role — INC-2026-06). + +### Level 2: Clean-room typecheck + unit tests + +From `~/cleanroom/420ai` inside WSL: + +```sh +npm run typecheck # tsc -b — expect exit 0, ~10 s (17.0 ubuntu-latest baseline) +npx vitest run # expect: 1034 passed | 680 skipped +``` + +### Level 3: Capture actually happened + +```sh +psql postgresql://420ai:420ai@localhost:5533/420ai \ + -c 'select count(*) from raw_source_records;' -c 'select count(*) from events;' +``` + +Expected: both **> 0**. This is the only proof the collector layer works on POSIX; a green Level 2 +with a zero here means the suite passed and the product did not. + +### Level 4: Artifact + honesty gate (manual, and non-negotiable) + +- [ ] `cleanroom-linux-x86_64-2026-08-09.md` exists and its title block states + `VM-verified (WSL2) — NOT hardware-verified`. +- [ ] Its headline names the **unmeasured** set before the measured set. +- [ ] **Every** row in every results table carries a verification tier. +- [ ] `grep -c "not measured" .agents/research/cleanroom-linux-x86_64-2026-08-09.md` → **> 0**. +- [ ] No new incident duplicates INC-2026-02…05 (known-defect rule). +- [ ] The milestone plan states 17.4/17.5/17.6 were **not** re-scored, with the reason. +- [ ] `node scripts/check-summary.mjs` → exit 0. + +--- + +## ACCEPTANCE CRITERIA + +- [ ] Substrate recorded as-found; **nothing installed or enabled** to make a measurement pass. +- [ ] Clean room on **ext4**, with its **own Postgres container** on **5533**, own home, own ports. +- [ ] Isolation verified **before** it was relied on, on the **read** side as well as the write side. +- [ ] `quickstart.md` steps 1–13 attempted verbatim and timed; every break recorded with its + workaround as a deviation. +- [ ] Capture proven end-to-end on `linux-x86_64` (raw + event counts > 0). +- [ ] `--home` confinement verified on POSIX; **0** source paths outside the clean-room home. +- [ ] `build-sea.mjs` measured end-to-end; result distinguished from the planning spike's isolated + step-[3] result. +- [ ] Test baseline compared against **1034 | 680**; deviations explained. +- [ ] Teardown proven: real credentials/queue mtimes unchanged, real archive `machines` unchanged, + **`npm run repo-health` green on the real repo**. +- [ ] Every finding carries a **verification tier**; every unmeasured area is listed with its reason. +- [ ] **17.2b** exists in the milestone plan with its blocked-on-hardware scope. +- [ ] **Nothing was fixed.** No product code changed (D-M17-5). The only permitted exception is an + isolation defect, which must still be recorded as an incident. + +--- + +## COMPLETION CHECKLIST + +- [ ] All tasks completed in order; each validation passed immediately +- [ ] Level 1–4 validations executed +- [ ] Full transcripts captured and referenced from the report +- [ ] Incidents written from `INC-2026-08` up, known-defect rule applied +- [ ] Milestone plan re-scored (17.3) and honestly un-re-scored (17.4–17.6); 17.2b added +- [ ] `SUMMARY.md` updated in the same commit as the execution report +- [ ] `/lril:code-review` run (docs-only diff, but the honesty gates are reviewable) + +--- + +## NOTES + +### Spikes actually run during planning (2026-08-09) + +Four, all executed against WSL2 Ubuntu 24.04.4; the throwaway script was deleted. + +1. **Substrate inventory** — Ubuntu 24.04.4 LTS, Linux x86_64, `HOME=/home/seanr`, node v24.18.0, + npm 11.16.0, git/psql/docker/pkg-config present, **cargo MISSING**, **webkit2gtk-4.1 MISSING**, + 833 G free. `ps -p 1` → `init(Ubuntu-24.04)`; `systemctl is-system-running` → **`offline`**. + → Fixed the scope table's "not measured" rows on evidence rather than assumption, and retired the + need for a Rust toolchain (17.0 already answers `cargo check`). +2. **`build-sea.mjs` on Linux** (reproducing lines 32, 39, 123) → **copy SUCCEEDS**, producing a + 118 MB `ELF 64-bit LSB executable, x86-64` named `collector-x86_64-pc-windows-msvc.exe`. + → **Correction C1**: the protocol's predicted loud failure is wrong; it is a silent mislabel. +3. **Filesystem case-sensitivity** → `/tmp` and `~` (ext4, `/dev/sdf`) **CASE-SENSITIVE**; `/mnt/c` + (DrvFs) **CASE-INSENSITIVE**. → Task 1's ext4 requirement and its abort-validation. +4. **Docker engine sharing** → WSL's `docker ps` lists `420ai-archive 0.0.0.0:5433`, i.e. the real + repo's archive; ports 5432–5435 all occupied. → **Correction C3**, Task 2's separate *container* + (separate cluster), and port 5533. + +### Symbols and harness verified by reading (not from memory) + +- `cursorStorePathFor(home, platform)` — `apps/collector/src/connectors/cursor-store.ts:66-77`, + plus its jsdoc measurement debt at `:11-13`. +- `resolveHome` = `getFlag(args, "--home") ?? homedir()` — `apps/collector/src/cli.ts:631`; `--home` + present on all six subcommands (`:682-692`). +- `TARGET_TRIPLE = "x86_64-pc-windows-msvc"` — `build-sea.mjs:32`; `outExe` `:39`; `copyFileSync` + `:123`. +- `ALTER ROLE %I LOGIN PASSWORD %L` — `packages/db/src/provision-app-role.ts:67` (the cluster-scoped + mechanism behind INC-2026-06). +- `container_name: 420ai-archive` (`docker-compose.yml:8`), host port 5433 (`:16`). +- 17.0 baseline **1034 passed | 680 skipped** — `m17-cross-platform-collectors.md:97-101`. +- Report shape — `cleanroom-2026-08-02.md` (baseline `:92-97`, timing `:106-128`, verdict `:163-169`). +- Incident format — `incidents.md:41-57`; highest existing ID `INC-2026-07`. + +### Design decisions and trade-offs + +- **Named 17.2a, not 17.2.** The milestone's stated binding constraint is *verification*; letting a + VM spike close the number reserved for a hardware spike would redefine done by omission. 17.2b + carries the remainder and is visibly blocked, which is the state the maintainer actually wants + represented. +- **Verification tiers as a required field, not prose.** D-M17-4 already labels Apple Silicon + "CI-verified, not hardware-verified". Making the label mandatory generalizes a decision the + milestone has already taken, rather than inventing policy inside a spike. +- **Substrate left as-found.** Enabling systemd would let 17.4 be *partially* measured — under WSL's + systemd, which is not a service manager any target ships. A qualified measurement nobody can act on + is worse than a clean "not measured", and D-M17-5 forbids reshaping the thing being measured. +- **Separate container over separate database.** The distinction is not pedantry: the cluster is the + scope roles live in, and this substrate has the real archive one `docker ps` away. + +### Residual risk (named, not hidden) + +`npm ci` on Linux is genuinely unverified — 16.0 saw npm block `esbuild`/`sharp` postinstall scripts +on Windows without consequence, and both ship platform-specific binaries. It is deliberately **not** +retired by a planning spike: installing the tree during planning would warm the very cache whose cold +behaviour Task 4 measures. Task 4 states that a break here is a **finding**, not a blocked executor, +so the plan carries no dependency on the outcome either way. diff --git a/.agents/research/cleanroom-linux-x86_64-2026-08-09.md b/.agents/research/cleanroom-linux-x86_64-2026-08-09.md new file mode 100644 index 0000000..2660bbc --- /dev/null +++ b/.agents/research/cleanroom-linux-x86_64-2026-08-09.md @@ -0,0 +1,513 @@ +# Clean-room deploy — `linux-x86_64` — 2026-08-09 + +> **Verification tier: `VM-verified` (WSL2 Ubuntu 24.04.4) — NOT hardware-verified.** +> Real Linux kernel (`6.18.33.2-microsoft-standard-WSL2`), real `ext4`, real POSIX `homedir()`, +> real signals. No GUI session, no init system, shared Docker engine and shared host with Windows. + +Slice **17.2a**, executing the subset of the [17.2 spike protocol](./m17-slice2-spike-protocol.md) +that this substrate can measure truthfully. Mirrors the timed clean-room deploy of 16.0 +([`cleanroom-2026-08-02.md`](./cleanroom-2026-08-02.md)). + +> **D-M17-5: this exercise MEASURES; it does not fix.** Every finding below is logged in +> [`incidents.md`](./incidents.md) and left standing. **One** repair was made, under the standing +> D-16.0-2 exception (a defect in the isolation the measurement itself depends on): see +> **INC-2026-08**, which was caused by a step of this very run and is recorded rather than hidden. + +--- + +## Headline — what was NOT measured comes first + +**Slices 17.4, 17.5 and 17.6 are NOT re-scored by this run.** Those are the three +lowest-confidence slices (75% / 60% / 50%) and precisely the ones 17.2 exists to re-score. This +substrate cannot see any of them: + +| Area | Why not, on this substrate | Tier | Carries to | +| --------------------------------------- | ------------------------------------------------------------------------------------------------------------ | ------------- | ------------ | +| **macOS: everything** | No Mac exists | `not measured`| 17.2b | +| **Gatekeeper / notarization** (D-M17-2) | Needs macOS + a real browser download to set the quarantine xattr | `not measured`| 17.2b | +| **`linux-arm64` on real silicon** | Only QEMU emulation available — `emulated ≠ hardware-verified` | `not measured`| 17.2b | +| **systemd service semantics** | systemd is **off** in default WSL2 — measured as-found: `ps -p 1` → `init(Ubuntu-24.04)`, `systemctl is-system-running` → `offline`. Deliberately NOT enabled (D-M17-5) | `not measured`| 17.2b → 17.4 | +| **Tauri desktop shell / `webkit2gtk`** | `pkg-config --exists webkit2gtk-4.1` → **MISSING**; no desktop session | `not measured`| 17.2b → 17.5 | +| **Installers / updater feed** | Downstream of the shell and of signing | `not measured`| 17.2b → 17.6 | +| **Cursor `linux` path CONVENTION** | Cursor is not installed on this target — confinement verified, convention not | `not measured`| 17.2b | +| **Human-scale onboarding time (§5.2)** | Dev workstation, warm caches, agent-executed — 16.0's three caveats all apply again | `not measured`| a human-run repeat | + +**What this run does re-score is the collector-layer portability claim that 17.3 rests on**, plus +the milestone's foundational assumption that the TS core is already portable. Both hold, with +evidence. + +## Then — what WAS measured + +| Measure | Result | Tier | +| -------------------------------------------------- | -------------------------------------------------------------------------- | ------------- | +| Total **mechanical** time to first verified capture | **≈ 64 s** (see the honesty caveat — this is not a human's 30-min budget) | `VM-verified` | +| Steps requiring undocumented intervention | **2** (`npm run build`, `db:provision-app-role`) — the same two as Windows | `VM-verified` | +| Steps that failed following the guide verbatim | **2** (both halves of quickstart step 3) — identical errors to Windows | `VM-verified` | +| **Capture works end-to-end on POSIX** | **Yes** — 3 raw / 16 events / 1 machine / 15 heartbeats in the clean archive | `VM-verified` | +| `--home` confinement on POSIX (17.1's fix) | **Holds** — 0 declared paths outside the given home, 0 naming `/mnt/c` | `VM-verified` | +| Read-side isolation (the check 16.0 owed) | **Holds** — 1120 fd samples; only clean-room files ever open | `VM-verified` | +| `tsc -b --force` | **✅ 9.47 s** (17.0 `ubuntu-latest`: ✅ 10 s) | `VM-verified` | +| `npx vitest run` | **✅ 1045 passed \| 680 skipped** (baseline 1034 \| 680 — deviation explained below) | `VM-verified` | +| `build-sea.mjs` on a non-Windows triple | **Exits 0 and prints `PASS`** while producing a mislabelled, unbundlable ELF | `VM-verified` | +| SIGINT graceful drain (POSIX signal path) | **503 ms, clean exit**, queue fully drained | `VM-verified` | +| Incidents raised | **6** — INC-2026-08…13 (0 high, 5 med, 1 low) | — | +| Real credentials / archive modified | **No** — content hash, machine id, token id, org/user counts all identical | `VM-verified` | +| Real repo collaterally broken | **No** — `npm run repo-health` **PASS**, 1725 tests, 0 skipped | `VM-verified` | +| Isolation breached during the run | **Yes, once** — by `build-sea.mjs --check` itself (**INC-2026-08**), repaired | `VM-verified` | + +--- + +## Honesty caveat — read before quoting the 64-second number + +All three of 16.0's caveats apply again, unchanged, plus one new one. + +1. **Executed by an agent, non-interactively.** Quickstart steps 4 and 9–11 are dashboard **UI** + steps; they were performed as direct API calls (steps 4/login) or skipped (9–11). That is a + **deviation from the documented path** and removes exactly the part of the budget a human spends. +2. **It measures machine time, not time-on-task.** Diagnosing `ERR_MODULE_NOT_FOUND` and + discovering that `db:provision-app-role` exists are compressed to zero here, and those two are + plausibly the whole 30-minute budget for a first-time user. +3. **Clone and install are not representative.** The clone was the **second** clone of the session + (warm DNS/CDN, 1.1 s) and `npm install` ran against a **warm 2.8 GB npm cache** (30.2 s). Both + are recorded apart from the headline and must not be folded into it. +4. **NEW — the first clone was not timed at all.** Its instrumentation failed (see *Methodology + corrections*), so the 1.1 s figure is a re-clone. A genuine cold-cache number for this substrate + **does not exist** and is not claimed. + +**What this run does establish reliably:** the documented path's two breaks reproduce **identically** +on Linux; capture, `--home` confinement, the POSIX shutdown path and the SEA toolchain all work; +and the SEA build's triple is wrong in a way that is worse than a failure. Those are facts about the +product. The wall-clock is a floor, not an estimate. + +--- + +## Isolation setup + +| Constraint | How | Verified | +| ------------------------- | --------------------------------------------------------------------------------------- | -------- | +| Separate checkout, on ext4 | `git clone` → `~/cleanroom/420ai` on **ext4** (`/dev/sdf`), never `/mnt/c` | ✅ | +| Separate Postgres **cluster** | Own container `420ai-cleanroom-db` on host **5533** — not a second database | ✅ | +| Separate collector home | Every collector call took `--home ~/cleanroom-home` | ✅ | +| Separate ports | ingest **8998**, dashboard **3998** | ✅ | +| Read-side confinement | 1120 `/proc//fd` samples over a live `watch` | ✅ | +| **No writes outside the room** | — | ❌ **breached by `build-sea.mjs --check` — INC-2026-08** | + +**The substrate-specific trap, measured rather than assumed.** `/mnt/c` is DrvFs/9p and +**case-insensitive**; `~` is ext4 and **case-sensitive**. A clean room under `/mnt/c` would have +measured *Windows* filesystem semantics while every path in the transcript looked like Linux: + +``` +$ df -T ~/cleanroom | tail -1 +/dev/sdf ext4 1055762868 128920476 873138920 13% / +$ cd ~/cleanroom && touch A.txt && [ -e a.txt ] && echo INSENSITIVE-ABORT || echo OK-case-sensitive +OK-case-sensitive +$ df -T /mnt/c | tail -1 +C:\ 9p 3906241364 2558874756 1347366608 66% /mnt/c +``` + +**Why a separate CONTAINER, not a separate database (INC-2026-06, carried and confirmed).** WSL's +Docker is the *same engine* as Windows' — `docker ps` from inside WSL lists the real +`420ai-archive` on `0.0.0.0:5433`, alongside 12 other stacks, with ports 5432/5433/5434/5435 all +occupied. A separate container is a separate **cluster**, and the cluster is the scope roles +actually live in. Proven directly rather than asserted: + +``` +cleanroom (5533) system_identifier = 7672069298924687404 +real archive (5433) system_identifier = 7650953580965343270 +cleanroom login roles : 420ai +real archive login roles : 420ai 420ai_app +``` + +After `npm run db:provision-app-role` ran **inside** the clean room, the clean-room cluster gained +its own `420ai_app` and the **real archive's role list was unchanged** — the containment INC-2026-06 +prescribed, verified working. + +### Baseline captured before starting + +16.0's non-contamination invariants (mtimes byte-identical, row counts unchanged) **assume a +quiescent machine, and this machine is not one** — the real collector is running and rewrites +`queue.sqlite` every 40–90 s. An invariant that always trips is no better than one that never does, +so the discriminating invariants were chosen as the ones the *real* collector never moves but a leak +would: + +``` +INVARIANT 1 real credentials.json sha256 = d76521c4…3457c mtime=1785924375 size=150 +INVARIANT 2 real machine id = a2657feb-e647-46af-9d60-d82c81ce48b0 + real token id = 0a1ce2e9-f52e-420e-a856-9d2b0d0004b9 + organizations=2 users=4 +INVARIANT 3 real archive login roles = {420ai, 420ai_app} +INVARIANT 4 ~/.420ai (WSL side) ABSENT +NOT an invariant (expected to grow): raw=107812 events=178826 queue.sqlite mtime +``` + +--- + +## Timing table + +`intervention?` means the documented path did not carry the step and something outside the guide was +required. Every row is tier `VM-verified`. + +| Quickstart step | Wall-clock | Intervention? | Notes | +| ---------------------------------------- | ---------: | ---------------------- | ---------------------------------------------------------------------------------------------------- | +| _(pre) clean-room Postgres container_ | ~4 s | isolation-only | Not a documented step. Own cluster on 5533. | +| _(pre) `git clone` from GitHub_ | 1.13 s | — | **Second clone**; warm DNS/CDN. 27 MB. The first clone was untimed — see caveat 4. | +| **1.** `npm install` | 30.15 s | — | **Warm 2.8 GB cache.** npm blocked 5 postinstalls (`esbuild` ×4, `sharp`) with `allow-scripts` — **did not bite on Linux** (see below). | +| **1.** `npm run setup` | 0.12 s | — | Wrote `.env` + `apps/dashboard/.env.local`. Its own "next steps" names a step the guide omits — INC-2026-03's root cause, reproduced. | +| **1.** set `ADMIN_PASSWORD` | 0 s | — | Guide is explicit this is not optional post-15.9. | +| _(iso) repoint DB + ports_ | 0 s | isolation-only | Also had to fix `apps/dashboard/.env.local` — **reproduced — INC-2026-04**. And blank `DATABASE_URL_TEST` — **new, INC-2026-10**. | +| **2.** `npm run db:up` | — | **SUBSTITUTED** | `docker compose up -d` would start the **real** `420ai-archive` (fixed `container_name`). Not run. | +| **2.** `npm run db:migrate` | 1.52 s | — | Clean. 33 tables created in the clean-room cluster. | +| **3a.** `npm run ingest:dev` | immediate | ❌ **FAILED** | `ERR_MODULE_NOT_FOUND: …/@420ai/db/dist/index.js` — **reproduced — INC-2026-02**. | +| ↳ intervention: `npm run build` | 9.54 s | ⚠️ **undocumented** | `tsc -b`. Confirmed absent: `grep -c 'npm run build' docs/guide/quickstart.md` → **0**. | +| **3a.** `npm run ingest:dev` (retry) | < 1 s | ❌ **FAILED** | Postgres `28P01`, `auth.c:329`, `auth_failed` for `420ai_app` — **reproduced — INC-2026-03**. | +| ↳ intervention: `db:provision-app-role` | 0.70 s | ⚠️ **undocumented** | Confirmed absent: `grep -c provision-app-role docs/guide/quickstart.md` → **0**; **2** in `setup-env.mjs`. | +| **3a.** `npm run ingest:dev` (3rd) | 2.07 s | — | Healthy at `/v1/health` (200). **`/health` → 404** — the guide names no path, same as Windows. | +| **3b.** `npm run dashboard:dev` | ≤ 6 s | — | `✓ Ready in 296ms`; `/login` **200**. 5 min of operator error excluded — see below. | +| **4.** generate pairing code | 0.02 s | **deviation** | Documented as a UI click; performed as `POST /v1/pairing-codes`. | +| ↳ login (`POST /v1/auth/login`) | 0.06 s | — | Admin auto-seeded from `ADMIN_EMAIL`/`ADMIN_PASSWORD` on ingest boot, as documented. | +| **5–6.** `collector pair … --home` | 0.51 s | — | **F2 fix confirmed on POSIX** — printed the clean-room path, not the real profile. See below. | +| **7–8.** `collector discover --home` | 0.36 s | — | `0 workspaces, 0 projects`. **Prints no source paths** — see the protocol correction below. | +| **9–11.** connector review/approval | — | **skipped (UI-only)** | Not performed. Capture happened anyway, as in 16.0. | +| **12.** `collector watch --home` | ≤ 12 s | — | First events reached the archive **on the first tick, with no append** — see INC-2026-10. | +| **13.** first capture verified | — | ✅ | `3 raw / 16 events / 1 machine / 15 heartbeats` in the clean-room archive. | + +**Excluded from the product timing: operator error, all mine.** (a) The dashboard was first started +as `npm run dashboard:dev -- --port 3998`, which npm forwards as `next dev 3998` — Next read `3998` +as a *directory* and refused to start. (b) A later `curl` hung 135 s against an ingest process my own +harness had reaped. Neither is a product defect, and both are recorded here rather than in the +timing so the measurement is not inflated. + +--- + +## The findings + +### 1. `build-sea.mjs` does not fail on a non-Windows triple — it reports **PASS** (INC-2026-09) + +The 17.0 protocol (line 86) predicted a loud failure. A planning spike corrected that to "silent +mislabel", measured on step [3] **in isolation**. This run measured the **whole script**, which is +the finding that matters, and it is worse than either prediction: + +``` +[1] esbuild bundle (src/serve.ts → CJS) ✓ bundled 137 KB +[2] node --experimental-sea-config → blob ✓ blob written +[3] copy node runtime → sidecar binary ✓ copied /usr/bin/node → …/collector-x86_64-pc-windows-msvc.exe +[4] postject inject NODE_SEA_BLOB 💉 Injection done! + ✓ SEA artifact ready: …/collector-x86_64-pc-windows-msvc.exe (118 MB) + +build-sea: PASS +SEA_FULL_RC=0 +``` + +The artifact: + +``` +TYPE: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), dynamically linked, + interpreter /lib64/ld-linux-x86-64.so.2, …, for GNU/Linux 3.2.0 +SIZE: 123866304 bytes +``` + +and it **works** — the produced "`.exe`" is a functioning Linux collector sidecar: + +``` +$ echo '{"cmd":"status"}' | ./collector-x86_64-pc-windows-msvc.exe serve +{"type":"ready","pid":4901,"collectorVersion":"0.0.0","paired":false} +{"type":"status","state":"idle","pending":0,"inflight":0,"lastSyncAt":null} +{"type":"ack","cmd":"stop"} +{"type":"stopped"} +``` + +Meanwhile Tauri's `externalBin` resolves by `-$TARGET_TRIPLE` suffix, so on this target it would +look for `collector-x86_64-unknown-linux-gnu` — **NOT FOUND**. The artifact is simultaneously +mislabelled, invisible to the thing that consumes it, and reported as a success. + +**This is good news for 17.3 and must be read that way.** Every portable part of the recipe — +esbuild, `--experimental-sea-config`, `copyFileSync(process.execPath, …)`, postject injection into +an ELF — works unmodified on Linux. Only the hardcoded constant and the `.exe` extension are wrong. +`postject` emitted ELF-specific warnings (`Can't find string offset for section name '.note.100'`) +rather than the Windows "signature seems corrupted" warning the script's own comment says to expect; +injection succeeded regardless. + +**Scope note kept honest:** the planning spike's isolated step-[3] result is **prior evidence for the +mechanism only**. The result reported here is the full script, run end-to-end. + +### 2. `npm ci`/`npm install` on Linux — the plan's named residual risk resolves to "no break" + +16.0 saw npm block `esbuild`/`sharp` postinstall scripts on Windows without consequence, and both +ship platform-specific binaries, so the plan flagged this as genuinely unverified. Measured: the +same 5 blocks occur on Linux (`esbuild@0.27.7/0.18.20/0.25.12/0.28.1`, `sharp@0.34.5`), `npm +install` exits **0** with 851 packages, and esbuild **works** — proven twice downstream, by +`build-sea.mjs --check` (which bundles with esbuild, 164 ms) and by the full 1725-test vitest run. +Modern esbuild resolves its binary from the `@esbuild/linux-x64` optional dependency rather than +from the postinstall, which is why the block is inert. + +### 3. `--home` confines on POSIX — 17.1's fix verified, and the C2 debt explicitly NOT discharged + +`discover` prints **no source paths**, so the protocol's prescribed isolation check (lines 37–38, +"assert the Cursor source path printed is under the clean-room home") is **not performable as +written**. Recorded as a protocol correction. The equivalent evidence was produced by enumerating +the registry's declared capture surface — the same surface `connector-approvals.ts` fingerprints — +for two synthetic homes: + +``` +platform = linux arch = x64 connectors registered = 8 +[OK] claude-code watchGlob /.claude/projects/*/*.jsonl +[OK] codex-cli watchGlob /.codex/sessions/*/*/*/rollout-*.jsonl +[OK] gemini-cli watchGlob /.gemini/tmp/*/chats/session-*.json +[OK] cursor poll.source /.config/Cursor/User/globalStorage/state.vscdb +[OK] claude-export watchGlob /.420ai/chat-imports/claude/*.json +[OK] chatgpt-export watchGlob /.420ai/chat-imports/chatgpt/*.json +[OK] gemini-export watchGlob /.420ai/chat-imports/gemini/*.json + +ASSERT paths outside their given home : 0 (must be 0) +ASSERT paths naming /mnt/c or AppData : 0 (must be 0 on linux) +``` + +(The 8th connector is `claudeLiveConnector`, push-mode — it declares no filesystem surface by +design, not by omission.) + +**The C2 debt is NOT discharged, and this is the sentence that matters.** `cursor-store.ts:11-13` +addresses a measurement debt to 17.2 by name: the `darwin`/`linux` rows are convention-derived from +the VS Code user-data layout and "NOT yet measured". This run verified **confinement** (the path is +a pure function of its argument) — it did **not** verify the **convention**, because Cursor is not +installed on this target. `cursorStorePathFor(home,'linux')` → +`/.config/Cursor/User/globalStorage/state.vscdb`: correct *shape*, unverified *value*. Tier: +**`not measured`**, carried to 17.2b. The closure of INC-2026-01 must not be read as closure of this. + +**And "the collector didn't complain" is confirmed worthless as evidence.** The Cursor store path +did not exist on this target for the entire ~8-minute `watch`. Afterwards the clean-room queue held +`poll_state = 0`, `connector_errors = 0`, and the watch log contained **not one line** about it. +That is exactly the shape a *wrong* darwin/linux constant would present in the field — the +`PollOutcome.unavailable`-has-no-readers defect the milestone plan carried into 17.2 as a +measurement item, now measured. Raised as **INC-2026-12**. + +### 4. Read-side isolation — the check 16.0's write-only checks missed + +16.0's closing lesson: *"They prove the clean room never **wrote** to real state. They say nothing +about **reads** — and INC-2026-01 is a read breach."* This run carried it. While `collector watch` +ran, `/proc//fd` was sampled at 4 Hz for 40 s (1120 samples) against the **real node process**, +while new session files were written to force filesystem activity: + +``` +distinct real files the collector held open: + /home/seanr/cleanroom-home/.420ai/queue.sqlite + /home/seanr/cleanroom-home/.420ai/queue.sqlite-shm + /home/seanr/cleanroom-home/.420ai/queue.sqlite-wal + /home/seanr/spike/watch.log + +ASSERT opened under /mnt/* (any Windows drive) : 0 +ASSERT opened under the REAL WSL home ~/.420ai : 0 +ASSERT opened under REAL WSL ~/.claude|.codex|.gemini|.config: 0 +``` + +**Stated limitation:** fd sampling observes files held open *at sample instants*; a read-and-close +inside one 250 ms window can be missed. It is strong evidence, not exhaustive proof, and it is +complementary to the declared-surface enumeration above rather than a substitute for it. + +### 5. INC-2026-05 did NOT reproduce — and its recorded root cause contradicts the code (INC-2026-11) + +The plan warned that a static seeded file would produce **zero** events because "the watcher cursors +pre-existing files at EOF", and that skipping the mid-run append would record a false "capture is +broken on Linux". The opposite happened: with a **static** pre-existing session file and **no +append**, the archive held `raw=2, events=8` within 12 s. + +Rather than assert a platform difference, the mechanism was read out of the code: + +``` +apps/collector/src/watcher/file-watcher.ts:136 + const fromOffset = cursor?.byteOffset ?? 0; +``` + +A file with **no saved cursor starts at offset 0** — the whole file is ingested. And that line is not +new: + +``` +$ git log -1 -S 'const fromOffset = cursor?.byteOffset ?? 0' -- apps/collector/src/watcher/file-watcher.ts +d02f390 2026-06-13 feat: Milestone 3 — collector foundation … +$ git log --until=2026-08-02 -1 --format='%h %ad' -- apps/collector/src/watcher/file-watcher.ts +990930b 2026-06-20 +``` + +So the EOF claim was **already false** on 2026-08-02, when INC-2026-05 was recorded. The symptom +16.0 observed may well be real; the **root cause written down for it is not**, and INC-2026-05 is +quoted verbatim as the acceptance criterion of 16.3's capture-health scorecard. A slice designed +against a misdiagnosed cause is the failure this matters for. Raised as **INC-2026-10** citing +INC-2026-05, per the known-defect rule ("a known defect behaving differently is the finding"), +rather than as a duplicate of it. + +### 6. The isolation breach this run caused itself (INC-2026-08) + +`build-sea.mjs --check` executes `execFileSync(process.execPath, [cjs, "serve"])` — the collector's +`serve` entrypoint with **no `--home`** — so it resolves the operator's real `homedir()`. It created +`~/.420ai/` with a fresh `queue.sqlite` and `connector-approvals.json` in the **real** WSL home, +timestamped `13:00:33.848` / `13:00:33.880`, which is the exact second Task 7 ran. The `PASS` message +the script prints to the operator (`verify with: echo '{"cmd":"status"}' | "" serve`) has the +same defect. + +Consequences, in order of importance: +1. **It breaks the single-owner invariant.** CLAUDE.md is explicit: only one collector may own a + given `queue.sqlite`. On a machine where the collector is running, the documented build/verify + commands boot a second one against the live queue. +2. **It silently contaminated an isolated run** — this one. Every other constraint held; this + arrived through a dimension the checklist did not name, which is precisely the general lesson + 16.0 drew and this run has now re-earned one level up: *the checklist enumerated the resources + the plan could see; a build script is not one of them.* + +**Repaired in place** under the D-16.0-2 standing exception (`rm -rf ~/.420ai`, restoring the +verified-absent baseline) and recorded rather than hidden. No product code was changed. + +### 7. A second checkout's `npx vitest run` would TRUNCATE the real test database (INC-2026-10) + +`npm run setup` generates `.env` with **every** handle pointing at `localhost:5433`, including +`DATABASE_URL_TEST` and `DATABASE_URL_TEST_APP`. On this host, 5433 is the **real** archive cluster, +and it does own a `420ai_test` database. `vitest.config.ts:7` loads `.env`, 61 `*.int.test.ts` files +gate only on `!TEST_URL`, and they open with statements like: + +``` +TRUNCATE invites, password_reset_tokens, project_grants, search_documents, session_git_links, + git_commit_files, git_commits, alert_firings, machine_heartbeats, report_artifacts, + workspace_keys, workspaces, projects, raw_source_records, events, ingest_tokens, + pairing_codes, machines, memberships, organizations, users RESTART IDENTITY CASCADE +``` + +**The mechanism was demonstrated, not fired** — the handles were blanked before any vitest run, and +`420ai_test` still holds its 33 tables at teardown. This is INC-2026-04's shape (setup hardcodes the +*first* installation's coordinates) with a destructive rather than a confusing consequence, and it is +a sibling of INC-2026-06 rather than a repeat: 06 is about roles crossing a database boundary, this +is about a generated config aiming a second checkout at the first one's cluster. + +### 8. F2 stays fixed on POSIX + +16.0's F2 (pair printing the real profile path under `--home`) is fixed and the fix now holds on a +platform it was never run on: + +``` +Paired. machineId=f71fd8d0-be2e-4358-95b0-fb871b0ee7dd +Ingest token (store securely): +Saved credentials to /home/seanr/cleanroom-home/.420ai/credentials.json <-- correct +``` + +with mode `-rw-------` (0600), and the real Windows `credentials.json` hash byte-identical throughout. + +### 9. The POSIX shutdown path + +Not in the protocol, and worth having because Windows cannot exercise it honestly: `SIGINT` to a +live `collector watch` drained and exited in **503 ms**, logging +`Stopping… (graceful drain)` → `draining queue before exit…` → `queue drained.`, leaving +`queue_items = 0` and `file_cursors = 4`. No hang — the UAT C.8 hazard class did not appear here. + +--- + +## Test-suite baseline vs 17.0 + +| Measure | 17.0 `ubuntu-latest` (run 31178775260) | This run | Verdict | +| ---------------------- | ---------------------------------------- | ---------------------------- | ------- | +| `tsc -b --force` | ✅ 10 s | ✅ **9.47 s** | match | +| `npx vitest run` | ✅ 1034 passed \| 680 skipped | ✅ **1045 passed \| 680 skipped** | **+11 passed, explained** | + +The deviation is **not** a finding about this machine. The 17.0 baseline was measured at PR #82; the +clean room is at `a855ebc`, which includes the **17.1** merge (PR #83). Counting test blocks in that +merge closes it exactly: + +``` +$ git diff a855ebc^1 a855ebc -- 'apps/**/*.test.ts' ':!*int.test.ts' | grep -cE '^\+\s*(it|test)\(' +12 +$ git diff … | grep -cE '^-\s*(it|test)\(' +1 +$ git diff a855ebc^1 a855ebc -- '*int.test.ts' | grep -cE '^\+\s*(it|test)\(' +0 +``` + +Net **+11 unit tests, 0 new integration tests** — precisely 1034 → 1045 with skipped unchanged at +680. Cross-checked from the other side: the real repo's `repo-health` (which *has* +`DATABASE_URL_TEST`) ran **1725 passed, 0 skipped**, and 1045 + 680 = 1725. + +Also worth recording, because CLAUDE.md warns about it specifically: the **incremental** +`npm run typecheck` returned in **204 ms** having done nothing, because `npm run build` had already +run. That is the false-green shape the repo-health note describes; the trustworthy number is the +`--force` one. + +--- + +## Verification that isolation held + +| Check | Baseline | After | Verdict | +| -------------------------------------------- | -------------------------------------- | -------------------- | ------- | +| Real `credentials.json` sha256 | `d76521c4…3457c` | `d76521c4…3457c` | ✅ unchanged (write-side) | +| Real `credentials.json` mtime/size | `1785924375` / 150 | `1785924375` / 150 | ✅ unchanged (write-side) | +| Real archive `machines` id | `a2657feb-…-d82c81ce48b0` | identical | ✅ untouched (write-side) | +| Real archive `ingest_tokens` id | `0a1ce2e9-…-9d2b0d0004b9`, not revoked | identical | ✅ untouched (write-side) | +| Real archive organizations / users | 2 / 4 | 2 / 4 | ✅ untouched (write-side) | +| Real archive **login roles** (INC-2026-06) | `{420ai, 420ai_app}` | `{420ai, 420ai_app}` | ✅ contained (write-side) | +| Real `420ai_test` schema (INC-2026-10 hazard) | 33 tables | 33 tables | ✅ not fired (write-side) | +| Collector open fds during `watch` | — | clean-room only | ✅ **read-side** | +| Real WSL `~/.420ai` | ABSENT | **PRESENT** | ❌ **breached — INC-2026-08**, repaired to ABSENT | +| Clean-room container / checkout / home | — | all absent | ✅ | +| Ports 5533 / 8998 / 3998 | — | released | ✅ | +| **Real repo `npm run repo-health`** | PASS | **PASS** (1725 passed, 0 skipped) | ✅ INC-2026-06 did not recur | + +**Recorded, not asserted** (the real collector is live): real archive `raw` 107812 → 107812, +`events` 178826 → 178826, real `queue.sqlite` mtime 1786292879 → 1786293468. The mtime moved for +innocent reasons and is not evidence of anything. + +**`repo-health` was run three times, and the middle run FAILED — say so plainly.** Run 1 (straight +after teardown) **PASS**, 1725 passed / 0 skipped. Run 2 **FAIL**, 559 failures, every one +`ECONNREFUSED …:5433`. Run 3 **PASS**, 1725 passed / 0 skipped. The cause of run 2 was **not** a +clean-room leak and the error discriminates it precisely: INC-2026-06's signature is Postgres +`28P01 auth_failed`, whereas `ECONNREFUSED` means nothing was listening — Docker Desktop had exited +for the second time this session, taking the real archive with it (`420ai-archive :: Exited (255)`), +and the container was then killed mid-crash-recovery by the restart cycle. Host-side instability, +independent of this slice: the same host also logged a transient `Wsl/0x8007000e` ("not enough +memory resources") at a moment when 72.9 GB of 127.9 GB was free, and a Postgres checkpoint of +**189.7 s** (`sync=136.4 s`, 120,812 files sync'd). Once the archive finished recovering, the gate +passed unchanged. This is recorded rather than smoothed over because "the gate failed once" is +exactly the kind of detail a summary drops and a reader needs. + +--- + +## Methodology corrections (so 17.2b does not repeat them) + +1. **`cargo` is installed; the plan's `cargo MISSING` baseline was a non-login-shell artifact.** + rustup appends `. "$HOME/.cargo/env"` to `~/.profile`, which only a **login** shell sources. + `bash -c 'which cargo'` reports MISSING on a machine running cargo 1.96.0. The plan's *decision* + (do not run `cargo check` — 17.0 already answered it on `ubuntu-latest`, ✅ 81 s) was right for a + stronger reason and is unchanged. `webkit2gtk-4.1` really is missing. +2. **Command substitution across the WSL interop boundary fabricated a plausible wrong value** + (**INC-2026-13**). `wsl … bash -lc '…$(git rev-parse --abbrev-ref HEAD)…'` reported the branch of + the **Windows** checkout (`m17-slice2a-linux-vm-spike`) while `.git/HEAD` in the clean room read + `ref: refs/heads/main`; a sibling `X=hello; echo "$X"` returned empty. Both anomalies vanished + when the same commands ran from a **script file**. Everything after that point was executed as + `bash script.sh`, and the affected transcript lines were re-measured. +3. **The protocol's `discover`-based isolation check is not performable as written** — `discover` + prints no source paths. Replace it in 17.2b with the registry-enumeration + `/proc` fd sampling + used here (§3, §4 above). +4. **16.0's teardown invariants assume a quiescent machine.** Choose invariants the real collector + never moves (content hashes, row identities), and declare growing counters as *expected to grow* + rather than asserting them flat. + +--- + +## Result against the §5.2 target + +**Mechanical time to first verified capture: ≈ 64 seconds** — nominally far inside the < 30 min +target, and **this should not be recorded as a pass**, for all four honesty-caveat reasons and for +16.0's fifth: a path that fails twice with unexplained errors has no meaningful wall-clock for a +first-time user, because the distribution is dominated by how long diagnosis takes. + +The honest statement, now with a second platform behind it: + +> The mechanical path is fast on Linux, and **breaks in exactly the same two places as on Windows**, +> with byte-identical errors. That is itself the useful result: INC-2026-02 and INC-2026-03 are +> platform-independent onboarding defects, not Windows quirks, so fixing them fixes onboarding +> everywhere. Time-to-first-capture still cannot be claimed against §5.2 until both are closed and +> the exercise is repeated **with the UI steps actually performed by a human**. + +--- + +## Evidence + +Full terminal transcripts for every step were captured to `~/spike/transcript.log` inside the WSL +instance, with per-step logs (`ingest.log`, `dashboard.log`, `watch.log`, `vitest.log`, +`fds.raw`). They are **not committed** — they contain a live session token, a pairing code, an +ingest token and a push-receiver token, and the §3 privacy rule governs this directory. Every +number, command and log excerpt quoted above is reproduced verbatim from them; nothing in this +report is a summary of an unrecorded observation. diff --git a/.agents/research/incidents.md b/.agents/research/incidents.md index 32b3574..f6ca87b 100644 --- a/.agents/research/incidents.md +++ b/.agents/research/incidents.md @@ -65,10 +65,259 @@ each one.** A fixed parser bug with no fixture is a bug that returns. -INC-2026-07 was raised during M16 pre-sign-off (2026-08-05). The five below it were raised by the -2026-08-02 clean-room deploy ([`cleanroom-2026-08-02.md`](./cleanroom-2026-08-02.md), slice 16.0 -part 4). **None of those five is fixed** — D-16.0-2: the exercise measures, and each entry is the -evidence that earns a fix under §2's scope-change rule. +INC-2026-08…13 were raised by the 2026-08-09 `linux-x86_64` clean-room deploy +([`cleanroom-linux-x86_64-2026-08-09.md`](./cleanroom-linux-x86_64-2026-08-09.md), slice **17.2a**), +tier **`VM-verified` (WSL2) — NOT hardware-verified**. INC-2026-07 was raised during M16 +pre-sign-off (2026-08-05). The five below that were raised by the 2026-08-02 clean-room deploy +([`cleanroom-2026-08-02.md`](./cleanroom-2026-08-02.md), slice 16.0 part 4). **None of those five is +fixed** — D-16.0-2: the exercise measures, and each entry is the evidence that earns a fix under +§2's scope-change rule. + +> **Verification tier (M17 17.2a).** Every entry raised from 17.2a onwards carries the tier its +> evidence was produced at — `hardware-verified` / `VM-verified` / `CI-verified` / `not measured`. +> The vocabulary generalizes **D-M17-4**, which already ships Apple Silicon as "CI-verified, not +> hardware-verified, labelled everywhere it is offered"; 17.2a makes the label a required field +> rather than a one-off courtesy, because `emulated ≠ hardware-verified` is the next member of the +> `skipped ≠ passed` family and an unlabelled finding is exactly how it would be minted. + +> **Known-defect rule (17.2a, prevents incident-log inflation).** Reproducing a known incident on a +> new platform is a **result, not a new incident** — it is recorded in that run's timed table as +> `reproduced — INC-2026-0N` and adds one line to the original entry's Evidence. A new ID is minted +> only for something genuinely new, or when a known defect behaves **differently** on the new +> platform (that difference is the finding, written as a new entry citing the old one — see +> INC-2026-11). Without this rule, three targets × four known defects fabricates twelve incidents +> and inflates the "incidents raised" headline that 17.3–17.6's re-score reads. + +## INC-2026-13 — command substitution across the WSL interop boundary FABRICATED a plausible wrong value + +- **Date / observed by:** 2026-08-09 / clean-room deploy `linux-x86_64`, slice 17.2a +- **Severity:** low +- **Category (§4.4):** capture _(tooling/environment — the closest §4.4 fit, same reasoning as + INC-2026-06: it is a defect in the measurement procedure, not in captured data)_ +- **Verification tier:** `VM-verified` (WSL2) +- **What happened:** Driving the spike as + `wsl -d Ubuntu-24.04 -- bash -lc '… $(git rev-parse --abbrev-ref HEAD) …'` from the Windows host, + the transcript recorded the branch as `m17-slice2a-linux-vm-spike` — the **Windows** checkout's + branch. The clean room's `.git/HEAD` read `ref: refs/heads/main` in the same command invocation, + and `git branch --show-current` from a script file read `main`. A sibling probe, + `X=hello; echo "X=$X"`, returned **empty**. Both anomalies disappeared entirely once the same + commands were run as `bash script.sh`. +- **User impact:** None on the product; the harm is to **evidence integrity**, which is the whole + output of a spike. The fabricated value was *plausible* — a real branch name, of the right repo, + in the right format. A timing instrument failed in the same layer and printed + `CLONE_WALL_CLOCK= - s`, which was obviously broken and therefore harmless; the branch name was + not obviously anything. This is the difference between an instrument that breaks and an instrument + that lies, and only the second one ships a wrong finding. +- **Root cause:** not fully diagnosed, and deliberately not chased further — at least one + substitution inside a single-quoted `bash -lc` string was evaluated in the **Windows** shell + context rather than by the WSL shell. Whatever the precise layer (Git Bash → `wsl.exe` → WSL + bash), the durable conclusion is that the three-layer quoting path is not trustworthy for + measurement. +- **Evidence:** the contradiction above, captured in-transcript; the `X=hello` probe; the identical + commands re-run from a script file returning `main` and a working `CLONE_WALL_CLOCK_MS=1125`. +- **Disposition:** **procedure fix, applied immediately in-slice** and recorded here so 17.2b + inherits it: **never interpolate across the interop boundary.** Write the script to a file, copy + it in (`tr -d '\r'`), and run `bash script.sh`. Everything in 17.2a after the anomaly was executed + that way and the affected lines were re-measured. **Not a product fix** — nothing in the shipped + code changed, so D-M17-5 does not apply. +- **Regression test?** Not applicable — a defect in the measurement *procedure*. The durable guard + is the corrected procedure above, which 17.2b will need on any Windows-driven remote target. + +## INC-2026-12 — a connector source path that does not exist leaves NO trace anywhere + +- **Date / observed by:** 2026-08-09 / clean-room deploy `linux-x86_64`, slice 17.2a +- **Severity:** med _(see the qualification under Disposition — it becomes **high** the moment a real + store exists at a path the constant gets wrong, which is precisely the `darwin`/`linux` case)_ +- **Category (§4.4):** capture +- **Verification tier:** `VM-verified` (WSL2) +- **What happened:** `collector watch --home ` ran for ~8 minutes on a target where the + Cursor store does not exist (Cursor is not installed). Afterwards the clean-room queue held + `poll_state = 0` and **`connector_errors = 0`**, and the watch log contained **not one line** about + the Cursor connector. The only outward sign was the cheerful `watching 8 connector(s)`. +- **User impact:** "The collector didn't complain" is worthless as evidence that a connector's path + is right. A store path that is simply **wrong** is indistinguishable, from every surface the + operator has, from a store that is legitimately absent — and from one that is working. This is + INC-2026-05's "cannot distinguish no-work from broken" one layer down, at connector granularity, + and it is the exact shape a bad `darwin`/`linux` path constant would present in the field. +- **Root cause:** already diagnosed by 17.1's PR review and carried into 17.2 as a measurement item: + **`PollOutcome.unavailable` has zero production readers.** `pollLoop` + (`apps/collector/src/capture-engine.ts:329-340`) discards the outcome and then calls `onSuccess`, + which calls `queue.clearConnectorError()` — so an unavailable source reports the connector healthy + **and wipes any pre-existing fault**, with no log line. 17.2a's contribution is the observation + rather than the diagnosis: this is what it looks like from outside, and it looks like nothing. +- **Evidence:** clean-room `queue.sqlite` at teardown — `poll_state=0`, `connector_errors=0`, + `file_cursors=4`, `queue_items=0`; `~/spike/watch.log` in full, containing no Cursor line; + `cursorStorePathFor('','linux')` → + `/.config/Cursor/User/globalStorage/state.vscdb`, which does not exist on this + target. +- **Disposition:** **earns a slice — 17.4**, where the milestone plan already routes it. Not fixed + here (D-M17-5). The reason it is med rather than high today is that on this target the source is + *genuinely* absent, so "unavailable" is the correct state and nothing was lost; the severity is + carried by what the silence would hide elsewhere. **This entry is also the standing argument for + why 17.2b must verify the `darwin`/`linux` Cursor paths by OBSERVATION** — a run that concludes + "the path is fine, the collector didn't error" would be reading a signal that cannot exist. +- **Regression test?** Not yet, and it must ship with the fix. The discriminating shape is + behavioural and mirrors INC-2026-07's: assert that a collector whose poll source path does **not + resolve** produces a state the operator can see, distinct both from "healthy" and from "no work + happened" — and negative-control it by pointing a connector at a nonexistent path and watching the + current code report green. + +## INC-2026-11 — INC-2026-05's recorded root cause contradicts the code, and 16.3 was designed against it + +- **Date / observed by:** 2026-08-09 / clean-room deploy `linux-x86_64`, slice 17.2a +- **Severity:** med +- **Category (§4.4):** user-understanding _(the "user" here is the maintainer reading the system: the + number was right and the cause was read wrong — §12's warning, applied one level up)_ +- **Verification tier:** `VM-verified` (WSL2) +- **What happened:** 17.2a's plan predicted, from INC-2026-05, that a **static** pre-existing session + file would yield **zero** events until it grew, and warned that skipping the mid-run append would + record a false "capture is broken on Linux". The opposite was measured: with a static seeded file + and **no append**, the clean-room archive held `raw=2, events=8` within 12 s of `watch` starting. +- **User impact:** None directly — capture working *more* eagerly than documented harms nobody. The + impact is on **the slice built on top of it**: INC-2026-05 is quoted verbatim as the acceptance + criterion of 16.3's capture-health scorecard, and a scorecard designed to explain a cursor-at-EOF + behaviour that does not exist is answering a question the product never asked. +- **Root cause:** INC-2026-05 states "the file watcher cursors pre-existing files at EOF (correct — + it must not re-ingest history on every start)". The code says otherwise: + `apps/collector/src/watcher/file-watcher.ts:136` reads `const fromOffset = cursor?.byteOffset ?? 0` + — a file with **no saved cursor starts at offset 0**, so the whole file is ingested on first sight. + That line was introduced in `d02f390` (**2026-06-13**, M3) and the file's last change before the + 16.0 clean room was `990930b` (**2026-06-20**), so the behaviour asserted in INC-2026-05 was + already false on the day it was written. **What 16.0 actually observed is therefore + undiagnosed** — the leading candidate is that its two seeded files did not match any connector's + glob (its `discover` reported `0 workspaces, 0 projects`), which would produce the same symptom for + an entirely different reason and a different fix. +- **Evidence:** the counts above from the 17.2a run; `file-watcher.ts:136` verbatim; the two `git + log` outputs establishing the line's age; and the contrast that 17.2a's own four seeded files each + produced a `file_cursors` row (4) with `queue_items` drained to 0. +- **Disposition:** **backlog → re-derive INC-2026-05 before 16.3's regression test is written.** + Not fixed here (D-M17-5), and deliberately raised as a NEW entry citing INC-2026-05 rather than + edited into it — the known-defect rule's "behaves differently is the finding" case. INC-2026-05's + **symptom** stands as observed; its **root cause** line should be treated as unverified until + someone reproduces it with the seeded paths recorded. +- **Regression test?** No — this is a defect in an incident record, not in code. The durable guard is + the one 16.3 already owes: a behavioural test that a collector which has seen no activity reports a + state distinguishable from a failing connector. Writing that test is what would have caught the + mismatch, because it cannot be written without deciding what the watcher actually does. + +## INC-2026-10 — `npm run setup` aims the TEST database handles at whatever cluster is already on the host + +- **Date / observed by:** 2026-08-09 / clean-room deploy `linux-x86_64`, slice 17.2a +- **Severity:** med +- **Category (§4.4):** capture _(tooling/environment — closest §4.4 fit, as INC-2026-06)_ +- **Verification tier:** `VM-verified` (WSL2). Mechanism **demonstrated, deliberately not fired.** +- **What happened:** `npm run setup` on a fresh second checkout generates a `.env` in which **every** + database handle points at `localhost:5433` — including `DATABASE_URL_TEST` and + `DATABASE_URL_TEST_APP`. On a machine that already runs 420AI, 5433 is the **real** archive + cluster, and it does own a `420ai_test` database. `vitest.config.ts:7` loads that `.env`; 61 + `*.int.test.ts` files gate only on `!TEST_URL`; and their `beforeEach` opens with statements like + `TRUNCATE invites, …, raw_source_records, events, ingest_tokens, pairing_codes, machines, + memberships, organizations, users RESTART IDENTITY CASCADE`. A single `npx vitest run` in the + second checkout — the command the spike protocol itself prescribes — would have truncated the real + test database. +- **User impact:** Silent, destructive, and aimed at exactly the people most likely to trigger it: a + clean room, a second machine, a staging checkout, or anyone evaluating a PR in a second worktree. + Nothing warns, because from the tooling's point of view the configuration is correct — 5433 *is* + the documented port. The blast radius is the test database rather than the archive, so it is + recoverable by `db:migrate`, which is the only reason this is med and not high. +- **Root cause:** `scripts/setup-env.mjs` copies `.env.example`'s literal defaults rather than + deriving from anything host-specific, and `.env.example` hardcodes `localhost:5433` on four + handles. This is **INC-2026-04's shape** (setup bakes in the *first* installation's coordinates) + with a destructive rather than a merely confusing consequence, and a **sibling of INC-2026-06** + rather than a repeat of it: 06 is a role crossing a database boundary, this is a generated config + aiming a second checkout at the first one's cluster. +- **Evidence:** the generated `.env` verbatim (four handles on `:5433`); `select datname from + pg_database` on the real archive returning both `420ai` and `420ai_test`; `vitest.config.ts:7`; + the `TRUNCATE` statements with their file:line; and the teardown check confirming + `tables_in_420ai_test = 33` — i.e. the hazard was shown, not fired, because the handles were + blanked before any vitest run. +- **Disposition:** **backlog → tooling fix.** Not fixed here (D-M17-5). Two candidate shapes, and the + second is the durable one: (a) have `setup-env.mjs` refuse to write test handles pointing at a + cluster it did not create, or (b) make the int-test bootstrap **assert the target database is not + the one `DATABASE_URL` points at on a foreign cluster** before it truncates — a guard at the + destructive step rather than at the config step, which is the only place that cannot be bypassed by + hand-editing `.env`. +- **Regression test?** Yes, and it is cheap: a unit test on `setup-env.mjs` asserting the generated + test handles are derivable from the root `.env` rather than literal (this also pins INC-2026-04's + dashboard `INGEST_URL`, which is the same defect on a different key). + +## INC-2026-09 — `build-sea.mjs` reports **PASS** on a non-Windows triple while producing an unbundlable, mislabelled artifact + +- **Date / observed by:** 2026-08-09 / clean-room deploy `linux-x86_64`, slice 17.2a +- **Severity:** med +- **Category (§4.4):** capture _(tooling/environment — the build/packaging surface; closest §4.4 fit, + as INC-2026-06)_ +- **Verification tier:** `VM-verified` (WSL2) +- **What happened:** `node apps/collector/scripts/build-sea.mjs` run end-to-end on `linux-x86_64` + completed **all four steps**, exited **0**, and printed `build-sea: PASS`. It produced a 118 MB + `ELF 64-bit LSB executable, x86-64 … for GNU/Linux 3.2.0` at + `apps/desktop/src-tauri/binaries/collector-x86_64-pc-windows-msvc.exe`. The artifact **works** — + piping `{"cmd":"status"}` into it returns `{"type":"ready"…}` / `{"type":"status"…}` — but Tauri's + `externalBin` resolves by `-$TARGET_TRIPLE` suffix and on this target looks for + `collector-x86_64-unknown-linux-gnu`, which does not exist. The artifact is simultaneously + mislabelled, invisible to the bundler that consumes it, and reported as a success. +- **User impact:** A cross-platform build that says PASS and silently yields nothing the packager can + use. It is the repo's recurring shape — a step that reports success while producing a wrong + artifact — and it is worse than the loud failure 17.0's protocol predicted (line 86), because a + loud failure is self-diagnosing. +- **Root cause:** `apps/collector/scripts/build-sea.mjs:32` pins + `const TARGET_TRIPLE = "x86_64-pc-windows-msvc"` and `:39` appends a literal `.exe`; + `copyFileSync(process.execPath, outExe)` at `:123` has no reason to fail on Linux, so the wrong + name is applied to a correct binary. **The rest of the recipe is fully portable, and this is the + finding 17.3 should act on**: esbuild (137 KB bundle), `--experimental-sea-config`, the + `process.execPath` copy and `postject` injection into an ELF all work unmodified. `postject` + emitted ELF-specific warnings (`Can't find string offset for section name '.note.100'`) rather than + the Windows "signature seems corrupted" warning the script's own comment says to expect; injection + succeeded regardless. +- **Evidence:** full script output including `build-sea: PASS` and `SEA_FULL_RC=0`; `file(1)` type + string and 123,866,304-byte size; the `collector-x86_64-unknown-linux-gnu` lookup returning NOT + FOUND; the successful `serve` smoke-run of the produced artifact; `--check` passing in 164 ms. +- **Disposition:** **earns a slice — 17.3**, which owns un-pinning the triple. Not fixed here + (D-M17-5); un-pinning it inside a measurement slice would convert the evidence 17.3 is being + re-scored on into a guess. The measurement **raises** 17.3's confidence: the portability risk is + one constant and one file extension, not the SEA mechanism. +- **Regression test?** Yes, and it is the cheap half of 17.3: assert the emitted artifact's basename + equals `collector-${rustTargetTriple()}` for the host, with no `.exe` off Windows — negative-control + it by forcing the host triple and checking the assertion fails. + +## INC-2026-08 — `build-sea.mjs --check` boots the collector against the operator's REAL home, breaking single-owner + +- **Date / observed by:** 2026-08-09 / clean-room deploy `linux-x86_64`, slice 17.2a +- **Severity:** med +- **Category (§4.4):** capture +- **Verification tier:** `VM-verified` (WSL2) +- **What happened:** `node apps/collector/scripts/build-sea.mjs --check` created `~/.420ai/` in the + **real** collector home — `queue.sqlite` (40 KB) and `connector-approvals.json` — on a run whose + every collector invocation had otherwise been confined with `--home `. The directory was + verified ABSENT twice before the run and appeared with mtimes `13:00:33.848` / `13:00:33.880`, the + exact second the build script executed. +- **User impact:** Two, and the first is the serious one. **(a)** CLAUDE.md states the invariant + plainly — only **one** collector may own a given `queue.sqlite`, and a service plus a desktop + "Run on login" is a double-writer bug. On a machine where the collector is running, the documented + build command and the verification line the script prints boot a **second** collector process + against the live queue. **(b)** It silently contaminates any isolated run: this breach is the one + isolation constraint of 17.2a that did not hold, and it arrived through a dimension the checklist + did not name. That is 16.0's own general lesson, re-earned one level up — the checklist enumerated + the resources the plan could see, and a build script is not one of them. +- **Root cause:** `build-sea.mjs:80` runs `execFileSync(process.execPath, [cjs, "serve"], …)` with no + `--home`, so `serve` resolves `homedir()` (`resolveHome` = `getFlag(args,"--home") ?? homedir()`, + `apps/collector/src/cli.ts:631`) and `QueueStore` `mkdir`s its parent. The same defect is in the + success message at `:144`, which tells the operator to run + `echo '{"cmd":"status"}' | "" serve` — also without `--home`. The smoke test is *correct* to + boot the real entrypoint; it is wrong to boot it into the real home. +- **Evidence:** `ls -la --time-style=full-iso ~/.420ai` showing the two files at `13:00:33.8…` + against a baseline verified ABSENT at 16:28:00Z and again at pairing time; `build-sea.mjs:74-84` + and `:144` verbatim; the run's other isolation checks (credentials hash, machine/token identity, + role list, `/proc` fd sampling) all passing, which is what makes the mechanism unambiguous. +- **Disposition:** **REPAIRED IN PLACE under the standing D-16.0-2 / D-M17-5 exception** — a defect + in the isolation the measurement itself depends on may be fixed so the spike's evidence is + trustworthy — and recorded here rather than hidden. The repair was to the **state**, not the code: + `rm -rf ~/.420ai`, restoring the verified-absent baseline. **No product code was changed**, so the + script still ships the defect and it **earns a slice — 17.3**, alongside INC-2026-09 (same file, + same slice, and the fix is one flag on two call sites plus the printed message). +- **Regression test?** Yes: assert `build-sea.mjs --check` runs `serve` with an explicit + `--home ` and that the operator's `homedir()`-derived collector home is untouched + afterwards — negative-controlled by removing the flag, which is exactly how this was found. ## INC-2026-07 — capture kept working for 8 days while DELIVERY was dead, and every surface said fine @@ -209,6 +458,10 @@ evidence that earns a fix under §2's scope-change rule. mentions either**. `npm install` does not build workspaces. - **Evidence:** full stack trace in the run; recovery was `npm run build` (8 s), after which ingest progressed to the next failure (INC-2026-03). + **Reproduces on `linux-x86_64`** (2026-08-09, slice 17.2a, tier `VM-verified`): byte-identical + `ERR_MODULE_NOT_FOUND` for `…/node_modules/@420ai/db/dist/index.js`, immediate; recovery + `npm run build` (9.5 s); `grep -c 'npm run build' docs/guide/quickstart.md` → still **0**. Platform + -independent, so fixing the guide fixes onboarding everywhere. - **Disposition:** backlog → doc fix. Add a build step to quickstart step 1 or 2. Cheap, but **not fixed here** (D-16.0-2) — it is onboarding friction, which is the thing being measured. - **Regression test?** No. The durable fix is not a test but a **smoke script** that runs the @@ -230,6 +483,10 @@ evidence that earns a fix under §2's scope-change rule. string at all (`grep -c provision-app-role docs/guide/quickstart.md` → **0**). The tool knows; the guide does not. Two sources of truth for the setup sequence, disagreeing. - **Evidence:** ingest log with the `28P01` payload; the grep above; recovery took 2 s once known. + **Reproduces on `linux-x86_64`** (2026-08-09, slice 17.2a, tier `VM-verified`): same `28P01`, + `severity: 'FATAL'`, `file: 'auth.c'`, `line: '329'`, `routine: 'auth_failed'` for `420ai_app`; + recovery `npm run db:provision-app-role` (0.7 s). The two-sources-of-truth root cause reproduces + too: `grep -c provision-app-role docs/guide/quickstart.md` → **0**, `scripts/setup-env.mjs` → **2**. - **Disposition:** backlog → doc fix, together with INC-2026-02. The deeper fix is to stop maintaining the sequence in two places — have the guide reference the setup script's output, or the script reference the guide. **Not fixed here** (D-16.0-2). @@ -252,6 +509,10 @@ evidence that earns a fix under §2's scope-change rule. - **Root cause:** the generated dashboard env takes the default rather than deriving from the repo-root `.env`'s `INGEST_URL`/`INGEST_PORT`. - **Evidence:** generated `.env.local` contents; corrected by hand to 8999 during the run. + **Reproduces on `linux-x86_64`** (2026-08-09, slice 17.2a, tier `VM-verified`): the generated + `apps/dashboard/.env.local` again carried a literal `INGEST_URL=http://localhost:8420`; corrected + by hand to 8998. See also **INC-2026-10**, the same defect shape on the root `.env`'s test handles, + where the consequence is destructive rather than confusing. - **Disposition:** backlog. **Not fixed here** (D-16.0-2). Note it is adjacent to, but not the same as, the F1/F2 class — it did not compromise this exercise's safety conclusion, because the write-side isolation checks all passed independently. @@ -276,6 +537,13 @@ evidence that earns a fix under §2's scope-change rule. "0 sessions seen yet, waiting for activity" state anywhere in the CLI output or the Monitor. - **Evidence:** watch log; archive at 0 events after the first pass; events appeared only after a session file was added mid-run. + **DID NOT reproduce on `linux-x86_64`** (2026-08-09, slice 17.2a, tier `VM-verified`): a static + pre-existing session file was captured on the first tick with **no append** (`raw=2, events=8` + within 12 s). The "cursors pre-existing files at EOF" root cause above **contradicts + `watcher/file-watcher.ts:136`** (`const fromOffset = cursor?.byteOffset ?? 0`), which has read that + way since 2026-06-13 — i.e. it was already false when this entry was written. See **INC-2026-11**: + the symptom stands as observed, the root cause line must be treated as unverified, and 16.3's + regression test should not be written against it until it is re-derived. - **Disposition:** **this is 16.3's headline case.** Not a bug to fix in isolation — it is the acceptance criterion of the capture health scorecard, quoted verbatim in `.agents/plans/m16-dogfood-instrumentation.md` §7 P0.1. Recorded here so 16.3 has a concrete, diff --git a/SUMMARY.md b/SUMMARY.md index 5ff1069..bd3ff3d 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -107,7 +107,9 @@ for a 24-week research period**. It built only what makes that period's evidence **M17 (Cross-platform collectors)** is the **ACTIVE** milestone — promoted 2026-08-07 by the deferral audit + scope conversation, on the same criterion and the first answer that is not someone the maintainer can watch over the shoulder: **anyone running this on hardware he does not own**. -That makes **verification, not implementation**, the binding constraint. Sliced **17.0–17.7**. See +That makes **verification, not implementation**, the binding constraint. Sliced **17.0–17.7**, with +17.2 split into **17.2a** (Linux VM, ✅ done) and **17.2b** (real hardware, **blocked on +procurement**) rather than letting a VM spike close the number reserved for a hardware one. See [`.agents/plans/m17-cross-platform-collectors.md`](./.agents/plans/m17-cross-platform-collectors.md). **M18–M20 remain committed and unsequenced.** @@ -343,10 +345,21 @@ through the deferral-audit + scope conversation that produced M12/M13/M14. Full function of `(home, platform)`, closing INC-2026-01 (`--home` did not confine the poll connector, so a clean-room run ingested 16,916 real records) and the off-Windows relative-path defect in one arrow function; the §10.4 fingerprint becomes argument- rather than environment-dependent, and a - registry-wide two-home guard stops the next poll connector repeating it · **17.2** SPIKE — truth on real - hardware · **17.3** Multi-target SEA build · **17.4** Service install (launchd + systemd) · - **17.5** Desktop shell portability · **17.6** Installers + updater feed + release lane · - **17.7** Cross-platform UAT + sign-off. Decisions **D-M17-1…6** are settled in the plan. + registry-wide two-home guard stops the next poll connector repeating it · + ✅ **17.2a** SPIKE — truth on a Linux VM — DONE 2026-08-09 (PR #NN): `linux-x86_64` clean room at tier + **`VM-verified`** proved capture end-to-end (3 raw / 16 events), `--home` confinement on POSIX + (0 paths outside the given home) and a read-side check 16.0 never made (1120 `/proc/fd` samples); + the documented path breaks in the **same two places as Windows** with byte-identical errors; and + `build-sea.mjs` **exits 0 printing `PASS`** while emitting an unbundlable ELF named `.exe`. Raised + **INC-2026-08…13**, one of them a breach the spike caused itself · **17.2b** SPIKE — truth on real + hardware — **BLOCKED ON PROCUREMENT** (Intel Mac, Gatekeeper, real arm64, systemd, `webkit2gtk`, + installers, the Cursor darwin/linux path convention) · **17.3** Multi-target SEA build · + **17.4** Service install (launchd + systemd) · **17.5** Desktop shell portability · + **17.6** Installers + updater feed + release lane · **17.7** Cross-platform UAT + sign-off. + Decisions **D-M17-1…6** are settled in the plan. **17.2a re-scored 17.3 up (65% → 80%) and + explicitly did NOT re-score 17.4/17.5/17.6** — a VM cannot see an init system, a desktop session + or an installer, and attaching a number to them anyway would be the inference the spike exists to + replace (`emulated ≠ hardware-verified`). **Planned off a reconnaissance sweep, which changed the milestone's shape.** The collector's TS core is **already portable** and must not be re-litigated: the watcher **polls** rather than using `fs.watch` (`watcher/file-watcher.ts:9-12`, a Windows-motivated decision that happens to make every @@ -1267,6 +1280,31 @@ records` and `0 ÷ 4000` are opposite facts. An HTTP test asserts the literal st negative-controlled by reintroducing the bug. Deliberate consequence (**D-17.1-3**): the platform-neutral permission prose moves the fingerprint, so each existing install's Cursor connector flips to `needs-approval` once — the gate firing on a real surface change. + ✅ **17.2a** DONE 2026-08-09 (PR #NN) — `linux-x86_64` clean-room deploy at tier **`VM-verified` (WSL2), + NOT hardware-verified** + ([`cleanroom-linux-x86_64-2026-08-09.md`](./.agents/research/cleanroom-linux-x86_64-2026-08-09.md)). + Split out of 17.2 because **none of the hardware D-M17-3 names exists**, and shipping a VM run + under the original number would have redefined "17.2 is done" by omission in the one milestone + whose stated binding constraint is verification. Measured: capture end-to-end on POSIX, 17.1's + `--home` confinement (0 declared paths outside the given home), the **read-side** check 16.0's + write-only checks missed, `tsc -b --force` 9.47 s and `1045 | 680` (= 1034 + 17.1's net +11 + tests, cross-checked against the real repo's `1725 | 0`), and the documented path breaking in + the **same two places as Windows** with byte-identical errors — so INC-2026-02/03 are + onboarding defects, not Windows quirks. Raised **INC-2026-08…13**: `build-sea.mjs` prints + `PASS` while emitting an ELF named `…-windows-msvc.exe` that Tauri's `externalBin` cannot find + (09) and boots `serve` into the operator's **real** collector home (08 — a double-writer + hazard, and the one isolation constraint this run breached, repaired in place under D-16.0-2); + `npm run setup` aims the test handles at whatever cluster is already on the host, so a second + checkout's `npx vitest run` would TRUNCATE the real test DB (10 — demonstrated, not fired); + **INC-2026-05's recorded root cause contradicts `file-watcher.ts:136`** and 16.3 was designed + against it (11); a nonexistent connector source leaves **no trace anywhere** (12); and WSL + interop fabricated a plausible wrong value in the transcript (13). **17.3 re-scored 65% → + 80%** (the SEA mechanism is fully portable; only a pinned constant is wrong); + **17.4/17.5/17.6 deliberately NOT re-scored** — systemd is off in default WSL2, + `webkit2gtk-4.1` is missing, and installers/Gatekeeper need hardware. + ⏸ **17.2b** BLOCKED ON PROCUREMENT — Intel Mac, Gatekeeper/notarization, real `linux-arm64` + silicon, systemd, `webkit2gtk`, installers + updater feed, the Cursor `darwin`/`linux` path + **convention** (`cursor-store.ts:11-13`, still undischarged), and a human-run §5.2 repeat. - [ ] **M18–M20 remain committed scope, unsequenced** (§3, PRD §25). Each still needs its own deferral-audit + scope conversation before it is executable. (**M20** is Cloud-hosted SaaS, renumbered from M16 on 2026-08-02 by slice 16.0 — same scope, new number.) From 8744e35b8418c60697a517c66407651ccfd6a7d5 Mon Sep 17 00:00:00 2001 From: Sean Wright Date: Sun, 9 Aug 2026 13:56:40 -0400 Subject: [PATCH 2/5] docs(m17): 17.2a code-review triage -- report accuracy, and the one source edit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the code review and execution report, and fixes all four review findings (maintainer chose "fix everything" at the triage gate). Report accuracy, all in the 17.2a clean-room report: - "1120 fd samples" conflated sampling ROUNDS with path OBSERVATIONS. The loop is 160 rounds at 4 Hz over 40 s and each round records every open path, so 1120 is 7 observations x 160 rounds -- the original phrasing overstated temporal coverage 7x, in the one number the read-side evidence rests on. Restated at all four sites (report x2, milestone plan, SUMMARY). The correction makes the existing "a read-and-close inside one 250 ms window can be missed" caveat MORE load-bearing, which is the honest direction. - Scoped the headline row to "Real WINDOWS credentials / archive modified". It was accurate but unscoped, and the run did create a real WSL-side collector home (INC-2026-08, stated two rows down) -- the only row that could be quoted misleadingly. - New section 4b derives the capture counts from their inputs (fixture, 4 seeded files, seed/append order, file_cursors=4) and DECLARES raw=3 unexamined rather than leaving a reader to infer a capture gap. It was the one result in the report presented without its derivation. The one source edit, disclosed because it changes what the diff is: - apps/collector/src/connectors/cursor-store.ts jsdoc addressed its measurement debt to "17.2" -- a slice number THIS BRANCH split into 17.2a (done) and 17.2b (blocked). A reader would see "17.2", find 17.2a marked done, and conclude the debt is discharged: the exact misreading the report warns about, at the one place the decision gets made. Now says 17.2b, and separates CONFINEMENT (verified by 17.2a on linux-x86_64) from CONVENTION (still unmeasured on darwin and linux). Comment-only. No executable product code changed and no defect this slice MEASURED was fixed -- INC-2026-08..12 all stand (D-M17-5). Verified the edit cannot move the §10.4 capture-surface fingerprint: captureSurfaceFingerprint hashes watchGlobs, requiredPermissions, poll.sources and push.origins -- runtime values, never source text -- so no install flips to needs-approval (the D-17.1-3 hazard, checked). Co-Authored-By: Claude Opus 5 (1M context) --- .../m17-slice2a-linux-vm-spike.md | 151 +++++++++++ .../m17-slice2a-linux-vm-spike.md | 235 ++++++++++++++++++ .../plans/m17-cross-platform-collectors.md | 2 +- .../cleanroom-linux-x86_64-2026-08-09.md | 32 ++- SUMMARY.md | 3 +- apps/collector/src/connectors/cursor-store.ts | 6 +- 6 files changed, 420 insertions(+), 9 deletions(-) create mode 100644 .agents/code-reviews/m17-slice2a-linux-vm-spike.md create mode 100644 .agents/execution-reports/m17-slice2a-linux-vm-spike.md diff --git a/.agents/code-reviews/m17-slice2a-linux-vm-spike.md b/.agents/code-reviews/m17-slice2a-linux-vm-spike.md new file mode 100644 index 0000000..4a396a0 --- /dev/null +++ b/.agents/code-reviews/m17-slice2a-linux-vm-spike.md @@ -0,0 +1,151 @@ +# Code review — M17 slice 17.2a (Linux VM spike) + +**Reviewed:** commit `66bf73c` on `m17-slice2a-linux-vm-spike`, against +[`.agents/plans/m17-slice2a-linux-vm-spike.md`](../plans/m17-slice2a-linux-vm-spike.md). + +**Stats:** + +- Files Modified: 3 +- Files Added: 2 +- Files Deleted: 0 +- New lines: 1532 +- Deleted lines: 21 + +## What was reviewable here + +The diff is **docs-only** — 0 source files, which is itself an acceptance criterion (D-M17-5: the +slice measures, it does not fix; verified with +`git status --porcelain | grep -E '\.(ts|tsx|mjs|js|json|sql|yml|toml|rs)$'` → 0 matches). The usual +review axes (logic errors, injection, N+1, memory) have no surface. What a measurement slice can get +**wrong** is different, and that is what was checked: + +1. **Factual accuracy** — every quoted number re-derived from the run that produced it. +2. **Citation correctness** — every `file:line` opened and confirmed. +3. **Internal consistency** — the same figure across report, incidents, milestone plan and SUMMARY. +4. **The honesty gates** — tier labels, unmeasured-before-measured, no incident-log inflation. +5. **Secret hygiene** — the §3 privacy rule, on a report written from a run that minted live tokens. + +### Checks that passed + +- **Secrets:** 0 occurrences in committed docs of the run's ingest token, push-receiver token, + pairing code, `APP_DB_PASSWORD` or admin password. The one token line quoted in the report is + ``. No hostname/PII (`Living-Room` → 0 hits). +- **Citations, all 10 opened and confirmed:** `file-watcher.ts:136`, `build-sea.mjs:32/:39/:80/:123/:144`, + `cli.ts:631`, `vitest.config.ts:7`, `cursor-store.ts:11-13`, `capture-engine.ts:329-340`. The last + one is *stronger* than the report claims — `outcome` is read only under `if (outcome.changed > 0)` + and `opts.onSuccess?.(opts.connector)` runs unconditionally after the loop, so INC-2026-12's + mechanism is visible in the cited lines rather than merely asserted. +- **Cross-doc links:** all 5 resolve. +- **Markdown structure:** the milestone slice table is 5 columns on every one of its 11 rows + (17.2a/17.2b insertion did not break it). +- **Severity tally:** 5 med + 1 low across INC-2026-08…13, matching the report's headline claim. +- **Known-defect rule:** no new ID duplicates INC-2026-02…05; INC-2026-11 explicitly cites + INC-2026-05 as a *difference*, which is the rule's prescribed shape. +- **`--force` discipline:** the report reports `tsc -b --force` (9.47 s) as the trustworthy number + and flags the 204 ms incremental run as the false-green shape, per CLAUDE.md. + +--- + +## Resolution (triage gate, 2026-08-09) + +The maintainer chose **"Fix everything"** — all 4 findings fixed, including finding 2's source edit, +against my recommendation to defer it. Consequence, recorded rather than left to be inferred: **the +diff is no longer docs-only.** `apps/collector/src/connectors/cursor-store.ts` now carries a +comment-only change, and the execution report's Meta section was corrected accordingly (it had +claimed 0 source files). No executable product code changed, and no defect this slice **measured** +was fixed — INC-2026-08…12 all stand. + +| # | Severity | Issue | File | Disposition | What was done | Status | +| --- | -------- | ---------------------------------------------- | ----------------------------------------------------- | ----------- | -------------------------------------------------------------------------------------------------------------- | ------ | +| 1 | Medium | "1120 fd samples" conflates rounds with observations | `cleanroom-linux-x86_64-2026-08-09.md:287` (+ plan, SUMMARY) | Fix | Restated as "160 rounds at 4 Hz over 40 s, 1120 fd observations (7/round)" at all 4 sites | Fixed | +| 2 | Medium | jsdoc addresses its debt to the now-split "17.2" | `apps/collector/src/connectors/cursor-store.ts:12` | Fix | → "17.2b", plus two sentences separating confinement (verified by 17.2a) from convention (still not measured) | Fixed | +| 3 | Low | "Real credentials / archive modified" unscoped | `cleanroom-linux-x86_64-2026-08-09.md:54` | Fix | Label scoped to "Real **Windows** credentials / archive" | Fixed | +| 4 | Low | capture counts given without their inputs | `cleanroom-linux-x86_64-2026-08-09.md:45` | Fix | New §4b derives the counts (fixture, 4 files, seed/append order) and **declares `raw = 3` unexamined** | Fixed | + +## Findings + +``` +severity: medium +file: .agents/research/cleanroom-linux-x86_64-2026-08-09.md +line: 287 +issue: "1120 fd samples" conflates sampling ROUNDS with path OBSERVATIONS, overstating temporal coverage 7x +detail: The producing loop is `for i in $(seq 1 160)` with `sleep 0.25` — 160 rounds over 40 s at + 4 Hz — and each round appends every currently-open `/`-prefixed fd path. The reported 1120 is + `wc -l` of the accumulated file, and 1120/160 = exactly 7 paths per round. So there were 160 + samples in time, not 1120. The report leans on this figure as the strength of its read-side + evidence ("Holds — 1120 fd samples") in a document whose thesis is that summaries are not + evidence, and it already carries a caveat that a read-and-close inside one 250 ms window can be + missed. Stating the number correctly makes that caveat MORE load-bearing, which is the honest + direction. The figure has propagated to three further sites. +suggestion: Rephrase as "160 sampling rounds over 40 s at 4 Hz, yielding 1120 open-fd observations" + at :287, and shorten the table cells at :48 and :92 to "160 rounds / 1120 fd observations". + Same edit in `.agents/plans/m17-cross-platform-collectors.md:144` and `SUMMARY.md:351`. +``` + +``` +severity: medium +file: apps/collector/src/connectors/cursor-store.ts +line: 12 +issue: the jsdoc addresses its measurement debt to "17.2", a slice number this commit split +detail: The comment reads "to be hardware-verified in 17.2 — NOT yet measured." This commit splits + 17.2 into 17.2a (✅ DONE) and 17.2b (blocked on procurement), and 17.2a verified the CONFINEMENT + half while explicitly NOT discharging the CONVENTION half. A reader who opens this file, sees + "17.2", and checks SUMMARY finds a 17.2a marked done — and concludes the debt is discharged. That + is precisely the misreading the report spends a paragraph warning against ("the closure of + INC-2026-01 must not be read as closure of this"), except the warning lives in the report and the + stale pointer lives at the site where the decision would actually be made. + NOTE FOR TRIAGE: fixing this edits product source, and "No product code changed (D-M17-5)" is an + acceptance criterion of this slice. A one-word reference correction is arguably documentation + rather than a fix, but it is the user's call, not mine. +suggestion: Either (a) change "17.2" to "17.2b" in the jsdoc and note in the execution report that + the only source edit was a slice-number reference, not a behaviour change; or (b) leave it and + accept that the plan + report carry the boundary. Option (a) puts the correction where the + misreading happens. +``` + +``` +severity: low +file: .agents/research/cleanroom-linux-x86_64-2026-08-09.md +line: 54 +issue: the headline row "Real credentials / archive modified — No" is unscoped, while the run DID create a real collector home +detail: The row is accurate as written (it covers the Windows `credentials.json` hash, the archive's + machine/token identity and org/user counts, all verified identical). But the run also created + `~/.420ai` in the real WSL home — a real collector home — via INC-2026-08. Row 56 states the breach + two lines later and the isolation table's last row marks it ❌, so the document as a whole is + honest. The risk is a reader who quotes row 54 alone. +suggestion: Scope the label: "Real **Windows** credentials / archive modified". Costs four + characters and removes the only row that can be quoted misleadingly. +``` + +``` +severity: low +file: .agents/research/cleanroom-linux-x86_64-2026-08-09.md +line: 45 +issue: the capture evidence reports outputs (3 raw / 16 events) without stating the inputs that produced them +detail: The report's Level-3 claim is "capture works end-to-end", and counts > 0 is exactly what the + plan required, so the conclusion is sound. But the counts are given with no account of the input — + four session files were seeded (one pre-`watch` and static, three written mid-run), each two lines + of the repo's `sample-session.jsonl` fixture. Without that, `raw = 3` from four files reads as an + unexplained shortfall, and a reader cannot sanity-check the numbers at all. In a report that + states "summaries are not evidence" and quotes its own commands verbatim elsewhere, this is the + one result presented without its derivation. +suggestion: Add one line to the "what WAS measured" section or the §5 finding: which fixture, how + many files, how many lines, and that per-file raw-record accounting was not part of the + measurement — so the shortfall is declared unexamined rather than left to be inferred. +``` + +--- + +## Not raised, and why + +- **The `git log -S` evidence for INC-2026-11.** `-S` finds when a *string* was added and can be + fooled by reformatting. It is paired here with `git log --until=2026-08-02 -1` showing the file + untouched between 2026-06-20 and 2026-08-02, which closes the gap. The report also correctly + refuses to claim 16.0's actual cause, marking it "undiagnosed" and offering the glob-mismatch + candidate as a candidate. Appropriately hedged; no finding. +- **`≈ 64 s` time-to-first-capture.** Re-added from the timing table (1.13 + 30.15 + 0.12 + 1.52 + + 9.54 + 0.70 + 2.07 + ~6 + 0.06 + 0.02 + 0.51 + 0.36 + ~12 = 64.2), isolation-only steps excluded + as 16.0 did. Consistent, and hedged by four honesty caveats. +- **The two operator errors and the failed intermediate `repo-health` run** are disclosed in the + report rather than smoothed over, including the discriminator (`ECONNREFUSED` ≠ `28P01`) that + rules out a leak. This is the behaviour the process wants; no finding. diff --git a/.agents/execution-reports/m17-slice2a-linux-vm-spike.md b/.agents/execution-reports/m17-slice2a-linux-vm-spike.md new file mode 100644 index 0000000..d9e1013 --- /dev/null +++ b/.agents/execution-reports/m17-slice2a-linux-vm-spike.md @@ -0,0 +1,235 @@ +# Execution report — M17 slice 17.2a (Linux VM spike) + +## Meta + +- **Plan file:** [`.agents/plans/m17-slice2a-linux-vm-spike.md`](../plans/m17-slice2a-linux-vm-spike.md) +- **Commit:** `66bf73c` on `m17-slice2a-linux-vm-spike` +- **Files added:** + - `.agents/plans/m17-slice2a-linux-vm-spike.md` (the plan itself, previously untracked) + - `.agents/research/cleanroom-linux-x86_64-2026-08-09.md` + - `.agents/code-reviews/m17-slice2a-linux-vm-spike.md` + - `.agents/execution-reports/m17-slice2a-linux-vm-spike.md` (this file) +- **Files modified:** + - `.agents/research/incidents.md` + - `.agents/plans/m17-cross-platform-collectors.md` + - `SUMMARY.md` +- **Lines changed:** +1532 −21 (commit `66bf73c`), plus the review/report files and the triage fixes +- **Source files changed: 1, comment-only** — `apps/collector/src/connectors/cursor-store.ts` jsdoc. + See "D-M17-5 and the one source edit" below. **No executable line of product code was changed**; + every defect this slice measured (INC-2026-08…12) was left standing. + +### D-M17-5 and the one source edit + +The plan's acceptance criteria say "Nothing was fixed. No product code changed (D-M17-5)." The slice +was executed that way, and commit `66bf73c` is a genuinely docs-only diff. The code review then +raised a fifth item that no document could fix from the outside: `cursor-store.ts:12` addresses its +measurement debt to **"17.2"**, a slice number **this commit split** into 17.2a (✅ done) and 17.2b +(blocked). A reader opening that file sees "17.2", checks `SUMMARY.md`, finds a 17.2a marked done, +and concludes the debt is discharged — the exact misreading the report spends a paragraph warning +against, occurring at the one place the decision would actually be made. + +**The maintainer was shown the trade-off at the triage gate and chose to fix it.** The change is a +slice-number reference correction plus two sentences distinguishing confinement (verified) from +convention (not measured). It repairs an inconsistency **this slice created**, rather than fixing a +defect this slice measured — which is the behaviour D-M17-5 exists to prevent. Recorded here in full +so the deviation is visible rather than inferred from a diff. + +## Validation Results + +| Gate | Result | +| ------------------------------------------- | --------------------------------------------------------------- | +| Syntax & Linting (`prettier --check`) | ✓ all changed `.md` clean (CI lints markdown; `repo-health` does not) | +| Type Checking (root `tsc -b`) | ✓ 0 errors · dashboard `tsc --noEmit` ✓ · desktop `tsc --noEmit` ✓ | +| Unit + Integration (`npm run repo-health`) | ✓ **171 files, 1725 passed, 0 skipped** — the DB layer actually RAN | +| Clean-room typecheck (`tsc -b --force`) | ✓ 9.47 s (17.0 `ubuntu-latest` baseline: 10 s) | +| Clean-room tests (`npx vitest run`) | ✓ 1045 passed \| 680 skipped — deviation from 1034 fully explained | +| Level 3 capture proof | ✓ 3 raw / 16 events / 1 machine / 15 heartbeats (pre-teardown) | +| Level 4 honesty gate | ✓ all 7 boxes, checked mechanically | +| `node scripts/check-summary.mjs` | ✓ exit 0 | +| Pre-commit hook (`repo-health --fast`) | ✓ PASS | + +**`skipped ≠ passed`, honestly reported:** the clean-room run shows 680 skipped **by design** — the +plan required `DATABASE_URL_TEST` unset so the comparison baseline stayed valid. The DB-backed layer +was exercised on the **real** repo instead, where `repo-health` ran 1725 tests with **0 skipped**, +and 1045 + 680 = 1725 cross-checks the two runs against each other. + +**`repo-health` was run three times and the middle run FAILED** (559 × `ECONNREFUSED …:5433`). Not a +leak — INC-2026-06's signature is `28P01 auth_failed`, and `ECONNREFUSED` means nothing was +listening. Docker Desktop exited twice this session, taking the real archive down mid-crash-recovery. +Recorded in the report rather than smoothed over. + +## What Went Well + +- **The plan's isolation design held where it mattered, and was provable rather than assertable.** + The separate-*container* decision (C3) produced a stronger proof than 16.0 could offer: distinct + `system_identifier` (7672069298924687404 vs 7650953580965343270) and disjoint role sets, then + `db:provision-app-role` demonstrably landing only in the clean-room cluster. That is INC-2026-06's + prescribed containment, verified working rather than assumed. +- **The ext4 abort-check earned its place immediately.** `/mnt/c` measured CASE-INSENSITIVE and + `~` CASE-SENSITIVE, exactly as the plan predicted, so finding #5 rests on a filesystem check that + ran before anything depended on it. +- **The known-defect rule worked as designed.** INC-2026-02/03/04 reproduced and cost **one line + each** on the existing entries instead of three new IDs. Without it this run would have minted 3 + duplicates and inflated the headline that 17.3–17.6's re-score reads. +- **`build-sea.mjs` turned out to be a re-score UP.** The plan positioned Task 7 as measuring a + defect; what it measured is that every portable part of the SEA recipe works on Linux and only a + hardcoded constant is wrong. That is a better input to 17.3 than a confirmed failure would have + been. +- **Reading the code beat inferring from behaviour, twice.** INC-2026-11 and the +11-test deviation + were both settled by opening the source (`file-watcher.ts:136`) and the history + (`git diff … | grep -c '^\+\s*(it|test)\('` → 12 added, 1 removed), rather than by reasoning about + what the numbers probably meant. + +## Challenges Encountered + +- **The measurement apparatus lied before the product did.** Command substitution inside + `wsl … bash -lc '…'` reported the **Windows** checkout's branch for a WSL clean room whose + `.git/HEAD` read `main`, and a sibling `X=hello; echo "$X"` came back empty. Two anomalies, one + obviously broken (an empty timer) and one *plausible* (a real branch name of the right repo). The + second is the dangerous kind. Everything after that point ran as `bash script.sh`. Raised as + INC-2026-13. +- **The host substrate was unstable in a way the plan could not anticipate.** Docker Desktop exited + twice, once mid-`docker run` and once between validation runs, taking the real archive with it and + requiring a restart plus a wait through Postgres crash recovery (the container also logged a + 189.7 s checkpoint). One `repo-health` run failed purely because of this. +- **16.0's teardown invariants were unusable and had to be redesigned mid-run.** `queue.sqlite`'s + mtime moved twice in six minutes because the **real collector is running on this machine**. An + invariant that always trips is no better than one that never does, so the discriminating + invariants were re-chosen (content hashes, machine/token row identity) and the growing counters + were declared *expected to grow* rather than asserted flat. +- **Two operator errors, both mine, both excluded from the product timing and named in the report:** + `npm run dashboard:dev -- --port 3998` forwards as `next dev 3998`, which Next reads as a + *directory*; and a later `curl` hung 135 s against an ingest process the harness had reaped when + its parent background task was collected. + +## Divergences from Plan + +**D1 — `cargo` is installed; the plan's `cargo MISSING` baseline was wrong** + +- **Planned:** Task 0's baseline asserted `cargo MISSING`, and the plan used it to retire the need + for a Rust toolchain. +- **Actual:** cargo 1.96.0 at `~/.cargo/bin/cargo`. The planning spike had probed with a **non-login** + shell; rustup appends `. "$HOME/.cargo/env"` to `~/.profile`, which only a login shell sources. +- **Reason:** a probe that reports a clean negative for a reason unrelated to the thing being probed. +- **Type:** Plan assumption wrong. The plan's *decision* (do not run `cargo check` — 17.0 already + answered it on `ubuntu-latest`, ✅ 81 s) was right for a stronger reason and is unchanged. + `webkit2gtk-4.1` really is missing. + +**D2 — INC-2026-05 did not reproduce, and its recorded root cause is wrong** + +- **Planned:** Task 6's gotcha warned that a static seeded file yields **zero** events and that + skipping the mid-run append would record a false "capture is broken on Linux". +- **Actual:** the static file was captured on the first tick with no append (`raw=2, events=8`). +- **Reason:** `file-watcher.ts:136` reads `const fromOffset = cursor?.byteOffset ?? 0` — no cursor + means offset 0, i.e. the whole file. Introduced 2026-06-13, file untouched between 2026-06-20 and + the 16.0 clean room, so the claim was already false when INC-2026-05 was written. +- **Type:** Plan assumption wrong (inherited from the incident log). Raised as **INC-2026-11**; the + plan's caution still paid for itself, because it made the discrepancy something to investigate + rather than something to shrug at. + +**D3 — the protocol's `discover`-based isolation check is not performable as written** + +- **Planned:** protocol lines 37–38 / Task 5: run `collector discover --home ` and assert + every printed source path is under the clean-room home. +- **Actual:** `discover` prints **no source paths** — only + `Discovered 0 workspaces, created 0 projects`. Replaced with (a) enumerating the connector + registry's declared capture surface (the same surface `connector-approvals.ts` fingerprints) across + two synthetic homes, and (b) `/proc//fd` sampling of the live `watch` process. +- **Reason:** the protocol was written by 17.0 from inference about what `discover` outputs. +- **Type:** Plan assumption wrong. The substitute is **stronger**: (b) is a direct observation of + reads, which is the read-side check 16.0's write-only checks missed. + +**D4 — `build-sea.mjs` reports PASS rather than mislabelling quietly** + +- **Planned:** correction C1 predicted a "silent mislabel", measured on step [3] in isolation. +- **Actual:** the full script completes all four steps, exits 0, prints `build-sea: PASS`, and the + 118 MB ELF it produces **runs** as a working Linux sidecar — while being invisible to Tauri's + `externalBin`, which looks for `collector-x86_64-unknown-linux-gnu`. +- **Reason:** only step [3] had been measured during planning; the plan explicitly said not to report + the isolated result as a whole-script result, and that instruction was followed. +- **Type:** Better information found. Re-scores **17.3 up (65% → 80%)**. + +**D5 — the run breached its own isolation, via a step of the measurement** + +- **Planned:** four isolation constraints, all expected to hold. +- **Actual:** `build-sea.mjs --check` boots `serve` with no `--home`, creating the operator's real + `~/.420ai`. Caught by Task 8's Invariant 4, diagnosed from mtimes matching Task 7 to the second. +- **Reason:** the isolation checklist enumerated the resources the plan could see; a build script is + not one of them — 16.0's own general lesson, re-earned one level up. +- **Type:** Security/isolation concern. **Repaired in place** under the standing D-16.0-2 exception + (state only: `rm -rf ~/.420ai`; no code changed) and recorded as **INC-2026-08**. + +**D6 — a destructive hazard found that the plan did not anticipate** + +- **Planned:** Task 2 said "do **not** set `DATABASE_URL_TEST`". +- **Actual:** `npm run setup` had **already set it**, to the real cluster on `:5433`, which does own a + `420ai_test`. The real hazard was not "do not set it" but "it is already set, to the real + cluster" — a clean-room `npx vitest run` would have truncated the real test DB. +- **Reason:** the plan reasoned about the comparison baseline; the destructive consequence sits one + step to the side of it. +- **Type:** Plan assumption wrong / security concern. Mechanism **demonstrated, deliberately not + fired**; raised as **INC-2026-10**. + +**D7 — the npm cache was warm and the first clone went untimed** + +- **Planned:** Task 1 required recording cold-vs-warm separately, because 16.0's local-path clone was + unrepresentative. +- **Actual:** the WSL npm cache was already 2.8 GB (warm), and the first clone's timing + instrumentation failed (`CLONE_WALL_CLOCK= - s`), so the reported 1.13 s is a **re-clone**. +- **Reason:** a shared developer workstation, plus the INC-2026-13 quoting defect. +- **Type:** Other. No cold-cache number is claimed anywhere; caveat 4 in the report says so + explicitly. + +## Skipped Items + +- **`.agents/execution-reports/…` was not written by Task 9** — the plan assigns it to + `/lril:execution-report`, which is this file. Not a skip; a hand-off, now closed. +- **Everything in the plan's "NOT MEASURED" table** — macOS, Gatekeeper, real arm64 silicon, + systemd, `webkit2gtk`, installers/updater, the Cursor path *convention*, and a human-run §5.2 + repeat. Each is skipped **deliberately and with its reason stated**, carried to **17.2b**, and + listed in the report's headline **before** anything that passed. +- **`cargo check`** — not re-measured, per the protocol's "do not re-measure" rule (17.0 answered it + on `ubuntu-latest`, ✅ 81 s). Note this was skipped for the *right* reason even though the stated + reason in the plan (no toolchain) was wrong — see D1. +- **No measured defect was fixed.** INC-2026-08…12 are all left standing. The only in-slice repair + was to *state* (`rm -rf ~/.420ai`, restoring the isolation the measurement depended on), and the + only source edit is the comment-only slice-number correction described under Meta — neither is a + fix to a defect this slice measured. + +## Recommendations + +**Plan-command improvements** + +- **A planning spike's probes must be run the way the execution will run them.** D1's `cargo MISSING` + came from a non-login shell. When a plan bakes a measured baseline into its text, record the exact + command **and its shell mode**, so the executor can re-run it identically instead of inheriting a + conclusion. +- **When a plan cites an open incident's root cause as a prediction, cite the CODE too.** The plan + quoted INC-2026-05's "cursors at EOF" as fact; one `grep` of `file-watcher.ts` at planning time + would have caught it. A plan that predicts behaviour should name the line the prediction comes + from — the same discipline the plan already applies to symbols. + +**Execute-command improvements** + +- **Prefer a script file over a nested shell string whenever a boundary is crossed** (WSL, SSH, + docker exec, remote runners). INC-2026-13 is the general case, and it fabricated a plausible wrong + value rather than an obvious error. +- **Long-lived processes started for a measurement need `setsid`.** Two ingest processes died when + the harness reaped their parent background task, costing a 135 s hang and a re-run. + +**CLAUDE.md additions — two candidates, both earned by this run** + +1. **A "Tooling gotchas (Windows)" entry for the interop boundary.** The section already carries the + Git Bash heredoc and force-with-lease rules; the natural sibling is: *never interpolate `$(…)` or + `$VAR` inside `wsl … bash -lc '…'` — at least one substitution evaluates in the Windows context + and returns a plausible wrong value. Write the script to a file, `tr -d '\r'`, and run + `bash script.sh`.* +2. **A note that non-contamination invariants must be chosen against a LIVE machine.** 16.0's + mtime/row-count checks assume nothing else is running; on the maintainer's workstation the real + collector is. The durable rule: *assert on what a leak would move and normal operation would not + (content hashes, row identities), and declare growing counters as expected-to-grow rather than + asserting them flat.* This is a sibling of `skipped ≠ passed` — an invariant that always trips is + as useless as one that never does. + +A third, narrower, is already captured as an incident rather than a convention: **a build script +must not boot the product against the operator's real home** (INC-2026-08). diff --git a/.agents/plans/m17-cross-platform-collectors.md b/.agents/plans/m17-cross-platform-collectors.md index ffed909..db6e584 100644 --- a/.agents/plans/m17-cross-platform-collectors.md +++ b/.agents/plans/m17-cross-platform-collectors.md @@ -141,7 +141,7 @@ WSL2 Ubuntu 24.04.4, real kernel + real `ext4` + real POSIX `homedir()`, node v2 | ---------------------------------------------------- | --------------------------------------------------------------------------- | | Capture works end-to-end on POSIX | ✅ 3 raw / 16 events / 1 machine / 15 heartbeats in an isolated archive | | `--home` confines on POSIX (17.1's fix) | ✅ 0 declared paths outside the given home; 0 naming `/mnt/c` or `AppData` | -| Read-side confinement (the check 16.0 owed) | ✅ 1120 `/proc//fd` samples — only clean-room files ever open | +| Read-side confinement (the check 16.0 owed) | ✅ 160 `/proc//fd` sampling rounds (1120 observations) — only clean-room files ever open | | `tsc -b --force` / `npx vitest run` | ✅ 9.47 s / ✅ 1045 passed \| 680 skipped (= 1034 + 17.1's net +11 tests) | | Documented path (`quickstart.md`, 13 steps) | ⚠️ breaks in the **same two places as Windows**, byte-identical errors | | `npm install` + blocked `esbuild`/`sharp` postinstall | ✅ exits 0; esbuild works (the plan's named residual risk resolves to no break) | diff --git a/.agents/research/cleanroom-linux-x86_64-2026-08-09.md b/.agents/research/cleanroom-linux-x86_64-2026-08-09.md index 2660bbc..d112fa8 100644 --- a/.agents/research/cleanroom-linux-x86_64-2026-08-09.md +++ b/.agents/research/cleanroom-linux-x86_64-2026-08-09.md @@ -43,15 +43,15 @@ evidence. | Total **mechanical** time to first verified capture | **≈ 64 s** (see the honesty caveat — this is not a human's 30-min budget) | `VM-verified` | | Steps requiring undocumented intervention | **2** (`npm run build`, `db:provision-app-role`) — the same two as Windows | `VM-verified` | | Steps that failed following the guide verbatim | **2** (both halves of quickstart step 3) — identical errors to Windows | `VM-verified` | -| **Capture works end-to-end on POSIX** | **Yes** — 3 raw / 16 events / 1 machine / 15 heartbeats in the clean archive | `VM-verified` | +| **Capture works end-to-end on POSIX** | **Yes** — 3 raw / 16 events / 1 machine / 15 heartbeats, from 4 seeded fixture files (below) | `VM-verified` | | `--home` confinement on POSIX (17.1's fix) | **Holds** — 0 declared paths outside the given home, 0 naming `/mnt/c` | `VM-verified` | -| Read-side isolation (the check 16.0 owed) | **Holds** — 1120 fd samples; only clean-room files ever open | `VM-verified` | +| Read-side isolation (the check 16.0 owed) | **Holds** — 160 sampling rounds / 1120 fd observations; only clean-room files ever open | `VM-verified` | | `tsc -b --force` | **✅ 9.47 s** (17.0 `ubuntu-latest`: ✅ 10 s) | `VM-verified` | | `npx vitest run` | **✅ 1045 passed \| 680 skipped** (baseline 1034 \| 680 — deviation explained below) | `VM-verified` | | `build-sea.mjs` on a non-Windows triple | **Exits 0 and prints `PASS`** while producing a mislabelled, unbundlable ELF | `VM-verified` | | SIGINT graceful drain (POSIX signal path) | **503 ms, clean exit**, queue fully drained | `VM-verified` | | Incidents raised | **6** — INC-2026-08…13 (0 high, 5 med, 1 low) | — | -| Real credentials / archive modified | **No** — content hash, machine id, token id, org/user counts all identical | `VM-verified` | +| Real **Windows** credentials / archive modified | **No** — content hash, machine id, token id, org/user counts all identical | `VM-verified` | | Real repo collaterally broken | **No** — `npm run repo-health` **PASS**, 1725 tests, 0 skipped | `VM-verified` | | Isolation breached during the run | **Yes, once** — by `build-sea.mjs --check` itself (**INC-2026-08**), repaired | `VM-verified` | @@ -89,7 +89,7 @@ product. The wall-clock is a floor, not an estimate. | Separate Postgres **cluster** | Own container `420ai-cleanroom-db` on host **5533** — not a second database | ✅ | | Separate collector home | Every collector call took `--home ~/cleanroom-home` | ✅ | | Separate ports | ingest **8998**, dashboard **3998** | ✅ | -| Read-side confinement | 1120 `/proc//fd` samples over a live `watch` | ✅ | +| Read-side confinement | 160 `/proc//fd` sampling rounds (1120 fd observations) over a live `watch` | ✅ | | **No writes outside the room** | — | ❌ **breached by `build-sea.mjs --check` — INC-2026-08** | **The substrate-specific trap, measured rather than assumed.** `/mnt/c` is DrvFs/9p and @@ -284,7 +284,8 @@ measurement item, now measured. Raised as **INC-2026-12**. 16.0's closing lesson: *"They prove the clean room never **wrote** to real state. They say nothing about **reads** — and INC-2026-01 is a read breach."* This run carried it. While `collector watch` -ran, `/proc//fd` was sampled at 4 Hz for 40 s (1120 samples) against the **real node process**, +ran, `/proc//fd` was sampled against the **real node process** — **160 rounds at 4 Hz over +40 s**, each round recording every open path, for **1120 fd observations** in total (7 per round) — while new session files were written to force filesystem activity: ``` @@ -303,6 +304,27 @@ ASSERT opened under REAL WSL ~/.claude|.codex|.gemini|.config: 0 inside one 250 ms window can be missed. It is strong evidence, not exhaustive proof, and it is complementary to the declared-surface enumeration above rather than a substitute for it. +### 4b. What the capture counts were derived from + +Stated so the numbers can be checked rather than taken. The seed was the repo's own +`apps/collector/src/fixtures/sample-session.jsonl` (4 lines, 1343 bytes, a `claude-code` session), +placed under `/.claude/projects/-home-seanr-cleanroom-proj/`: + +- **1 file** seeded **before** `watch` started, carrying the fixture's **first 2 lines**, left static. +- **3 more files** written **during** the run (2 lines each, `sessionId` rewritten per file) to force + filesystem activity while `/proc//fd` was being sampled. +- The first file was later **appended** with the fixture's remaining 2 lines. + +Final state at teardown: clean-room archive `3 raw / 16 events / 1 machine / 15 heartbeats`; +clean-room `queue.sqlite` `file_cursors = 4` (one per seeded file), `queue_items = 0` (fully +drained), `poll_state = 0`, `connector_errors = 0`. + +**`file_cursors = 4` is the per-file evidence; `raw = 3` is not per-file and was not investigated.** +`raw_source_records` counts ingest-batch records rather than files, so 4 files need not yield 4 rows +(16.0's run produced 539 raw from 2 files). The Level-3 requirement was counts **> 0**, which is met; +per-file raw accounting was never part of this measurement and is **declared unexamined** rather than +implied to be understood. + ### 5. INC-2026-05 did NOT reproduce — and its recorded root cause contradicts the code (INC-2026-11) The plan warned that a static seeded file would produce **zero** events because "the watcher cursors diff --git a/SUMMARY.md b/SUMMARY.md index bd3ff3d..3a3824f 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -348,7 +348,8 @@ through the deferral-audit + scope conversation that produced M12/M13/M14. Full registry-wide two-home guard stops the next poll connector repeating it · ✅ **17.2a** SPIKE — truth on a Linux VM — DONE 2026-08-09 (PR #NN): `linux-x86_64` clean room at tier **`VM-verified`** proved capture end-to-end (3 raw / 16 events), `--home` confinement on POSIX - (0 paths outside the given home) and a read-side check 16.0 never made (1120 `/proc/fd` samples); + (0 paths outside the given home) and a read-side check 16.0 never made (160 `/proc/fd` sampling + rounds, 1120 observations); the documented path breaks in the **same two places as Windows** with byte-identical errors; and `build-sea.mjs` **exits 0 printing `PASS`** while emitting an unbundlable ELF named `.exe`. Raised **INC-2026-08…13**, one of them a breach the spike caused itself · **17.2b** SPIKE — truth on real diff --git a/apps/collector/src/connectors/cursor-store.ts b/apps/collector/src/connectors/cursor-store.ts index 80ae575..35de118 100644 --- a/apps/collector/src/connectors/cursor-store.ts +++ b/apps/collector/src/connectors/cursor-store.ts @@ -9,8 +9,10 @@ import { join } from "node:path"; * - darwin: `/Library/Application Support/Cursor/User/globalStorage/state.vscdb` * - other: `/.config/Cursor/User/globalStorage/state.vscdb` * (darwin/linux rows are convention-derived from the VS Code user-data layout; to be - * hardware-verified in 17.2 — NOT yet measured.) The store holds a `cursorDiskKV` - * key/value table: + * hardware-verified in 17.2b — NOT yet measured. 17.2a verified only that this path is a pure + * function of `(home, platform)` — i.e. CONFINEMENT — on linux-x86_64; Cursor was not installed + * on that target, so the CONVENTION below is still unmeasured on both darwin and linux.) The + * store holds a `cursorDiskKV` key/value table: * - `composerData:` → one row per conversation ("composer") * - `bubbleId::` → one row per message ("bubble") * From 7861280ee6f354d89e8c4e311e2b547ec51a1e6f Mon Sep 17 00:00:00 2001 From: Sean Wright Date: Sun, 9 Aug 2026 13:57:56 -0400 Subject: [PATCH 3/5] docs(m17): 17.2a re-review -- fix an ordering error introduced by the last fix Re-review pass 2 caught a defect in the previous commit's own fix, which is what the code-review -> fix -> code-review loop is for. Section 4b said the seeded session file "was later appended", AFTER the three extra files were written. The real order is the reverse: append at step 3, extra files at step 4. The order is load-bearing -- step 2 -> 3 is the INC-2026-05 evidence (a static file captured with no append), so getting it backwards undermines the finding it exists to support. Rewritten as a stepwise table carrying the archive counts observed at each point. That also exposes something the prose had hidden: step 4 added 3 files and 6 lines, moved events by +6, and moved raw not at all. Now declared unexamined and flagged for 17.2b rather than left invisible. Pass 3 is clean. Co-Authored-By: Claude Opus 5 (1M context) --- .../m17-slice2a-linux-vm-spike.md | 10 +++++++- .../cleanroom-linux-x86_64-2026-08-09.md | 24 ++++++++++++------- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/.agents/code-reviews/m17-slice2a-linux-vm-spike.md b/.agents/code-reviews/m17-slice2a-linux-vm-spike.md index 4a396a0..3adae12 100644 --- a/.agents/code-reviews/m17-slice2a-linux-vm-spike.md +++ b/.agents/code-reviews/m17-slice2a-linux-vm-spike.md @@ -60,7 +60,15 @@ was fixed — INC-2026-08…12 all stand. | 1 | Medium | "1120 fd samples" conflates rounds with observations | `cleanroom-linux-x86_64-2026-08-09.md:287` (+ plan, SUMMARY) | Fix | Restated as "160 rounds at 4 Hz over 40 s, 1120 fd observations (7/round)" at all 4 sites | Fixed | | 2 | Medium | jsdoc addresses its debt to the now-split "17.2" | `apps/collector/src/connectors/cursor-store.ts:12` | Fix | → "17.2b", plus two sentences separating confinement (verified by 17.2a) from convention (still not measured) | Fixed | | 3 | Low | "Real credentials / archive modified" unscoped | `cleanroom-linux-x86_64-2026-08-09.md:54` | Fix | Label scoped to "Real **Windows** credentials / archive" | Fixed | -| 4 | Low | capture counts given without their inputs | `cleanroom-linux-x86_64-2026-08-09.md:45` | Fix | New §4b derives the counts (fixture, 4 files, seed/append order) and **declares `raw = 3` unexamined** | Fixed | +| 4 | Low | capture counts given without their inputs | `cleanroom-linux-x86_64-2026-08-09.md:45` | Fix | New §4b derives the counts and **declares the `raw` behaviour unexamined**. First attempt got the order wrong (see below); corrected to a stepwise table | Fixed on the second pass | + +**Re-review pass 2 found one defect in the fixes themselves**, which is the whole reason the loop +exists. Finding 4's first version said the seeded file "was later appended" **after** the three extra +files were written. The actual order is the reverse — append at step 3, extra files at step 4 — and +the order is load-bearing, because step 2→3 is the INC-2026-05 evidence. Rewritten as a stepwise +table carrying the archive counts observed at each point, which also exposes something the prose had +hidden: **step 4 added 3 files and 6 lines, moved `events` by +6, and moved `raw` not at all.** That +is now declared unexamined and flagged for 17.2b rather than left invisible. Pass 3 is clean. ## Findings diff --git a/.agents/research/cleanroom-linux-x86_64-2026-08-09.md b/.agents/research/cleanroom-linux-x86_64-2026-08-09.md index d112fa8..324b109 100644 --- a/.agents/research/cleanroom-linux-x86_64-2026-08-09.md +++ b/.agents/research/cleanroom-linux-x86_64-2026-08-09.md @@ -310,20 +310,26 @@ Stated so the numbers can be checked rather than taken. The seed was the repo's `apps/collector/src/fixtures/sample-session.jsonl` (4 lines, 1343 bytes, a `claude-code` session), placed under `/.claude/projects/-home-seanr-cleanroom-proj/`: -- **1 file** seeded **before** `watch` started, carrying the fixture's **first 2 lines**, left static. -- **3 more files** written **during** the run (2 lines each, `sessionId` rewritten per file) to force - filesystem activity while `/proc//fd` was being sampled. -- The first file was later **appended** with the fixture's remaining 2 lines. +In the order they actually happened, with the archive counts observed at each point: + +| # | Action | After | +| - | --------------------------------------------------------------------------------- | ----- | +| 1 | **1 file** seeded **before** `watch` started — fixture's first 2 lines, left static | — | +| 2 | `watch` runs 12 s, **no append** (this is the INC-2026-05 check, §5) | `raw=2, events=8` | +| 3 | That file **appended** with the fixture's remaining 2 lines, mid-run | `raw=3, events=10` | +| 4 | **3 more files** written (2 lines each, `sessionId` rewritten per file) to force filesystem activity during `/proc//fd` sampling | `raw=3, events=16` | Final state at teardown: clean-room archive `3 raw / 16 events / 1 machine / 15 heartbeats`; clean-room `queue.sqlite` `file_cursors = 4` (one per seeded file), `queue_items = 0` (fully drained), `poll_state = 0`, `connector_errors = 0`. -**`file_cursors = 4` is the per-file evidence; `raw = 3` is not per-file and was not investigated.** -`raw_source_records` counts ingest-batch records rather than files, so 4 files need not yield 4 rows -(16.0's run produced 539 raw from 2 files). The Level-3 requirement was counts **> 0**, which is met; -per-file raw accounting was never part of this measurement and is **declared unexamined** rather than -implied to be understood. +**`file_cursors = 4` is the per-file evidence; `raw = 3` is not per-file, and step 4 is where that is +visible** — three new files and six new lines moved `events` by +6 while `raw` did not move at all. +`raw_source_records` counts ingest-batch records rather than files (16.0's run produced 539 raw from +2 files), so nothing here is anomalous on its face. But **why step 4 added no raw row was not +investigated**: the Level-3 requirement was counts **> 0**, which is met, and per-file raw accounting +was never part of this measurement. It is **declared unexamined** rather than implied to be +understood, and it is a reasonable thing for 17.2b to pin down. ### 5. INC-2026-05 did NOT reproduce — and its recorded root cause contradicts the code (INC-2026-11) From 17e300598ed09b1b41714950325a8579b728feb8 Mon Sep 17 00:00:00 2001 From: Sean Wright Date: Sun, 9 Aug 2026 14:05:00 -0400 Subject: [PATCH 4/5] docs(m17): fill in PR #84 for 17.2a in SUMMARY (both blocks) Co-Authored-By: Claude Opus 5 (1M context) --- SUMMARY.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SUMMARY.md b/SUMMARY.md index 3a3824f..aa911fe 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -346,7 +346,7 @@ through the deferral-audit + scope conversation that produced M12/M13/M14. Full so a clean-room run ingested 16,916 real records) and the off-Windows relative-path defect in one arrow function; the §10.4 fingerprint becomes argument- rather than environment-dependent, and a registry-wide two-home guard stops the next poll connector repeating it · - ✅ **17.2a** SPIKE — truth on a Linux VM — DONE 2026-08-09 (PR #NN): `linux-x86_64` clean room at tier + ✅ **17.2a** SPIKE — truth on a Linux VM — DONE 2026-08-09 (PR #84): `linux-x86_64` clean room at tier **`VM-verified`** proved capture end-to-end (3 raw / 16 events), `--home` confinement on POSIX (0 paths outside the given home) and a read-side check 16.0 never made (160 `/proc/fd` sampling rounds, 1120 observations); @@ -1281,7 +1281,7 @@ records` and `0 ÷ 4000` are opposite facts. An HTTP test asserts the literal st negative-controlled by reintroducing the bug. Deliberate consequence (**D-17.1-3**): the platform-neutral permission prose moves the fingerprint, so each existing install's Cursor connector flips to `needs-approval` once — the gate firing on a real surface change. - ✅ **17.2a** DONE 2026-08-09 (PR #NN) — `linux-x86_64` clean-room deploy at tier **`VM-verified` (WSL2), + ✅ **17.2a** DONE 2026-08-09 (PR #84) — `linux-x86_64` clean-room deploy at tier **`VM-verified` (WSL2), NOT hardware-verified** ([`cleanroom-linux-x86_64-2026-08-09.md`](./.agents/research/cleanroom-linux-x86_64-2026-08-09.md)). Split out of 17.2 because **none of the hardware D-M17-3 names exists**, and shipping a VM run From 027b016a01ee8304dbdb811f8d8efa4a28eaff6a Mon Sep 17 00:00:00 2001 From: Sean Wright Date: Sun, 9 Aug 2026 14:44:07 -0400 Subject: [PATCH 5/5] docs(m17): 17.2a prp-review triage -- correct the claims this slice got wrong Seven specialist agents reviewed PR #84. The valuable finding is that a slice whose whole thesis is "an unverified claim is a defect" shipped several. All fixed. Wrong claims that would have misdirected later slices: - INC-2026-08's root cause cited cli.ts:631 resolveHome, but `serve` is its own entrypoint and never consults it: cli.ts dispatches no `serve` command, serve.ts:572 only tests argv.includes("serve") and parses no flags, and the home is pinned in THREE places (serve.ts:107 QUEUE_PATH import-time constant; runEngine called with neither home nor queuePath so capture-engine.ts:363-364 falls back; loadCredentials -> CREDENTIALS_PATH). So the prescribed "one --home flag on two call sites" fix would be silently ignored and the prescribed regression test would FAIL with the fix applied. Rewritten: serve defaulting to homedir() is correct for its real job (it is the desktop/service sidecar, serve.ts:291) -- the defect is that a BUILD SCRIPT invokes it, so the fix belongs in the caller (env override), and the test must assert argv/env or use a hermetic HOME override. - The 17.3 re-score was over-generalized. 17.3 is MULTI-target; the run measured ELF only. build-sea.mjs has no --macho-segment-name NODE_SEA and no codesign remove/re-sign (grep -c 'macho-segment-name|codesign|darwin' -> 0), both of which Node's SEA recipe requires on macOS, and ad-hoc signing intersects D-M17-2. Re-scored to "80% for the LINUX triples; darwin unchanged at 65%, tier not measured". - scripts/sidecar-stub.mjs was never run (a protocol step) and never recorded as skipped -- and it already exports sidecarFileName(hostTriple, platform) with a `rustc -vV` derivation, which is the strongest available support for the 17.3 re-score. Recorded, and cited in INC-2026-09. - "1045 + 680 = 1725" was a tautology sold as corroboration: vitest prints the total in the same line, so passed+skipped==total always. Replaced with the check that can fail -- all 63 skipIf sites gate on DATABASE_URL_TEST, no test is platform-gated, and the 61 skipped files are exactly the *.int.test.ts set. - Three regression-test lines could not fail: INC-2026-08's "real home untouched" (this report's own baseline section explains why -- and its negative control was DESTRUCTIVE), INC-2026-09's self-referential basename oracle with an inert control, and INC-2026-10's derivation test whose oracle has no referent. All rewritten with controls that discriminate; both build-sea tests also record an unacknowledged precondition (the script has zero exports). - Two wrong incident IDs in the report (INC-2026-10 where INC-2026-11 was meant), the pre-sign-off tally ("five routed to 17.3/17.4" -> three), 17.4's "measured nothing here" (INC-2026-12 was measured by 17.2a and routed to 17.4), and incidents.md's preamble ("the five below" -> six; INC-2026-01 IS fixed, in 17.1). - D-M17-3 still read "all hardware-verified ... a Linux VM", asserting exactly what this slice exists to deny. Amended with its true current state. - npm ci was never run (the documented path says npm install); the lockfile-integrity half of the named residual risk is not measured, not resolved. - Row 10 (case-sensitive dedup) was routed to 17.2b, which is procurement-blocked -- but the test needs no hardware, so parking it there means it never happens. Re-routed to 17.3/17.7 with the other substrate-independent leftovers. - Level 4 was claimed "all 7 boxes, checked mechanically"; box 3 (every results row carries a tier) failed on four tables. Fixed and disclosed. - 17.7 was silently unaddressed though its premise is now known to be procurement-blocked; it also now owns enforcing the tier field. Source (comment-only, maintainer's call at the triage gate): - cursor-store.ts:35-36 documented the poll loop as reporting unavailable connector health -- which THIS SLICE measured as false (INC-2026-12), while :105-106 of the same file already said the opposite. Corrected, naming the mechanism. - The new jsdoc claim carried NO verification tier, violating the acceptance criterion this branch itself added. Now tiered, and it separates purity from confinement (a constant is pure and confines nothing) and names the guarding tests. - connector.ts documented PollOutcome.unavailable as surfaced/degraded; it is written but never read. D-M17-5 gains a written second exception (comment-only correction of a reference THIS SLICE made stale), bounded and recorded where the decision lives rather than only in an execution report. npm run repo-health -- --require-db: PASS, 680 integration tests ran, 0 skipped. Co-Authored-By: Claude Opus 5 (1M context) --- .../m17-slice2a-linux-vm-spike.md | 7 +- .../m17-slice2a-linux-vm-spike.md | 17 +- .../plans/m17-cross-platform-collectors.md | 62 ++++++-- .../cleanroom-linux-x86_64-2026-08-09.md | 96 +++++++++-- .agents/research/incidents.md | 150 +++++++++++++++--- SUMMARY.md | 46 +++--- apps/collector/src/connectors/connector.ts | 10 +- apps/collector/src/connectors/cursor-store.ts | 22 ++- 8 files changed, 319 insertions(+), 91 deletions(-) diff --git a/.agents/code-reviews/m17-slice2a-linux-vm-spike.md b/.agents/code-reviews/m17-slice2a-linux-vm-spike.md index 3adae12..595aed2 100644 --- a/.agents/code-reviews/m17-slice2a-linux-vm-spike.md +++ b/.agents/code-reviews/m17-slice2a-linux-vm-spike.md @@ -13,7 +13,9 @@ ## What was reviewable here -The diff is **docs-only** — 0 source files, which is itself an acceptance criterion (D-M17-5: the +_(Superseded at the triage gate — see Resolution below, and the prp-review pass after it: the diff +now carries three comment-only source edits.)_ At review time the diff was **docs-only** — 0 source +files, which is itself an acceptance criterion (D-M17-5: the slice measures, it does not fix; verified with `git status --porcelain | grep -E '\.(ts|tsx|mjs|js|json|sql|yml|toml|rs)$'` → 0 matches). The usual review axes (logic errors, injection, N+1, memory) have no surface. What a measurement slice can get @@ -22,6 +24,9 @@ review axes (logic errors, injection, N+1, memory) have no surface. What a measu 1. **Factual accuracy** — every quoted number re-derived from the run that produced it. 2. **Citation correctness** — every `file:line` opened and confirmed. 3. **Internal consistency** — the same figure across report, incidents, milestone plan and SUMMARY. + _(Gap this pass missed, found by prp-review: I checked `file:line` citations and numeric figures + but **not incident cross-references**, and two `INC-2026-10`/`INC-2026-11` mixups survived. A + grep for `file:line` shapes cannot catch an ID pointing at the wrong entry — check both.)_ 4. **The honesty gates** — tier labels, unmeasured-before-measured, no incident-log inflation. 5. **Secret hygiene** — the §3 privacy rule, on a report written from a run that minted live tokens. diff --git a/.agents/execution-reports/m17-slice2a-linux-vm-spike.md b/.agents/execution-reports/m17-slice2a-linux-vm-spike.md index d9e1013..4b00cdd 100644 --- a/.agents/execution-reports/m17-slice2a-linux-vm-spike.md +++ b/.agents/execution-reports/m17-slice2a-linux-vm-spike.md @@ -44,7 +44,7 @@ so the deviation is visible rather than inferred from a diff. | Clean-room typecheck (`tsc -b --force`) | ✓ 9.47 s (17.0 `ubuntu-latest` baseline: 10 s) | | Clean-room tests (`npx vitest run`) | ✓ 1045 passed \| 680 skipped — deviation from 1034 fully explained | | Level 3 capture proof | ✓ 3 raw / 16 events / 1 machine / 15 heartbeats (pre-teardown) | -| Level 4 honesty gate | ✓ all 7 boxes, checked mechanically | +| Level 4 honesty gate | ✓ 7/7 — but box 3 needed a fix the first check missed, see below | | `node scripts/check-summary.mjs` | ✓ exit 0 | | Pre-commit hook (`repo-health --fast`) | ✓ PASS | @@ -53,10 +53,17 @@ plan required `DATABASE_URL_TEST` unset so the comparison baseline stayed valid. was exercised on the **real** repo instead, where `repo-health` ran 1725 tests with **0 skipped**, and 1045 + 680 = 1725 cross-checks the two runs against each other. -**`repo-health` was run three times and the middle run FAILED** (559 × `ECONNREFUSED …:5433`). Not a -leak — INC-2026-06's signature is `28P01 auth_failed`, and `ECONNREFUSED` means nothing was -listening. Docker Desktop exited twice this session, taking the real archive down mid-crash-recovery. -Recorded in the report rather than smoothed over. +**`repo-health` was run three times and the middle run FAILED** — the full account, with the +`28P01`-vs-`ECONNREFUSED` discriminator that rules out a leak, is in the report's "Verification that +isolation held". Summarized under *Challenges* below. + +**Level 4 box 3 was claimed "checked mechanically" and was not met.** The box reads "**Every** row in +every results table carries a verification tier". My check confirmed the *headline* table's tier +column and stopped there; four further tables (Isolation setup, §4b, Test-suite baseline, +Verification-that-isolation-held) carried none. Caught by prp-review, not by me, and fixed with a +per-table tier statement in the style the timing table already used. Recorded because "checked +mechanically" attached to a box a mechanical check would have failed is the exact overclaim this +slice exists to refuse. ## What Went Well diff --git a/.agents/plans/m17-cross-platform-collectors.md b/.agents/plans/m17-cross-platform-collectors.md index db6e584..0521d57 100644 --- a/.agents/plans/m17-cross-platform-collectors.md +++ b/.agents/plans/m17-cross-platform-collectors.md @@ -90,7 +90,7 @@ path is where a real Cursor install actually keeps its store is tier **`not meas of INC-2026-01 must not be read as closure of this; half-discharging it silently is the failure mode 17.2a was shaped to avoid. -**Carried INTO 17.2 as a measurement item, and to 17.4 as a fix** (found by 17.1's PR review, not +**Carried INTO 17.2 as a measurement item (17.2a MEASURED it — INC-2026-12), and to 17.4 as a fix** (found by 17.1's PR review, not fixed there because it is engine behaviour rather than connector logic): `PollOutcome.unavailable` has **zero production readers** — `pollLoop` (`capture-engine.ts:329-340`) discards it and then calls `onSuccess` → `queue.clearConnectorError()`. So a store path that is simply WRONG reports the @@ -163,8 +163,17 @@ Three findings worth more than the green cells: **Row 10 of the reconnaissance table (case-sensitive FS + un-normalized dedup keys) is still _(impact inferred)_.** 17.2a confirmed the *substrate* is case-sensitive (ext4; `/mnt/c` is not) but -did not construct a colliding-case session path, so the dedup-key question is untested. It carries to -17.2b. +did not construct a colliding-case session path, so the dedup-key question is untested — and the +plan's in-scope item #5 ("filesystem semantics — case-sensitivity, separators, `homedir()` shape") +therefore produced a clean-room **validity check** rather than a **result**: separators and +`homedir()` shape were never measured as such at all. + +**It does NOT carry to 17.2b, and an earlier draft of this paragraph routing it there was wrong.** +17.2b is defined as *blocked on procurement with no date*, while 17.2a's own report states the +substrate's ext4 case behaviour is **identical to real Linux** — so a colliding-case dedup test +needs no hardware and parking it behind procurement would mean it never happens. Route it to +**17.3** (or 17.7's UAT lanes), together with the two other substrate-independent leftovers 17.2a +declared unexamined: §4b's `raw`-record accounting and its 2-lines-to-8-events fan-out. --- @@ -175,12 +184,12 @@ did not construct a colliding-case session path, so the dedup-key question is un | **17.0** | Truth: CI matrix + spike protocol | M | 90% | Add a `cross-platform.yml` matrix over `{ubuntu-latest, ubuntu-24.04-arm, macos-15-intel, macos-latest (ARM), windows-latest}`; build the Rust/Tauri crate in CI for the first time. Write 17.2's measurement protocol so it runs the day hardware lands. Output is a **table of what is green where**, replacing every inference above with a measurement. **No product code change.** See `.agents/plans/m17-slice0-cross-platform-ci-matrix.md`. | | **17.1** | Connector portability | S–M | 90% | Fix #1 (`APPDATA`) and #2 (INC-2026-01) plus the approval-fingerprint knock-on. Audit every `watchGlobs(home)`/`sources(home)` implementation against the `connector.ts` contract. Windows-verifiable today; no hardware needed. | | **17.2a** | **SPIKE** — truth on a Linux VM | M | _n/a_ | ✅ **DONE 2026-08-09.** The subset of the 17.2 protocol a WSL2 substrate can measure truthfully: `linux-x86_64`, tier **`VM-verified`**. Capture end-to-end, `--home` confinement on POSIX, filesystem semantics, the documented path timed verbatim, and `build-sea.mjs` end-to-end. Raised **INC-2026-08…13**. See [`cleanroom-linux-x86_64-2026-08-09.md`](../research/cleanroom-linux-x86_64-2026-08-09.md). | -| **17.2b** | **SPIKE** — truth on real hardware | M | _n/a_ | **BLOCKED ON PROCUREMENT** — no hardware exists (confirmed with the maintainer, 2026-08-09). Carries everything 17.2a could not see: **Intel Mac** (all of it) · **Gatekeeper / notarization** (D-M17-2 — needs a real browser download to set the quarantine xattr; `curl`/`scp` do not) · **`linux-arm64` on real silicon** (`emulated ≠ hardware-verified`) · **systemd** service semantics · **`webkit2gtk` + a real desktop session** · **installers + updater feed** · the **Cursor `darwin`/`linux` path CONVENTION** (`cursor-store.ts:11-13`, still undischarged) · a **human-run** §5.2 onboarding repeat. Re-scores **17.4–17.6**. | -| **17.3** | Multi-target SEA build | M | 65% → **80%** | Un-pin `build-sea.mjs`'s triple. SEA copies `process.execPath`, so it is **build-on-target, not cross-compilable** — this slice depends structurally on 17.0's matrix existing. **Re-scored UP by 17.2a, on measurement:** the whole recipe runs unmodified on Linux — esbuild → `--experimental-sea-config` → `copyFileSync(process.execPath)` → `postject` into an ELF, exit 0, and the produced artifact **runs**. Only the pinned constant and the `.exe` suffix are wrong (**INC-2026-09**), plus a `--home` leak in the same file (**INC-2026-08**). The risk was the mechanism; the mechanism works. | -| **17.4** | Service install: launchd + systemd | M | 75% (**not re-scored**) | Port the WinSW definition. The non-zero-exit contract (INC-2026-07) carries over unchanged. Two real gaps, not renames: roll-by-size logging has no launchd equivalent, and launchd cannot express the 5/10/20s backoff ladder. **17.2a measured nothing here** — systemd is off in default WSL2 (`ps -p 1` → `init`, `systemctl is-system-running` → `offline`) and was deliberately left off (D-M17-5). Blocked on **17.2b**. | +| **17.2b** | **SPIKE** — truth on real hardware | M | _n/a_ | **BLOCKED ON PROCUREMENT** — no hardware exists (confirmed with the maintainer, 2026-08-09). Carries everything 17.2a could not see: **Intel Mac** (all of it) · **Gatekeeper / notarization** (D-M17-2 — needs a real browser download to set the quarantine xattr; `curl`/`scp` do not) · **`linux-arm64` on real silicon** (`emulated ≠ hardware-verified`) · **systemd** service semantics · **`webkit2gtk` + a real desktop session** · **installers + updater feed** · the **Cursor `darwin`/`linux` path CONVENTION** (`cursor-store.ts:11-14`, still undischarged) · a **human-run** §5.2 onboarding repeat. Re-scores **17.4–17.6**. | +| **17.3** | Multi-target SEA build | M | 65% → **80% for the LINUX triples; darwin unchanged at 65%** | Un-pin `build-sea.mjs`'s triple. SEA copies `process.execPath`, so it is **build-on-target, not cross-compilable** — this slice depends structurally on 17.0's matrix existing. **Re-scored UP by 17.2a on measurement, but ONLY for ELF:** the recipe runs unmodified on Linux — esbuild → `--experimental-sea-config` → `copyFileSync(process.execPath)` → `postject` into an ELF, exit 0, and the artifact **runs**. There, only the pinned constant and the `.exe` suffix are wrong (**INC-2026-09**), plus a `--home` leak in the same file (**INC-2026-08**), and `scripts/sidecar-stub.mjs` already ships a reusable `sidecarFileName(hostTriple, platform)`. **The darwin half is neither measured nor implemented** — no `--macho-segment-name NODE_SEA`, no `codesign` remove/re-sign (`grep -c 'macho-segment-name\|codesign\|darwin'` → 0), and ad-hoc signing intersects **D-M17-2**. Mach-O SEA is tier `not measured`, carried to 17.2b. | +| **17.4** | Service install: launchd + systemd | M | 75% (**not re-scored**) | Port the WinSW definition. The non-zero-exit contract (INC-2026-07) carries over unchanged. Two real gaps, not renames: roll-by-size logging has no launchd equivalent, and launchd cannot express the 5/10/20s backoff ladder. **17.2a measured nothing about the SERVICE-MANAGER half** — systemd is off in default WSL2 (`ps -p 1` → `init`, `systemctl is-system-running` → `offline`) and was deliberately left off (D-M17-5); that half is blocked on **17.2b**. It *did* produce the first observational evidence for **INC-2026-12** (`PollOutcome.unavailable` has no readers), which this slice also owns — measured, but not enough to move the score, because the score is dominated by launchd/systemd. | | **17.5** | Desktop shell portability | M | 60% (**not re-scored**) | Per-target `keyring` feature gating; Tauri Linux `webkit2gtk` toolchain; autostart docs corrected to match the already-portable implementation. **17.2a measured nothing here** — `pkg-config --exists webkit2gtk-4.1` → MISSING and there is no desktop session on this substrate. Blocked on **17.2b**. The `keyring` mock-fallback question is a *runtime* one and survives a restart-based test only, which no VM lane provides. | | **17.6** | Installers + updater feed + release lane | M–L | 50% (**not re-scored**) | Bundle targets beyond `["nsis"]`; updater feed keys beyond `windows-x86_64`; un-park the CI release workflow (`tauri-action`). Lowest confidence and most spike-dependent. **17.2a measured nothing here** — installers are downstream of the desktop shell and of signing, and Gatekeeper needs macOS. Blocked on **17.2b**. | -| **17.7** | Cross-platform UAT + sign-off | M | 85% | Extend `UAT.md` with per-platform lanes. Sign-off requires each target exercised on the hardware class it claims. | +| **17.7** | Cross-platform UAT + sign-off | M | 85% (**not re-scored, but its premise is now known to be procurement-blocked**) | Extend `UAT.md` with per-platform lanes. Sign-off requires each target exercised on the hardware class it claims — a precondition strictly *stronger* than 17.4/17.5/17.6's, so 17.7 is at least as blocked on **17.2b** as they are, and 85% should not be read as "the highest-confidence unblocked slice". 17.2a measured nothing that moves the number. It also **owns enforcing the verification-tier field** across `UAT.md`, release notes and `operations.md` (D-M17-4 + 17.2a's pre-sign-off box below), which previously had no slice. | `→` marks a confidence score the spike was expected to move. **Only 17.3 has actually moved.** 17.4/17.5/17.6 are explicitly **not re-scored**, and the reason is stated per row above rather than @@ -199,7 +208,7 @@ produces real macOS and Windows signal — it converts the reconnaissance table into measurement, and it does so while the Mac and Linux boxes are still being set up. 17.1 second because both its defects are already documented incidents with nothing left to learn from re-measuring them (D-M17-6), and leaving them in would make every Cursor finding in the spike -downstream of one known bug. 17.2 third, the moment hardware exists. 17.3–17.6 only after the spike +downstream of one known bug. 17.2 third — SUPERSEDED 2026-08-09: the hardware never arrived, so the spike split into 17.2a (VM, done) and 17.2b (hardware, blocked), and 17.3 has been re-scored off 17.2a. 17.4–17.6 still wait on 17.2b; 17.3–17.6 only after the spike re-scores them. 17.7 last, because it verifies the others. --- @@ -232,6 +241,15 @@ un-parking macOS signing alone becomes a scope-change proposal, not a silent fix Chosen against what can actually be run: an Intel Mac, a Linux VM, and real ARM hardware. Each of the three is verified on the hardware class it claims. +**Amended 2026-08-09 (17.2a) — this decision as originally written contradicts the tier vocabulary +17.2a introduced, and the contradiction is the point.** It lists "a Linux VM" among targets declared +"all hardware-verified", which is exactly the `emulated ≠ hardware-verified` substitution 17.2a +exists to refuse. Current state: **none** of the three hardware targets exists (confirmed with the +maintainer, 2026-08-09). `linux-x86_64` is **`VM-verified`** as of 17.2a; `darwin-x86_64` and +`linux-arm64` are **`not measured`**. The hardware-verified claim in this decision's title is +**unmet for all three** and is blocked on 17.2b procurement. Do not read the heading as a statement +of fact until 17.2b closes. + ### D-M17-4 — Apple Silicon ships as a universal binary, CI-verified only macOS builds emit a **universal** bundle, so Apple Silicon users are not excluded — but ARM is @@ -244,22 +262,34 @@ a claim of support nobody had checked. The enabling fact: **the repo is PUBLIC**, so GitHub Actions macOS runners are free and unlimited. On a private repo macOS bills at 10× and this decision would have to be justified on cost. -### D-M17-5 — Slice 17.2 MEASURES; it does not fix +### D-M17-5 — The 17.2 spike (both halves, 17.2a and 17.2b) MEASURES; it does not fix Inherited wholesale from D-16.0-2, which paid for itself: 16.0's clean-room deploy raised five incidents, fixed none, and the headline finding (INC-2026-01, a read breach while every write-side -check passed) would have been invisible had the spike been allowed to repair as it went. Every 17.2 +check passed) would have been invisible had the spike been allowed to repair as it went. Every 17.2a/17.2b finding becomes an `.agents/research/incidents.md` entry that earns a fix in a later slice under the scope-change rule. Fixing pre-emptively converts evidence into a guess. **The one standing exception**, also inherited: defects in the isolation the measurement's own safety constraint depends on may be fixed in-slice, because the spike cannot be trusted without them. +17.2a exercised it once (**INC-2026-08**, `rm -rf ~/.420ai` — state, not code). + +**A SECOND, narrower exception was created and exercised on 2026-08-09 (17.2a), and is recorded here +rather than only in an execution report.** A **comment-only correction to a reference that THIS +SLICE made stale** may be applied in-slice: `cursor-store.ts`'s jsdoc addressed its measurement debt +to "17.2", a number 17.2a split into 17.2a/17.2b, so leaving it would have let a reader conclude the +debt was discharged. Its bound: **no behaviour change, no executable line, and the staleness must be +caused by the slice itself** — this is *not* licence to fix a defect the slice measured. The +subsequent prp-review extended it once more, to two comments in the same area that the slice had +**measured false** and left standing (INC-2026-12's silence). Both were the maintainer's call at a +triage gate, and both are disclosed in +[`m17-slice2a-linux-vm-spike.md`](../execution-reports/m17-slice2a-linux-vm-spike.md). ### D-M17-6 — 17.1 lands before the spike, as a bounded exception to D-M17-5 -Both Cursor defects are already written down (INC-2026-01 in `.agents/research/incidents.md:150`, +Both Cursor defects are already written down (INC-2026-01 in `.agents/research/incidents.md`, cited in `docs/guide/data-boundary.md:59-62`). Re-measuring a documented incident produces no -evidence, and leaving it in place means every Cursor observation in 17.2 is downstream of one known +evidence, and leaving it in place means every Cursor observation in 17.2a/17.2b is downstream of one known bug rather than of the platform. The exception is bounded to these two defects and their approval-fingerprint knock-on; nothing else is fixed before the spike. @@ -268,7 +298,7 @@ approval-fingerprint knock-on; nothing else is fixed before the spike. ## Non-goals (name in every PR; do NOT build here) - **CA code signing, Authenticode, MSI/WiX** — parked by 12.8, see D-M17-2. macOS notarization is - in scope only as a **measurement** in 17.2. + in scope only as a **measurement** in 17.2b (17.2a had no Mac). - **Windows ARM (`aarch64-pc-windows-msvc`)** — no hardware, no demand, no verification story. - **Mobile** — the mobile consumption app is M19. - **Re-architecting the watcher.** It already polls and is already portable @@ -285,7 +315,7 @@ approval-fingerprint knock-on; nothing else is fixed before the spike. Mitigation: 17.0 and 17.1 are deliberately hardware-free and sequenced first, so the milestone does not idle. Explicitly **not** mitigated by letting the spike slip into CI-only — a CI-runner-only spike cannot observe real session files, GUI install, or Gatekeeper. -2. **macOS may prove unusable unsigned** (D-M17-2's tension). Mitigation: measured in 17.2 before +2. **macOS may prove unusable unsigned** (D-M17-2's tension). Mitigation: measured in 17.2b before 17.6 is planned, so the discovery lands while the distribution slice is still re-plannable. 3. **Tauri Linux (`webkit2gtk`) toolchain drift** is the least predictable build dependency in the stack. Mitigation: 17.0 puts the Rust build in CI on Linux _before_ 17.5 needs it, so the @@ -324,7 +354,9 @@ standard too, so D-M17-4's "free ARM Mac runners" premise holds for the Intel ha - [ ] `npm run repo-health -- --require-db` passes. - [ ] Every 17.2a **and** 17.2b finding is either fixed in a later slice or carried as an open `.agents/research/incidents.md` entry with a stated reason. (17.2a raised **INC-2026-08…13**; - all six are open, five routed to 17.3/17.4 and one to a re-derivation of INC-2026-05.) + **three** routed to a slice — 08, 09 → **17.3**; 12 → **17.4** — one to a re-derivation of + INC-2026-05 (11), one to a tooling backlog (10), and one (13) a procedure fix already applied + in-slice and inherited by 17.2b.) - [ ] **No claim in the release notes, docs or UAT carries a verification tier it did not earn.** Every `linux-x86_64` claim traceable only to 17.2a is `VM-verified`, not `hardware-verified` (`emulated ≠ hardware-verified`) — the same discipline D-M17-4 already applies to diff --git a/.agents/research/cleanroom-linux-x86_64-2026-08-09.md b/.agents/research/cleanroom-linux-x86_64-2026-08-09.md index 324b109..d7ed6ce 100644 --- a/.agents/research/cleanroom-linux-x86_64-2026-08-09.md +++ b/.agents/research/cleanroom-linux-x86_64-2026-08-09.md @@ -83,6 +83,10 @@ product. The wall-clock is a floor, not an estimate. ## Isolation setup +Every row below is tier `VM-verified` (WSL2), as are the tables in "Test-suite baseline vs 17.0", +"Verification that isolation held" and §4b — stated once per table rather than per row, the same +way the timing table does it. + | Constraint | How | Verified | | ------------------------- | --------------------------------------------------------------------------------------- | -------- | | Separate checkout, on ext4 | `git clone` → `~/cleanroom/420ai` on **ext4** (`/dev/sdf`), never `/mnt/c` | ✅ | @@ -168,7 +172,7 @@ required. Every row is tier `VM-verified`. | **5–6.** `collector pair … --home` | 0.51 s | — | **F2 fix confirmed on POSIX** — printed the clean-room path, not the real profile. See below. | | **7–8.** `collector discover --home` | 0.36 s | — | `0 workspaces, 0 projects`. **Prints no source paths** — see the protocol correction below. | | **9–11.** connector review/approval | — | **skipped (UI-only)** | Not performed. Capture happened anyway, as in 16.0. | -| **12.** `collector watch --home` | ≤ 12 s | — | First events reached the archive **on the first tick, with no append** — see INC-2026-10. | +| **12.** `collector watch --home` | ≤ 12 s | — | First events reached the archive **on the first tick, with no append** — see INC-2026-11. | | **13.** first capture verified | — | ✅ | `3 raw / 16 events / 1 machine / 15 heartbeats` in the clean-room archive. | **Excluded from the product timing: operator error, all mine.** (a) The dashboard was first started @@ -220,9 +224,25 @@ Meanwhile Tauri's `externalBin` resolves by `-$TARGET_TRIPLE` suffix, so on this look for `collector-x86_64-unknown-linux-gnu` — **NOT FOUND**. The artifact is simultaneously mislabelled, invisible to the thing that consumes it, and reported as a success. -**This is good news for 17.3 and must be read that way.** Every portable part of the recipe — -esbuild, `--experimental-sea-config`, `copyFileSync(process.execPath, …)`, postject injection into -an ELF — works unmodified on Linux. Only the hardcoded constant and the `.exe` extension are wrong. +**This is good news for 17.3 — but only for the LINUX triples, and the distinction is the whole +point of this slice.** Every part of the recipe exercised here — esbuild, +`--experimental-sea-config`, `copyFileSync(process.execPath, …)`, postject injection into an +**ELF** — works unmodified on Linux, where only the hardcoded constant and the `.exe` extension are +wrong. + +**The darwin half is neither measured nor implemented, and must not be read as covered.** 17.3 is a +*multi-target* slice. Node's SEA recipe additionally requires `--macho-segment-name NODE_SEA` on the +postject call for Mach-O, plus a `codesign` remove-then-ad-hoc-re-sign around the injection (an +injected binary with an invalidated signature is killed by the kernel). `build-sea.mjs` has none of +it — `grep -c 'macho-segment-name\|codesign\|darwin'` → **0**, and `:133-136` passes only +`--sentinel-fuse`. So on darwin the gap is larger than a constant, it intersects **D-M17-2** +(ad-hoc signing is signing), and it is tier **`not measured`**, carried to 17.2b. The re-score +below is scoped accordingly. + +**The fix has more in-repo support than first credited.** `scripts/sidecar-stub.mjs:50` (17.0) +already exports `sidecarFileName(hostTriple, platform)` — `.exe` on `win32` only — deriving the +triple from `rustc -vV`, and is already unit-tested. 17.3 should reuse it rather than write a second +derivation. `postject` emitted ELF-specific warnings (`Can't find string offset for section name '.note.100'`) rather than the Windows "signature seems corrupted" warning the script's own comment says to expect; injection succeeded regardless. @@ -230,7 +250,7 @@ injection succeeded regardless. **Scope note kept honest:** the planning spike's isolated step-[3] result is **prior evidence for the mechanism only**. The result reported here is the full script, run end-to-end. -### 2. `npm ci`/`npm install` on Linux — the plan's named residual risk resolves to "no break" +### 2. `npm install` on Linux — HALF the plan's named residual risk resolves to "no break" 16.0 saw npm block `esbuild`/`sharp` postinstall scripts on Windows without consequence, and both ship platform-specific binaries, so the plan flagged this as genuinely unverified. Measured: the @@ -240,6 +260,16 @@ install` exits **0** with 851 packages, and esbuild **works** — proven twice d Modern esbuild resolves its binary from the `@esbuild/linux-x64` optional dependency rather than from the postinstall, which is why the block is inert. +**But `npm ci` itself was never run, and the plan named the risk as `npm ci` three times.** The +documented path (`quickstart.md:16`) says `npm install`, and following it verbatim is what this +slice exists to do — so `npm install` is what ran. `npm ci` is not a synonym: it wipes +`node_modules` and **hard-fails on lockfile desync**, a failure mode this repo already has on +record for newly-added workspaces. The postinstall-blocking half of the risk is fairly answered; +the **lockfile-integrity half is `not measured`**, and the protocol's own core sequence +(`m17-slice2-spike-protocol.md:82`) prescribes `npm ci` rather than `npm install` — a disagreement +between the protocol and the guide that 17.2b should resolve in the guide's favour or record as a +deliberate divergence. + ### 3. `--home` confines on POSIX — 17.1's fix verified, and the C2 debt explicitly NOT discharged `discover` prints **no source paths**, so the protocol's prescribed isolation check (lines 37–38, @@ -265,8 +295,9 @@ ASSERT paths naming /mnt/c or AppData : 0 (must be 0 on linux) (The 8th connector is `claudeLiveConnector`, push-mode — it declares no filesystem surface by design, not by omission.) -**The C2 debt is NOT discharged, and this is the sentence that matters.** `cursor-store.ts:11-13` -addresses a measurement debt to 17.2 by name: the `darwin`/`linux` rows are convention-derived from +**The C2 debt is NOT discharged, and this is the sentence that matters.** `cursor-store.ts:11-14` +addresses a measurement debt to 17.2b by name (it said "17.2" until this slice split the number — +see Methodology corrections): the `darwin`/`linux` rows are convention-derived from the VS Code user-data layout and "NOT yet measured". This run verified **confinement** (the path is a pure function of its argument) — it did **not** verify the **convention**, because Cursor is not installed on this target. `cursorStorePathFor(home,'linux')` → @@ -323,13 +354,19 @@ Final state at teardown: clean-room archive `3 raw / 16 events / 1 machine / 15 clean-room `queue.sqlite` `file_cursors = 4` (one per seeded file), `queue_items = 0` (fully drained), `poll_state = 0`, `connector_errors = 0`. +**Two things in that table are declared unexamined rather than explained.** First, the event +fan-out is not linear: step 2's **2 lines produced 8 events**, while step 4's **6 lines produced +6**. Second, `raw` behaves differently from both. + **`file_cursors = 4` is the per-file evidence; `raw = 3` is not per-file, and step 4 is where that is visible** — three new files and six new lines moved `events` by +6 while `raw` did not move at all. `raw_source_records` counts ingest-batch records rather than files (16.0's run produced 539 raw from -2 files), so nothing here is anomalous on its face. But **why step 4 added no raw row was not -investigated**: the Level-3 requirement was counts **> 0**, which is met, and per-file raw accounting -was never part of this measurement. It is **declared unexamined** rather than implied to be -understood, and it is a reasonable thing for 17.2b to pin down. +2 files), so nothing here is anomalous on its face. But **neither the raw non-movement at step 4 nor +the 2-lines-to-8-events fan-out at step 2 was investigated**: the Level-3 requirement was counts +**> 0**, which is met, and per-file/per-line accounting was never part of this measurement. Both are +**declared unexamined** rather than implied to be understood. Pinning them needs no hardware, so +they belong with the other substrate-independent leftovers rather than in the procurement-blocked +17.2b — see the milestone plan's row-10 note. ### 5. INC-2026-05 did NOT reproduce — and its recorded root cause contradicts the code (INC-2026-11) @@ -358,7 +395,7 @@ $ git log --until=2026-08-02 -1 --format='%h %ad' -- apps/collector/src/watcher/ So the EOF claim was **already false** on 2026-08-02, when INC-2026-05 was recorded. The symptom 16.0 observed may well be real; the **root cause written down for it is not**, and INC-2026-05 is quoted verbatim as the acceptance criterion of 16.3's capture-health scorecard. A slice designed -against a misdiagnosed cause is the failure this matters for. Raised as **INC-2026-10** citing +against a misdiagnosed cause is the failure this matters for. Raised as **INC-2026-11** citing INC-2026-05, per the known-defect rule ("a known defect behaving differently is the finding"), rather than as a duplicate of it. @@ -446,8 +483,27 @@ $ git diff a855ebc^1 a855ebc -- '*int.test.ts' | grep -cE '^\+\s*(it|test)\(' ``` Net **+11 unit tests, 0 new integration tests** — precisely 1034 → 1045 with skipped unchanged at -680. Cross-checked from the other side: the real repo's `repo-health` (which *has* -`DATABASE_URL_TEST`) ran **1725 passed, 0 skipped**, and 1045 + 680 = 1725. +680. + +**What licenses comparing a Linux run to a Windows run at all** — and note this is *not* the +arithmetic. An earlier draft offered "1045 + 680 = 1725" as corroboration; that is a **tautology**, +because vitest prints the total in the same line (`Tests 1045 passed | 680 skipped (1725)`) and +`passed + skipped == total` holds for every run that has ever executed. A clean-room result of +`1040 | 685` would falsify the +11 explanation entirely and still sum to 1725. All the sum +establishes is that both runs *collected* 1725 tests, i.e. they are at the same commit — worth +having, but a commit-identity check, not a partition check. + +The load-bearing facts are these, and they had to be verified separately: + +- All **63** `skipIf` occurrences in the suite gate on `DATABASE_URL_TEST` / `TEST_URL` — a grep for + `skipIf` excluding those two tokens returns **zero** lines. +- **No test in the repo is platform-gated.** The `process.platform` hits inside test files are all + within assertions, not skip conditions. +- 61 `*.int.test.ts` files exist, and the blanked run skipped exactly **61 files**. + +So the clean room's 680 skipped are the integration layer and nothing else, and the real repo's +`repo-health` — which *has* `DATABASE_URL_TEST` — ran **1725 passed, 0 skipped over 171 files**, +exercising exactly the layer the clean room skipped. Also worth recording, because CLAUDE.md warns about it specifically: the **incremental** `npm run typecheck` returned in **204 ms** having done nothing, because `npm run build` had already @@ -511,6 +567,18 @@ exactly the kind of detail a summary drops and a reader needs. 4. **16.0's teardown invariants assume a quiescent machine.** Choose invariants the real collector never moves (content hashes, row identities), and declare growing counters as *expected to grow* rather than asserting them flat. +5. **A prescribed protocol step was skipped without being recorded — `scripts/sidecar-stub.mjs`.** + The protocol's collector-side core (`m17-slice2-spike-protocol.md:85`) lists it immediately + before `cargo check`; `cargo check` was excluded with a stated reason in three places, and + `sidecar-stub.mjs` was simply not run and not mentioned. Recorded here rather than left as an + omission, because it mattered: that script already ships `sidecarFileName(hostTriple, platform)` + with a `rustc -vV` triple derivation, which is the strongest available support for the 17.3 + re-score and was left on the table. **17.2b must run it.** +6. **A comment-only source edit was made in a measure-only slice**, at the maintainer's triage + decision: `cursor-store.ts`'s jsdoc addressed its debt to "17.2", a number this slice split. + See the execution report's "D-M17-5 and the one source edit". The subsequent prp-review found + two further claims in the same file that this slice had **measured false** and left standing + (INC-2026-12's silence, documented as if the poll loop surfaced it); those were corrected too. --- diff --git a/.agents/research/incidents.md b/.agents/research/incidents.md index f6ca87b..11b006e 100644 --- a/.agents/research/incidents.md +++ b/.agents/research/incidents.md @@ -46,6 +46,8 @@ feature request. - **Date / observed by:** - **Severity:** high / med / low - **Category (§4.4):** capture / parser / attribution / pricing / projection / UX / user-understanding +- **Verification tier:** hardware-verified / VM-verified / CI-verified / not measured + _(required from M17 17.2a onwards — see the note under "Entries")_ - **What happened:** - **User impact:** - **Root cause:** (or "not yet diagnosed") @@ -68,10 +70,13 @@ each one.** A fixed parser bug with no fixture is a bug that returns. INC-2026-08…13 were raised by the 2026-08-09 `linux-x86_64` clean-room deploy ([`cleanroom-linux-x86_64-2026-08-09.md`](./cleanroom-linux-x86_64-2026-08-09.md), slice **17.2a**), tier **`VM-verified` (WSL2) — NOT hardware-verified**. INC-2026-07 was raised during M16 -pre-sign-off (2026-08-05). The five below that were raised by the 2026-08-02 clean-room deploy -([`cleanroom-2026-08-02.md`](./cleanroom-2026-08-02.md), slice 16.0 part 4). **None of those five is -fixed** — D-16.0-2: the exercise measures, and each entry is the evidence that earns a fix under -§2's scope-change rule. +pre-sign-off (2026-08-05). The **six** below that were raised by the 2026-08-02 clean-room deploy +([`cleanroom-2026-08-02.md`](./cleanroom-2026-08-02.md), slice 16.0 part 4 — its own headline says +"Incidents raised **6**"). **Five of those six are still unfixed** — D-16.0-2: the exercise +measures, and each entry is the evidence that earns a fix under §2's scope-change rule. The +exception is **INC-2026-01**, which shipped fixed in 17.1 (PR #83) and carries a `Resolved:` line. +_(This sentence undercounted the six as five, and called all of them unfixed, from 2026-08-05 until +17.2a's review caught it — a small demonstration of why the log needs reading, not just appending.)_ > **Verification tier (M17 17.2a).** Every entry raised from 17.2a onwards carries the tier its > evidence was produced at — `hardware-verified` / `VM-verified` / `CI-verified` / `not measured`. @@ -190,6 +195,14 @@ fixed** — D-16.0-2: the exercise measures, and each entry is the evidence that - **Evidence:** the counts above from the 17.2a run; `file-watcher.ts:136` verbatim; the two `git log` outputs establishing the line's age; and the contrast that 17.2a's own four seeded files each produced a `file_cursors` row (4) with `queue_items` drained to 0. + **The behaviour is not confined to append-tail connectors, and the re-derivation must not assume + it is.** The snapshot branch (`file-watcher.ts:121-133`) passes `prev = undefined` when there is + no cursor and `readSnapshot` gates on `if (prev && …)`, so whole-file-rewrite sources + (`gemini-cli`, the three chat-export connectors) also ingest the entire file on first sight; poll + mode is the same shape, since `pollChanged` (`queue-store.ts:246-252`) returns + `prev?.content_hash !== hash`, which is `true` with no prior row. **First sight = full ingest in + every capture mode** — so concluding "this only applies to tail connectors" would reopen the same + misdiagnosis one connector class over. - **Disposition:** **backlog → re-derive INC-2026-05 before 16.3's regression test is written.** Not fixed here (D-M17-5), and deliberately raised as a NEW entry citing INC-2026-05 rather than edited into it — the known-defect rule's "behaves differently is the finding" case. INC-2026-05's @@ -237,9 +250,24 @@ fixed** — D-16.0-2: the exercise measures, and each entry is the evidence that the one `DATABASE_URL` points at on a foreign cluster** before it truncates — a guard at the destructive step rather than at the config step, which is the only place that cannot be bypassed by hand-editing `.env`. -- **Regression test?** Yes, and it is cheap: a unit test on `setup-env.mjs` asserting the generated - test handles are derivable from the root `.env` rather than literal (this also pins INC-2026-04's - dashboard `INGEST_URL`, which is the same defect on a different key). +- **Regression test?** Yes, and it must sit at the **destructive step**, not the config step: a test + on the integration bootstrap asserting it **refuses to `TRUNCATE`** when `DATABASE_URL_TEST` + shares host:port with a cluster this checkout did not create — negative-controlled by pointing + both handles at one cluster and asserting the guard throws. That fails on today's code (no such + guard exists) and passes only after the fix. + + **An earlier draft proposed "a unit test on `setup-env.mjs` asserting the test handles are + derivable from the root `.env`", which cannot discriminate**, for two independent reasons. + (i) The oracle has no referent: `setup-env.mjs` *generates* the root `.env` and refuses to run + when one exists (pinned by `scripts/setup-env.test.ts`), so in the scenario this entry + describes — a fresh second checkout — there is no prior `.env` to derive from. (ii) The + charitable reading passes on the hazard: if `DATABASE_URL_TEST` is derived from the same file's + `DATABASE_URL`, all four handles agree on `localhost:5433`, which **is** the hazard state. The + test would go green on current code and on the "fixed" code alike. The derivation test is a valid + oracle for **INC-2026-04** (dashboard `.env.local` vs the root `.env` the same invocation just + wrote — two files, one generated from the other, the shape `setup-env.test.ts` already asserts + for `SESSION_SECRET`), and borrowing it here is what made a broken line read as sound. Note the + Disposition above already named the right answer as option (b). ## INC-2026-09 — `build-sea.mjs` reports **PASS** on a non-Windows triple while producing an unbundlable, mislabelled artifact @@ -269,23 +297,59 @@ fixed** — D-16.0-2: the exercise measures, and each entry is the evidence that emitted ELF-specific warnings (`Can't find string offset for section name '.note.100'`) rather than the Windows "signature seems corrupted" warning the script's own comment says to expect; injection succeeded regardless. + + **Scope this claim to ELF — the darwin half is neither measured nor implemented.** 17.3 is + *multi-target*, and on macOS Node's SEA recipe additionally requires + `--macho-segment-name NODE_SEA` on the postject call and a `codesign` remove-then-ad-hoc-re-sign + around the injection (an injected binary with an invalidated signature is killed by the kernel). + `build-sea.mjs` has **none** of that: `grep -c 'macho-segment-name\|codesign\|darwin'` → **0**, + and the postject invocation at `:133-136` passes only `--sentinel-fuse`. So "the rest of the + recipe is fully portable" is proven for the two Linux triples and is **`not measured`** for + darwin — where it also intersects D-M17-2, since ad-hoc signing is signing. Carried to 17.2b. + + **The fix has more in-repo support than this entry first credited.** `scripts/sidecar-stub.mjs` + (17.0, PR #82) already exports `sidecarFileName(hostTriple, platform)` — appending `.exe` on + `win32` only — and derives the host triple by parsing the `host:` line of `rustc -vV`. It is + already unit-tested (`scripts/sidecar-stub.test.ts`). 17.3 should reuse it rather than invent a + second triple derivation. Note the protocol prescribed running `sidecar-stub.mjs` during the + spike (`m17-slice2-spike-protocol.md:85`) and 17.2a did **not** run it — see the report's + Methodology corrections. - **Evidence:** full script output including `build-sea: PASS` and `SEA_FULL_RC=0`; `file(1)` type string and 123,866,304-byte size; the `collector-x86_64-unknown-linux-gnu` lookup returning NOT FOUND; the successful `serve` smoke-run of the produced artifact; `--check` passing in 164 ms. - **Disposition:** **earns a slice — 17.3**, which owns un-pinning the triple. Not fixed here (D-M17-5); un-pinning it inside a measurement slice would convert the evidence 17.3 is being - re-scored on into a guess. The measurement **raises** 17.3's confidence: the portability risk is - one constant and one file extension, not the SEA mechanism. -- **Regression test?** Yes, and it is the cheap half of 17.3: assert the emitted artifact's basename - equals `collector-${rustTargetTriple()}` for the host, with no `.exe` off Windows — negative-control - it by forcing the host triple and checking the assertion fails. + re-scored on into a guess. The measurement **raises** 17.3's confidence **for the Linux triples + only**: there the portability risk is one constant and one file extension, not the SEA mechanism. + The darwin triples are unchanged and unmeasured (see the Mach-O gap above). +- **Regression test?** Yes, and the oracle must be a **literal table**, not the producer. Pin the + triple derivation against hardcoded pairs — `win32/x64 → x86_64-pc-windows-msvc`, + `linux/x64 → x86_64-unknown-linux-gnu`, `linux/arm64 → aarch64-unknown-linux-gnu`, + `darwin/x64 → x86_64-apple-darwin`, `darwin/arm64 → aarch64-apple-darwin` — taking + platform/arch as **arguments**, the `cursorStorePathFor(home, platform)` shape 17.1 established + for exactly this reason. Then assert the emitted basename against the literal for the injected + pair, and `.exe` **iff** `win32`. Negative-control by pinning the constant back to + `x86_64-pc-windows-msvc` and confirming the linux/darwin rows fail. + + **An earlier draft of this line proposed `basename === collector-${rustTargetTriple()}`, which + cannot fail** — it compares the code to itself, so every wrong-triple bug *inside* the helper + (musl vs gnu, `x86_64` vs `aarch64` under Rosetta, or still returning the Windows triple) + satisfies both sides; and its stated control ("force the host triple and check the assertion + fails") is inert, because forcing the triple moves both sides together. Same shape as CLAUDE.md's + "a per-FILE grep exempts the file, not the call site": a structural assertion cannot decide a + value. Precondition, as for INC-2026-08: `build-sea.mjs` exports nothing and must be refactored + the way `scripts/setup-env.mjs` was before any of this is testable. ## INC-2026-08 — `build-sea.mjs --check` boots the collector against the operator's REAL home, breaking single-owner - **Date / observed by:** 2026-08-09 / clean-room deploy `linux-x86_64`, slice 17.2a - **Severity:** med - **Category (§4.4):** capture -- **Verification tier:** `VM-verified` (WSL2) +- **Verification tier:** `VM-verified` (WSL2) — **but the defect itself is platform-independent.** + `homedir()` with no confinement misfires identically on Windows and macOS; what was + Linux-specific is only its **detectability**. On the Windows host `~/.420ai` already exists and + is being written by the running collector, so the breach leaves no visible trace — which is why + it survived every prior Windows run of this script and one Linux run caught it. - **What happened:** `node apps/collector/scripts/build-sea.mjs --check` created `~/.420ai/` in the **real** collector home — `queue.sqlite` (40 KB) and `connector-approvals.json` — on a run whose every collector invocation had otherwise been confined with `--home `. The directory was @@ -299,12 +363,27 @@ fixed** — D-16.0-2: the exercise measures, and each entry is the evidence that isolation constraint of 17.2a that did not hold, and it arrived through a dimension the checklist did not name. That is 16.0's own general lesson, re-earned one level up — the checklist enumerated the resources the plan could see, and a build script is not one of them. -- **Root cause:** `build-sea.mjs:80` runs `execFileSync(process.execPath, [cjs, "serve"], …)` with no - `--home`, so `serve` resolves `homedir()` (`resolveHome` = `getFlag(args,"--home") ?? homedir()`, - `apps/collector/src/cli.ts:631`) and `QueueStore` `mkdir`s its parent. The same defect is in the - success message at `:144`, which tells the operator to run - `echo '{"cmd":"status"}' | "" serve` — also without `--home`. The smoke test is *correct* to - boot the real entrypoint; it is wrong to boot it into the real home. +- **Root cause:** `build-sea.mjs:80` runs `execFileSync(process.execPath, [cjs, "serve"], …)`, and + `serve` has **no way to be pointed anywhere else**. The same defect is in the success message at + `:144`, which tells the operator to run `echo '{"cmd":"status"}' | "" serve`. The smoke test + is *correct* to boot the real entrypoint; it is wrong to boot it into the real home. + + **`serve` is NOT the `cli.ts` command surface, and this matters for the fix.** It is its own + entrypoint (`serve.ts:571-586`); `cli.ts` dispatches no `serve` command at all, so + `resolveHome` (`cli.ts:631`) and the `--home` flag never enter this path — `serve.ts:572` only + tests `process.argv.slice(1).includes("serve")` and parses no flags. The home is pinned in + **three independent places**, none of them argv: + - `serve.ts:107` — `new QueueStore(QUEUE_PATH)`, the **import-time** constant + (`identity.ts:23`, which bakes in `homedir()` at module load). This is the call that creates + `queue.sqlite`. + - `serve.ts:270-324` — `runEngine` is called with neither `home` nor `queuePath`, so + `capture-engine.ts:363-364` falls back to `QUEUE_PATH` / `homedir()`. + - `loadCredentials` defaults to `CREDENTIALS_PATH` (`identity.ts:20`), also import-time. + + `serve.ts:132`'s `deps.home ?? homedir()` is a **dependency-injection seam for tests**, not a + flag. And `serve` defaulting to the real home is *correct for its actual job* — it is the desktop + / service sidecar, as its own comment at `serve.ts:291` says. **The defect is not that `serve` + uses `homedir()`; it is that a BUILD SCRIPT invokes it.** So the fix belongs in the caller. - **Evidence:** `ls -la --time-style=full-iso ~/.420ai` showing the two files at `13:00:33.8…` against a baseline verified ABSENT at 16:28:00Z and again at pairing time; `build-sea.mjs:74-84` and `:144` verbatim; the run's other isolation checks (credentials hash, machine/token identity, @@ -314,10 +393,35 @@ fixed** — D-16.0-2: the exercise measures, and each entry is the evidence that trustworthy — and recorded here rather than hidden. The repair was to the **state**, not the code: `rm -rf ~/.420ai`, restoring the verified-absent baseline. **No product code was changed**, so the script still ships the defect and it **earns a slice — 17.3**, alongside INC-2026-09 (same file, - same slice, and the fix is one flag on two call sites plus the printed message). -- **Regression test?** Yes: assert `build-sea.mjs --check` runs `serve` with an explicit - `--home ` and that the operator's `homedir()`-derived collector home is untouched - afterwards — negative-controlled by removing the flag, which is exactly how this was found. + same slice). + + **It is NOT "one `--home` flag on two call sites"** — an earlier draft of this entry said so, and + it was wrong: `serve` parses no flags, so such a change would be silently ignored while the leak + continued. Two shapes actually work. **(a) Override the environment in the caller:** spawn with + `HOME`/`USERPROFILE` pointed at a tmpdir, which `os.homedir()` honours, moving all three pins at + once — genuinely a one-line change, and the right scope given `serve`'s own home default is + correct for its real job. **(b) Teach `serve.ts` a real `--home`** and thread it through the + queue, credentials and engine — materially larger than this entry originally budgeted, and worth + doing only if the desktop/service path needs it for its own reasons. +- **Regression test?** Yes, and it must not assert on the operator's real home. Two parts: + 1. **Argv/env level, the discriminator:** extract the `--check` child invocation so its args+env + are constructible without spawning (`buildCheckArgs(cjs, home)`), and assert the spawn is + confined to an injected tmpdir. Assert the same for the printed success message at + `build-sea.mjs:144`, which carries the identical defect and nothing else would cover. + 2. **Hermetic filesystem check:** spawn with `HOME`/`USERPROFILE` overridden to a tmpdir and + assert `/.420ai` is **absent** afterwards. Fails today, passes after the fix, on every + machine. + + **Do not** assert "the operator's `homedir()`-derived home is untouched" — this report's own + baseline section explains why: on any machine where the collector runs (i.e. where this + incident's impact is real) `~/.420ai` already exists and its `queue.sqlite` mtime moves every + 40–90 s, so an existence check can never fail and an mtime check always does. Worse, the + obvious negative control for it — removing the confinement and re-running — would boot a second + collector against the operator's live queue, which is the double-writer bug this entry is about. + **Precondition for both parts, unacknowledged until 17.2a's review:** `build-sea.mjs` has **zero + exports** and ends in `main().catch(...)`, so importing it from a test executes a full build. + It needs the `scripts/setup-env.mjs` treatment (which exports `buildEnvFiles`/`setupEnv` for + exactly this reason) before either test can be written. That extraction belongs in 17.3's scope. ## INC-2026-07 — capture kept working for 8 days while DELIVERY was dead, and every surface said fine diff --git a/SUMMARY.md b/SUMMARY.md index aa911fe..b4532f5 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -109,7 +109,7 @@ deferral audit + scope conversation, on the same criterion and the first answer the maintainer can watch over the shoulder: **anyone running this on hardware he does not own**. That makes **verification, not implementation**, the binding constraint. Sliced **17.0–17.7**, with 17.2 split into **17.2a** (Linux VM, ✅ done) and **17.2b** (real hardware, **blocked on -procurement**) rather than letting a VM spike close the number reserved for a hardware one. See +procurement**). See [`.agents/plans/m17-cross-platform-collectors.md`](./.agents/plans/m17-cross-platform-collectors.md). **M18–M20 remain committed and unsequenced.** @@ -346,21 +346,16 @@ through the deferral-audit + scope conversation that produced M12/M13/M14. Full so a clean-room run ingested 16,916 real records) and the off-Windows relative-path defect in one arrow function; the §10.4 fingerprint becomes argument- rather than environment-dependent, and a registry-wide two-home guard stops the next poll connector repeating it · - ✅ **17.2a** SPIKE — truth on a Linux VM — DONE 2026-08-09 (PR #84): `linux-x86_64` clean room at tier - **`VM-verified`** proved capture end-to-end (3 raw / 16 events), `--home` confinement on POSIX - (0 paths outside the given home) and a read-side check 16.0 never made (160 `/proc/fd` sampling - rounds, 1120 observations); - the documented path breaks in the **same two places as Windows** with byte-identical errors; and - `build-sea.mjs` **exits 0 printing `PASS`** while emitting an unbundlable ELF named `.exe`. Raised - **INC-2026-08…13**, one of them a breach the spike caused itself · **17.2b** SPIKE — truth on real - hardware — **BLOCKED ON PROCUREMENT** (Intel Mac, Gatekeeper, real arm64, systemd, `webkit2gtk`, - installers, the Cursor darwin/linux path convention) · **17.3** Multi-target SEA build · - **17.4** Service install (launchd + systemd) · **17.5** Desktop shell portability · - **17.6** Installers + updater feed + release lane · **17.7** Cross-platform UAT + sign-off. - Decisions **D-M17-1…6** are settled in the plan. **17.2a re-scored 17.3 up (65% → 80%) and - explicitly did NOT re-score 17.4/17.5/17.6** — a VM cannot see an init system, a desktop session - or an installer, and attaching a number to them anyway would be the inference the spike exists to - replace (`emulated ≠ hardware-verified`). + ✅ **17.2a** SPIKE — truth on a Linux VM — DONE 2026-08-09 (PR #84): `linux-x86_64` clean room at + tier **`VM-verified`** (WSL2, not hardware) — capture, `--home` confinement and read-side + isolation all hold on POSIX; the documented path breaks in the **same two places as Windows**; + raised **INC-2026-08…13**, one of them a breach the spike caused itself · + **17.2b** SPIKE — truth on real hardware — **BLOCKED ON PROCUREMENT** (scope in the plan's 17.2b + row) · **17.3** Multi-target SEA build · **17.4** Service install (launchd + systemd) · + **17.5** Desktop shell portability · **17.6** Installers + updater feed + release lane · + **17.7** Cross-platform UAT + sign-off. Decisions **D-M17-1…6** are settled in the plan, whose + slice table is also the confidence ledger (17.2a moved 17.3's Linux half only, and deliberately + moved nothing else). **Planned off a reconnaissance sweep, which changed the milestone's shape.** The collector's TS core is **already portable** and must not be re-litigated: the watcher **polls** rather than using `fs.watch` (`watcher/file-watcher.ts:9-12`, a Windows-motivated decision that happens to make every @@ -382,7 +377,7 @@ through the deferral-audit + scope conversation that produced M12/M13/M14. Full (12.8 unchanged) and records the tension rather than resolving it: unsigned is survivable on Windows only because Tauri's minisign updater key is decoupled from any CA cert (`updater.ts:12`), and that decoupling **does not hold on macOS**, where Gatekeeper hard-blocks an - unnotarized download — so 17.2 **measures** what actually happens before 17.6 is planned. + unnotarized download — so 17.2b **measures** what actually happens before 17.6 is planned. **D-M17-4** ships Apple Silicon as a universal binary verified **only** by free public-repo `macos-latest` runners, labelled "CI-verified, not hardware-verified" everywhere it is offered — naming the gap is what keeps it from being another skipped layer reporting green. @@ -1264,7 +1259,7 @@ records` and `0 ÷ 4000` are opposite facts. An HTTP test asserts the literal st [`.agents/plans/m17-cross-platform-collectors.md`](./.agents/plans/m17-cross-platform-collectors.md). Targets are `darwin-x86_64` (Intel Mac), `linux-x86_64` and `linux-arm64`, **all hardware-verified**, plus a universal macOS bundle whose ARM half is CI-verified only and - labelled as such (D-M17-4). **17.2 is a SPIKE that measures and does not fix** (D-M17-5, + labelled as such (D-M17-4). **17.2a/17.2b are SPIKES that measure and do not fix** (D-M17-5, inherited from D-16.0-2, which earned it — 16.0's clean-room deploy raised five incidents, fixed none, and its headline finding would have been invisible had the spike repaired as it went). 17.0 and 17.1 are deliberately **hardware-free and sequenced first**, because the Mac @@ -1299,13 +1294,14 @@ records` and `0 ÷ 4000` are opposite facts. An HTTP test asserts the literal st checkout's `npx vitest run` would TRUNCATE the real test DB (10 — demonstrated, not fired); **INC-2026-05's recorded root cause contradicts `file-watcher.ts:136`** and 16.3 was designed against it (11); a nonexistent connector source leaves **no trace anywhere** (12); and WSL - interop fabricated a plausible wrong value in the transcript (13). **17.3 re-scored 65% → - 80%** (the SEA mechanism is fully portable; only a pinned constant is wrong); - **17.4/17.5/17.6 deliberately NOT re-scored** — systemd is off in default WSL2, - `webkit2gtk-4.1` is missing, and installers/Gatekeeper need hardware. - ⏸ **17.2b** BLOCKED ON PROCUREMENT — Intel Mac, Gatekeeper/notarization, real `linux-arm64` - silicon, systemd, `webkit2gtk`, installers + updater feed, the Cursor `darwin`/`linux` path - **convention** (`cursor-store.ts:11-13`, still undischarged), and a human-run §5.2 repeat. + interop fabricated a plausible wrong value in the transcript (13). **17.3 re-scored 65% → 80% + for the LINUX triples only** — the ELF half of the SEA recipe runs unmodified, while the + Mach-O half (`--macho-segment-name`, `codesign`) is neither measured nor implemented; + **17.4/17.5/17.6/17.7 deliberately NOT re-scored**. Per-slice reasons live in the plan's + slice table, which is the confidence ledger. + ⏸ **17.2b** BLOCKED ON PROCUREMENT — scope enumerated in the plan's 17.2b row (Mac, + Gatekeeper, real arm64 silicon, systemd, `webkit2gtk`, installers, Mach-O SEA, and the Cursor + `darwin`/`linux` path **convention** — `cursor-store.ts:11-14`, still undischarged). - [ ] **M18–M20 remain committed scope, unsequenced** (§3, PRD §25). Each still needs its own deferral-audit + scope conversation before it is executable. (**M20** is Cloud-hosted SaaS, renumbered from M16 on 2026-08-02 by slice 16.0 — same scope, new number.) diff --git a/apps/collector/src/connectors/connector.ts b/apps/collector/src/connectors/connector.ts index de826fe..e8de08a 100644 --- a/apps/collector/src/connectors/connector.ts +++ b/apps/collector/src/connectors/connector.ts @@ -99,7 +99,7 @@ export interface PollContext { commit(key: string, content: string): void; } -/** A poll pass summary, surfaced to the engine's logger. */ +/** A poll pass summary. Only `changed` is surfaced to the engine's logger — see `unavailable`. */ export interface PollOutcome { /** Units swept by the cheap top-level scan. */ swept: number; @@ -109,7 +109,13 @@ export interface PollOutcome { rawRecords: number; /** Events enqueued across the changed units. */ events: number; - /** True iff the store was absent/locked this pass (degraded, not fatal). */ + /** + * True iff the store was absent/locked this pass — the INTENT is "degraded, not fatal". + * **Written but never read in production** (INC-2026-12, measured by 17.2a): `pollLoop` + * (`capture-engine.ts:329-340`) discards the outcome and calls `onSuccess` → + * `clearConnectorError()` regardless, so until 17.4 reads this an absent — or simply WRONG — + * source path is indistinguishable from a healthy one, with no log line. + */ unavailable?: boolean; } diff --git a/apps/collector/src/connectors/cursor-store.ts b/apps/collector/src/connectors/cursor-store.ts index 35de118..42fd82c 100644 --- a/apps/collector/src/connectors/cursor-store.ts +++ b/apps/collector/src/connectors/cursor-store.ts @@ -9,10 +9,15 @@ import { join } from "node:path"; * - darwin: `/Library/Application Support/Cursor/User/globalStorage/state.vscdb` * - other: `/.config/Cursor/User/globalStorage/state.vscdb` * (darwin/linux rows are convention-derived from the VS Code user-data layout; to be - * hardware-verified in 17.2b — NOT yet measured. 17.2a verified only that this path is a pure - * function of `(home, platform)` — i.e. CONFINEMENT — on linux-x86_64; Cursor was not installed - * on that target, so the CONVENTION below is still unmeasured on both darwin and linux.) The - * store holds a `cursorDiskKV` key/value table: + * hardware-verified in 17.2b — NOT yet measured. What IS guarded, on every platform and every + * run, is CONFINEMENT — a distinct property: the path derives only from its arguments, never + * from the environment (INC-2026-01), and resolves under the given `home`. A constant would be + * "pure" and confine nothing, so the two are pinned separately: `cursor-store.test.ts` for + * env-independence, the registry-wide two-home sweep in `connector.test.ts` for under-home. + * 17.2a re-observed that on a real `linux` target at tier `VM-verified` (WSL2 — NOT + * hardware-verified). Cursor was not installed there, so the CONVENTION in the table ABOVE + * remains unmeasured on both darwin and linux.) The store holds a `cursorDiskKV` key/value + * table: * - `composerData:` → one row per conversation ("composer") * - `bubbleId::` → one row per message ("bubble") * @@ -32,8 +37,13 @@ import { join } from "node:path"; * adds ZERO native dependencies. * * Library file (CLAUDE.md process boundaries): it never logs or exits. `openCursorStore` - * throws if the DB can't be opened (absent/locked); the poll loop catches it and reports - * the source as unavailable connector health, never a crash. + * throws if the DB can't be opened (absent/locked), and `runCursorPoll` catches it and sets + * `PollOutcome.unavailable` — but NOTHING READS THAT FIELD. `pollLoop` + * (`capture-engine.ts:329-340`) inspects only `outcome.changed`, then calls `onSuccess` → + * `queue.clearConnectorError()` unconditionally, so an absent or WRONG store path is reported + * HEALTHY forever and wipes any pre-existing fault, with no log line (INC-2026-12 — measured by + * 17.2a, fix routed to 17.4). This is exactly why 17.2b must verify the darwin/linux path above + * BY OBSERVATION and never by "the collector didn't complain". */ /** A composer row: `id` is the `composerData:` key stripped of its prefix; `value` is the JSON blob. */