@jesdi/skills 0.0.29 → 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.29",
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
  }
@@ -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-08T12:46:28.852Z",
3
+ "generatedAt": "2026-10-08T23:26:31.695Z",
4
4
  "skills": [
5
5
  {
6
6
  "name": "backlog",
@@ -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",