Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions .claude/skills/running-tend/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,6 @@ No project-specific tend preferences yet beyond the notes below. Add guidance he

When asking permission to file an issue upstream (e.g. at `max-sixty/tend`), do **not** include the standing-exception offer ("I can treat this target as file-directly going forward"). nedtwigg wants to keep approving each cross-repo issue individually — keep asking each time, and skip the offer. ([diffplug/dormouse#168](https://github.com/diffplug/dormouse/issues/168#issuecomment-4836133002))

## The Chromatic `UI Tests` check is human-gated — don't wait it out in a gated-approval poll

After approving a visually-changing PR the approval is gated, so the CI Monitoring poll runs to dismiss-on-red. One status context — **`UI Tests`** (Chromatic, `target_url` → `chromatic.com/build...`) — stays `PENDING` for the entire poll because Chromatic holds it open until a maintainer accepts or rejects the visual diffs in the Chromatic UI; it does **not** auto-terminalize in-session. Its sibling **`Storybook Publish`** (also Chromatic) *does* terminalize normally, so this applies only to `UI Tests`.

When `UI Tests` is the only non-terminal check and every automated check is green (Build & Test, Visual Regression Tests, verify, Standalone Smoketest, Cloudflare Pages, Storybook Publish), treat it as human-gated: stop polling, confirm nothing flipped to FAILURE, and keep the approval standing — don't wait out the poll cap. Polling it to the cap wastes ~9–17 job-minutes per visually-changing PR with no added signal. (Observed on #203, #289, #317.)

## A restart starts clean — don't carry a superseded PR's findings forward

Long-running work here is often closed and reopened as a fresh PR ("Supersedes #N"), and that restart is deliberate — nedtwigg: *"When I start over, I usually **want** to start over. The original conversation grew too unfocused and out of hand."* So review the successor on its own terms: don't fetch the predecessor's bot comments and reviews in order to re-raise findings from them, and don't treat a finding dropped that way as a gap in the review machinery. Carrying the closed thread's context forward is the thing the restart was for.
Expand Down
1 change: 0 additions & 1 deletion .config/tend.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,4 @@ workflows:
ci-fix:
watched_workflows:
- CI
- Chromatic
- Argos
2 changes: 1 addition & 1 deletion .github/renovate.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"Security fixes must not wait for the Monday window. Everything else here is batched to one day a week, which is right for routine bumps and wrong for a known-vulnerable dependency, so this overrides the schedule only.",
"`minimumReleaseAge` is set here EXPLICITLY, and must stay that way. Renovate\u0027s own default for this block is `minimumReleaseAge: null`, force-applied before lookup — so omitting it does not inherit the cooldown from packageRules, it DROPS the cooldown entirely. Stating it is the only way to keep it.",
"Keeping it is the deliberate choice: the cooldown guards the opposite threat — a compromised release that gets yanked within a day — and a reviewer reading a Renovate diff cannot detect a supply-chain compromise the way the ecosystem\u0027s own yank process can. Nothing here auto-merges, and the Dependabot alert already makes the vulnerability visible the moment it is published, so what the cooldown costs is a day before the remediation PR appears, not a day before anyone knows.",
"`draftPR` is set here EXPLICITLY for the same reason: the root sets it true so routine dependency PRs do not spend Chromatic snapshots until someone marks them ready, and this block inherits PR-stage options from the root — `labels` below is one. A draft security PR would put a manual state flip in front of the remediation, and tend-review cannot approve a draft, so it would arrive with no approval signal. Snapshot cost of exempting it is nil; security PRs are rare.",
"`draftPR` is set here EXPLICITLY for the same reason: the root sets it true so routine dependency PRs do not spend Argos snapshots until someone marks them ready, and this block inherits PR-stage options from the root — `labels` below is one. A draft security PR would put a manual state flip in front of the remediation, and tend-review cannot approve a draft, so it would arrive with no approval signal. Snapshot cost of exempting it is nil; security PRs are rare.",
"See docs/specs/security-supply-chain.md, Cooldown and alerts."
],
"enabled": true,
Expand Down
38 changes: 29 additions & 9 deletions .github/workflows/argos.yml
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
name: Argos

# Runs beside Chromatic while Argos is evaluated as its replacement: same
# stories, and the same triggers and draft and fork gates as `chromatic.yml`,
# whose comments give the reasons. Argos renders in this job
# (Playwright, via the Storybook Vitest addon) rather than in its own cloud, so
# a play-function exception fails the run here instead of passing silently.
# Argos renders in this job (Playwright, via the Storybook Vitest addon) rather
# than in its own cloud, so a play-function exception fails the run here instead
# of passing silently.
on:
push:
branches:
- main
paths:
- 'lib/**'
- 'docs/stories/**'
# The lockfile is the root's, so an in-range bump of a rendering dependency
# reaches the stories while matching neither glob above. Here it refreshes
# the baseline against the commit that moved the dependency, so the pixel
# change is attributed there rather than surfacing inside the next
# unrelated `lib/**` PR and being accepted as that PR's. Not on
# `pull_request`, where every dependency PR would bill a full run.
- 'pnpm-lock.yaml'
# `lib/.storybook/main.ts` aliases these three to source, putting them in the
# story dependency graph exactly as `lib/src` is: `Wall` reaches all three,
# and `PocketWall.stories.tsx` imports `remote-lib-common` directly. Push-only
# like the lockfile; they nearly always change alongside `lib/**`, so the
# marginal build count is close to zero.
- 'dor/**'
- 'remote-lib-common/**'
- 'dor-lib-common/**'
Expand All @@ -21,26 +30,37 @@ on:
paths:
- 'lib/**'
- 'docs/stories/**'
# Manual rebaseline. No glob catches every rendering input, and a commit editing
# these globs — this file — matches none of them, so without a dispatch the only
# way to refresh main's baseline is to wait for an unrelated qualifying commit.
workflow_dispatch:

permissions:
contents: read

concurrency:
# One in-flight build per PR, so a superseded push stops burning runner time.
# Keyed by SHA on push so main never cancels: those builds are the baselines.
group: argos-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
argos:
name: Argos Visual Tests
# A draft PR does not spend snapshots: marking it ready is the deliberate
# trigger. A skipped job reports success, but GitHub refuses to merge a draft at
# all, so for that clause the draft state is itself the gate.
# Fork PRs are skipped because secrets are withheld from them, leaving the upload
# without `ARGOS_TOKEN`. That clause has no such backstop — if this check is ever
# made required, a fork PR touching `lib/**` merges with it green rather than
# blocking as it does today.
if: >-
github.event_name != 'pull_request'
|| (github.event.pull_request.draft == false
&& github.event.pull_request.head.repo.full_name == github.repository)
runs-on: ubuntu-latest
# Unlike Chromatic's, this job does the rendering, and `--with-deps` runs an
# unbounded apt-get; without a cap a stalled mirror holds the runner 6h.
# This job does the rendering, and `--with-deps` runs an unbounded apt-get;
# without a cap a stalled mirror holds the runner 6h.
timeout-minutes: 30
steps:
- name: Checkout
Expand Down Expand Up @@ -70,8 +90,8 @@ jobs:
env:
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}

