@jesdi/skills 0.0.28 → 0.0.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -50,19 +50,27 @@ npx @jesdi/skills-cli uninstall <skill> [--global]
50
50
  npx @jesdi/skills-cli pin <skill> [--global] # keep the installed version
51
51
  npx @jesdi/skills-cli unpin <skill> [--global]
52
52
  npx @jesdi/skills-cli hook # register the session update hook
53
+ npx @jesdi/skills-cli session-update # run the session checks now
53
54
  ```
54
55
 
55
56
  ## Automatic updates
56
57
 
57
58
  A global install registers a session-start hook in Claude Code
58
59
  (`~/.claude/settings.json`) and Codex (`~/.codex/hooks.json`), for each one
59
- that is present. The first session of a day runs
60
- `skills-cli update --all --global` in the background; the result is in
61
- `~/.my-skills/.session-update.log`. Project installs do not change: they stay
60
+ that is present. Every session runs `skills-cli session-update` in the
61
+ background. It checks all external skills on every run and updates own global
62
+ skills once a day. The result is in `~/.my-skills/.session-update.log`.
63
+ Claude and Codex share a lock so concurrent starts cannot change the installs
64
+ at the same time. External checks run before npm updates, whose requests have
65
+ a 30-second deadline. Because the check runs in the background, a repaired skill
66
+ may appear in the next session. Project installs stay
62
67
  at the versions committed in `.my-skills.json`.
63
68
 
69
+ Existing hooks keep the CLI version that registered them. After a CLI release,
70
+ run `npx -y @jesdi/skills-cli@latest hook` once to register the new behavior.
71
+
64
72
  To keep a skill at its installed version, pin it: `skills-cli pin <skill>
65
- --global`. No update touches a pinned skill until `unpin`. To stop all
73
+ --global`. No own-skill update touches a pinned skill until `unpin`. To stop all
66
74
  automatic updates, remove the hook entry from the two config files; the next
67
75
  global install or `skills-cli hook` adds it again.
68
76
 
@@ -71,19 +79,52 @@ global install or `skills-cli hook` adds it again.
71
79
  Skills authored by other people are **not** copied into this repo — they keep
72
80
  their own authors, upstreams, and licenses. `external-skills.json` records
73
81
  which ones are part of the standard setup, where they come from, and (under
74
- `pins`) the upstream commit the box set is taken from. Install them from
75
- upstream with the [skills.sh](https://skills.sh/) CLI:
82
+ `pins`) the upstream commits for the standard setup and the box set.
83
+ Session checks fetch this catalogue from this repository's `main` branch at
84
+ most once a day. They compare installed files, including resources and executable
85
+ permissions, with the files at each pinned ref. Missing or different files
86
+ are repaired from upstream or a verified local cache. A failed or invalid
87
+ catalogue refresh uses the last valid cached catalogue and reports the error.
88
+ Ref changes take effect
89
+ even when the version label stays the same. Sources without version labels
90
+ are identified by their commit ref.
91
+
92
+ External installs live in `~/.agents/skills/` for Codex and OpenCode, with links
93
+ in `~/.claude/skills/` when Claude Code is present. Their source, path, ref,
94
+ optional version and content hash are recorded in
95
+ `~/.config/my-skills/external.json`. Upstream attribution and the identities
96
+ of directories created by the updater establish
97
+ ownership. Interrupted swaps retain ownership only of their known directory
98
+ identities; failed replacements roll back uncommitted provenance. Existing
99
+ skills.sh entries from the same
100
+ source can be adopted, including Claude copies with matching content and current
101
+ source attribution. Copies awaiting recovery also retain their directory identity.
102
+ Publication and rollback use the OS's atomic no-replace rename through Koffi:
103
+ [`RENAME_EXCL`](https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/APFS_Guide/ToolsandAPIs/ToolsandAPIs.html)
104
+ on macOS, [`RENAME_NOREPLACE`](https://man7.org/linux/man-pages/man2/rename.2.html)
105
+ on Linux, and [`MoveFileExW`](https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-movefileexw)
106
+ without replacement flags on Windows. Unsupported filesystems fail without a
107
+ replacement fallback. If a concurrent install blocks rollback, both copies are
108
+ preserved and the log names the backup location. Current
109
+ conflicting skills.sh provenance and independently replaced Claude directories
110
+ are preserved and reported in
111
+ the log. An enabled Claude frontend-design plugin takes precedence over a
112
+ direct Claude install of that skill; the Codex entry is still installed.
113
+
114
+ For a manual setup, use the [skills.sh](https://skills.sh/) CLI:
76
115
 
77
116
  ```bash
78
- npx skills add mattpocock/skills # grill-me, grill-with-docs, grilling, improve-codebase-architecture, to-questionnaire, tdd, code-review, codebase-design, diagnosing-bugs, resolving-merge-conflicts, handoff, teach, wait-what
117
+ npx skills add mattpocock/skills # grill-me, grill-with-docs, grilling, improve-codebase-architecture, to-questionnaire, tdd, code-review, codebase-design, diagnosing-bugs, resolving-merge-conflicts, pr, handoff, teach, wait-what
79
118
  npx skills add JuliusBrussee/caveman # caveman suite
80
119
  npx skills add vercel-labs/skills # find-skills
81
120
  npx skills add vercel-labs/agent-skills # vercel-react-best-practices
82
121
  npx skills add anthropics/skills # frontend-design (skip if using the Claude Code plugin)
83
122
  ```
84
123
 
85
- The skills.sh lockfile (`~/.agents/.skill-lock.json`) tracks installed
86
- versions; `npx skills update` refreshes them.
124
+ The skills.sh lockfile (`~/.agents/.skill-lock.json`) records its own installs.
125
+ Session checks leave that file in place. `npx skills update` can move an install
126
+ away from the configured pin; the next session check restores the catalogue's
127
+ version.
87
128
 
88
129
  ### Forks (the one exception)
89
130
 
@@ -99,6 +140,7 @@ them afterwards). `external-skills.json` lists them under `forks`.
99
140
 
100
141
  `external-skills.json` → `sets.box` names the skills the agent-ops box
101
142
  receives: `own` from this package, `external` from the pinned upstream.
143
+ `pr` is in the external set and is installed directly from mattpocock/skills.
102
144
  agent-ops vendors that set into its claude-home seed with its own refresh
103
145
  command; this repo's job is to publish the set and keep the pins resolvable.
104
146
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jesdi/skills",
3
- "version": "0.0.28",
3
+ "version": "0.0.30",
4
4
  "description": "jesdi's agent skills (content package consumed by @jesdi/skills-cli)",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -13,8 +13,12 @@
13
13
  "skills-manifest.json"
14
14
  ],
