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.
@@ -0,0 +1,372 @@
1
+ // Worker adapters for the headless runner. BOTH the Claude and Codex adapters
2
+ // live here. Each exposes the shared interface the runner drives generically:
3
+ //
4
+ // buildInvocation(questRecord, config, opts) -> { cmd, args, env, prompt, artifactsFile? }
5
+ // parseResult(stdout, artifacts) -> { session_id?, cost_usd?, tokens?, events? }
6
+ //
7
+ // plus a higher-level `runSession(ctx)` that composes those two into one runner
8
+ // iteration (a single worker SESSION). Codex's session additionally owns the
9
+ // native-goal correction + same-session continuation resumes; Claude's is a
10
+ // single non-interactive `claude -p` call in native /goal mode.
11
+ //
12
+ // Design invariants:
13
+ // - Adapters never touch the quest store directly. They only shape and parse
14
+ // worker invocations; the runner owns all `quest` CLI reads/writes.
15
+ // - USD cost is only ever reported when the worker reports it (Claude's
16
+ // total_cost_usd). Codex cost stays token-only — never fabricated.
17
+
18
+ import { readFileSync } from "node:fs";
19
+ import { join } from "node:path";
20
+ import { tmpdir } from "node:os";
21
+ import { randomUUID } from "node:crypto";
22
+
23
+ // Non-interactive backstop injected into the Claude system prompt. A headless
24
+ // worker must never block on input; a human-only decision becomes a blocked
25
+ // checkpoint and a stop, which the runner then observes.
26
+ const CLAUDE_APPEND_SYSTEM_PROMPT =
27
+ "Non-interactive headless run: never wait for input or ask a question. " +
28
+ "If a decision can only be made by a human, checkpoint the quest with " +
29
+ "quest_status blocked (via `quest checkpoint`) stating the decision needed, then stop.";
30
+
31
+ // The machine-generated, conversation-verifiable stopping condition shared by
32
+ // both workers. `runStartIso` makes it verifiable: only a checkpoint newer than
33
+ // the run start satisfies it, so a stale prior checkpoint can't be mistaken for
34
+ // progress.
35
+ function stoppingCondition(id, runStartIso) {
36
+ return (
37
+ "the output of `quest show " +
38
+ id +
39
+ " --json` shown in this conversation contains a NEW checkpoint (timestamp after " +
40
+ runStartIso +
41
+ ") with quest_status complete or blocked"
42
+ );
43
+ }
44
+
45
+ function workInstructions(id) {
46
+ return (
47
+ "Work quest " +
48
+ id +
49
+ " per the /quest:work skill. The quest store is in this directory. " +
50
+ "End every iteration by running quest show " +
51
+ id +
52
+ "."
53
+ );
54
+ }
55
+
56
+ // ---------------------------------------------------------------------------
57
+ // Shared parse helpers
58
+ // ---------------------------------------------------------------------------
59
+
60
+ function tryParseJson(text) {
61
+ if (!text) return undefined;
62
+ const trimmed = text.trim();
63
+ if (!trimmed) return undefined;
64
+ try {
65
+ return JSON.parse(trimmed);
66
+ } catch {
67
+ // Fall back to the last non-empty line (streamed multi-object stdout).
68
+ const lines = trimmed.split("\n").filter((l) => l.trim());
69
+ for (let i = lines.length - 1; i >= 0; i--) {
70
+ try {
71
+ return JSON.parse(lines[i]);
72
+ } catch {
73
+ /* keep scanning upward */
74
+ }
75
+ }
76
+ return undefined;
77
+ }
78
+ }
79
+
80
+ function parseJsonl(text) {
81
+ if (!text) return [];
82
+ return text
83
+ .split("\n")
84
+ .map((l) => l.trim())
85
+ .filter(Boolean)
86
+ .map((l) => {
87
+ try {
88
+ return JSON.parse(l);
89
+ } catch {
90
+ return null;
91
+ }
92
+ })
93
+ .filter(Boolean);
94
+ }
95
+
96
+ function num(v) {
97
+ return typeof v === "number" && Number.isFinite(v) ? v : undefined;
98
+ }
99
+
100
+ function sumTokens(usage) {
101
+ if (!usage || typeof usage !== "object") return undefined;
102
+ const input = num(usage.input_tokens) ?? num(usage.prompt_tokens) ?? 0;
103
+ const output = num(usage.output_tokens) ?? num(usage.completion_tokens) ?? 0;
104
+ const total = num(usage.total_tokens);
105
+ if (total !== undefined) return total;
106
+ const sum = input + output;
107
+ return sum > 0 ? sum : undefined;
108
+ }
109
+
110
+ // ---------------------------------------------------------------------------
111
+ // Claude adapter
112
+ // ---------------------------------------------------------------------------
113
+
114
+ export const claude = {
115
+ name: "claude",
116
+
117
+ buildInvocation(questRecord, config, opts) {
118
+ const { id } = opts;
119
+ const prompt =
120
+ "/goal " +
121
+ stoppingCondition(id, opts.runStartIso) +
122
+ "\n" +
123
+ workInstructions(id);
124
+ const args = [
125
+ "-p",
126
+ prompt,
127
+ "--plugin-dir",
128
+ opts.pluginRoot,
129
+ "--model",
130
+ opts.model,
131
+ "--output-format",
132
+ "json",
133
+ "--permission-mode",
134
+ "acceptEdits",
135
+ "--allowedTools",
136
+ "Bash,Read,Edit,Write,Glob,Grep,Skill",
137
+ "--append-system-prompt",
138
+ CLAUDE_APPEND_SYSTEM_PROMPT,
139
+ ];
140
+ if (opts.resumeSessionId) args.push("--resume", opts.resumeSessionId);
141
+ // Effort is passed via env: CLAUDE_EFFORT is observed-supported; no stable
142
+ // documented flag at this CLI version, so env is the deliberate seam.
143
+ // PATH prepends the plugin's bin/ so the worker's Bash can reach the `quest`
144
+ // CLI to record checkpoints (Claude Code does not guarantee plugin bins on
145
+ // the child's PATH); QUEST_DIR is added by the runner so records stay central.
146
+ const basePath = opts.env?.PATH ?? "";
147
+ const env = {
148
+ PATH: `${join(opts.pluginRoot, "bin")}:${basePath}`,
149
+ ...(opts.effort ? { CLAUDE_EFFORT: String(opts.effort) } : {}),
150
+ };
151
+ return { cmd: "claude", args, env, prompt };
152
+ },
153
+
154
+ parseResult(stdout /* , artifacts */) {
155
+ const obj = tryParseJson(stdout);
156
+ if (!obj) return {};
157
+ return {
158
+ session_id: typeof obj.session_id === "string" ? obj.session_id : undefined,
159
+ cost_usd: num(obj.total_cost_usd),
160
+ tokens: sumTokens(obj.usage),
161
+ };
162
+ },
163
+
164
+ // One Claude session = one `claude -p` call in native /goal mode.
165
+ async runSession(ctx) {
166
+ const inv = this.buildInvocation(ctx.questRecord, ctx.config, ctx.opts);
167
+ const { stdout } = await ctx.spawn(inv);
168
+ const res = this.parseResult(stdout, {});
169
+ return { ...res, invocations: [inv], sawGoal: true };
170
+ },
171
+ };
172
+
173
+ // ---------------------------------------------------------------------------
174
+ // Codex adapter
175
+ // ---------------------------------------------------------------------------
176
+
177
+ const CODEX_MAX_RESUMES = 3;
178
+
179
+ // Legal `codex exec --sandbox` modes and the default. workspace-write (the
180
+ // default) write-protects `.git`, so a codex worker under it CANNOT `git commit`
181
+ // (the index.lock write fails — confirmed empirically in quest 8's walkthrough).
182
+ // Commit-requiring quests must explicitly opt into danger-full-access via the
183
+ // runner's --codex-sandbox flag (or config defaults.codex.sandbox). The runner
184
+ // validates the resolved value; buildInvocation only consumes it.
185
+ export const CODEX_SANDBOX_MODES = ["read-only", "workspace-write", "danger-full-access"];
186
+ export const DEFAULT_CODEX_SANDBOX = "workspace-write";
187
+
188
+ function codexArtifactsFile() {
189
+ return join(tmpdir(), `quest-run-codex-${randomUUID()}.json`);
190
+ }
191
+
192
+ // Recognise an ACTUAL create_goal tool invocation in the codex `--json` event
193
+ // stream — not prose that merely mentions it. Only tool/function/mcp-call event
194
+ // items count, so a narrated "I will create_goal…" is correctly treated as
195
+ // missing and triggers the corrective resume.
196
+ function isToolCallItem(item) {
197
+ if (!item || typeof item !== "object") return false;
198
+ const t = String(item.type || "");
199
+ return /tool_call|function_call|mcp|tool_use|command/i.test(t);
200
+ }
201
+
202
+ function toolCallName(item) {
203
+ return String(item?.tool || item?.name || item?.tool_name || item?.function?.name || "");
204
+ }
205
+
206
+ export function codexUsedCreateGoal(events) {
207
+ for (const e of events) {
208
+ // Newer shape: { type: "item.*", item: {...} }
209
+ if (e && e.item && isToolCallItem(e.item) && toolCallName(e.item).includes("create_goal")) return true;
210
+ // Legacy shape: { msg: { type, ... } } / flat tool-call event.
211
+ if (e && e.msg && isToolCallItem(e.msg) && toolCallName(e.msg).includes("create_goal")) return true;
212
+ if (isToolCallItem(e) && toolCallName(e).includes("create_goal")) return true;
213
+ }
214
+ return false;
215
+ }
216
+
217
+ function codexSessionId(events) {
218
+ for (const e of events) {
219
+ const id =
220
+ e?.session_id ||
221
+ e?.thread_id ||
222
+ e?.item?.thread_id ||
223
+ e?.session?.id ||
224
+ e?.thread?.id ||
225
+ (e?.msg && (e.msg.session_id || e.msg.thread_id));
226
+ if (typeof id === "string" && id) return id;
227
+ }
228
+ return undefined;
229
+ }
230
+
231
+ function codexTokens(events) {
232
+ let tokens;
233
+ for (const e of events) {
234
+ const usage = e?.usage || e?.item?.usage || e?.msg?.usage || (e?.type === "token_count" ? e : undefined);
235
+ const t = sumTokens(usage);
236
+ if (t !== undefined) tokens = t; // last usage event wins (running total)
237
+ }
238
+ return tokens;
239
+ }
240
+
241
+ const CODEX_CREATE_GOAL_CORRECTION = (id) =>
242
+ "You did not invoke the create_goal tool — you narrated it instead. Call the create_goal tool now " +
243
+ "(a real tool call, not prose) with the stopping condition, verify with get_goal, then continue working quest " +
244
+ id +
245
+ " per /quest:work.";
246
+
247
+ export const codex = {
248
+ name: "codex",
249
+
250
+ buildInvocation(questRecord, config, opts) {
251
+ const { id } = opts;
252
+ const prompt =
253
+ "First step: Create a goal for this thread using the create_goal tool (not as prose) with this exact " +
254
+ "stopping condition: " +
255
+ stoppingCondition(id, opts.runStartIso) +
256
+ '. Verify with get_goal. Only call update_goal(status="complete") AFTER `quest checkpoint` succeeded.' +
257
+ "\n" +
258
+ workInstructions(id);
259
+ const artifactsFile = codexArtifactsFile();
260
+ const args = [
261
+ "exec",
262
+ prompt,
263
+ "--json",
264
+ "-m",
265
+ opts.model,
266
+ "-C",
267
+ opts.cwd,
268
+ "--sandbox",
269
+ opts.codexSandbox ?? DEFAULT_CODEX_SANDBOX,
270
+ "--skip-git-repo-check",
271
+ "-o",
272
+ artifactsFile,
273
+ "--output-schema",
274
+ opts.schemaPath,
275
+ ];
276
+ if (opts.effort) args.push("-c", `model_reasoning_effort=${opts.effort}`);
277
+ // Prepend the plugin's bin/ so the `quest` CLI resolves inside the sandbox.
278
+ const basePath = opts.env?.PATH ?? "";
279
+ const env = { PATH: `${join(opts.pluginRoot, "bin")}:${basePath}` };
280
+ return { cmd: "codex", args, env, prompt, artifactsFile };
281
+ },
282
+
283
+ // Build a `codex exec resume` invocation for the corrective/continuation
284
+ // segments. `sessionArg` is "--last" or a concrete session/thread id.
285
+ buildResume(questRecord, config, opts, sessionArg, text) {
286
+ const artifactsFile = codexArtifactsFile();
287
+ const args = ["exec", "resume"];
288
+ if (sessionArg === "--last") args.push("--last");
289
+ else args.push(sessionArg);
290
+ args.push(text, "--json", "-C", opts.cwd, "--skip-git-repo-check", "-o", artifactsFile, "--output-schema", opts.schemaPath);
291
+ if (opts.effort) args.push("-c", `model_reasoning_effort=${opts.effort}`);
292
+ const basePath = opts.env?.PATH ?? "";
293
+ const env = { PATH: `${join(opts.pluginRoot, "bin")}:${basePath}` };
294
+ return { cmd: "codex", args, env, prompt: text, artifactsFile };
295
+ },
296
+
297
+ parseResult(stdout, artifacts = {}) {
298
+ const events = parseJsonl(stdout);
299
+ return {
300
+ session_id: codexSessionId(events),
301
+ cost_usd: undefined, // codex reports no USD — never fabricated
302
+ tokens: codexTokens(events),
303
+ events,
304
+ lastMessage: artifacts.lastMessage,
305
+ };
306
+ },
307
+
308
+ readArtifacts(inv) {
309
+ if (!inv.artifactsFile) return {};
310
+ try {
311
+ return { lastMessage: readFileSync(inv.artifactsFile, "utf8") };
312
+ } catch {
313
+ return {};
314
+ }
315
+ },
316
+
317
+ // One Codex iteration: initial segment, then (if create_goal was narrated
318
+ // rather than invoked) exactly one corrective resume, then same-session
319
+ // continuation resumes while the stopping condition is unmet — capped at
320
+ // CODEX_MAX_RESUMES total resume segments per iteration.
321
+ async runSession(ctx) {
322
+ const { questRecord, config, opts } = ctx;
323
+ const invocations = [];
324
+ let cost, tokens, sessionId;
325
+ let resumes = 0;
326
+ let correctiveResume = false;
327
+
328
+ const runSegment = async (inv) => {
329
+ invocations.push(inv);
330
+ const { stdout } = await ctx.spawn(inv);
331
+ const res = this.parseResult(stdout, this.readArtifacts(inv));
332
+ if (res.session_id) sessionId = res.session_id;
333
+ if (res.tokens !== undefined) tokens = res.tokens;
334
+ if (res.cost_usd !== undefined) cost = res.cost_usd;
335
+ return res;
336
+ };
337
+
338
+ // Initial segment.
339
+ const first = await runSegment(this.buildInvocation(questRecord, config, opts));
340
+ let sawGoal = codexUsedCreateGoal(first.events || []);
341
+
342
+ // Corrective resume: create_goal was narrated, not invoked.
343
+ if (!sawGoal) {
344
+ correctiveResume = true;
345
+ resumes += 1;
346
+ const corr = this.buildResume(questRecord, config, opts, "--last", CODEX_CREATE_GOAL_CORRECTION(opts.id));
347
+ const cres = await runSegment(corr);
348
+ if (codexUsedCreateGoal(cres.events || [])) sawGoal = true;
349
+ }
350
+
351
+ // Same-session continuation while the stopping condition is unmet.
352
+ while (resumes < CODEX_MAX_RESUMES) {
353
+ const state = await ctx.readState();
354
+ const newCheckpoint = (state?.checkpoints?.length ?? 0) > ctx.checkpointsBefore;
355
+ if (newCheckpoint) break; // condition met — the outer loop will observe it
356
+ resumes += 1;
357
+ const arg = sessionId || "--last";
358
+ const text =
359
+ `The stopping condition is not yet met: no new checkpoint since ${opts.runStartIso}. ` +
360
+ `Continue working quest ${opts.id}.`;
361
+ await runSegment(this.buildResume(questRecord, config, opts, arg, text));
362
+ }
363
+
364
+ return { session_id: sessionId, cost_usd: cost, tokens, invocations, sawGoal, correctiveResume, resumes };
365
+ },
366
+ };
367
+
368
+ export function getAdapter(worker) {
369
+ if (worker === "codex") return codex;
370
+ if (worker === "claude") return claude;
371
+ throw new Error(`unknown worker "${worker}" (expected claude or codex)`);
372
+ }
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "quest-loop",
3
+ "version": "0.1.0",
4
+ "description": "Goal-loop engineering for coding agents: quest contracts, iterative execution with evidence checkpoints, Claude + Codex workers. Ships the `quest` store CLI and the `quest-run` headless runner.",
5
+ "type": "module",
6
+ "engines": { "node": ">=20" },
7
+ "bin": {
8
+ "quest": "./bin/quest",
9
+ "quest-run": "./bin/quest-run"
10
+ },
11
+ "files": [
12
+ "bin/",
13
+ "lib/",
14
+ "schemas/",
15
+ "skills/",
16
+ "agents/",
17
+ "hooks/",
18
+ "CHANGELOG.md"
19
+ ],
20
+ "scripts": {
21
+ "test": "node --test tests/*.test.mjs",
22
+ "check:parity": "node scripts/check-agent-parity.mjs",
23
+ "check:hygiene": "node scripts/check-hygiene.mjs",
24
+ "check:manifests": "node scripts/validate-manifests.mjs"
25
+ },
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/robertsreberski/quest.git"
29
+ },
30
+ "homepage": "https://github.com/robertsreberski/quest",
31
+ "bugs": "https://github.com/robertsreberski/quest/issues",
32
+ "keywords": ["goal-loop", "agents", "orchestration", "workflow", "planning", "checkpoints", "claude", "codex", "cli"],
33
+ "author": "Robert Sreberski",
34
+ "license": "MIT",
35
+ "publishConfig": {
36
+ "registry": "https://registry.npmjs.org/",
37
+ "access": "public"
38
+ }
39
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "title": "Quest worker final report",
4
+ "type": "object",
5
+ "additionalProperties": false,
6
+ "required": ["quest_status", "checkpoint_recorded", "evidence_summary"],
7
+ "properties": {
8
+ "quest_status": {
9
+ "type": "string",
10
+ "enum": ["in_progress", "complete", "blocked"],
11
+ "description": "The quest_status of the checkpoint this session recorded."
12
+ },
13
+ "checkpoint_recorded": {
14
+ "type": "boolean",
15
+ "description": "True only if `quest checkpoint` was actually run and succeeded this session."
16
+ },
17
+ "evidence_summary": {
18
+ "type": "string",
19
+ "description": "One line citing the decisive command and its observed result."
20
+ }
21
+ }
22
+ }
@@ -0,0 +1,74 @@
1
+ ---
2
+ name: orchestrate
3
+ description: Run the quest loop as orchestrator — dispatch workers on ready quests, verify checkpoints, rule on results, manage waves. Use when asked to orchestrate quests, run an epic, or drive multiple quests to completion.
4
+ argument-hint: "[epic-id]"
5
+ ---
6
+
7
+ # Orchestrate quests
8
+
9
+ **What this does:** drives quests through workers and rules on the evidence —
10
+ you are the loop engineer, not the implementer.
11
+ **Use when:** more than one quest is in play, or a quest is dispatched rather
12
+ than worked inline.
13
+ **Input:** optionally an epic id to scope to; otherwise the whole store.
14
+ **What you get:** quests moved to complete/blocked with verified checkpoint
15
+ trails, and escalations only where a human ruling is genuinely needed.
16
+
17
+ ## The cycle
18
+
19
+ 1. **Adopt state** (especially at session start):
20
+ ```bash
21
+ quest list --ready --json # the dispatch queue (deps met, priority order)
22
+ quest runs --active # headless runners that outlived prior sessions
23
+ ```
24
+ 2. **Dispatch** each ready quest per its record:
25
+ - `worker: claude` → spawn the `quest-executor` subagent with the record's
26
+ `model`/`effort` as the dispatch override; prompt = "Work quest <id> per
27
+ /quest:work."
28
+ - `worker: codex`, parallel batches, or anything long-running → run
29
+ `quest-run <id>` in **background Bash** and keep working; you'll be
30
+ notified when it exits. Parallel file-disjoint quests:
31
+ `quest-run --ready --parallel 3` (add `--isolate worktree` when they touch
32
+ the same files).
33
+ 3. **Verify before you believe:** when a worker stops, run
34
+ `quest show <id> --json`. A stop WITHOUT a new checkpoint is a protocol
35
+ violation — redispatch with exactly that instruction. Never accept a chat
36
+ summary in place of a recorded checkpoint.
37
+ 4. **Review before accepting complete:** for non-trivial quests, spawn
38
+ `quest-reviewer` on the diff + checkpoint evidence. Every finding gets a
39
+ disposition: fixed / follow-up quest filed / rejected-with-reason.
40
+ 5. **Rule** (quote evidence, never adjectives):
41
+ - **accept** — the validation_summary's commands actually discharge the
42
+ Done-when items.
43
+ - **iterate-with-feedback** — send the specific gap back (continue the
44
+ subagent, or `quest-run <id> --continue-session`).
45
+ - **split** — bigger than it looked → `/quest:plan` to decompose; cancel or
46
+ re-parent the original honestly.
47
+ - **escalate-to-human** — surface human-only decisions verbatim. Never
48
+ guess a ruling the human should make.
49
+ 6. **Wave done?** When `quest list --ready` empties and nothing is in flight:
50
+ run `/quest:retro` before starting the next wave.
51
+
52
+ ## Autonomous waves
53
+
54
+ For an unattended wave, pin your own session to the outcome with a native goal:
55
+
56
+ ```
57
+ /goal every quest in this wave shows complete or blocked in `quest list --json` output
58
+ ```
59
+
60
+ The harness then keeps you cycling until the wave is genuinely done.
61
+
62
+ ## Worked example
63
+
64
+ ```bash
65
+ quest list --ready --json # → [{"id":12,"worker":"claude"…},{"id":13,"worker":"codex"…}]
66
+ # 12 → dispatch quest-executor subagent (model/effort from the record)
67
+ # 13 → background Bash: quest-run 13
68
+ # …executor stops →
69
+ quest show 12 --json # new checkpoint? quest_status? evidence?
70
+ # reviewer on 12's diff → findings dispositioned → accept
71
+ ```
72
+
73
+ **Next:** contracts weak? `/quest:plan` to fix them first. Wave finished?
74
+ `/quest:retro`. Vocabulary and stop rules: `/quest:protocol`.
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "Orchestrate Quests"
3
+ short_description: "Dispatch workers on ready quests, verify checkpoints, rule on evidence"
4
+ default_prompt: "Use $orchestrate to drive the ready quests to completion."
@@ -0,0 +1,69 @@
1
+ ---
2
+ name: plan
3
+ description: Turn an idea, feature request, or bug into quest contracts. Use when planning work as quests, decomposing a large ask into an epic with waves, or when asked to "create a quest" for something.
4
+ argument-hint: "<the ask>"
5
+ ---
6
+
7
+ # Plan work as quests
8
+
9
+ **What this does:** decomposes an ask into one or more quest contracts —
10
+ Objective, evidence-checkable Done-when, exact Validation loop — written via
11
+ `quest create`.
12
+ **Use when:** turning any ask into executable, verifiable units of work.
13
+ **Input:** the ask, plus whatever code/context you need to read to size it.
14
+ **What you get:** lint-clean quest record(s) an executor can run with zero
15
+ extra context.
16
+
17
+ ## Size it first
18
+
19
+ | Size | Shape | Rule of thumb |
20
+ |---|---|---|
21
+ | Small | 1 quest, work it inline now | one sitting, one validation run |
22
+ | Medium | 1 quest, dispatch an executor | needs iterations, fits one Objective |
23
+ | Large | epic parent + child quests in waves | multiple objectives; order via `depends_on` |
24
+
25
+ For epics: create the parent first, then children with `--parent <id>` and
26
+ `--depends-on` expressing the real order. `quest list --ready` becomes the
27
+ dispatch queue — that is the whole wave mechanic.
28
+
29
+ ## Author the contract
30
+
31
+ Every field earns its place:
32
+
33
+ - **Objective** (≤3 sentences): one concrete outcome. It is the anchor — it
34
+ will never be rewritten, so don't bury multiple outcomes in it.
35
+ - **Done when**: each item independently checkable WITH EVIDENCE. Write the
36
+ check into the item ("`npm test` passes including new theme tests"), not a
37
+ vibe ("works well").
38
+ - **Validation loop**: the exact commands the executor runs every iteration.
39
+ If you can't state them, the quest isn't ready to dispatch.
40
+ - **Constraints**: what must hold along the way (not a wish list).
41
+ - **Milestones**: discrete testable units for anything beyond one sitting.
42
+ - **Context**: files + symbols (`resolveConfig` in `lib/config.mjs`), related
43
+ quests. NEVER bare line numbers — they rot.
44
+ - **Out of scope**: the adjacent work you are explicitly not doing.
45
+ - `--worker` / `--model` / `--effort` / `--max-iterations`: match the tier to
46
+ the difficulty; the defaults come from `.quests/config.json`.
47
+
48
+ **Anti-patterns** (lint catches some, you catch the rest): adjective done-whens
49
+ ("fast", "clean"); validation loops that are prose, not commands; objectives
50
+ hiding three objectives; context by line number; budgets so big they never bind.
51
+
52
+ ## Worked example
53
+
54
+ ```bash
55
+ quest create --title "Add dark mode to settings" \
56
+ --objective "The settings page offers a dark theme that persists across reloads." \
57
+ --done-when "toggling theme switches the UI and survives reload" \
58
+ --done-when "\`npm test\` passes including new theme tests" \
59
+ --validation "npm test" \
60
+ --constraint "no new dependencies" \
61
+ --milestone "theme toggle renders and switches CSS variables" \
62
+ --milestone "choice persists via localStorage" \
63
+ --context "Settings page: src/settings/Page.tsx; theme tokens: src/theme.ts" \
64
+ --out-of-scope "system-preference auto-detection"
65
+ quest lint 12 # always, before dispatch
66
+ ```
67
+
68
+ **Next:** dispatch with `/quest:orchestrate` (or work it yourself via
69
+ `/quest:work <id>`). Rules and vocabulary: `/quest:protocol`.
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "Plan Quests"
3
+ short_description: "Turn an ask into evidence-checkable quest contracts"
4
+ default_prompt: "Use $plan to decompose this ask into quest contracts via `quest create`."
@@ -0,0 +1,39 @@
1
+ ---
2
+ name: protocol
3
+ description: The quest loop rules, checkpoint format, and vocabulary. Use when you need the protocol itself — stop conditions, checkpoint fields, status enums, scope-fence rules — or when judging whether a quest was worked correctly.
4
+ ---
5
+
6
+ # The quest protocol
7
+
8
+ **What this does:** gives you the canonical rules every quest execution follows.
9
+ **Use when:** working, orchestrating, reviewing, or planning quests and you need
10
+ the exact rule — don't paraphrase from memory.
11
+ **Input:** none.
12
+ **What you get:** the base protocol + this store's local amendments.
13
+
14
+ ## Read it
15
+
16
+ - [references/protocol.md](references/protocol.md) — the loop: orient → one
17
+ milestone → verify with stated commands → commit green → checkpoint →
18
+ stop conditions (`complete` needs every Done-when enumerated with evidence;
19
+ `blocked` beats improvising), scope fence, honesty rules, rulings, sizing.
20
+ - [references/contract-spec.md](references/contract-spec.md) — the record
21
+ format: frontmatter fields, body sections, checkpoint bytes, GitHub mapping,
22
+ exit codes.
23
+
24
+ In a store, prefer the CLI — it appends the store's own amendments:
25
+
26
+ ```bash
27
+ quest protocol
28
+ ```
29
+
30
+ ## The vocabulary that matters most
31
+
32
+ - Store status: `todo | in_progress | blocked | complete | cancelled`.
33
+ - Checkpoint verdict (`quest_status`): `in_progress | complete | blocked` — the
34
+ ONLY legal checkpoint vocabulary.
35
+ - A `complete` checkpoint cites backticked commands in `validation_summary`
36
+ and enumerates every Done-when item as Done / Blocked / Cancelled.
37
+
38
+ **Next:** plan with `/quest:plan`, execute with `/quest:work`, drive with
39
+ `/quest:orchestrate`, improve with `/quest:retro`.
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "Quest Protocol"
3
+ short_description: "The quest loop rules, checkpoint format, and vocabulary"
4
+ default_prompt: "Use $protocol to look up the exact quest loop rule before acting."