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 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
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { run } from "../lib/cli.mjs";
3
+ process.exit(await run(process.argv.slice(2)));
package/bin/quest-run ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { run } from "../lib/runner.mjs";
3
+ process.exit(await run(process.argv.slice(2)));
@@ -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
+ }