15
15
  "devDependencies": {
16
+ "@types/jsdom": "^30.0.0",
16
17
  "@types/node": "^22.0.0",
18
+ "@types/proper-lockfile": "^4.1.4",
19
+ "@vitest/coverage-istanbul": "2.1.9",
17
20
  "gray-matter": "^4.0.3",
21
+ "jsdom": "^30.1.1",
18
22
  "tsx": "^4.19.0",
19
23
  "typescript": "^5.5.0",
20
24
  "vitest": "^2.1.0"
@@ -24,6 +28,10 @@
24
28
  },
25
29
  "scripts": {
26
30
  "test": "vitest run",
31
+ "test:coverage": "vitest run --coverage --coverage.reportOnFailure",
32
+ "test:python": "mkdir -p .crap && python3 -m coverage run -m pytest skills/backlog skills/crap-gate -q && python3 -m coverage json -o .crap/python.json",
33
+ "crap-gate": "sh skills/crap-gate/run.sh",
34
+ "check": "sh scripts/check.sh",
27
35
  "generate-manifest": "tsx scripts/generate-manifest.ts"
28
36
  }
29
37
  }
@@ -11,7 +11,7 @@ Changes: a model tier per subagent; a context-isolated test-writer commits
11
11
  locked acceptance tests at the ticket's Seam, the implementer turns them green
12
12
  with /tdd and reports to a contract; per-ticket review before merge, fix loop
13
13
  capped at 4 rounds with escalation; a ruling ledger and four stop conditions;
14
- a per-ticket gate (check command + crap-gate) before DONE, re-run by the reviewer;
14
+ a per-ticket gate (check command + crap-gate) before DONE, verified by the reviewer;
15
15
  already-green locked tests need a demonstrated mutation; a Codex correctness
16
16
  pass per risky ticket; tickets touching the same functions run in sequence;
17
17
  check command after each merge; final /review-diff on the most capable model, one
@@ -19,6 +19,11 @@ fixer; ticket-grouped history with fixes folded in before review; closes
19
19
  tickets only if they are issues; no push and no PR until every
20
20
  ticket is merged and the final review has passed. Review loop and ledger adapt
21
21
  superpowers' subagent-driven-development (obra/superpowers, MIT).
22
+ Verification schedule (after a 44-hour run with 65 full gates for six tickets):
23
+ one gate record per candidate, verified by the reviewer instead of re-run;
24
+ mutation proofs once, then only for changed lines; a budget ruling after round 2;
25
+ fixture corrections outside the fix loop; a named fallback when codex exec is refused;
26
+ one report file per agent; plain-sentence ledger.
22
27
  -->
23
28
 
24
29
  You have been provided a spec. This spec should have tickets associated with it, describing how to implement the spec.
@@ -29,19 +34,19 @@ The tickets are not a list of steps. They are a **task graph** with blocking rel
29
34
 
30
35
  Communication to and from subagents should be sparse. Communicate primarily through **context pointers**: to the spec, tickets, research notes, and previous commits. Don't duplicate information already available via pointers.
31
36
 
32
- Run ticket subagents in the background where possible for **maximum concurrency**.
37
+ Run ticket subagents in the background where possible for **maximum concurrency**. Wait on a running subagent with one long wait, in minutes. Each subagent leaves one report file plus its gate log in the notes directory; mutation evidence is a table in the report, and a tree-hash manifest or a per-mutant log is a sign the agent is building its own procedure.
33
38
 
34
39
  **Every dispatch prompt says: never `git stash`, reset or check out other branches; worktrees share one stash and one ref store.**
35
40
 
36
41
  **Name the model on every dispatch.** Omitted, it inherits yours. Exploration, test-writer, reviewer: mid tier (reviewer higher for risky diffs). Implementer: mid tier; cheapest for a small, fully specified ticket; most capable for design judgment or broad codebase work. Merger: cheapest. Escalations: one tier above the agent that got stuck. Final reviewer: most capable.
