Skip to content

Commit 8e41b9b

Browse files
ymansurozerclaude
andcommitted
refactor: make galley spec the single source of truth for the agent contract
The agent-facing contract (review modes, the await/comment/reload loop, await exit semantics, ReviewResult, how to act on a review, the guided-review schema) was hand-duplicated across SKILL.md, the AGENTS.md snippet, and partially in guide-spec — and CLAUDE.md warned to update SKILL.md on every contract change. The skill and snippet drift from the installed binary's real behavior. Consolidate the full contract into a CLI-printed spec so the binary and its instructions ship together and can't diverge: - Rename src/guidespec.ts -> src/spec.ts; the SPEC export now holds the full contract, with the guided-review JSON schema folded in as a section. - Add `galley spec`; remove `galley guide-spec` entirely (clean break). Repoint the server error `docs` field and the inline schema pointer to `galley spec`. - Shrink SKILL.md and the AGENTS.md snippet to bootstrap only (what Galley is, when to use it, install, the 3 start commands) plus a once-per-session "run `galley spec` and follow it" pointer. - Redirect CLAUDE.md's maintenance rule and README's pointer at src/spec.ts / `galley spec`. - Add src/spec.test.ts guarding that SPEC carries every consolidated section and has no dangling skill/guide-spec references. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent b9f4452 commit 8e41b9b

9 files changed

Lines changed: 398 additions & 255 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ The two worlds do not import each other. `src/types.ts` (backend) and `src/ui/ty
4646
- `contentHash`/`reviewedHash` pairs detect staleness: if the agent rewrites a block (or a file) after it was decided/approved, the decision/approval resets to pending on reload. The same pattern invalidates comment anchors (`anchorText` → re-anchoring → `unanchored`) and guides (`baseDiffHash`).
4747
- Desks are idempotent per repo+session: `stablePort` hashes repo+session to a port in 41000–50999 so a restarted desk binds the same origin and an open tab self-heals; a desk lock file is trusted only if the server actually answers (`deskAlive`).
4848

49-
**Agent contract:** plain JSON on stdout. `galley await` long-polls and prints one tagged event — `{"kind":"question",…}` (answer now via `galley comment`) or `{"kind":"review","result":{…ReviewResult…}}` (the reviewer hit Send). The contract is documented for consuming agents in `skills/galley/SKILL.md` (and the short form in `skills/galley/agents-snippet.md`); the server's error responses point there too. **If you change the CLI flags, events, or ReviewResult shape, update SKILL.md in the same change.** `scripts/smoke.mjs` exercises this contract end-to-end and is the regression net for it.
49+
**Agent contract:** plain JSON on stdout. `galley await` long-polls and prints one tagged event — `{"kind":"question",…}` (answer now via `galley comment`) or `{"kind":"review","result":{…ReviewResult…}}` (the reviewer hit Send). The contract is the single source of truth in `src/spec.ts` (printed by `galley spec`); the skill (`skills/galley/SKILL.md`) and the AGENTS.md snippet (`skills/galley/agents-snippet.md`) are bootstrap-only and point consuming agents at `galley spec`, and the server's error responses do too. **If you change the CLI flags, events, or ReviewResult shape, update `src/spec.ts` in the same change.** `scripts/smoke.mjs` and `src/spec.test.ts` exercise this contract and are the regression net for it.
5050

5151
**UI:** an Alpine.js app with a global store (`src/ui/store.ts`); `poll.ts` polls `/api/state`, `render.ts` renders the diff via `@pierre/diffs` (which renumbers lines per render — display anchors are derived, raw file lines stay canonical), `keys.ts` holds the keyboard-first command map, and `guide.ts`/`tree.ts`/`decisions.ts` etc. are feature modules.
5252

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ I'm not saying this is *the* review surface. I built it in a week and I'm still
4848
galley pr feature-branch # review a branch's commits vs its merge-base
4949
```
5050

51-
Galley opens in your browser and stays open. You review and click **Send to Agent**; the agent attaches, acts on each send, and replies in the same tab. More flags (`--repo`, `--path`, `--port`, `--no-open`, `--guide`, …) are documented in the skill.
51+
Galley opens in your browser and stays open. You review and click **Send to Agent**; the agent attaches, acts on each send, and replies in the same tab. The full agent contract — modes, the event loop, all flags (`--repo`, `--path`, `--port`, `--no-open`, `--guide`, …), `ReviewResult`, and the guided-review schema — is printed by **`galley spec`** (always current with your installed version); the skill is a thin pointer to it.
5252

5353
## Features
5454

skills/galley/SKILL.md

Lines changed: 12 additions & 181 deletions
Large diffs are not rendered by default.

