diff --git a/CLAUDE.md b/CLAUDE.md index 2978e242aa..dd69a2740d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -503,6 +503,8 @@ These are prescriptive rules not derivable from reading the code: - **Who a seat addresses (Sam, 2026-09-27).** Operator work (press, deploy, merge order, review routing) goes to the lane's operator account: `@lily-shen` for Sharpen, `@connector-ops` for connectors. A decision only Sam can make goes to him as a decision card, never a plain `@sam` in a thread. One re-ask after 12 hours of operator silence, then a decision card. Full rule: [`docs/agents/who-to-address.md`](docs/agents/who-to-address.md). +- **Browser walks may use any ego lite profile except Qing's, which is for Rememly work only (Sam, 2026-10-25); a borrowed session never widens what a seat may do.** Full rule: [`docs/agents/browser-profiles.md`](docs/agents/browser-profiles.md). + - **A silent seat is not evidence of a broken seat.** Check the pod ledger before the log — `grep -c "posted via tool"` cannot detect a seat that is posting correctly, because `silentReply` is evaluated before `agentPostedItself`. Read the live spawn (`ps -ww -o args=`; the prompt and `--model`/`--allowedTools`/`--mcp-config` are all in argv, the token is not). Diagnose before mutating, one variable at a time. Full checklist and the incident that earned it: [`docs/runbooks/diagnosing-a-silent-seat.md`](docs/runbooks/diagnosing-a-silent-seat.md). - **Verify a ship at the CONSUMER, not at the registry or the workflow.** Two different indirections bit this on 2026-08-19. (a) `npm publish` reaches only part of the local fleet, and misses exactly the part that would exercise the change: mcp@0.3.2 was published, unpacked and content-verified, and still reached none of the five working seats. **Re-measured 2026-08-30 across all 31 files in `~/.commonly/tokens/`:** 27 declare `environment.mcp`; **22 are `npx -y @commonlyai/mcp@latest`** — a floating spec resolved at spawn, so a publish DOES reach those on their next spawn — and **5 hardcode `node ~/.commonly/mcp-staging/commonly-mcp/src/index.js`**: `fable-lead`, `pod-architect`, `sprint-impl`, `sprint-review`, `ux-lead`, i.e. the sprint seats. That staging copy is hand-patched at **0.3.4** (npm latest 0.3.5), carrying `package.json.bak-0.3.0` and `.bak-0.3.1` beside the live one, and no publish can reach it. **An earlier version of this line said the token files carry no mcp path at all. That was false, and it was false because the scan enumerated TOP-LEVEL keys while `mcp` is nested under `environment`** — this entry's own error class recurring inside its correction; two seats hit the identical false negative the same night. The token file is the DECLARATION; the **spawn argv** is the LOAD — `--mcp-config /mcp-config.json`, whose `mcpServers.commonly.args[0]` is the resolved path, and that tmpdir is regenerated per spawn, so **argv at runtime is the only instrument that answers 'which build is this seat running'** for a floating spec. Separately `/opt/homebrew/bin/commonly` is now an ordinary global install (`../lib/node_modules/@commonlyai/cli/src/index.js`, cli **0.1.24**), not a worktree; the worktree symlink migrated to the *mcp* package (`/opt/homebrew/lib/node_modules/@commonlyai/mcp` → a `~/.claude/jobs/…/tmp/` checkout, **0.3.0**), which no seat loads. **A locator decays faster than the rule it supports** — re-derive the path before trusting it to check the version, and descend into nested objects when you do. (b) A deploy's green tick is not the enforcement boundary — Kubernetes serves from the OLD pod through a rolling update, so take the cutover from `kubectl get pod -o jsonpath='{.status.startTime}'`. Splitting a measurement on the workflow's completion time put a pre-fix run inside the "enforcing" window and made a working change look broken. The existing "smoke the shipped artifact" rule is necessary and insufficient: it proves the artifact is correct, never that the consumer loads it. Full write-ups: AX audit entries 34 and 35. diff --git a/docs/agents/browser-profiles.md b/docs/agents/browser-profiles.md new file mode 100644 index 0000000000..ad506f0d5d --- /dev/null +++ b/docs/agents/browser-profiles.md @@ -0,0 +1,21 @@ +# Browser profiles for seat walks + +**House rule, ruled by Sam on 2026-10-25.** A seat doing browser work for Commonly (vendor dashboards and consoles, GitHub, commonly.me, connector walks) may use the signed-in session of **any** ego lite profile on the host, the operators' own profiles included. Use `ego-browser nodejs`. + +**One exception: Qing's profile is opened only for Rememly work**, never for Commonly work. + +**Before asking Sam to sign in, check every profile.** List them with `await listProfiles()`, open the site in each profile eligible for the work (for Commonly work, every profile except Qing's), and confirm which account is signed in **on the site itself** (its account or profile page). "Signed out" is a claim about every profile, not the first one you tried. A reply once sat unsent for five days because the one profile holding the session was never opened. + +**A borrowed session does not widen what a seat may do.** A browser action in an operator's profile acts as that operator, so the seat does in the browser only what it may already do as itself: read, walk, and verify. Anything that is Sam's alone (money, sending or posting as Sam or Lily, account, billing or security settings, merges, OAuth consents), or anything outward-facing that the seat's own rules would ask first for, still needs that ask. A logged-in session is not the approval. + +**Never type a password, a 2FA code or card data**, in any profile. Reusing a session that is already signed in, including an existing OAuth consent, is fine. A sign-in that needs a secret goes to Sam as a decision card. + +**Only the raw API honours a profile.** `newTaskSpace(name, {profile})`, `newTaskSpace(name, 'id')` and `useOrCreateTaskSpace(name, 'id')` all silently create the space on ego's default profile. Create and confirm like this: + +```js +const t = await ego.createTaskSpace('', ''); +(await listTaskSpaces()).find(s => s.id === t.id).profileId; // must equal +// later runs: await useOrCreateTaskSpace(t.id) — by numeric id, it never re-creates +``` + +**ego profile ids are not Chrome's directory names**, and a profile's name does not tell you which account it holds. Choose by the `name` from `listProfiles()`, then confirm the account on the site before acting.