37
42
 
38
- **Rule, don't stall.** Keep a **ledger** file in the notes directory: every decision as `Ruling: <what> — <why> — <cost if wrong>`, and each ticket's state, so you can resume after compaction. Don't check in between tickets. Stop and ask only for: an irreversible or destructive operation, a security-sensitive action, a side effect outside the worktrees (a push to a shared branch, a publish), or a spec so broken that every path forward is a guess.
43
+ **Rule, don't stall.** Keep a **ledger** file in the notes directory: every decision as `Ruling: <what> — <why> — <cost if wrong>`, and each ticket's state, so you can resume after compaction. Ledger entries are plain sentences, one line per event, written for a human who was not there. After a compaction, read the ledger and continue from its last state: a recorded green gate, review or merge stays settled. Don't check in between tickets. Stop and ask only for: an irreversible or destructive operation, a security-sensitive action, a side effect outside the worktrees (a push to a shared branch, a publish), or a spec so broken that every path forward is a guess.
39
44
 
40
45
  ## Steps
41
46
 
42
47
  1. Read the spec and tickets. Read enough to understand the task graph.
43
48
 
44
- 2. Create a notes directory outside the repo, readable by all subagents, and start the ledger there. (optional) An **exploration subagent** saves its notes on relevant code and docs there, so implementers focus on implementation.
49
+ 2. Create a notes directory outside the repo, readable by all subagents, and start the ledger there. (optional) An **exploration subagent** saves its notes on relevant code and docs there, so implementers focus on implementation. It is the only exploration or preparation agent: a ticket starts with its test-writer, when its frontier turn comes.
45
50
 
46
51
  3. Create the PR branch locally. Do **not** push it and do **not** open a PR yet: nothing leaves the
47
52
  machine until every ticket is merged and the final review has passed (step 10). Intermediate
@@ -50,13 +55,21 @@ Run ticket subagents in the background where possible for **maximum concurrency*
50
55
 
51
56
  4. Each ticket gets its own worktree and branch. For a ticket with behavioral criteria, a **test-writer subagent** goes first. It sees ONLY the spec, the ticket, CONTEXT.md/ADRs and the public interface of the ticket's **Seam** line (pointers, not summaries), never the implementer's plan or code. It writes one black-box acceptance test per criterion through that interface (defining the interface if it doesn't exist yet), confirms each fails for the right reason (missing behavior, not an import or syntax error), commits them red, and reports the SHA and files. A test that is already green before any implementation is vacuous until proven otherwise (an earlier guard or early return often satisfies it for an unrelated reason): the test-writer rewrites it to fail, or, when the criterion truly holds already, reports the production-code mutation that turns it red and the red output. Those files are now **locked**. A ticket with no behavioral criteria (Seam: `none — refactor`) skips this: its existing tests are the contract, unmodified.
52
57
 
53
- 5. An **implementer subagent** starts from that commit and makes the locked tests green, using /tdd vertical slices for anything internal. It never edits locked files and never dispatches subagents. Before reporting DONE it runs the **gate** in its worktree: the repo's check command (lint, typecheck, full test suite: `make check`, or what AGENTS.md documents) and, when the repo has `.crap-gate.json`, crap-gate scoped to the ticket (`run.sh --base <PR branch>`, or the repo's `make crap-gate` wrapper). DONE means both are green; a gate it can't turn green is BLOCKED, with the output. It reports: status (DONE, DONE_WITH_CONCERNS, NEEDS_CONTEXT or BLOCKED), commits, red→green evidence for locked tests and its own slices, the gate's final lines, files touched, concerns. If a locked test contradicts the spec or ticket, it reports BLOCKED with evidence; you rule, and either send the test-writer a correction or overrule the implementer. Handle any BLOCKED by changing something: more context, a stronger model, a split ticket, or a ruling. Never retry unchanged.
58
+ 5. An **implementer subagent** starts from that commit and makes the locked tests green, using /tdd vertical slices for anything internal. It never edits locked files and never dispatches subagents. While working it runs targeted tests. Before reporting DONE it runs the **gate** once, on a clean tree at its final commit, as one **gate record**: the repo's check command (lint, typecheck, full test suite: `make check`, or what AGENTS.md documents) and, when the repo has `.crap-gate.json`, crap-gate scoped to the ticket (`run.sh --base <PR branch>`, or the repo's `make crap-gate` wrapper), wrapped so the log in the notes directory carries the start and end time, the commit, and the status:
54
59
 