# Publish even when a story fails capture, as Chromatic does, so one throwing
# `play` on main does not freeze the standing Storybook link.
# Publish even when a story fails capture, so one throwing `play` on main
# does not freeze the standing Storybook link.
- name: Build Storybook
if: ${{ !cancelled() }}
run: pnpm build-storybook
Expand Down
92 changes: 0 additions & 92 deletions .github/workflows/chromatic.yml

This file was deleted.

2 changes: 1 addition & 1 deletion .github/workflows/tend-ci-fix.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
name: tend-ci-fix
on:
workflow_run:
workflows: ["CI", "Chromatic", "Argos"]
workflows: ["CI", "Argos"]
types: [completed]
branches: ["main"]

Expand Down
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ pnpm build # build lib, vscode extension, Pocket, and website
The harness opens its browser pane and prints its URL and browser command
(`docs/specs/standalone.md` → "Standalone browser-dev harness").

**Open every PR as a draft.** Chromatic and Argos bill per snapshot and skip drafts, so
marking a PR ready for review is what spends them.
**Open every PR as a draft.** Argos bills per snapshot and skips drafts, so marking a
PR ready for review is what spends snapshots.

## Worktrees

Expand Down
4 changes: 2 additions & 2 deletions TESTING_AND_MODIFICATION_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ needed. If a PR is squash-merged by mistake, PR N+1 will show PR N's changes
again until `main` is merged into its branch (that merge resolves cleanly, since
both sides carry identical content). The repo's default is a merge commit; keep it.

Keep every PR a draft until it is actually up for review (Chromatic and Argos
bill on ready-for-review).
Keep every PR a draft until it is actually up for review (Argos bills on
ready-for-review).

## 2. Setup in a worktree

