# Independent audit: PR #23193 (GitCafe #23172), `fix/issue-20222` → `main`

Head reviewed: `1723431e82249a7e44a15b19ea0db4b5d09d6057`. The worktree is clean and contains `origin/main`. Read-only: no edits, no forge actions, no dev server, no browser.
Diff (`origin/main...HEAD`): `apps/web/src/components/ChatView.logic.ts` (+5/-4) and `ChatView.logic.test.ts` (+20).

## Confirmed findings

**None blocking in the code.** No correctness bug was found at this head.

- **Correctness.** `isCompactCommand = text.toLowerCase() === "/compact" || /^\/compact\s/.test(text)`. Before this change, `/compact <instructions>` failed the bare equality check, so `compactBeforeSend` was true. `useComposerSend.ts:1154` then dispatched an extra bare `/compact` and queued the instructed one. This is the double compaction in the issue, and the fix removes it.
- **Input shape.** `snapshotComposerSend` trims `messageText`, so leading and trailing whitespace never reaches the predicate. Boundary probes:
  - Recognized: `/compact`, `/COMPACT`, `/compact x`, `/compact\tx`, `/compact\nx`.
  - Not recognized: `/compactx`, `/compacting`, `/Compact x`.
- **Mixed case.** `/Compact x` keeps the pre-existing behavior (compact first). This matches the PR's claim that Claude treats a non-lowercase command with arguments as prose. The native transcript observed that claim for `/Compact`. I did not re-verify it.
- **Server compatibility.** Every server check (`CommandPolicy.isNativeMaintenanceCommand`, `ProviderTurnStartService`, `RunExecutionService`, `IdleCompaction`, `ProjectionStore`) matches only the bare `trim().toLowerCase() === "/compact"`. An instructed `/compact x` is therefore an ordinary message on the server. The change is client-only, needs no contract change, and is safe with older and newer servers.
- **Prompt formatting.** `applyClaudePromptEffortPrefix` (`packages/shared/src/model.ts:520`) does not add the `ultrathink` prefix to slash commands, so the outgoing text stays `/compact …`.
- **Surfaces.** `planComposerTurn` has one caller, `useComposerSend.ts:778`, which is shared by web and desktop. Mobile has no compact-before-send path. The chip label in `ChatComposer.tsx:1318` ("Open composer to compact and send") applies only to the collapsed empty composer, so it is unaffected. No provider other than Claude reaches this gate.
- **Smallest shape.** One predicate and one comment. It adds no abstraction. The comment explains why, which fits the repository's comment rules.

## Regex linearity / performance

- `^\/compact\s` is anchored, has no quantifiers or alternation, and cannot backtrack. It costs O(1) after at most 9 characters.
- The only linear cost is `toLowerCase()` on the full text. This is inherited from `main`, which already lowercased the whole message.
- Timings (Node, 2000 iterations each, `regex-timing.txt`):

  | Input | Length (chars) | Result | Time |
  |---|---|---|---|
  | Malformed ~3000 tokens, `/compact` + 12k non-space | 12,008 | false | 10.6 µs/call |
  | ~3000 tokens of whitespace runs | 12,008 | true | 5.3 µs/call |
  | Repeated `/compac` | 12,005 | false | 2.2 µs/call |
  | Instructed ~3000 tokens | 11,979 | true | 2.4 µs/call |
  | 1 MB, no match | 1,000,007 | false | 1.7 ms/call |
  | 1 MB, match | 1,000,008 | true | 1.3 ms/call |

  Growth is linear and the predicate runs once per send. There is no ReDoS risk.

## Tests

- `vp test run apps/web/src/components/ChatView.logic.test.ts`: 49/49 pass at the head (`independent-tests.log`).
- The new cases cover a space, a double space and a newline. The mixed-case case pins the deliberate asymmetry. The existing `/COMPACT` case still pins bare case-insensitivity.
- Red on `main` follows by inspection: the old predicate returns `compactBeforeSend: true` for `/compact x`. I did not re-run it, because that would mean editing the worktree.
- Optional nit (does not block): no test asserts that `/compactx` and `/compacting` still compact first. The `\s` boundary is the part a future refactor (for example to `startsWith("/compact")`) would most likely break. Adding one case to the `it.each` with the opposite expectation would cover it.
- The tab case is covered by `\s` but not tested. That is acceptable.

## Code verdict vs evidence verdict

- **Code: MERGE-quality.** It is correct, minimal, client-only, compatible with older servers, linear, and has focused tests.
- **Evidence: INSUFFICIENT, as the PR body itself admits.**
  - The captures come from `70e5e384da` → `fafd55cbfa`, not from the final head.
  - Final-head runtime behavior is not captured. Its logic for lowercase input matches `fafd55cbfa`, and the later commit only narrowed mixed-case input back to `main` behavior.
  - A1 is partial, with only the 1440/light pair. The A2 after-capture is missing. Rows A3–A5 and C1–C6 are empty. Desktop/Electron and remote modes are unverified.
  - My independent pass adds pure-function evidence: the boundary probes and timings above. It does not add runtime capture.
  - The proportionate runtime proof for a 5-line pure predicate is one before/after pair at the final head for `/compact <instructions>` (one send vs. two), plus the bare `/compact` and `/Compact x` controls. The full 13-row matrix is beyond what this risk needs; whether to waive the rest is Julius's call.

## FRICTION

- `pr.json` has no top-level `body`. The description is at `data.resource.description`.
- The PR body is about 30 KB of evidence narrative for a 9-line diff. Most of it describes infrastructure failure and unrelated diffs, which makes a reviewer's job harder rather than easier.
- The evidence matrix sets a bar (8 recordings for some rows, desktop Electron) that the available infrastructure could not meet. The PR is gated on process, not on code risk.
- `vp test` prints the "built-in command" notice. That is harmless.

VERDICT: FIX_FIRST