55
- 6. On DONE, a **reviewer subagent** re-runs the gate first (a red gate is a Critical finding; don't trust the report), then checks the diff against the ticket and spec, then for quality. It also checks that `git diff <test-writer-sha>..HEAD -- <locked files>` is empty and that every criterion has a locked test. For each locked test, it breaks the production line the test exists to protect (a temporary edit, reverted after) and confirms that test goes red; a test that stays green is Critical. It re-runs every mutation the test-writer reported. Name your specific suspicions in the reviewer prompt (a sibling caller, a key built from the old identity, a guard another ticket added): named suspicions find regressions a generic review misses.
60
+ ```sh
61
+ { date -u; git rev-parse HEAD; git status --short; <gate commands>; echo "gate exit $?"; date -u; } 2>&1 | tee <notes>/<ticket>-gate-$(git rev-parse --short HEAD).log
62
+ ```
56
63
 
57
- A **risky** ticket touches shared state, persistence keys or identities, concurrency, money or auth. For those, run the correctness pass from /review-diff yourself, in parallel with the reviewer: `codex exec` with that skill's `correctness.md` on range `<PR branch>...<ticket branch>` (three dots: from the merge-base, so tickets merged meanwhile don't show up), in quick mode (the command is in the review-diff skill). Its findings join the reviewer's, with the same severities.
64
+ DONE means the record is green; a gate it can't turn green is BLOCKED, with the output. CRAP findings are the implementer's to resolve inside the ticket, within this same gate. It reports: status (DONE, DONE_WITH_CONCERNS, NEEDS_CONTEXT or BLOCKED), commits, red→green evidence for locked tests and its own slices, the gate record's path and final lines, files touched, concerns. If a locked test contradicts the spec or ticket, it reports BLOCKED with evidence; you rule, and either send the test-writer a correction or overrule the implementer. Handle any BLOCKED by changing something: more context, a stronger model, a split ticket, or a ruling. Never retry unchanged.
58
65
 
59
- Critical or Important findings start a fix loop of at most 4 rounds, each a fix plus a re-review scoped to that fix: rounds 1-2 resume the implementer; rounds 3-4 go to a fresh implementer one tier up. After round 4, rule on each open finding in the ledger and move on. Minor findings go to the ledger for the final review.
66
+ 6. On DONE, a **reviewer subagent** verifies the gate record first: the recorded commit is the ticket tip, the status was clean, and the tail is green. A missing, mismatched or red record is a Critical finding, and only then does the reviewer run the gate itself. It then checks the diff against the ticket and spec, then for quality. It also checks that `git diff <test-writer-sha>..HEAD -- <locked files>` is empty and that every criterion has a locked test; that one diff is the whole locked-file check. In the first review, for each locked test, it breaks the production line the test exists to protect (a temporary edit, reverted after) and confirms that test goes red; a test that stays green is Critical. It re-runs every mutation the test-writer reported. A mutation proof is keyed to its test and production line: a later round repeats it only when that line changed. Name your specific suspicions in the reviewer prompt (a sibling caller, a key built from the old identity, a guard another ticket added): named suspicions find regressions a generic review misses.
67
+
68
+ A **risky** ticket touches shared state, persistence keys or identities, concurrency, money or auth. For those, run the correctness pass from /review-diff yourself, in parallel with the reviewer: `codex exec` with that skill's `correctness.md` on range `<PR branch>...<ticket branch>` (three dots: from the merge-base, so tickets merged meanwhile don't show up), in quick mode (the command is in the review-diff skill). Its findings join the reviewer's, with the same severities. When `codex exec` is refused or unavailable, the reviewer's brief gains `correctness.md` for the ticket's risk; that reviewer is the correctness pass, with no second agent.
69
+
70
+ A finding that a locked test encodes a stale path, format, fixture layout or setup detail is a **fixture correction**, outside the fix loop: one test-writer dispatch fixes it, the ledger records the file and reason, and the review continues. A setup or environment failure (a missing dependency link, a cache permission, a leftover process) is fixed at its cause before any gate runs again.
71
+
72
+ Critical or Important findings start a fix loop of at most 4 rounds, each a fix plus a re-review scoped to that fix: the implementer uses targeted tests, ends the round with one gate record on its final commit, and the re-review verifies that record and the changed lines. Rounds 1-2 resume the implementer; rounds 3-4 go to a fresh implementer one tier up. After round 2, rule in the ledger before continuing: continue, split the ticket, or redesign it. After round 4, rule on each open finding in the ledger and move on. Minor findings go to the ledger for the final review.
60
73
 
61
74
  7. Once a ticket passes review, a **merger subagent** merges it to the PR branch and runs the repo's check command (lint, typecheck, full suite). If it fails, an implementer fixes the PR branch before the next merge. Record the ticket's commits and any integration fixes in the ledger. After the merge and checks pass, remove that ticket's worktree and delete its branch with `git branch -d` from the PR branch, while its commits are still ancestors of the PR tip. Cleanup is part of completing the ticket, before any rebase.
62
75
 
