quest-loop 0.3.4 → 0.3.6
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 +35 -1
- package/README.md +36 -19
- package/hooks/session-start.mjs +23 -11
- package/hooks/subagent-stop.mjs +40 -20
- package/lib/cli.mjs +179 -13
- package/lib/codex-native.mjs +215 -5
- package/lib/graph.mjs +82 -0
- package/lib/help.mjs +79 -34
- package/lib/runner.mjs +31 -34
- package/lib/store-github.mjs +15 -13
- package/lib/store-local.mjs +12 -12
- package/lib/store.mjs +2 -0
- package/lib/workers.mjs +1 -1
- package/package.json +1 -1
- package/skills/orchestrate/SKILL.md +19 -10
- package/skills/plan/SKILL.md +16 -7
- package/skills/setup/SKILL.md +27 -11
package/lib/store-github.mjs
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
serializeRecord,
|
|
32
32
|
} from "./contract.mjs";
|
|
33
33
|
import { parseFrontmatter, serializeFrontmatter } from "./frontmatter.mjs";
|
|
34
|
+
import { computeQueue, lintGraphReferences } from "./graph.mjs";
|
|
34
35
|
import { NotFoundError } from "./store-local.mjs";
|
|
35
36
|
|
|
36
37
|
export class GhError extends Error {
|
|
@@ -268,20 +269,15 @@ export function listQuests(repo, env) {
|
|
|
268
269
|
});
|
|
269
270
|
}
|
|
270
271
|
|
|
271
|
-
// Same
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
// closed by the orchestrator inline, never auto-dispatched; a cancelled child
|
|
275
|
-
// is terminal so it cannot wedge the epic forever.
|
|
272
|
+
// Same shared queue rule as store-local.readyQuests, over the remote list.
|
|
273
|
+
// readyQuests returns only worker-dispatchable quests; inline-close-ready epics
|
|
274
|
+
// are exposed separately by queueState.
|
|
276
275
|
export function readyQuests(repo, env) {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
);
|
|
282
|
-
return all
|
|
283
|
-
.filter((q) => q.status === "todo" && !openParents.has(q.id) && (q.depends_on ?? []).every((d) => done.has(d)))
|
|
284
|
-
.sort((a, b) => a.priority.localeCompare(b.priority) || a.id - b.id);
|
|
276
|
+
return queueState(repo, env).workerReady;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function queueState(repo, env) {
|
|
280
|
+
return computeQueue(listQuests(repo, env));
|
|
285
281
|
}
|
|
286
282
|
|
|
287
283
|
export function createQuest(repo, defaults, fields, sections, env) {
|
|
@@ -433,14 +429,20 @@ export function editQuest(repo, id, { addDoneWhen = [], addMilestone = [], addCo
|
|
|
433
429
|
|
|
434
430
|
export function lintAll(repo, env) {
|
|
435
431
|
const results = [];
|
|
432
|
+
const valid = [];
|
|
436
433
|
for (const issue of listIssues(repo, env)) {
|
|
437
434
|
try {
|
|
438
435
|
const rec = reconstruct(issue);
|
|
439
436
|
results.push({ file: rec.file, id: rec.front.id, problems: lintRecord(rec, { filename: rec.file }) });
|
|
437
|
+
valid.push({ ...rec.front, file: rec.file });
|
|
440
438
|
} catch (err) {
|
|
441
439
|
results.push({ file: `#${issue.number}`, id: issue.number ?? null, problems: [err.message] });
|
|
442
440
|
}
|
|
443
441
|
}
|
|
442
|
+
const graphProblems = lintGraphReferences(valid);
|
|
443
|
+
for (const result of results) {
|
|
444
|
+
if (result.id !== null) result.problems.push(...(graphProblems.get(result.id) ?? []));
|
|
445
|
+
}
|
|
444
446
|
return results;
|
|
445
447
|
}
|
|
446
448
|
|
package/lib/store-local.mjs
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmdirSync, writeFileSync, statSync, appendFileSync } from "node:fs";
|
|
6
6
|
import { join, basename } from "node:path";
|
|
7
7
|
import { ContractError, appendToSection, appendUnderCheckpoints, assertReopen, assertTransition, lintRecord, makeCheckpoint, nowIso, parseRecord, recordFilename, renderBody, serializeRecord, CHECKPOINT_MARKER } from "./contract.mjs";
|
|
8
|
+
import { computeQueue, lintGraphReferences } from "./graph.mjs";
|
|
8
9
|
|
|
9
10
|
export class NotFoundError extends Error {
|
|
10
11
|
constructor(id) {
|
|
@@ -83,18 +84,11 @@ export function listQuests(storeDir) {
|
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
export function readyQuests(storeDir) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
// cancelled child is terminal so it cannot wedge the epic forever.
|
|
92
|
-
const openParents = new Set(
|
|
93
|
-
all.filter((q) => q.parent !== undefined && q.status !== "complete" && q.status !== "cancelled").map((q) => q.parent),
|
|
94
|
-
);
|
|
95
|
-
return all
|
|
96
|
-
.filter((q) => q.status === "todo" && !openParents.has(q.id) && (q.depends_on ?? []).every((d) => done.has(d)))
|
|
97
|
-
.sort((a, b) => a.priority.localeCompare(b.priority) || a.id - b.id);
|
|
87
|
+
return queueState(storeDir).workerReady;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function queueState(storeDir) {
|
|
91
|
+
return computeQueue(listQuests(storeDir));
|
|
98
92
|
}
|
|
99
93
|
|
|
100
94
|
export function createQuest(storeDir, defaults, fields, bodySections) {
|
|
@@ -238,16 +232,22 @@ export function readRuns(storeDir) {
|
|
|
238
232
|
|
|
239
233
|
export function lintAll(storeDir) {
|
|
240
234
|
const results = [];
|
|
235
|
+
const valid = [];
|
|
241
236
|
for (const file of recordFiles(storeDir)) {
|
|
242
237
|
const text = readFileSync(join(questsDir(storeDir), file), "utf8");
|
|
243
238
|
try {
|
|
244
239
|
const record = parseRecord(text);
|
|
245
240
|
const problems = lintRecord(record, { filename: file });
|
|
246
241
|
results.push({ file, id: record.front.id, problems });
|
|
242
|
+
valid.push({ ...record.front, file });
|
|
247
243
|
} catch (err) {
|
|
248
244
|
results.push({ file, id: null, problems: [err.message] });
|
|
249
245
|
}
|
|
250
246
|
}
|
|
247
|
+
const graphProblems = lintGraphReferences(valid);
|
|
248
|
+
for (const result of results) {
|
|
249
|
+
if (result.id !== null) result.problems.push(...(graphProblems.get(result.id) ?? []));
|
|
250
|
+
}
|
|
251
251
|
return results;
|
|
252
252
|
}
|
|
253
253
|
|
package/lib/store.mjs
CHANGED
|
@@ -15,6 +15,7 @@ export function openStore(config, ctx = {}) {
|
|
|
15
15
|
loadQuest: (id) => github.loadQuest(repo, id, env),
|
|
16
16
|
listQuests: () => github.listQuests(repo, env),
|
|
17
17
|
readyQuests: () => github.readyQuests(repo, env),
|
|
18
|
+
queueState: () => github.queueState(repo, env),
|
|
18
19
|
startQuest: (id) => github.startQuest(repo, id, env),
|
|
19
20
|
appendCheckpoint: (id, cp) => github.appendCheckpoint(repo, id, cp, env),
|
|
20
21
|
cancelQuest: (id, reason) => github.cancelQuest(repo, id, reason, env),
|
|
@@ -29,6 +30,7 @@ export function openStore(config, ctx = {}) {
|
|
|
29
30
|
loadQuest: (id) => local.loadQuest(dir, id),
|
|
30
31
|
listQuests: () => local.listQuests(dir),
|
|
31
32
|
readyQuests: () => local.readyQuests(dir),
|
|
33
|
+
queueState: () => local.queueState(dir),
|
|
32
34
|
startQuest: (id) => local.startQuest(dir, id),
|
|
33
35
|
appendCheckpoint: (id, cp) => local.appendCheckpoint(dir, id, cp),
|
|
34
36
|
cancelQuest: (id, reason) => local.cancelQuest(dir, id, reason),
|
package/lib/workers.mjs
CHANGED
|
@@ -353,7 +353,7 @@ export const codex = {
|
|
|
353
353
|
if (!sawGoal && opts.codexGoalMode === "require") {
|
|
354
354
|
correctiveResume = true;
|
|
355
355
|
resumes += 1;
|
|
356
|
-
const corr = this.buildResume(questRecord, config, opts, "--last", CODEX_CREATE_GOAL_CORRECTION(opts.id));
|
|
356
|
+
const corr = this.buildResume(questRecord, config, opts, sessionId || "--last", CODEX_CREATE_GOAL_CORRECTION(opts.id));
|
|
357
357
|
const cres = await runSegment(corr);
|
|
358
358
|
if (codexUsedCreateGoal(cres.events || [])) sawGoal = true;
|
|
359
359
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "quest-loop",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
4
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
5
|
"type": "module",
|
|
6
6
|
"engines": { "node": ">=20" },
|
|
@@ -18,9 +18,16 @@ trails, and escalations only where a human ruling is genuinely needed.
|
|
|
18
18
|
|
|
19
19
|
1. **Adopt state** (especially at session start):
|
|
20
20
|
```bash
|
|
21
|
-
quest list --
|
|
21
|
+
quest list --queue --json # orchestration state: worker_ready + inline_close_ready_epics
|
|
22
22
|
quest runs --active # headless runners that outlived prior sessions
|
|
23
23
|
```
|
|
24
|
+
`worker_ready` is the worker dispatch queue (deps met, priority order);
|
|
25
|
+
`inline_close_ready_epics` is the set of epics you close yourself after
|
|
26
|
+
verifying children and the epic validation loop. `quest list --ready --json`
|
|
27
|
+
remains a dispatch-only shortcut for `worker_ready`.
|
|
28
|
+
When the checkout, plugin cache, and installed package may differ, run the
|
|
29
|
+
checkout binary (`./bin/quest`) or verify `PATH` with `quest --version`
|
|
30
|
+
before trusting queue or dispatch behavior.
|
|
24
31
|
For an implementation accepted from `$quest:plan` in Codex Plan Mode, stay in
|
|
25
32
|
this orchestrator role. Do not implement product code inline; create/lint the
|
|
26
33
|
quest records if needed, then dispatch workers.
|
|
@@ -31,7 +38,7 @@ trails, and escalations only where a human ruling is genuinely needed.
|
|
|
31
38
|
- In Claude Code, start the turn with `/goal` using the same condition.
|
|
32
39
|
If native goal mode is unavailable, say so and keep the Quest checkpoint
|
|
33
40
|
trail as the hard stop signal; do not pretend a goal was set.
|
|
34
|
-
3. **Dispatch** each
|
|
41
|
+
3. **Dispatch** each `worker_ready` quest per its record:
|
|
35
42
|
- In Codex, the default path is native subagents for both serial and
|
|
36
43
|
parallel waves. If `spawn_agent` is not visible, call `tool_search` once
|
|
37
44
|
for subagent tools before choosing any fallback. When available, spawn:
|
|
@@ -73,16 +80,17 @@ trails, and escalations only where a human ruling is genuinely needed.
|
|
|
73
80
|
re-parent the original honestly.
|
|
74
81
|
- **escalate-to-human** — surface human-only decisions verbatim. Never
|
|
75
82
|
guess a ruling the human should make.
|
|
76
|
-
7. **Wave done?** When `quest list --
|
|
77
|
-
run `$quest:retro` before starting the next wave.
|
|
83
|
+
7. **Wave done?** When `quest list --queue --json` shows no `worker_ready` or
|
|
84
|
+
`inline_close_ready_epics`, and nothing is in flight, run `$quest:retro` (read skill `$quest:retro`) before starting the next wave.
|
|
78
85
|
|
|
79
86
|
## Closing an epic
|
|
80
87
|
|
|
81
88
|
An epic is an ordinary quest that other quests name as `parent`. It is **never
|
|
82
|
-
dispatched to a worker**:
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
89
|
+
dispatched to a worker**: it does not belong in `worker_ready`, and
|
|
90
|
+
`quest-run --ready` refuses it even once every child is terminal (a direct
|
|
91
|
+
`quest-run <id>` on an epic still runs, but don't — it burns a worker on pure
|
|
92
|
+
verification). Once it is closeable, `quest list --queue --json` reports it in
|
|
93
|
+
`inline_close_ready_epics`; close it inline yourself, spending zero worker
|
|
86
94
|
tokens:
|
|
87
95
|
|
|
88
96
|
1. **Verify the children are genuinely done:**
|
|
@@ -119,7 +127,8 @@ quest reopen <id> --reason "review found npm audit criticals after completion"
|
|
|
119
127
|
This flips `complete → in_progress` and appends an audited checkpoint carrying
|
|
120
128
|
`reopen_reason`, so the loop keeps custody of the defect trail. Then dispatch the
|
|
121
129
|
quest directly by id (`quest-run <id>` or a `quest-executor` subagent) — reopened
|
|
122
|
-
quests are `in_progress`, so they do **not** re-appear in `
|
|
130
|
+
quests are `in_progress`, so they do **not** re-appear in `worker_ready` or
|
|
131
|
+
`quest list --ready`.
|
|
123
132
|
Reopening a child of a **complete** parent epic is allowed (a stderr warning, not
|
|
124
133
|
a block); you then rule whether the epic's completion verdict is falsified and,
|
|
125
134
|
if so, reopen the epic too. `cancelled` is fully terminal — file a new quest.
|
|
@@ -139,7 +148,7 @@ genuinely done.
|
|
|
139
148
|
## Worked example
|
|
140
149
|
|
|
141
150
|
```bash
|
|
142
|
-
quest list --
|
|
151
|
+
quest list --queue --json # → {"worker_ready":[{"id":12…},{"id":13…}],"inline_close_ready_epics":[]}
|
|
143
152
|
# 12 → spawn quest-executor with a child /goal or create_goal prompt
|
|
144
153
|
# 13 → spawn quest-executor too; use quest-run only if native subagents are unavailable
|
|
145
154
|
# …executor stops →
|
package/skills/plan/SKILL.md
CHANGED
|
@@ -27,14 +27,16 @@ extra context.
|
|
|
27
27
|
In Codex Plan Mode, do **not** implement product code after the user accepts a
|
|
28
28
|
plan. The same parent agent/session that processes `$quest:plan` owns the
|
|
29
29
|
handoff: make the quest records real, then adopt `$quest:orchestrate` by
|
|
30
|
-
default.
|
|
30
|
+
default when the user asks to implement the plan. The parent session never uses
|
|
31
|
+
`$quest:work <id>` after a Plan Mode handoff unless the user explicitly asks to.
|
|
31
32
|
|
|
32
33
|
1. Create or confirm the quest records with `quest create` and `quest lint`.
|
|
33
34
|
2. If the user accepted a plan and asked to implement it, the same parent
|
|
34
35
|
session becomes `$quest:orchestrate`, not `$quest:work`. Do not start editing
|
|
35
|
-
product code in the parent session.
|
|
36
|
-
3. In `$quest:orchestrate`,
|
|
37
|
-
goal-mode workers
|
|
36
|
+
product code in the parent session. Read skill `$quest:orchestrate` and follow its rules for dispatching workers and closing epics inline.
|
|
37
|
+
3. In `$quest:orchestrate`, inspect `quest list --queue --json`, set the
|
|
38
|
+
orchestrator goal for the wave, then spawn goal-mode workers for
|
|
39
|
+
`worker_ready` quests.
|
|
38
40
|
4. Stop after listing the ready quest ids and validation commands only when the
|
|
39
41
|
user explicitly asked for create-only/no-dispatch behavior such as "only
|
|
40
42
|
create quests", "do not dispatch", or "stop after planning".
|
|
@@ -46,8 +48,15 @@ unless the user explicitly asks to bypass orchestration for a genuinely small
|
|
|
46
48
|
single quest.
|
|
47
49
|
|
|
48
50
|
For epics: create the parent first, then children with `--parent <id>` and
|
|
49
|
-
`--depends-on` expressing the real order. `quest list --
|
|
50
|
-
|
|
51
|
+
`--depends-on` expressing the real order. `quest list --queue --json` shows the
|
|
52
|
+
wave order: `worker_ready` quests are dispatched, and
|
|
53
|
+
`inline_close_ready_epics` are closed inline by the orchestrator after children
|
|
54
|
+
finish. `quest list --ready` remains only the dispatch shortcut for
|
|
55
|
+
`worker_ready`.
|
|
56
|
+
|
|
57
|
+
When local checkout, plugin cache, and installed package versions can differ,
|
|
58
|
+
author and lint with the checkout binary (`./bin/quest`) or verify `PATH` with
|
|
59
|
+
`quest --version` before relying on queue semantics or generated records.
|
|
51
60
|
|
|
52
61
|
Keep the **epic itself thin**. Its Done-when is **integration-level only**: it
|
|
53
62
|
checks that the children compose into a working whole (the end-to-end behavior,
|
|
@@ -104,4 +113,4 @@ quest lint 12 # always, before dispatch
|
|
|
104
113
|
|
|
105
114
|
**Next:** dispatch with `$quest:orchestrate`. Only work it yourself via
|
|
106
115
|
`$quest:work <id>` for genuinely small inline work outside Plan Mode and outside
|
|
107
|
-
an accepted Plan Mode handoff. Rules and vocabulary: `$quest:protocol`.
|
|
116
|
+
an accepted Plan Mode handoff, when user explicitly asked for it. Rules and vocabulary: `$quest:protocol`.
|
package/skills/setup/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: setup
|
|
3
|
-
description: Use when setting up, validating, updating, or troubleshooting Quest's native Codex or Claude plugin integration.
|
|
4
|
-
argument-hint: "[doctor|install-agents|init]"
|
|
3
|
+
description: Use when setting up, validating, updating, opening, or troubleshooting Quest's native Codex or Claude plugin integration.
|
|
4
|
+
argument-hint: "[doctor|fix|open|install-agents|init]"
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Set up Quest native agents
|
|
@@ -19,9 +19,10 @@ agent templates for both providers:
|
|
|
19
19
|
- `.claude/agents/quest-reviewer.md`
|
|
20
20
|
|
|
21
21
|
Use `quest init --no-agents` when you only want the quest store. If an existing
|
|
22
|
-
project template
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
project template is stale and still identifies itself as Quest's agent, init
|
|
23
|
+
replaces it. If another file conflicts, init fails before creating `.quests/`;
|
|
24
|
+
inspect the file, run the explicit provider install command with `--force` only
|
|
25
|
+
when you intend replacement, then rerun `quest init`.
|
|
25
26
|
|
|
26
27
|
Project-scoped agent templates install at the Git repository root. For a nested
|
|
27
28
|
quest store, use `quest init --no-agents` in the nested directory and set
|
|
@@ -32,7 +33,7 @@ quest store, use `quest init --no-agents` in the nested directory and set
|
|
|
32
33
|
Check the installed Codex-facing state from the actual Codex surfaces:
|
|
33
34
|
|
|
34
35
|
```bash
|
|
35
|
-
quest codex doctor
|
|
36
|
+
quest codex doctor --fix
|
|
36
37
|
```
|
|
37
38
|
|
|
38
39
|
This verifies the Codex CLI, `quest` binary on PATH, `multi_agent` and `goals`
|
|
@@ -45,7 +46,14 @@ use `quest-run --codex-goal-mode require` only as the headless fallback.
|
|
|
45
46
|
Check the installed Claude-facing state from the actual Claude surfaces:
|
|
46
47
|
|
|
47
48
|
```bash
|
|
48
|
-
quest claude doctor
|
|
49
|
+
quest claude doctor --fix
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Open an interactive provider only after the same repair/check gate passes:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
quest codex open
|
|
56
|
+
quest claude open
|
|
49
57
|
```
|
|
50
58
|
|
|
51
59
|
## Install Native Agents
|
|
@@ -70,9 +78,10 @@ quest claude install-agents --scope user
|
|
|
70
78
|
```
|
|
71
79
|
|
|
72
80
|
If an existing file conflicts, inspect it first. Use `--force` only when you
|
|
73
|
-
intend to replace that custom agent with Quest's bundled definition.
|
|
74
|
-
|
|
75
|
-
|
|
81
|
+
intend to replace that custom agent with Quest's bundled definition. Stale
|
|
82
|
+
Quest-owned templates are replaced without `--force`. Symlinked agent template
|
|
83
|
+
files can be written through with `--force`; keep the provider agent directories
|
|
84
|
+
themselves as real directories.
|
|
76
85
|
|
|
77
86
|
## Update Installed Plugin
|
|
78
87
|
|
|
@@ -82,7 +91,14 @@ refresh and reinstall, then start a new Codex thread:
|
|
|
82
91
|
```bash
|
|
83
92
|
codex plugin marketplace upgrade quest
|
|
84
93
|
codex plugin add quest@quest
|
|
85
|
-
quest codex doctor
|
|
94
|
+
quest codex doctor --fix
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Claude updates through its plugin manager. After updating, restart Claude Code:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
claude plugin update quest@quest
|
|
101
|
+
quest claude doctor --fix
|
|
86
102
|
```
|
|
87
103
|
|
|
88
104
|
If hooks changed, review/trust the updated hook definitions in Codex when
|