skills/galley/agents-snippet.md

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,15 @@
11
# Galley — AGENTS.md / CLAUDE.md snippet
22

3-
Paste the block below into your agent's instruction file (`AGENTS.md`, `CLAUDE.md`, `.cursorrules`, …) so the agent always routes plan/PR/change review through Galley. It's deliberately short and points at the skill (`skills/galley/SKILL.md`) for the full contract modes, the event loop, CLI options, and the `ReviewResult` shape.
3+
Paste the block below into your agent's instruction file (`AGENTS.md`, `CLAUDE.md`, `.cursorrules`, …) so the agent always routes plan/PR/change review through Galley. It's deliberately short: it points at `galley spec` — printed by the installed binary, always current — for the full contract (modes, the event loop, CLI options, `ReviewResult`, and how to act).
44

55
---
66

77
## Reviewing with Galley
88

9-
When the user should review a plan, a PR, or code changes you've made, hand it to **Galley** — a living browser review desk.
9+
When the user should review a plan, a PR, or code changes you've made, hand it to **Galley** — a living browser review desk. Three ways to start (each stays alive across rounds):
1010

1111
- **Changes you made**`galley --session <task-id> &` (working tree; `--diff staged` for staged only).
1212
- **A markdown plan / single artifact**`galley file <path> &`.
1313
- **A branch / PR**`galley pr <ref> &`.
1414

15-
Optionally attach an AI **guided review** so the human gets an overview + a logical, annotated file order: add `--guide <file>` to the start command (see the skill for the JSON schema). Write the guide file **outside the repo working tree** (a temp path or gitignored dir) — working-tree review surfaces untracked files, so a guide left in the repo shows up as a stray addition.
16-
17-
Then loop: `galley await --session <task-id>` blocks for the next event and prints a tagged JSON envelope.
18-
- `{"kind":"question",…}` → answer **now** with `galley comment --path … --line … --side … --body "…"` at the question's location. While gathering the answer (or acting on a review), post brief `galley status --session <task-id> --body "Reading X…"` lines — they show live next to the reviewer's waiting indicator so they're not staring at a static spinner.
19-
- `{"kind":"review","result":{…}}` → act on `result`: revert **rejected**, make **requestedChanges**, leave **accepted** alone, leave **approvedFiles** (signed off as-is) untouched, don't touch **stagedFiles** unless a change requires it. (Editing an approved file invalidates its approval → it needs re-review next round.)
20-
21-
After editing code, run `galley reload --session <task-id>` so your edits show in the open tab (add `--guide <file>` to swap in a regenerated guide), then `galley await` again for the next round. The desk stays open across rounds and the reviewer keeps **one tab**: starting is idempotent (a live desk is reused, never duplicated) and the port is stable per session, so even after a desk process dies, re-running the start command brings the same tab back to life — never open a second desk for the same session.
22-
23-
Full reference (modes, options, exit semantics, `ReviewResult`): **`skills/galley/SKILL.md`**.
15+
**For the full contract — the `await`/`comment`/`reload` loop, event shapes, `ReviewResult`, how to act on accepted/rejected/requested changes, the guided-review schema, and all options — run `galley spec` and follow it (once per session before your first review).** `galley spec` ships with the binary, so it never goes stale; this snippet intentionally doesn't duplicate it.

