@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 +51 -9
- package/package.json +9 -1
- package/skills/review-page/SKILL.md +70 -0
- package/skills/review-page/schema.md +59 -0
- package/skills/review-page/template.html +232 -0
- package/skills-manifest.json +7 -1
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.
|
|
60
|
-
|
|
61
|
-
`~/.my-skills/.session-update.log`.
|
|
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
|
|
75
|
-
|
|
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`)
|
|
86
|
-
|
|
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.
|
|
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>
|
package/skills-manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-10-
|
|
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",
|