@@ -0,0 +1,70 @@
1
+ ---
2
+ name: review-page
3
+ description: One interactive HTML page for a plan review or a questionnaire, built from a fixed template. Use when a pipeline session must deliver a plan review or a questionnaire as an interactive page the console shows.
4
+ ---
5
+
6
+ # Review page
7
+
8
+ The session fills a fixed template. The console shows the page in an iframe and reads the operator's answers from it. Messages and the answers file are in [schema.md](schema.md).
9
+
10
+ ## Modes
11
+
12
+ Set the mode on the body: `<body data-mode="plan">` or `<body data-mode="questionnaire">`.
13
+
14
+ - `plan`: tickets, open questions, corrections, a track, and a bar with the count of answered questions.
15
+ - `questionnaire`: questions only. The template hides the other sections.
16
+
17
+ The page has no buttons. The console shows "Send changes" and "Approve" (or "Send answers") outside the page, so a page can never submit or approve. Never add a button or a `submit` value to the page.
18
+
19
+ ## Steps
20
+
21
+ 1. Copy [template.html](template.html) to the path the prompt names: `.agent/review.html` (plan) or `.agent/questionnaire.html` (questionnaire).
22
+ 2. Set `data-mode` on the body.
23
+ 3. Replace the content between `<!-- slot: NAME -->` and `<!-- /slot -->` in each slot. Keep the `<section data-slot="...">` wrappers. Slots: `header` (eyebrow, `h1`, lede), `tickets`, `questions`, `corrections`, `track`. Do not nest a `<section>` in a slot.
24
+ 4. Open the file and check that it renders.
25
+
26
+ ## Slot shapes
27
+
28
+ Ticket, in `ol.tickets`:
29
+
30
+ ```html
31
+ <li><span class="n">01</span><span>Title <span class="by">· blocked by none · seam: file.py</span></span></li>
32
+ ```
33
+
34
+ Question, in `questions`. The `data-q` value, the radio `name` and the note id `note-ID` use the same id:
35
+
36
+ ```html
37
+ <div class="q" data-q="format">
38
+ <header><h3>Question text</h3><span class="chip waiting">needs you</span></header>
39
+ <p class="why">Why it matters.</p>
40
+ <div class="opts">
41
+ <label class="opt"><input type="radio" name="format" value="a"><span><span class="t">Option A <span class="chip rec">recommended</span></span><br><span class="d">Trade-off.</span></span></label>
42
+ <label class="opt"><input type="radio" name="format" value="b"><span><span class="t">Option B</span><br><span class="d">Trade-off.</span></span></label>
43
+ </div>
44
+ <textarea id="note-format" placeholder="Note (optional)"></textarea>
45
+ </div>
46
+ ```
47
+
48
+ Correction, in `corrections`:
49
+
50
+ ```html
51
+ <div class="corr"><p class="was">Old statement.</p><p>New statement.</p></div>
52
+ ```
53
+
54
+ Track pills, in `track`. Check the proposed track:
55
+
56
+ ```html
57
+ <div class="tracks" role="radiogroup" aria-label="track">
58
+ <label><input type="radio" name="track" value="standard" checked>standard</label>
59
+ </div>
60
+ ```
61
+
62
+ ## Rules
63
+
64
+ - Never edit the `<script>` or the CSS. Only fill slots.
65
+ - Every question has an id that matches `[a-z0-9_-]{1,64}`. The id `track` is reserved for the track pills. Slot content never carries the id `state`, or the name `track` outside the track slot: the script owns them.
66
+ - Every question has exactly one recommended option, marked with `chip rec`. It is one recommended option, never zero and never two.
67
+ - Option values are short ids. Put no option text in a value.
68
+ - The page has no network, no storage and no external file: no script `src`, no `<link>`, no image or font URL.
69
+ - Keep `<meta name="agent-ops-review" content="1">`. The dispatcher looks for it.
70
+ - Re-write the file after a plan revision. Remove the questions and tickets that no longer apply.
@@ -0,0 +1,59 @@
1
+ # Review page schema
2
+
3
+ Every message carries `v`. `v` is the integer `1`. It changes only when the console cannot read the old shape.
4
+
5
+ ## Messages
6
+
7
+ Page to console, once, when its script listens:
8
+
9
+ ```json
10
+ {"type": "ready", "v": 1}
11
+ ```
12
+
13
+ Page to console, on every change:
14
+
15
+ ```json
16
+ {"type": "answers", "v": 1, "answers": {"format": "a", "track": "standard"}, "submit": null}
17
+ ```
18
+
19
+ `submit` is always `null`: a page sends drafts only, and the console ignores any other value. The buttons belong to the console: when the operator presses "Send changes" or "Send answers" it posts the last set with `"changes"`, and on the second tap on "Approve" with `"approve"`. The console adds the request's `revision` when it posts the set to the box.
20
+
21
+ Console to page, after `ready` and whenever saved answers arrive:
22
+
23
+ ```json
24
+ {"type": "restore", "v": 1, "answers": {"format": "b", "track": "standard"}}
25
+ ```
26
+
27
+ The page accepts `restore` only from its parent window, and only when `v` is `1`.
28
+
29
+ ## The `answers` object
30
+
31
+ `answers` is a flat JSON object. A key is a question id, a question id followed by `.note`, or `track`. A value is a string (one chosen option id, a note, or a track name), a list of strings (a multi-choice question), or a boolean. A question with no choice has no key. A note appears only when it is not empty.
32
+
33
+ A question id is the `data-q` value of the question block. It matches `[a-z0-9_-]{1,64}`.
34
+
35
+ ## Answers file
36
+
37
+ The dispatcher writes `.agent/review-answers.json` (plan) or `.agent/questionnaire-answers.json` (spec) in the task worktree. Sessions read it. A session that takes a text answer writes it in the same shape.
38
+
39
+ ```json
40
+ {
41
+ "v": 1,
42
+ "stage": "plan",
43
+ "submitted": "changes",
44
+ "submitted_at": "2026-10-12T10:12:03+00:00",
45
+ "actor": "jesdi",
46
+ "revision": "9f2c…",
47
+ "answers": {"debounce": "draft", "debounce.note": "flush on pagehide", "track": "standard"}
48
+ }
49
+ ```
50
+
51
+ - `v`: the integer `1`.
52
+ - `stage`: `"plan"` for `.agent/review-answers.json`, `"spec"` for `.agent/questionnaire-answers.json`.
53
+ - `submitted`: `null` for a draft, `"changes"` or `"approve"` for a submission.
54
+ - `submitted_at`: the time of the intent that was applied, or `null` for a draft.
55
+ - `actor`: the operator's console login, or `"text"` when the session wrote the file from a typed reply.
56
+ - `revision`: the page revision the answers were made on (the request's `revision`). A session that writes the file from a typed reply leaves it out; such a submission blocks later drafts until the page changes.
57
+ - `answers`: the object above.
58
+
59
+ The file holds no option texts.
@@ -0,0 +1,232 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <meta name="agent-ops-review" content="1">
7
+ <title>Review</title>
8
+ <style>
9
+ /* Layout: one column, phone first; the console's own token system so the page
10
+ reads as part of the console, not as a foreign document. */
11
+ :root {
12
+ --surface: #f5f7fa; --raised: #ffffff; --ink: #1e293b; --muted: #5b6577;
13
+ --border: #dce1e8; --attention: #fbf6ec;
14
+ --waiting-bg: #fef3c7; --waiting-fg: #92400e;
15
+ --running-bg: #dcfce7; --running-fg: #166534;
16
+ --parked-bg: #ede9fe; --parked-fg: #5b21b6;
17
+ --accent: #1e40af; --accent-bg: #dbeafe;
18
+ --sans: "IBM Plex Sans", ui-sans-serif, system-ui, sans-serif;
19
+ --mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
20
+ }
21
+ @media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) {
22
+ --surface: #0f141a; --raised: #181e27; --ink: #e4e8ee; --muted: #98a2b1;
23
+ --border: #2b3340; --attention: #1f1b15;
24
+ --waiting-bg: #372812; --waiting-fg: #fcd34d;
25
+ --running-bg: #10301e; --running-fg: #86efac;
26
+ --parked-bg: #291f45; --parked-fg: #c4b5fd;
27
+ --accent: #93c5fd; --accent-bg: #1e293b; color-scheme: dark } }
28
+ :root[data-theme="dark"] {
29
+ --surface: #0f141a; --raised: #181e27; --ink: #e4e8ee; --muted: #98a2b1;
30
+ --border: #2b3340; --attention: #1f1b15;
31
+ --waiting-bg: #372812; --waiting-fg: #fcd34d;
32
+ --running-bg: #10301e; --running-fg: #86efac;
33
+ --parked-bg: #291f45; --parked-fg: #c4b5fd;
34
+ --accent: #93c5fd; --accent-bg: #1e293b; color-scheme: dark }
35
+
36
+ * { box-sizing: border-box }
37
+ body { margin: 0; background: var(--surface); color: var(--ink); font: 14px/1.5 var(--sans) }
38
+ main { max-width: 680px; margin: 0 auto; padding: 16px 16px 112px }
39
+ h1 { font-size: 18px; font-weight: 600; margin: 0; text-wrap: balance; line-height: 1.3 }
40
+ h2 { font-size: 11px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); margin: 0 }
41
+ section { display: grid; gap: 10px; margin-top: 24px }
42
+ p { margin: 0 }
43
+ code { font: 12px var(--mono); overflow-wrap: anywhere }
44
+ .eyebrow { font: 500 12px var(--mono); color: var(--muted); margin-bottom: 6px; overflow-wrap: anywhere }
45
+ .lede { color: var(--muted); margin-top: 6px }
46
+ .chip { display: inline-block; border-radius: 4px; padding: 0 6px; font-size: 11px; font-weight: 500; white-space: nowrap }
47
+ .chip.waiting { background: var(--waiting-bg); color: var(--waiting-fg) }
48
+ .chip.rec { background: var(--running-bg); color: var(--running-fg) }
49
+ .chip.parked { background: var(--parked-bg); color: var(--parked-fg) }
50
+
51
+ /* Tickets: a plain list, numbered because the order is the build order. */
52
+ ol.tickets { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px }
53
+ ol.tickets li { display: grid; grid-template-columns: 28px 1fr; gap: 8px; align-items: baseline }
54
+ ol.tickets .n { font: 500 12px var(--mono); color: var(--muted); font-variant-numeric: tabular-nums }
55
+ ol.tickets .by { color: var(--muted); font-size: 12px }
56
+
57
+ /* Questions: the one boxed element, because it takes input. */
58
+ .q { background: var(--raised); border: 1px solid var(--border); border-radius: 6px; padding: 12px }
59
+ .q.answered { border-color: color-mix(in srgb, var(--running-fg) 40%, var(--border)) }
60
+ .q header { display: flex; justify-content: space-between; gap: 8px; align-items: baseline }
61
+ .q h3 { font-size: 14px; font-weight: 600; margin: 0; text-wrap: balance }
62
+ .q .why { color: var(--muted); margin-top: 4px }
63
+ .opts { display: grid; gap: 6px; margin-top: 10px }
64
+ .opt { display: grid; grid-template-columns: 18px 1fr; gap: 10px; align-items: start;
65
+ border: 1px solid var(--border); border-radius: 6px; padding: 10px; cursor: pointer; min-height: 44px }
66
+ .opt input { margin: 3px 0 0; width: 16px; height: 16px; accent-color: var(--accent) }
67
+ .opt:has(input:checked) { border-color: var(--accent); background: var(--accent-bg) }
68
+ .opt:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 2px }
69
+ .opt .t { font-weight: 500 }
70
+ .opt .d { color: var(--muted); font-size: 13px }
71
+ .q textarea { width: 100%; margin-top: 10px; min-height: 44px; border: 1px solid var(--border); border-radius: 6px;
72
+ padding: 8px 10px; font: 13px/1.5 var(--sans); background: var(--surface); color: var(--ink); resize: vertical }
73
+ .q textarea:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px }
74
+
75
+ /* Corrections: before / after pairs. */
76
+ .corr { display: grid; gap: 4px; padding-left: 10px; border-left: 2px solid var(--border) }
77
+ .corr .was { color: var(--muted); text-decoration: line-through }
78
+
79
+ /* Track selector: a row of pills. */
80
+ .tracks { display: flex; flex-wrap: wrap; gap: 6px }
81
+ .tracks label { border: 1px solid var(--border); border-radius: 999px; padding: 6px 12px; min-height: 36px;
82
+ display: inline-flex; align-items: center; cursor: pointer; background: var(--raised) }
83
+ .tracks label:has(input:checked) { border-color: var(--accent); background: var(--accent-bg); color: var(--accent); font-weight: 500 }
84
+ .tracks label:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 2px }
85
+ .tracks input { position: absolute; opacity: 0; width: 0; height: 0 }
86
+
87
+ /* State bar: fixed to the bottom. The console owns the buttons: a page never submits. */
88
+ .bar { position: fixed; left: 0; right: 0; bottom: 0; background: var(--raised); border-top: 1px solid var(--border);
89
+ padding: 10px 16px calc(10px + env(safe-area-inset-bottom, 0px)) }
90
+ .bar .in { max-width: 680px; margin: 0 auto; display: flex; gap: 8px; align-items: center; flex-wrap: wrap }
91
+ .bar .state { flex: 1 1 100%; font-size: 12px; color: var(--muted); min-width: 0 }
92
+ .bar .state.saved { color: var(--running-fg) }
93
+
94
+ @media (prefers-reduced-motion: no-preference) { .opt, .tracks label { transition: background .12s, border-color .12s } }
95
+
96
+ /* Questionnaire mode: questions only. */
97
+ body[data-mode="questionnaire"] section[data-slot="tickets"],
98
+ body[data-mode="questionnaire"] section[data-slot="corrections"],
99
+ body[data-mode="questionnaire"] section[data-slot="track"] { display: none }
100
+ </style>
101
+ </head>
102
+ <body data-mode="plan">
103
+ <main>
104
+ <!-- slot: header -->
105
+ <div class="eyebrow">plan review · owner/repo #123 · branch task/123-example</div>
106
+ <h1>Title of the plan</h1>
107
+ <p class="lede">2 tickets · 1 open question · 1 correction. Pick an option under each question, then send.</p>
108
+ <!-- /slot -->
109
+
110
+ <section data-slot="tickets">
111
+ <!-- slot: tickets -->
112
+ <h2>Tickets</h2>
113
+ <ol class="tickets">
114
+ <li><span class="n">01</span><span>First ticket title <span class="by">· seam: module.py</span></span></li>
115
+ <li><span class="n">02</span><span>Second ticket title <span class="by">· blocked by 01 · seam: other.py</span></span></li>
116
+ </ol>
117
+ <!-- /slot -->
118
+ </section>
119
+
120
+ <section data-slot="questions">
121
+ <!-- slot: questions -->
122
+ <h2>Open questions</h2>
123
+ <div class="q" data-q="format">
124
+ <header><h3>Which format does the export use?</h3><span class="chip waiting">needs you</span></header>
125
+ <p class="why">One sentence on why the answer matters.</p>
126
+ <div class="opts">
127
+ <label class="opt"><input type="radio" name="format" value="a"><span><span class="t">Option A <span class="chip rec">recommended</span></span><br><span class="d">What A costs and gives.</span></span></label>
128
+ <label class="opt"><input type="radio" name="format" value="b"><span><span class="t">Option B</span><br><span class="d">What B costs and gives.</span></span></label>
129
+ </div>
130
+ <textarea id="note-format" placeholder="Note (optional)"></textarea>
131
+ </div>
132
+ <!-- /slot -->
133
+ </section>
134
+
135
+ <section data-slot="corrections">
136
+ <!-- slot: corrections -->
137
+ <h2>Corrections</h2>
138
+ <div class="corr">
139
+ <p class="was">The old statement.</p>
140
+ <p>The corrected statement.</p>
141
+ <p class="why" style="color:var(--muted)">Why: the reason.</p>
142
+ </div>
143
+ <!-- /slot -->
144
+ </section>
145
+
146
+ <section data-slot="track">
147
+ <!-- slot: track -->
148
+ <h2>Track</h2>
149
+ <p class="why" style="color:var(--muted)">The plan session proposed <strong>standard</strong>. Change it only when needed.</p>
150
+ <div class="tracks" role="radiogroup" aria-label="track">
151
+ <label><input type="radio" name="track" value="trivial">trivial</label>
152
+ <label><input type="radio" name="track" value="standard" checked>standard</label>
153
+ <label><input type="radio" name="track" value="security">security</label>
154
+ </div>
155
+ <!-- /slot -->
156
+ </section>
157
+ </main>
158
+
159
+ <div class="bar">
160
+ <div class="in">
161
+ <p class="state" id="state">0 of 0 questions answered</p>
162
+ </div>
163
+ </div>
164
+
165
+ <script>
166
+ // Fixed part of the template: collect, post, restore. The session never edits this.
167
+ const V = 1;
168
+ const state = document.querySelector('.bar #state'); // lives in the bar, never in a slot
169
+ const embedded = window.parent !== window;
170
+ const questions = () => [...document.querySelectorAll('.q[data-q]')];
171
+ let restored = false;
172
+
173
+ function collect() {
174
+ const answers = {};
175
+ for (const el of questions()) {
176
+ const q = el.dataset.q;
177
+ const picked = [...document.getElementsByName(q)].find(i => i.checked);
178
+ if (picked) answers[q] = picked.value;
179
+ const note = document.getElementById('note-' + q);
180
+ if (note && note.value.trim()) answers[q + '.note'] = note.value.trim();
181
+ }
182
+ const track = document.querySelector('input[name="track"]:checked');
183
+ if (track) answers.track = track.value;
184
+ return answers;
185
+ }
186
+
187
+ function post() {
188
+ // In the console this is the only way out of the sandbox. Always a draft:
189
+ // the console's own buttons submit, so no page script can approve.
190
+ if (embedded) window.parent.postMessage({ type: 'answers', v: V, answers: collect(), submit: null }, '*');
191
+ paint();
192
+ }
193
+
194
+ function paint() {
195
+ let n = 0;
196
+ const all = questions();
197
+ for (const el of all) {
198
+ const done = [...document.getElementsByName(el.dataset.q)].some(i => i.checked);
199
+ el.classList.toggle('answered', done);
200
+ const chip = el.querySelector('header .chip.waiting, header .chip.rec');
201
+ if (chip) { chip.textContent = done ? 'answered' : 'needs you'; chip.className = done ? 'chip rec' : 'chip waiting'; }
202
+ if (done) n++;
203
+ }
204
+ state.textContent = `${n} of ${all.length} questions answered` + (restored ? ' · saved answers restored' : '');
205
+ state.classList.toggle('saved', restored);
206
+ }
207
+
208
+ function restore(answers) {
209
+ for (const [k, v] of Object.entries(answers || {})) {
210
+ if (k.endsWith('.note')) { const t = document.getElementById('note-' + k.slice(0, -5)); if (t) t.value = v; continue; }
211
+ const input = [...document.getElementsByName(k)].find(i => i.value === v);
212
+ if (input) input.checked = true;
213
+ }
214
+ paint();
215
+ }
216
+
217
+ document.addEventListener('change', e => {
218
+ if (e.target.type === 'radio') post();
219
+ });
220
+ document.addEventListener('input', e => { if (e.target.tagName === 'TEXTAREA') post(); });
221
+
222
+ // From the console: saved answers, when the page reopens on any device.
223
+ window.addEventListener('message', e => {
224
+ const d = e.data;
225
+ if (e.source !== window.parent || !d || d.v !== V || d.type !== 'restore') return;
226
+ restored = Object.keys(d.answers || {}).length > 0; restore(d.answers);
227
+ });
228
+ if (embedded) window.parent.postMessage({ type: 'ready', v: V }, '*');
229
+ paint();
230
+ </script>
231
+ </body>
232
+ </html>
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generatedAt": "2026-10-08T10:41:10.504Z",
3
+ "generatedAt": "2026-10-08T23:26:31.695Z",
4
4
  "skills": [
5
5
  {
6
6
  "name": "backlog",
@@ -46,8 +46,8 @@
46
46
  },
47
47
  {
48
48
  "name": "implement-spec",
49
- "version": "0.1.6",
50
- "hash": "sha256-836f28be9230179d7389ef5ed8e1dacadd43d649a20a06f32adb891d5a4c1357",
49
+ "version": "0.1.7",
50
+ "hash": "sha256-dfcd2f6a13fd06dd4fb7a45bb0d85ae39228ec804dc94c845310afa72808d3d5",
51
51
  "description": "Implement a specification in code."
52
52
  },
53
53
  {
@@ -74,6 +74,12 @@
74
74
  "hash": "sha256-70beaf1478820d66e1831d45739ce97ced50224b46c5003d887ea62c8438f251",
75
75
  "description": "Review a branch before merge: first a gate (the repo's check command plus crap-gate), then three independent reviewers in parallel — spec (did we build what was asked, no more, no less), correctness via Codex (races, idempotency, money, time, authz, indexes, N+1, migrations, error paths, test quality) and structure (deep-quality-review). `--quick` runs only spec and correctness, shortened, for a live interview. Use when the user says \"/review-diff\", \"review this branch\", \"review this slice\", \"review before merge\", or before closing out a spec task."
76
76
  },
77
+ {
78
+ "name": "review-page",
79
+ "version": "0.1.0",
80
+ "hash": "sha256-f5fc0964e0558c6b9ca5d9ed77c3647b22cbc1d9710a9dfdf270d8c062caa741",
81
+ "description": "One interactive HTML page for a plan review or a questionnaire, built from a fixed template. Use when a pipeline session must deliver a plan review or a questionnaire as an interactive page the console shows."
82
+ },
77
83
  {
78
84
  "name": "to-openspec",
79
85
  "version": "0.1.3",