src/cli.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
#!/usr/bin/env node
2-
import http from "node:http";
32
import { readFileSync, unlinkSync, writeFileSync } from "node:fs";
3+
import http from "node:http";
44
import path from "node:path";
5+
56
import { getBranch, getGitRoot, gh, git } from "./git.js";
7+
import { validateGuide } from "./guide.js";
8+
import { startServer } from "./server.js";
9+
import { SPEC } from "./spec.js";
610
import {
711
appendComment,
812
buildReviewState,
@@ -17,11 +21,8 @@ import {
1721
stablePort,
1822
syncGitState,
1923
} from "./state.js";
20-
import { validateGuide } from "./guide.js";
21-
import { GUIDE_SPEC } from "./guidespec.js";
22-
import { startServer } from "./server.js";
23-
import { maybeOfferUpdate } from "./update.js";
2424
import type { ReviewMode } from "./types.js";
25+
import { maybeOfferUpdate } from "./update.js";
2526

2627
function parseArgs(argv: string[]) {
2728
const out: Record<string, string | boolean> = { diff: "working", open: true };
@@ -498,7 +499,7 @@ Usage:
498499
galley await [--timeout <s>] Block for the next desk event (question | review)
499500
galley reload [--guide <file>] Re-diff the working tree into the open desk
500501
(--guide swaps the attached review guide too)
501-
galley guide-spec Print the guide JSON schema + authoring rules
502+
galley spec Print the full agent contract (modes, loop, ReviewResult, guide schema)
502503
503504
Common flags:
504505
--repo <path> Repo to review (default: cwd)
@@ -533,8 +534,8 @@ async function main() {
533534
if (sub === "status") return runStatus(args);
534535
if (sub === "await") return runAwait(args);
535536
if (sub === "reload") return runReload(args);
536-
if (sub === "guide-spec") {
537-
process.stdout.write(GUIDE_SPEC + "\n");
537+
if (sub === "spec") {
538+
process.stdout.write(SPEC + "\n");
538539
return;
539540
}
540541
if (sub === "file") {
@@ -548,7 +549,7 @@ async function main() {
548549
if (sub === "pr") return runDesk("pr", positional, args);
549550
if (sub) {
550551
console.error(
551-
`Unknown command "${sub}". Use: galley | galley file <path> | galley pr <ref|number|url> | comment | status | await | reload | guide-spec.`,
552+
`Unknown command "${sub}". Use: galley | galley file <path> | galley pr <ref|number|url> | comment | status | await | reload | spec.`,
552553
);
553554
process.exitCode = 1;
554555
return;

src/guidespec.ts

Lines changed: 0 additions & 47 deletions
This file was deleted.

src/server.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1-
import crypto from "node:crypto";
21
import { execFile } from "node:child_process";
2+
import crypto from "node:crypto";
33
import { promises as fs } from "node:fs";
44
import http from "node:http";
55
import path from "node:path";
66
import { fileURLToPath } from "node:url";
77
import { promisify } from "node:util";
8+
9+
import { resolveEditorCommand } from "./editor.js";
810
import { git, listProjectTree, patchForChange } from "./git.js";
11+
import { validateGuide } from "./guide.js";
912
import {
1013
anchorTextFor,
1114
buildReviewResult,
@@ -19,13 +22,11 @@ import {
1922
syncGitState,
2023
writeGlobalSettings,
2124
} from "./state.js";
22-
import { validateGuide } from "./guide.js";
23-
import { resolveEditorCommand } from "./editor.js";
2425
import type { AgentActivity, AwaitEvent, DeskStatus, ReviewState } from "./types.js";
2526

2627
const execFileAsync = promisify(execFile);
2728
const __dirname = path.dirname(fileURLToPath(import.meta.url));
28-
const DOCS = "skills/galley/SKILL.md";
29+
const DOCS = "Run `galley spec` for the full agent contract.";
2930

3031
export type ServerOptions = {
3132
state: ReviewState;
@@ -379,7 +380,7 @@ export async function startServer(options: ServerOptions): Promise<ServerHandle>
379380
422,
380381
"INVALID_GUIDE",
381382
`Invalid guide: ${result.reason}.`,
382-
"Run `galley guide-spec` for the schema.",
383+
"Run `galley spec` for the guided-review schema.",
383384
);
384385
validatedGuide = result.guide;
385386
}

src/spec.test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import assert from "node:assert/strict";
2+
import { test } from "node:test";
3+
4+
import { SPEC } from "./spec.js";
5+
6+
// The SPEC string is the single source of truth for the agent contract (printed by
7+
// `galley spec`). These anchors guard that consolidating the skill/snippet into it didn't
8+
// silently hollow out a section — if you intentionally rename a section, update the anchor.
9+
const ANCHORS = [
10+
// modes
11+
"Review modes",
12+
"repo (default)",
13+
"galley file <path>",
14+
"galley pr <ref>",
15+
// the loop + events
16+
"galley await",
17+
"galley comment",
18+
"galley reload",
19+
"galley status",
20+
'"kind":"question"',
21+
'"kind":"review"',
22+
// result + acting
23+
"ReviewResult",
24+
"approvedFiles",
25+
"How to act on a review",
26+
// guided review schema (folded in from the old guide-spec)
27+
"Guide JSON schema",
28+
'"overview"',
29+
'"files"',
30+
'"path"',
31+
'"summary"',
32+
// the rest of the operational contract
33+
"reload vs restart",
34+
"desk.lock",
35+
"Settings",
36+
"PATCH_CONFLICT",
37+
];
38+
39+
test("SPEC carries every consolidated section", () => {
40+
for (const anchor of ANCHORS) {
41+
assert.ok(SPEC.includes(anchor), `galley spec is missing the "${anchor}" anchor`);
42+
}
43+
});
44+
45+
test("SPEC has no dangling references to the old skill/command", () => {
46+
assert.ok(
47+
!SPEC.includes("guide-spec"),
48+
"SPEC should not reference the removed guide-spec command",
49+
);
50+
assert.ok(
51+
!SPEC.includes("SKILL.md"),
52+
"SPEC must be self-contained — no 'see the skill' pointers",
53+
);
54+
});

0 commit comments

Comments
 (0)