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/lib/workers.mjs
ADDED
|
@@ -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,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,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`.
|