Expand Down
2 changes: 1 addition & 1 deletion docs/specs/security-ci.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ This repository runs the [tend](https://github.com/max-sixty/tend) agent harness

**The notifications poll widens its input.** `tend-notifications.yaml` alone takes its subjects from the bot account's own unread feed rather than from an event payload or a fixed scheduled sweep, and its inlined pre-check `PUT /repos/diffplug/dormouse/subscription -F subscribed=true` runs every `*/15` cycle to keep that feed wide. `dormouse-bot` watches all repository activity; its prompt decides whether to respond (rationale). What still bounds the bot on an undispatched thread is `author_association` tiering and the admin gate on `main`. **Never expect unwatching by hand to stick**: the PUT is idempotent and repeated every cycle, so the lever is `tend-notifications.yaml`, not the Unwatch button.

**Reachable repo-level secrets.** `CHROMATIC_PROJECT_TOKEN` and `ARGOS_TOKEN` are reachable by any workflow the bot can author: `.github/workflows/chromatic.yml` and `.github/workflows/argos.yml` are `pull_request`-triggered, and GitHub environment policies cannot distinguish a bot from a human contributor at the ref level. **Accepted, with rotation as the mitigation** — each token is scoped to a single project and easy to rotate. `OVSX_PAT` and `VSCE_PAT` are protected: they live only in the `vscode-extension-publish` environment, whose deployment-branch-policy admits only `v*` tags, and tag creation is admin-only.
**Reachable repo-level secrets.** `CHROMATIC_PROJECT_TOKEN` and `ARGOS_TOKEN` are reachable by any workflow the bot can author: `.github/workflows/argos.yml` is `pull_request`-triggered, and GitHub environment policies cannot distinguish a bot from a human contributor at the ref level. **Accepted, with rotation as the mitigation** — each token is scoped to a single project and easy to rotate. `OVSX_PAT` and `VSCE_PAT` are protected: they live only in the `vscode-extension-publish` environment, whose deployment-branch-policy admits only `v*` tags, and tag creation is admin-only.

**Inert secret plumbing.** Every generated `tend-*.yaml` passes `anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}` to `max-sixty/tend/claude`; no such secret exists at repo or org level, so it resolves to the empty string and the harness authenticates with `CLAUDE_CODE_OAUTH_TOKEN` instead. **Never create an `ANTHROPIC_API_KEY` secret**: the input cannot be deleted locally, so the `FAIL IF` below is what makes adding one a deliberate, documented expansion of the bot's reach (rationale).

Expand Down
2 changes: 1 addition & 1 deletion docs/specs/theme.rationale.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ showed the stored theme.

**Why the picker row's `X` keeps a gap from the select target.** The two paths above do not recover symmetrically: `Remove` leaves the extension row on screen to re-install, while the `X` means re-finding the extension through an OpenVSX search. The gap prices in the harder undo.

**Why a story, not only a unit test, pins the short-viewport cap.** A unit test can stub the trigger and menu rectangles to pin side selection and resize recomputation, including a visual viewport shrinking while the layout viewport stays fixed, but it cannot prove that the real list yields height while the footer survives. `lib/src/components/design.test.ts` pins the viewport inset, `lib/src/components/use-anchored-menu.test.tsx` the geometry, and the Chromatic story the rendered result.
**Why a story, not only a unit test, pins the short-viewport cap.** A unit test can stub the trigger and menu rectangles to pin side selection and resize recomputation, including a visual viewport shrinking while the layout viewport stays fixed, but it cannot prove that the real list yields height while the footer survives. `lib/src/components/design.test.ts` pins the viewport inset, `lib/src/components/use-anchored-menu.test.tsx` the geometry, and the Argos-snapshotted story the rendered result.

**Why the colour tokens are their own file.** The website compiles the library from source, so its Tailwind root has to scan `lib/src` or none of the library's utilities are emitted there — which is how the picker once lost its width, cap, and stacking on the docs pages, silently and with no build error. Scanning alone was not enough: the colour utilities resolve against an `@theme` the website did not declare, so the picker, the OpenVSX store, and the theme debugger each rendered with no surface, border, or text colour, and the picker carried a private inline stylesheet to compensate. Importing the whole of `theme.css` was not an option either — its `@theme` retunes `--text-xs`/`--text-sm` for a dense terminal UI, which would shrink type across the marketing site. Splitting the colour half out lets a host take the tokens without the app, and the inline stylesheet was deleted.

Expand Down
2 changes: 1 addition & 1 deletion docs/stories/pairing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ you meet it: stand up a Relay, build a Burrow that is allowed to reach it, enrol
the laptop, scan its code with the phone, approve the pairing, connect.

The screens below are the real components, rendered from the same stories
Chromatic watches. They are static — nothing here is wired to a live Relay.
Argos watches. They are static — nothing here is wired to a live Relay.
One of them carries `autoplay` because a docs page does not run a story's `play`
by default, and its state — a painted terminal — only exists after it runs.

Expand Down
2 changes: 1 addition & 1 deletion lib/.storybook/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const config: StorybookConfig = {
dor: path.resolve(here, '..', '..', 'dor', 'src'),
// Same reason: `Wall` → `RemotePairingModalHost` pulls in the remote host
// modules, which import `remote-lib-common`. Its package `exports` point
// at a `dist` the Storybook/Chromatic job never builds, so alias the bare
// at a `dist` the Storybook/Argos job never builds, so alias the bare
// specifier to source too.
'remote-lib-common': path.resolve(here, '..', '..', 'remote-lib-common', 'src'),
// And `Wall` → `useDorControl` → `connect-port` imports
Expand Down
2 changes: 1 addition & 1 deletion vscode-ext/test/webview-boot.smoketest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { launchChromium } from './launch-chromium';
* substitutes for it. jsdom is not an option: it does not enforce CSP at all.
*
* Deliberately shallow. It asserts the app mounts and the policy blocked
* nothing — not what the UI looks like, which is Storybook's and Chromatic's
* nothing — not what the UI looks like, which is Storybook's and Argos's
* job.
*/

Expand Down
Loading