quest-loop 0.1.0
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/CHANGELOG.md +34 -0
- package/LICENSE +21 -0
- package/README.md +221 -0
- package/agents/quest-executor.md +42 -0
- package/agents/quest-executor.toml +27 -0
- package/agents/quest-reviewer.md +32 -0
- package/agents/quest-reviewer.toml +21 -0
- package/bin/quest +3 -0
- package/bin/quest-run +3 -0
- package/hooks/hooks.json +32 -0
- package/hooks/session-start.mjs +61 -0
- package/hooks/subagent-stop.mjs +107 -0
- package/lib/cli.mjs +401 -0
- package/lib/config.mjs +64 -0
- package/lib/contract.mjs +225 -0
- package/lib/frontmatter.mjs +61 -0
- package/lib/help.mjs +195 -0
- package/lib/runner.mjs +682 -0
- package/lib/store-github.mjs +415 -0
- package/lib/store-local.mjs +224 -0
- package/lib/store.mjs +37 -0
- package/lib/workers.mjs +372 -0
- package/package.json +39 -0
- package/schemas/final-report.schema.json +22 -0
- package/skills/orchestrate/SKILL.md +74 -0
- package/skills/orchestrate/agents/openai.yaml +4 -0
- package/skills/plan/SKILL.md +69 -0
- package/skills/plan/agents/openai.yaml +4 -0
- package/skills/protocol/SKILL.md +39 -0
- package/skills/protocol/agents/openai.yaml +4 -0
- package/skills/protocol/references/contract-spec.md +197 -0
- package/skills/protocol/references/protocol.md +93 -0
- package/skills/retro/SKILL.md +50 -0
- package/skills/retro/agents/openai.yaml +4 -0
- package/skills/work/SKILL.md +73 -0
- package/skills/work/agents/openai.yaml +4 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow
|
|
5
|
+
[SemVer](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [0.1.0] — 2026-07-07
|
|
8
|
+
|
|
9
|
+
First public release. The build of this release was itself executed as quests
|
|
10
|
+
1–12 in [`.quests/`](./.quests/) — contracts, checkpoints, an adversarial
|
|
11
|
+
review round, and a retro with five protocol amendments.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- `quest` CLI — the quest store: contracts with Objective / Done-when /
|
|
15
|
+
Validation-loop anchors, evidence-citing checkpoints, wave scheduling
|
|
16
|
+
(`list --ready`), compatible-expansion edits, lint, protocol + amendments.
|
|
17
|
+
Zero dependencies; strict fail-honest parsing; guided help on every command.
|
|
18
|
+
- Two store backends: local markdown (default) and GitHub Issues via `gh`
|
|
19
|
+
(labels mirror status/priority, checkpoints are issue comments, identical
|
|
20
|
+
bytes across backends).
|
|
21
|
+
- `quest-run` — headless runner driving **Claude** (`claude -p`, native `/goal`
|
|
22
|
+
mode) and **Codex** (`codex exec`, goal-tools prompt + corrective/continuation
|
|
23
|
+
resume) workers; deterministic budgets (sessions, cost, tokens), wall-clock
|
|
24
|
+
session timeout, stall enforcement, runs journal, `--notify`, `--parallel`
|
|
25
|
+
with optional worktree isolation, configurable codex sandbox mode.
|
|
26
|
+
- Five skills (`/quest:plan`, `/quest:work`, `/quest:orchestrate`,
|
|
27
|
+
`/quest:retro`, `/quest:protocol`) and two agents (`quest-executor`,
|
|
28
|
+
`quest-reviewer`), served to both harnesses from one tree.
|
|
29
|
+
- Hooks: SessionStart in-flight summary; SubagentStop checkpoint enforcement.
|
|
30
|
+
- CI: tests (Node 20/24), hygiene, manifest validation, agent/skill parity,
|
|
31
|
+
gitleaks secret scan.
|
|
32
|
+
- Repository scaffold: dual plugin manifests (Claude Code + Codex), protocol
|
|
33
|
+
and record-format specifications, bootstrap quest store tracking this
|
|
34
|
+
project's own build.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Robert Sreberski
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# quest
|
|
2
|
+
|
|
3
|
+
Goal-loop engineering for coding agents — one plugin for **Claude Code** and
|
|
4
|
+
**Codex**, plus a zero-dependency CLI.
|
|
5
|
+
|
|
6
|
+
quest turns asks into **evidence-checkable contracts** ("quests"), executes them
|
|
7
|
+
in **iterative loops that end in verifiable checkpoints**, orchestrates **Claude
|
|
8
|
+
and Codex workers** (in-session or headless, serial or parallel), and mines
|
|
9
|
+
retrospectives into **numbered protocol amendments** your future sessions
|
|
10
|
+
actually read.
|
|
11
|
+
|
|
12
|
+
- `quest` — the quest store: contracts, checkpoints, wave scheduling. Local
|
|
13
|
+
markdown files by default; GitHub Issues opt-in.
|
|
14
|
+
- `quest-run` — the headless runner: drives `claude -p` or `codex exec` workers
|
|
15
|
+
in native goal mode with deterministic budgets and notifications.
|
|
16
|
+
- Five skills — `/quest:plan`, `/quest:work`, `/quest:orchestrate`,
|
|
17
|
+
`/quest:retro`, `/quest:protocol`.
|
|
18
|
+
- Two agents — `quest-executor`, `quest-reviewer`.
|
|
19
|
+
|
|
20
|
+
## The idea in 30 seconds
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
you: /quest:plan add dark mode to the settings page
|
|
24
|
+
agent: creates quest 12 — Objective, Done-when, Validation loop… (quest create)
|
|
25
|
+
you: /quest:orchestrate
|
|
26
|
+
agent: dispatches a worker on quest 12; it iterates: milestone → validate →
|
|
27
|
+
commit → checkpoint. You review evidence, not vibes.
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Every quest ends in a checkpoint trail a fresh session can resume from — that is
|
|
31
|
+
the whole trick.
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
### CLI (any environment)
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm install -g quest-loop
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Puts `quest` and `quest-run` on your PATH everywhere — no harness required.
|
|
42
|
+
The plugin installs below add the skills, agents, and hooks on top.
|
|
43
|
+
|
|
44
|
+
### Claude Code
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
claude plugin marketplace add robertsreberski/quest
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Then, inside a Claude Code session:
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
/plugin install quest@quest
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
For local development against a checkout, point Claude Code at the repo directly
|
|
57
|
+
— no marketplace needed:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
claude --plugin-dir .
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Codex
|
|
64
|
+
|
|
65
|
+
The plugin ships a `.codex-plugin/` manifest and discovers its skills from the
|
|
66
|
+
repo checkout (`skills/`, surfaced under `.agents/` for Codex). Until a public
|
|
67
|
+
Codex marketplace listing is approved upstream, wire it up from a clone or
|
|
68
|
+
submodule:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
git clone https://github.com/robertsreberski/quest
|
|
72
|
+
# add the checkout to your Codex plugin/skill discovery path (clone or submodule)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The CLI (`quest`, `quest-run`) is harness-agnostic — it works the same whether
|
|
76
|
+
you drive it from Claude Code, Codex, or a plain shell.
|
|
77
|
+
|
|
78
|
+
## Quickstart
|
|
79
|
+
|
|
80
|
+
Create a store in your project, author a quest, check it, and work it:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
# 1. Create a quest store (.quests/) here
|
|
84
|
+
quest init
|
|
85
|
+
|
|
86
|
+
# 2. Author a quest — the CLI is the only way records are born
|
|
87
|
+
quest create --title "Add dark mode" \
|
|
88
|
+
--objective "Settings page offers a dark theme that persists." \
|
|
89
|
+
--done-when "toggling theme updates the UI and survives reload" \
|
|
90
|
+
--done-when "\`npm test\` passes including new theme tests" \
|
|
91
|
+
--validation "npm test"
|
|
92
|
+
|
|
93
|
+
# 3. Check it against the contract spec
|
|
94
|
+
quest lint --all
|
|
95
|
+
|
|
96
|
+
# 4. See what's ready to work (dependencies met)
|
|
97
|
+
quest list --ready
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Work the quest **in-session** with the skill:
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
/quest:work 12
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
…or **headless** with the runner:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
quest-run 12
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Each iteration ends by recording evidence — a checkpoint a fresh session can
|
|
113
|
+
resume from:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
quest checkpoint 12 --status complete \
|
|
117
|
+
--summary "M2 done — theme persistence via localStorage" \
|
|
118
|
+
--validation "\`npm test\` → 42 passed, 0 failed"
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Run `quest <command> --help` for flags and a copy-pasteable example of any
|
|
122
|
+
command.
|
|
123
|
+
|
|
124
|
+
## The loop
|
|
125
|
+
|
|
126
|
+
A quest is a goal contract: one Objective, evidence-checkable *Done-when*
|
|
127
|
+
conditions, a Validation loop of exact commands, and optional milestones. Each
|
|
128
|
+
iteration picks the smallest unfinished milestone, implements it end-to-end,
|
|
129
|
+
runs the Validation loop **exactly as written**, commits green, and records a
|
|
130
|
+
checkpoint citing the commands it ran and what they returned. A quest is
|
|
131
|
+
`complete` only when every Done-when item is enumerated with its evidence. The
|
|
132
|
+
full base protocol lives in
|
|
133
|
+
[`skills/protocol/references/protocol.md`](./skills/protocol/references/protocol.md)
|
|
134
|
+
(print it, with this store's local amendments, via `quest protocol`).
|
|
135
|
+
|
|
136
|
+
## CLI overview
|
|
137
|
+
|
|
138
|
+
| Command | Purpose |
|
|
139
|
+
|---|---|
|
|
140
|
+
| `quest init` | Create a quest store (`.quests/`) in the current directory |
|
|
141
|
+
| `quest create` | Create a new quest (the only way records are born) |
|
|
142
|
+
| `quest list` | List quests (filter by status, parent, or readiness) |
|
|
143
|
+
| `quest show` | Show a quest record in full |
|
|
144
|
+
| `quest start` | Mark a quest in_progress (todo → in_progress) |
|
|
145
|
+
| `quest checkpoint` | Record iteration evidence and drive the quest's status |
|
|
146
|
+
| `quest cancel` | Cancel a quest (terminal; reason is recorded) |
|
|
147
|
+
| `quest edit` | Compatibly expand a quest (additions only; anchors are immutable) |
|
|
148
|
+
| `quest lint` | Check records against the contract spec |
|
|
149
|
+
| `quest amend` | Append a numbered protocol amendment (retro output) |
|
|
150
|
+
| `quest protocol` | Print the loop protocol + this store's local amendments |
|
|
151
|
+
| `quest runs` | Show headless runner activity (from `.quests/runs.ndjson`) |
|
|
152
|
+
|
|
153
|
+
## quest-run (headless runner)
|
|
154
|
+
|
|
155
|
+
`quest-run <id>` drives a worker through the same loop without you in the chair:
|
|
156
|
+
|
|
157
|
+
- **Workers** — `claude` (`claude -p`) or `codex` (`codex exec`), selected per
|
|
158
|
+
quest. Both run in **native goal mode** with a machine-verifiable completion
|
|
159
|
+
condition.
|
|
160
|
+
- **Budgets** — deterministic iteration, cost, token, and per-session
|
|
161
|
+
wall-clock (`--session-timeout`, default 1800s) caps; two sessions without a
|
|
162
|
+
new checkpoint (a killed hung session counts as one) auto-writes a `blocked`
|
|
163
|
+
checkpoint and stops.
|
|
164
|
+
- **Backends** — drives `local` and `github`-backed stores alike; all record IO
|
|
165
|
+
goes through the `quest` CLI, and the runs journal stays local.
|
|
166
|
+
- **`--parallel N`** — with `--ready`, promotes and works newly-ready quests
|
|
167
|
+
across dependency waves, up to N at a time.
|
|
168
|
+
- **`--notify '<cmd>'`** — runs a templated command on run start/stop so you get
|
|
169
|
+
pinged; notify failures are isolated from the run.
|
|
170
|
+
- **`--codex-sandbox <mode>`** — selects the `codex exec` sandbox
|
|
171
|
+
(`read-only` | `workspace-write` | `danger-full-access`; resolved as flag →
|
|
172
|
+
config `defaults.codex.sandbox` → default `workspace-write`). Honest tradeoff:
|
|
173
|
+
the default **`workspace-write` write-protects `.git`, so a codex worker cannot
|
|
174
|
+
`git commit` under it** (the `index.lock` write fails). A quest whose worker
|
|
175
|
+
must commit has to opt into **`danger-full-access`** — which also grants full
|
|
176
|
+
disk and network access. The runner never escalates the sandbox silently; the
|
|
177
|
+
safe `workspace-write` stays the default. (Claude workers ignore this flag.)
|
|
178
|
+
|
|
179
|
+
Inspect activity with `quest runs --active`.
|
|
180
|
+
|
|
181
|
+
> **Note:** `quest-run` ships with this build's runner milestone (tracked as
|
|
182
|
+
> quest 5). On a pre-0.1.0 checkout where `bin/quest-run` isn't present yet, use
|
|
183
|
+
> `/quest:work` in-session instead.
|
|
184
|
+
|
|
185
|
+
## Store backends
|
|
186
|
+
|
|
187
|
+
- **`local`** (default) — records are markdown files under `.quests/quests/`;
|
|
188
|
+
config, amendments, and the runs journal stay local. Zero dependencies.
|
|
189
|
+
- **`github`** (opt-in) — `quest init --backend github --repo owner/name` stores
|
|
190
|
+
records as GitHub Issues via the `gh` CLI (labels mirror status/priority;
|
|
191
|
+
checkpoints become issue comments). Config, amendments, and runs stay local.
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
quest init --backend github --repo owner/name
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
The GitHub backend (`quest init --backend github`) and the headless runner
|
|
198
|
+
(`quest-run`) work together — `quest-run <id>` drives quests in a github-backed
|
|
199
|
+
store exactly as it does locally.
|
|
200
|
+
|
|
201
|
+
## Exit codes
|
|
202
|
+
|
|
203
|
+
| Code | Meaning |
|
|
204
|
+
|---|---|
|
|
205
|
+
| 0 | success |
|
|
206
|
+
| 2 | usage error (bad flags/arguments) |
|
|
207
|
+
| 3 | no quest store found / config invalid |
|
|
208
|
+
| 4 | quest not found |
|
|
209
|
+
| 5 | contract violation (lint failure, malformed record, illegal transition) |
|
|
210
|
+
| 6 | backend unavailable (`gh` missing, unauthenticated, network) |
|
|
211
|
+
| 10 | (`quest-run`) ended blocked |
|
|
212
|
+
| 11 | (`quest-run`) budget exhausted |
|
|
213
|
+
|
|
214
|
+
## Status
|
|
215
|
+
|
|
216
|
+
The build of quest is itself tracked as quests — see [`.quests/`](./.quests/).
|
|
217
|
+
Contributions welcome; see [CONTRIBUTING.md](./CONTRIBUTING.md).
|
|
218
|
+
|
|
219
|
+
## License
|
|
220
|
+
|
|
221
|
+
MIT
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: quest-executor
|
|
3
|
+
description: Works exactly one quest record iteratively per the quest protocol until complete or blocked, recording checkpoint evidence via the quest CLI. Dispatch with the quest id; pass the record's model/effort as the dispatch override. <example>Orchestrator sees quest 12 ready with worker claude → dispatches quest-executor with "Work quest 12 per /quest:work"; it iterates milestone-by-milestone and ends with a recorded checkpoint.</example> <example>A quest sits blocked after a human ruling → redispatch quest-executor with the ruling; it resumes from the checkpoint trail alone.</example>
|
|
4
|
+
model: opus
|
|
5
|
+
effort: xhigh
|
|
6
|
+
tools: Bash, Read, Edit, Write, Glob, Grep, WebFetch, WebSearch, NotebookEdit
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
You execute ONE quest. Decomposition, dispatching, and rulings belong to the
|
|
10
|
+
orchestrator — which is why you have no agent-spawning tools. Your work exists
|
|
11
|
+
only insofar as it is recorded in the quest's checkpoint trail.
|
|
12
|
+
|
|
13
|
+
## First actions, always
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
quest show <id> --json # the contract and every prior checkpoint
|
|
17
|
+
quest protocol # the loop rules + this store's amendments
|
|
18
|
+
git log --oneline -5
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Then follow the work skill (`/quest:work`) exactly: smallest unfinished
|
|
22
|
+
milestone → the quest's STATED validation loop → commit green → `quest
|
|
23
|
+
checkpoint`. The record is your entire spec; if you need context it doesn't
|
|
24
|
+
give you, read the code it points at — and if it's genuinely insufficient,
|
|
25
|
+
checkpoint `blocked` saying exactly what's missing.
|
|
26
|
+
|
|
27
|
+
## Non-negotiable backstops
|
|
28
|
+
|
|
29
|
+
- Never stop — for any reason — without recording a checkpoint via
|
|
30
|
+
`quest checkpoint`. A stop without one is a protocol violation.
|
|
31
|
+
- Never edit or delete existing tests to make them pass.
|
|
32
|
+
- Never fake success or hide a failure behind a fallback; report the real state.
|
|
33
|
+
- Never substitute a stated validation check silently — name any substitution.
|
|
34
|
+
- An unsatisfiable Done-when, the same error twice, or a human-only decision →
|
|
35
|
+
`--status blocked` with the exact discrepancy. Blocked beats improvised.
|
|
36
|
+
- Respect `max_iterations`; if exhausted, checkpoint blocked with the reason.
|
|
37
|
+
|
|
38
|
+
## Final report
|
|
39
|
+
|
|
40
|
+
End with `quest show <id>` so the recorded state is visible, then report
|
|
41
|
+
exactly three things: the quest_status you recorded, the checkpoint timestamp,
|
|
42
|
+
and a one-line evidence summary citing the decisive command + result.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
name = "quest-executor"
|
|
2
|
+
description = "Works exactly one quest record iteratively per the quest protocol until complete or blocked, recording checkpoint evidence via the quest CLI."
|
|
3
|
+
model_reasoning_effort = "high"
|
|
4
|
+
developer_instructions = """
|
|
5
|
+
You execute ONE quest. Decomposition, dispatching, and rulings belong to the
|
|
6
|
+
orchestrator. Your work exists only insofar as it is recorded in the quest's
|
|
7
|
+
checkpoint trail.
|
|
8
|
+
|
|
9
|
+
First actions, always: `quest show <id> --json`, `quest protocol`,
|
|
10
|
+
`git log --oneline -5`. Then follow the work skill exactly: smallest unfinished
|
|
11
|
+
milestone -> the quest's STATED validation loop -> commit green ->
|
|
12
|
+
`quest checkpoint`. The record is your entire spec; if it is genuinely
|
|
13
|
+
insufficient, checkpoint blocked saying exactly what is missing.
|
|
14
|
+
|
|
15
|
+
Non-negotiable backstops:
|
|
16
|
+
- Never stop without recording a checkpoint via `quest checkpoint`.
|
|
17
|
+
- Never edit or delete existing tests to make them pass.
|
|
18
|
+
- Never fake success or hide a failure behind a fallback.
|
|
19
|
+
- Never substitute a stated validation check silently — name any substitution.
|
|
20
|
+
- Unsatisfiable Done-when, same error twice, or a human-only decision ->
|
|
21
|
+
--status blocked with the exact discrepancy. Blocked beats improvised.
|
|
22
|
+
- Respect max_iterations; if exhausted, checkpoint blocked with the reason.
|
|
23
|
+
|
|
24
|
+
Final report: run `quest show <id>` so recorded state is visible, then report
|
|
25
|
+
the quest_status you recorded, the checkpoint timestamp, and a one-line
|
|
26
|
+
evidence summary citing the decisive command + result.
|
|
27
|
+
"""
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: quest-reviewer
|
|
3
|
+
description: Adversarial reviewer for quests claiming complete — verifies the checkpoint evidence actually discharges every Done-when item and hunts silent substitutions, fake greens, and scope drift. Dispatch before accepting any non-trivial complete ruling. <example>Executor checkpoints quest 12 complete → orchestrator dispatches quest-reviewer on the diff + record; it re-runs the validation loop and reports findings with required dispositions.</example>
|
|
4
|
+
model: opus
|
|
5
|
+
effort: xhigh
|
|
6
|
+
tools: Bash, Read, Glob, Grep
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
You verify, adversarially, that a quest claiming `complete` earned it. You do
|
|
10
|
+
not fix anything — you report findings the orchestrator must disposition.
|
|
11
|
+
|
|
12
|
+
## Procedure
|
|
13
|
+
|
|
14
|
+
1. Read the contract and trail: `quest show <id> --json`, `quest protocol`.
|
|
15
|
+
2. Read the ACTUAL changes (diff/commits the checkpoints cite), not the
|
|
16
|
+
summary of them.
|
|
17
|
+
3. Re-run the quest's stated Validation loop yourself. The checkpoint's claims
|
|
18
|
+
must reproduce; "it said so" is not evidence.
|
|
19
|
+
4. Interrogate each Done-when item: which command output proves it? Was a
|
|
20
|
+
stated check substituted without being named? Did tests get edited to pass?
|
|
21
|
+
Did scope drift past the Objective without a recorded expansion?
|
|
22
|
+
5. Check the trail itself: could a zero-context session resume this quest from
|
|
23
|
+
the record alone? Are references symbol+file (not rotted line numbers)?
|
|
24
|
+
|
|
25
|
+
## Report
|
|
26
|
+
|
|
27
|
+
Findings ordered by severity, each with: what's wrong, the evidence (command +
|
|
28
|
+
output or file + symbol), and why it blocks acceptance. If nothing survived
|
|
29
|
+
your scrutiny, say "no findings" plainly — do not invent nitpicks. End with a
|
|
30
|
+
verdict: **accept** / **iterate** (list exactly what must change) — the
|
|
31
|
+
orchestrator rules; every finding you raise must receive a disposition
|
|
32
|
+
(fixed / follow-up quest / rejected-with-reason) before the quest is accepted.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
name = "quest-reviewer"
|
|
2
|
+
description = "Adversarial reviewer for quests claiming complete — verifies the checkpoint evidence actually discharges every Done-when item and hunts silent substitutions, fake greens, and scope drift."
|
|
3
|
+
model_reasoning_effort = "high"
|
|
4
|
+
developer_instructions = """
|
|
5
|
+
You verify, adversarially, that a quest claiming complete earned it. You do not
|
|
6
|
+
fix anything — you report findings the orchestrator must disposition.
|
|
7
|
+
|
|
8
|
+
Procedure: read the contract and trail (`quest show <id> --json`,
|
|
9
|
+
`quest protocol`); read the ACTUAL changes the checkpoints cite, not the
|
|
10
|
+
summary; re-run the quest's stated Validation loop yourself — claims must
|
|
11
|
+
reproduce; interrogate each Done-when item (which command output proves it?
|
|
12
|
+
was a stated check substituted silently? were tests edited to pass? did scope
|
|
13
|
+
drift past the Objective without a recorded expansion?); check the trail
|
|
14
|
+
itself (could a zero-context session resume from the record alone?).
|
|
15
|
+
|
|
16
|
+
Report findings ordered by severity, each with the evidence (command + output
|
|
17
|
+
or file + symbol) and why it blocks acceptance. If nothing survived scrutiny,
|
|
18
|
+
say "no findings" plainly — do not invent nitpicks. End with a verdict:
|
|
19
|
+
accept, or iterate with exactly what must change. Every finding must receive a
|
|
20
|
+
disposition (fixed / follow-up quest / rejected-with-reason) before acceptance.
|
|
21
|
+
"""
|
package/bin/quest
ADDED
package/bin/quest-run
ADDED
package/hooks/hooks.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "Quest goal-loop hooks: inject in-flight quest context at session start, and block quest-executor subagents that try to stop without recording a checkpoint.",
|
|
3
|
+
"hooks": {
|
|
4
|
+
"SessionStart": [
|
|
5
|
+
{
|
|
6
|
+
"matcher": "startup",
|
|
7
|
+
"hooks": [
|
|
8
|
+
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/session-start.mjs\"" }
|
|
9
|
+
]
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"matcher": "clear",
|
|
13
|
+
"hooks": [
|
|
14
|
+
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/session-start.mjs\"" }
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"matcher": "compact",
|
|
19
|
+
"hooks": [
|
|
20
|
+
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/session-start.mjs\"" }
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
],
|
|
24
|
+
"SubagentStop": [
|
|
25
|
+
{
|
|
26
|
+
"hooks": [
|
|
27
|
+
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/subagent-stop.mjs\"" }
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SessionStart hook (startup / clear / compact). When a `.quests/` store exists
|
|
3
|
+
// at or above the session's cwd, write a short one-paragraph summary of in-flight
|
|
4
|
+
// quests and active runs to stdout — Claude Code injects stdout as session
|
|
5
|
+
// context. No store, a non-local backend, an empty store, or ANY error → a silent
|
|
6
|
+
// no-op (exit 0). Reads only; no network and no child processes, so it stays fast.
|
|
7
|
+
|
|
8
|
+
import { writeSync } from "node:fs";
|
|
9
|
+
import { findStoreDir, loadConfig } from "../lib/config.mjs";
|
|
10
|
+
import { listQuests, readRuns } from "../lib/store-local.mjs";
|
|
11
|
+
|
|
12
|
+
const STATUS_ORDER = ["in_progress", "blocked", "todo", "complete", "cancelled"];
|
|
13
|
+
|
|
14
|
+
async function readStdin() {
|
|
15
|
+
const chunks = [];
|
|
16
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
17
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function activeRunCount(storeDir) {
|
|
21
|
+
const runs = readRuns(storeDir);
|
|
22
|
+
const ended = new Set(runs.filter((r) => r.event === "run_ended").map((r) => r.run_id));
|
|
23
|
+
return runs.filter((r) => r.event === "run_started" && !ended.has(r.run_id)).length;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// One paragraph (<= ~3 lines): counts by status, up to 3 in-flight quests, the
|
|
27
|
+
// active-run count, and the ready-list hint. Returns null when there is nothing
|
|
28
|
+
// worth injecting (empty store).
|
|
29
|
+
function buildContext(storeDir) {
|
|
30
|
+
const all = listQuests(storeDir);
|
|
31
|
+
if (!all.length) return null;
|
|
32
|
+
const counts = {};
|
|
33
|
+
for (const q of all) counts[q.status] = (counts[q.status] ?? 0) + 1;
|
|
34
|
+
const countStr = STATUS_ORDER.filter((s) => counts[s]).map((s) => `${counts[s]} ${s}`).join(", ");
|
|
35
|
+
const inFlight = all
|
|
36
|
+
.filter((q) => q.status === "in_progress" || q.status === "blocked")
|
|
37
|
+
.sort((a, b) => (a.status === b.status ? a.id - b.id : a.status === "in_progress" ? -1 : 1))
|
|
38
|
+
.slice(0, 3)
|
|
39
|
+
.map((q) => `#${q.id} ${q.title} [${q.status}]`);
|
|
40
|
+
const lines = [`Quest store (.quests): ${all.length} quest${all.length === 1 ? "" : "s"} — ${countStr}.`];
|
|
41
|
+
if (inFlight.length) lines.push(`In flight: ${inFlight.join("; ")}.`);
|
|
42
|
+
lines.push(`Active runs: ${activeRunCount(storeDir)}. Ready to work next: \`quest list --ready\`.`);
|
|
43
|
+
return lines.join("\n");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const raw = await readStdin();
|
|
48
|
+
let payload = {};
|
|
49
|
+
try { payload = raw.trim() ? JSON.parse(raw) : {}; } catch { payload = {}; }
|
|
50
|
+
const cwd = typeof payload.cwd === "string" && payload.cwd ? payload.cwd : process.cwd();
|
|
51
|
+
const storeDir = findStoreDir(cwd, process.env);
|
|
52
|
+
if (!storeDir) process.exit(0); // no store here — silent no-op, NOT an error
|
|
53
|
+
if (loadConfig(storeDir, process.env).backend !== "local") process.exit(0); // only local is readable today
|
|
54
|
+
const context = buildContext(storeDir);
|
|
55
|
+
if (context) writeSync(1, context + "\n");
|
|
56
|
+
process.exit(0);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
// Never turn a best-effort context hint into a session error.
|
|
59
|
+
writeSync(2, `quest session-start hook: ${err.message}\n`);
|
|
60
|
+
process.exit(0);
|
|
61
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SubagentStop hook. Blocks a quest-executor subagent that tries to stop without
|
|
3
|
+
// recording a checkpoint — the protocol's "no stop without a checkpoint" rule,
|
|
4
|
+
// enforced deterministically.
|
|
5
|
+
//
|
|
6
|
+
// A subagent counts as a quest-executor iff its transcript contains a
|
|
7
|
+
// `quest show <id> --json` invocation (the mandatory orientation marker). We take
|
|
8
|
+
// the FIRST id. No marker → not our concern, allow silently. We then compare the
|
|
9
|
+
// quest's latest checkpoint against the subagent's start time (the transcript's
|
|
10
|
+
// first timestamp): a checkpoint newer than start clears the stop; so does a
|
|
11
|
+
// terminal store status (complete/blocked/cancelled) reached during the run.
|
|
12
|
+
//
|
|
13
|
+
// Conservative by construction: any missing/unreadable input or parse failure →
|
|
14
|
+
// allow (exit 0), with a one-line stderr diagnostic. We never false-positive-block
|
|
15
|
+
// unrelated subagents.
|
|
16
|
+
//
|
|
17
|
+
// Block contract (Claude Code command hook): print {"decision":"block","reason"}
|
|
18
|
+
// to stdout and exit 0. (Note: the {"ok":false,...} shape is for prompt/agent
|
|
19
|
+
// hooks, not command hooks — see the checkpoint notes for this quest.)
|
|
20
|
+
|
|
21
|
+
import { readFileSync, writeSync } from "node:fs";
|
|
22
|
+
import { findStoreDir } from "../lib/config.mjs";
|
|
23
|
+
import { loadQuest } from "../lib/store-local.mjs";
|
|
24
|
+
|
|
25
|
+
const MARKER = /quest\s+show\s+(\d+)\s+--json/;
|
|
26
|
+
const TERMINAL = ["complete", "blocked", "cancelled"];
|
|
27
|
+
|
|
28
|
+
async function readStdin() {
|
|
29
|
+
const chunks = [];
|
|
30
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
31
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function allow() { process.exit(0); }
|
|
35
|
+
function diag(msg) { writeSync(2, `quest subagent-stop hook: ${msg}\n`); }
|
|
36
|
+
function block(id) {
|
|
37
|
+
const reason = `quest ${id}: record a checkpoint via \`quest checkpoint ${id}\` before stopping (protocol: no stop without a checkpoint)`;
|
|
38
|
+
writeSync(1, JSON.stringify({ decision: "block", reason }) + "\n");
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function toMs(v) {
|
|
43
|
+
if (typeof v !== "string" || !v) return null;
|
|
44
|
+
const ms = Date.parse(v);
|
|
45
|
+
return Number.isNaN(ms) ? null : ms;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// The subagent's start = the first parseable `timestamp` in the JSONL transcript.
|
|
49
|
+
function firstTimestamp(text) {
|
|
50
|
+
for (const line of text.split("\n")) {
|
|
51
|
+
if (!line.trim()) continue;
|
|
52
|
+
let obj;
|
|
53
|
+
try { obj = JSON.parse(line); } catch { continue; }
|
|
54
|
+
const ms = obj && typeof obj === "object" ? toMs(obj.timestamp) : null;
|
|
55
|
+
if (ms != null) return ms;
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const raw = await readStdin();
|
|
62
|
+
let payload = {};
|
|
63
|
+
try { payload = raw.trim() ? JSON.parse(raw) : {}; } catch { payload = {}; }
|
|
64
|
+
|
|
65
|
+
const transcriptPath = payload.transcript_path;
|
|
66
|
+
if (typeof transcriptPath !== "string" || !transcriptPath) { diag("no transcript_path in payload; allowing"); allow(); }
|
|
67
|
+
|
|
68
|
+
let transcript;
|
|
69
|
+
try { transcript = readFileSync(transcriptPath, "utf8"); }
|
|
70
|
+
catch (err) { diag(`cannot read transcript (${err.code || err.message}); allowing`); allow(); }
|
|
71
|
+
|
|
72
|
+
const m = MARKER.exec(transcript);
|
|
73
|
+
if (!m) allow(); // not a quest-executor subagent — leave it entirely alone, silently
|
|
74
|
+
const id = Number(m[1]);
|
|
75
|
+
|
|
76
|
+
// Prefer an explicit start field if a future payload carries one; else the
|
|
77
|
+
// transcript's first timestamp.
|
|
78
|
+
const start = toMs(payload.start_time) ?? toMs(payload.started_at) ?? firstTimestamp(transcript);
|
|
79
|
+
if (start == null) { diag(`quest ${id}: could not determine subagent start time; allowing`); allow(); }
|
|
80
|
+
|
|
81
|
+
const cwd = typeof payload.cwd === "string" && payload.cwd ? payload.cwd : process.cwd();
|
|
82
|
+
let storeDir;
|
|
83
|
+
try { storeDir = findStoreDir(cwd, process.env); }
|
|
84
|
+
catch (err) { diag(`quest ${id}: ${err.message}; allowing`); allow(); }
|
|
85
|
+
if (!storeDir) { diag(`quest ${id}: no store found from ${cwd}; allowing`); allow(); }
|
|
86
|
+
|
|
87
|
+
let quest;
|
|
88
|
+
try { quest = loadQuest(storeDir, id); }
|
|
89
|
+
catch (err) { diag(`quest ${id}: ${err.message}; allowing`); allow(); }
|
|
90
|
+
|
|
91
|
+
// A checkpoint recorded after the subagent started clears the stop.
|
|
92
|
+
const latestCp = quest.checkpoints.reduce((max, cp) => {
|
|
93
|
+
const ms = toMs(cp.timestamp);
|
|
94
|
+
return ms != null && ms > max ? ms : max;
|
|
95
|
+
}, -Infinity);
|
|
96
|
+
if (latestCp > start) allow();
|
|
97
|
+
|
|
98
|
+
// A terminal store status reached during this run also clears it — covers
|
|
99
|
+
// `quest cancel`, which records a note rather than a checkpoint marker.
|
|
100
|
+
const updated = toMs(quest.front.updated);
|
|
101
|
+
if (TERMINAL.includes(quest.front.status) && updated != null && updated > start) allow();
|
|
102
|
+
|
|
103
|
+
block(id);
|
|
104
|
+
} catch (err) {
|
|
105
|
+
diag(err.message);
|
|
106
|
+
process.exit(0);
|
|
107
|
+
}
|